Eventos
Um evento é algo que aconteceu no seu sistema e que o Workfuse precisa saber: uma inscrição concluída, um boleto pago, uma aula assistida. Ao receber um evento, o Workfuse resolve de que contato ele é e atualiza as propriedades de Contato e de Negócio que a instituição mapeou.
É a diferença para um CRM genérico: lá o evento entra na timeline e alguém monta uma automação para transformar isso em dado. Aqui o mapeamento é parte do evento.
Todos os caminhos são relativos a API_BASE/public/v1/crm.
Antes de enviar: a definição do evento
Você não envia um evento avulso. A instituição primeiro cria uma definição no painel (Configurações → Eventos), e ela declara três coisas:
- O nome interno (
inscricao_concluida) — é o que vai no campoeventName. - O schema: que propriedades o payload carrega, de que tipo, e quais são
obrigatórias. Tipos aceitos:
TEXT,EMAIL,NUMBER,DATE. - Os mapeamentos: qual propriedade do evento atualiza qual propriedade de Contato ou de Negócio.
definição (painel) → POST /crm/events → 201 Created
localiza o negócio → resolve contato → grava as propriedades
Enviar um eventName que não existe, ou cuja definição está inativa, responde 404.
Enviar um evento
POST /events
curl -X POST "API_BASE/public/v1/crm/events" \
-H "Authorization: Bearer wf_sua_chave_aqui" \
-H "Content-Type: application/json" \
-d '{
"eventName": "inscricao_concluida",
"deliveryId": "insc-2026-88431",
"occurredAt": "2026-09-17T12:58:03.000Z",
"contact": {
"id": "ana.souza@exemplo.com",
"externalId": "aluno-4471",
"properties": {
"curso": "Engenharia Civil",
"origem": "Feira de profissões"
}
},
"deal": {
"externalId": "inscricao-88431",
"properties": {
"valor_da_taxa": 85.5
}
}
}'
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
eventName | string | sim | O nome interno da definição. |
deliveryId | string | sim | A sua chave de idempotência — ver abaixo. |
occurredAt | string | sim | ISO 8601, quando o fato aconteceu no seu sistema — não quando você enviou. |
contact | object | não* | O contato do evento. |
contact.id | string | não | O id do Workfuse, o e-mail ou o CPF/CNPJ. O Workfuse distingue pela forma. |
contact.externalId | string | não | O id do contato no seu sistema. |
contact.anonymousId | string | não | Um token seu para quem ainda não se identificou. |
contact.properties | object | não | Propriedades do Contato a gravar. Chave fora do catálogo da instituição é ignorada, sem erro. email, taxId e externalId também são ignorados: para mudar a identidade, use PATCH /contacts. |
deal | object | não* | O Negócio do evento. Informado e inexistente nesta instituição, a resposta é 404. |
deal.id | string | não | O id do Negócio no Workfuse. |
deal.externalId | string | não | O id do Negócio no seu sistema. |
deal.properties | object | não | Propriedades do Negócio a gravar, pelas chaves do catálogo. Chave fora do catálogo é ignorada, sem erro. |
* Pelo menos um entre contact e deal precisa trazer id ou externalId (ou contact.anonymousId). Sem isso, a resposta é 422. Com deal.id e deal.externalId juntos, vale o id.
As propriedades de contact.properties e deal.properties são validadas juntas contra o schema da definição. Se a mesma chave vier nos dois lados, vale a do deal.
Resposta 201:
{
"id": "clzd8p3r40004s6vp6c2n5t7b",
"deliveryId": "insc-2026-88431",
"contactResolution": "RESOLVED_EMAIL",
"contactId": "clzc1a2b30001s6vpabcd1234",
"offSchemaKeys": [],
"rejectionReason": null
}
A resolução do contato volta na própria resposta. offSchemaKeys lista as chaves enviadas que
a definição não declara, e rejectionReason só vem preenchido em REJECTED_UNKNOWN_CONTACT.
Idempotência — deliveryId
O deliveryId é seu, e é o que torna o reenvio seguro. A segunda entrega com o mesmo
deliveryId não cria um segundo evento e não aplica a propriedade duas vezes — devolve
201 de novo, com o mesmo id da primeira, e não grava a propriedade do Contato duas vezes.
É o mesmo papel do Idempotency-Key que você conhece de outras APIs, com duas diferenças:
ele é obrigatório (não opcional), e vai no corpo, não no header — porque é propriedade
do evento, não da requisição HTTP.
Use um id que o seu lado já tem e que é estável para aquele fato: o id da inscrição, o id da transação. Nunca um timestamp nem um random por tentativa — aí cada retentativa vira um evento novo.
O escopo é a instituição: duas instituições numerando as entregas a partir de 1 não colidem.
Como o contato é resolvido
O Workfuse tenta os identificadores nesta ordem, e para no primeiro que casar:
contact.id → contact.externalId → taxId → email → anonymousId
A busca é sempre dentro da instituição da sua API key. O resultado volta na resposta, no
campo contactResolution:
contactResolution | Significa |
|---|---|
RESOLVED_ID | Achou pelo contact.id (id do Workfuse). |
RESOLVED_EXTERNAL_ID | Achou pelo externalId. |
RESOLVED_TAX_ID | Achou pelo CPF/CNPJ enviado em contact.id. |
RESOLVED_EMAIL | Achou pelo e-mail enviado em contact.id. |
RESOLVED_ANONYMOUS_ID | Achou pelo anonymousId de um evento anterior. |
RESOLVED_DEAL | Evento só com deal: o contato é o primário do Negócio. |
CONTACT_CREATED | Não achou, e a definição manda criar. |
PENDING_ANONYMOUS | Não achou; o evento ficou guardado no anonymousId. |
REJECTED_UNKNOWN_CONTACT | Não achou, e a definição manda recusar. |
Quando não acha ninguém
A definição escolhe a política, no painel:
| Política | O que faz |
|---|---|
CREATE_CONTACT | Cria o contato, se vier email ou taxId válido. |
HOLD_ANONYMOUS | Guarda o evento no anonymousId. Quando um evento posterior com o mesmo anonymousId trouxer um identificador, todos os eventos guardados são religados ao contato de uma vez. |
REJECT | Recusa, com motivo nomeado. O evento é gravado assim mesmo — item recusado que não deixa rastro é indistinguível de item que nunca chegou. |
HOLD_ANONYMOUS é o que serve o fluxo mais comum: o visitante navega anônimo, você manda
eventos com um anonymousId do seu lado, e no momento em que ele preenche o formulário o
histórico inteiro cola no contato.
Identificador de outra instituição
Se o identificador casa com um contato que existe, mas em outra instituição, o evento é
recusado com 422. É quase sempre API key errada, e tratar como "desconhecido"
esconderia um erro real de integração.
Como as propriedades são escritas
Há dois caminhos para uma propriedade chegar ao Negócio. Pelo mapeamento, que é da
definição: você manda properties com as chaves do evento, e a definição diz para onde cada
uma vai. Ou direto: as chaves de deal.properties que existem no catálogo de Negócio da
instituição são gravadas com o próprio nome. Se os dois mirarem a mesma propriedade, o valor
de deal.properties é o que fica. null em deal.properties é ignorado — para limpar uma
propriedade, use PATCH /deals.
properties.origem → Contato.origem_do_lead
properties.curso → Negócio.curso_de_interesse
Três regras, valem para os dois caminhos:
- O modo vem do tipo do destino. Propriedade
MULTI_SELECTacrescenta o valor à lista (sem duplicar); todo o resto substitui. REFERENCEeUSERnão podem ser destino. Uma string vinda de sistema de terceiro não é id confiável de registro interno.- Chave fora do schema não é erro. Ela é guardada com o evento e devolvida em
offSchemaKeysna resposta — você vê que mandou algo que a definição não declara, sem perder a entrega.
Qual Negócio recebe
O Negócio é a oportunidade aberta sobre o contato — uma inscrição, uma rematrícula.
O evento escreve só no Negócio de deal.id ou deal.externalId, sempre dentro da
instituição da sua API key. Sem deal no corpo, nenhum Negócio é escrito, e as propriedades
de Contato são aplicadas do mesmo jeito. O evento nunca cria um Negócio: se o deal
informado não existe, a resposta é 404 e nada é gravado. Para criar, use
POST /deals.
Mandar só deal, sem contact, funciona: o contato do evento é o primário do Negócio
(RESOLVED_DEAL). Mandando os dois, o contato do evento é o de contact.
Erros
Recusado com 4xx, e nada é gravado:
| HTTP | errorCode | Quando |
|---|---|---|
400 | — | Corpo malformado (ex.: sem eventName, deliveryId ou occurredAt). |
401 | — | API key ausente, inválida ou revogada. |
403 | — | A chave não tem o módulo crm no escopo. |
404 | EVENT_DEFINITION_NOT_FOUND | eventName desconhecido nesta instituição. |
404 | EVENT_DEFINITION_INACTIVE | A definição existe, mas está inativa. |
404 | DEAL_NOT_FOUND | O deal informado não existe nesta instituição. |
422 | EVENT_IDENTIFIER_REQUIRED | Nem contact nem deal trouxeram id ou externalId (ou contact.anonymousId). O formato antigo, com identifiers, cai aqui. |
422 | EVENT_CONTACT_FROM_ANOTHER_INSTITUTION | O identificador casa com um contato de outra instituição. |
422 | EVENT_PAYLOAD_SCHEMA_ERROR | Propriedade obrigatória ausente, ou valor que não converte para o tipo declarado. details traz missingKeys e invalidTypeKeys. |
429 | — | Limite de requisições excedido, quando os limites entrarem em vigor (hoje em prévia). |
Quando dá 4xx, nada foi gravado. Corrija o payload e reenvie com o mesmo
deliveryId.
A política REJECT não é erro: o evento é gravado e a resposta é 201, com
contactResolution: REJECTED_UNKNOWN_CONTACT e o rejectionReason.
Se você vem do HubSpot
O equivalente lá é o custom behavioral event (POST /events/v3/send). A ergonomia é a
mesma; três diferenças mudam a integração:
| No HubSpot | Aqui |
|---|---|
POST /events/v3/send | POST /crm/events |
eventName, occurredAt, properties | Iguais |
uuid (deduplicação, opcional) | deliveryId — mesmo papel, obrigatório |
objectId / email / utk, soltos e com ordem de prioridade | contact{} e deal{}, objetos nomeados |
| Evento vai para a timeline do contato, com as propriedades do evento | O evento também atualiza propriedade do Contato e do Negócio, pelo mapeamento da definição |
| Propriedade do registro se atualiza por workflow, à parte | O mapeamento é parte da definição do evento |
Medido em developers.hubspot.com e knowledge.hubspot.com em 17/09/2026. A
documentação do HubSpot descreve o evento indo para a timeline e não declara atualização
de propriedade do registro pelo envio — por isso a penúltima linha está escrita como
divergência nossa, não como paridade.