Webhooks do CRM
Webhook é o caminho de volta: o Workfuse faz um POST assinado para a sua URL quando algo
muda aqui. É assim que você descobre o resultado do que enviou de forma
assíncrona. Para o lote, também
dá para consultar o resultado.
O mecanismo de webhook já existe e está em produção (configuração, envelope,
assinatura, retry). contact.created e contact.updated já são emitidos. O que
continua prévia é event.received, que ainda não é emitido.
O mecanismo — como registrar a URL, validar a assinatura HMAC e tratar a entrega — é o mesmo de todos os módulos e está em Webhooks. Esta página traz só o que o CRM emite.
O catálogo
| Evento | Quando dispara |
|---|---|
contact.created | Um contato foi criado pela API, pelo lote ou por evento (política CREATE_CONTACT). |
contact.updated | Uma propriedade de contato mudou pela API, pelo lote ou por evento. |
event.received | Um evento enviado por você terminou de ser processado. Ainda não é emitido. |
Só as vias de integração emitem. Contato criado ou alterado no painel, em formulário, no chat, na inscrição, pelo ERP ou por vínculo de usuário não dispara nada.
Ao registrar o webhook, você assina os que interessam — ou nenhum, e recebe todos.
contact.created
{
"id": "evt_01J8ZQ9M2P",
"event": "contact.created",
"data": {
"contactId": "clz9q2k8r0001s6vp2h4m7x1e",
"externalId": "aluno-4471",
"origin": "API",
"createdAt": "2026-09-17T13:02:11.482Z"
}
}
origin diz de onde veio: API (criação direta), BATCH (lote) ou EVENT (política
CREATE_CONTACT de uma definição).
Um evento que cria o contato e traz contact.properties gera dois webhooks:
contact.created e, em seguida, contact.updated com as chaves gravadas.
contact.updated
{
"id": "evt_01J8ZQ9M2Q",
"event": "contact.updated",
"data": {
"contactId": "clz9q2k8r0001s6vp2h4m7x1e",
"externalId": "aluno-4471",
"changedProperties": ["phone", "interesse_em_bolsa"],
"origin": "BATCH",
"updatedAt": "2026-09-17T13:07:44.201Z"
}
}
changedProperties lista só as chaves que mudaram de valor, nativas (email, phone,
…) e customizadas. Gravar o mesmo valor de novo não dispara o evento — o que importa é a
mudança, não a escrita.
O payload não carrega os valores, só as chaves. Para ler o valor novo, chame
GET /crm/contacts/{contactId}. É de propósito: propriedade de
contato pode conter dado pessoal, e a URL de webhook é configuração do cliente.
event.received
O desfecho do que você mandou em POST /crm/events:
{
"id": "evt_01J8ZQ9M2R",
"event": "event.received",
"data": {
"eventId": "clzd8p3r40004s6vp6c2n5t7b",
"deliveryId": "insc-2026-88431",
"eventName": "inscricao_concluida",
"contactResolution": "RESOLVED_EXTERNAL_ID",
"contactId": "clz9q2k8r0001s6vp2h4m7x1e",
"dealId": "clzc4n7p20003s6vp1f5j8w3a",
"appliedProperties": {
"contact": ["origem_do_lead"],
"deal": ["curso_de_interesse"]
},
"offSchemaKeys": ["valor_da_taxa"],
"rejectionReason": null
}
}
| Campo | O que é |
|---|---|
deliveryId | O seu id. É por ele que você correlaciona com o que enviou. |
contactResolution | Como o contato foi achado — a tabela está em Eventos. |
contactId | null quando não resolveu. |
dealId | O Negócio que recebeu propriedade, ou null. |
appliedProperties | O que foi gravado de fato, por objeto. |
offSchemaKeys | Chaves que você mandou e a definição não declara. |
rejectionReason | Preenchido só quando contactResolution é REJECTED_UNKNOWN_CONTACT. |
Dispara também quando o evento foi recusado — com rejectionReason explicando. Recusa
que não avisa é indistinguível de entrega que nunca chegou.
Três coisas para acertar no receiver
- Correlacione por
deliveryId, não poreventId. OdeliveryIdé o seu; oeventIdé nosso. - A entrega é at-least-once e pode repetir. O
iddo envelope (evt_…) é o mesmo em todas as tentativas: use-o como chave de deduplicação e responda2xxrápido, processando de forma assíncrona do seu lado. Sem2xx, tentamos de novo por cerca de 3h45 (10 tentativas). - Não presuma ordem.
contact.createdecontact.updateddo mesmo contato podem chegar fora de ordem.
Cuidado com o laço
contact.updated dispara para mudança feita pelas vias de integração — inclusive a que a
sua própria integração acabou de fazer. Um receiver que reage a contact.updated escrevendo de volta
no contato fecha um laço infinito.
Filtre por origin, ou compare changedProperties com o que você acabou de enviar, antes
de escrever de volta.