Webhooks de saída · Desenvolvedores

Eventos do Codewo
no seu servidor.

Cadastre um endereço https:// e escolha os eventos. Quando uma conversa, um contato ou uma venda muda, o Codewo envia um POST com JSON assinado para o seu servidor. Seu sistema reage logo em seguida, sem consultar a API de tempos em tempos.

Cabeçalho de assinatura
X-Codewo-Signature: t=<unix>,v1=<hmac_hex>

HMAC-SHA256 de "{t}.{corpo}" com o segredo do endereço. Nas 24 h depois de trocar o segredo, chegam dois v1.

Entrega

11 eventos para escolher

5 tentativas, de 0 s a 2 h

Pausa automática após 5 falhas seguidas.

Visão geral

Você cadastra um endereço https:// e escolhe os eventos que ele recebe. Quando um deles acontece, o Codewo põe a entrega numa fila e faz um POST com JSON assinado para o seu servidor. Resposta 2xx encerra a entrega; qualquer outra coisa entra nas retentativas.

Cadastre o endereço

Em Configurações › Integrações › Webhooks: nome, endereço e eventos. O segredo de assinatura aparece uma vez só.

O Codewo envia

POST JSON com até 10 s para responder. Os cabeçalhos dizem o tipo e o id do evento.

Você responde 2xx

Outra resposta, ou nenhuma, gera nova tentativa. 5 falhas seguidas pausam o endereço.

Bom saber

Webhooks estão nos planos a partir do Professional e no teste grátis de 7 dias. Quem cadastra precisa da permissão "Gerenciar integrações". Você pode ter mais de um endereço, cada um com o próprio segredo e os próprios eventos, e acompanhar cada entrega num histórico com status, tentativa e tempo de resposta.

Formato do envio

Todo POST tem o mesmo envelope, com quatro campos.

Envelope
json
{
  "id": "01M3MA8R8GT3V6W9X2Y5Z8A1BC",
  "type": "quote.accepted",
  "created_at": "2026-09-28T12:31:23-03:00",
  "data": { "...": "conteúdo do evento" }
}
id
Id do evento. É o mesmo em todas as tentativas automáticas e igual ao cabeçalho X-Codewo-Delivery.
type
Um dos 11 eventos abaixo, igual ao cabeçalho X-Codewo-Event.
created_at
Hora deste POST, em ISO 8601 no horário de Brasília. Muda a cada tentativa: não use para ordenar eventos.
data
O conteúdo do evento, fotografado no momento em que ele aconteceu. Uma retentativa horas depois leva o mesmo conteúdo; para o estado atual, consulte a API.
Dois formatos de data

Conversa e contato vêm dentro de um objeto (data.conversation, data.contact). Os 4 eventos de venda põem os campos direto na raiz (data.id, data.number...). Um leitor genérico precisa dos dois caminhos. Campos novos podem ser acrescentados com o tempo: ignore os que o seu código não conhece.

Os 11 eventos

Cada endereço assina os eventos que quiser. Cada evento sai uma vez por acontecimento, e o exemplo de corpo de cada um traz os mesmos campos, na mesma ordem e com os mesmos formatos que o Codewo envia.

Conversas

conversation.createdConversa criada

Disparado quando uma nova conversa é aberta em qualquer canal.

  • Uma vez por conversa nova, em qualquer um dos 8 canais: a que começa pela mensagem do cliente e a que a equipe abre pela Caixa de Entrada, por uma Transmissão ou por um aviso da Agenda. Quando é o primeiro contato da pessoa, chega também um contact.created.
  • Conversa aberta por uma ligação do WhatsApp, por um encaminhamento de mensagem ou por uma automação ainda não gera este evento. Os eventos seguintes dela (atribuição, status, resolução) saem normalmente.
  • channel_type diz o canal: whatsapp é a API oficial do WhatsApp e whatsapp_web é a conexão por QR Code.
conversation.assignedConversa atribuída

Disparado quando muda o responsável pela conversa (pessoa, equipe ou atendente virtual), inclusive quando ela fica sem responsável.

  • assigned_to.type é user (pessoa), team (equipe) ou ai_agent (atendente virtual). Tirar o responsável também dispara, com assigned_to igual a null.
  • Se o status muda no mesmo momento da atribuição, sai só este evento, já com o status novo. A resolução é a exceção: ela sempre sai como conversation.closed.
conversation.status_changedStatus da conversa alterado

Disparado quando o status muda sem ser resolução: aberta, pendente, em andamento, adiada ou spam, inclusive ao reabrir. Resolver dispara "Conversa resolvida".

  • Cobre open, pending, in_progress, snoozed e spam, inclusive a reabertura de uma conversa resolvida. A resolução sai como conversation.closed, não aqui.
conversation.closedConversa resolvida

Disparado quando a conversa é resolvida (status = resolved).

  • O evento se chama closed, mas o status que chega é resolved: não existe status closed nas conversas. Em geral resolved_at vem preenchido; em dois casos raros (a conversa que uma Transmissão já abre resolvida e o grupo do WhatsApp de que o seu número saiu) ele chega null.

Contatos

contact.createdContato criado

Disparado quando um novo contato é cadastrado, inclusive o que nasce da primeira mensagem num canal.

  • Inclui o contato que nasce da primeira mensagem num canal. Nesse caso source traz o tipo do canal e status chega null, embora o contato seja gravado como active. O mesmo vale para o contato criado sem status pela API.
  • A origem medida pela atribuição (anúncio, campanha) não vem aqui: leia em GET /v1/contacts/{id}, no campo attribution.
  • Contato criado pela API já com dono da carteira chega como contact.created seguido de contact.updated com assigned_to em changed_fields.
contact.updatedContato atualizado

Disparado quando muda nome, e-mail, telefone, empresa, status, origem do cadastro, foto ou o dono da carteira do contato.

  • Só sai quando muda name, email, phone, company_name, status, source, avatar_url ou assigned_to. changed_fields lista quais mudaram.
  • O dono da carteira e a foto não vêm no objeto do contato: quando changed_fields traz assigned_to, leia o novo dono em GET /v1/contacts/{id} (assigned_to e portfolio_owner).
  • Mudança só em campos personalizados não dispara. Etiqueta tem evento próprio.
contact.tag_addedEtiqueta adicionada ao contato

Disparado quando uma etiqueta entra no contato, um evento por etiqueta.

  • Sai por qualquer caminho: ficha do contato e painel da conversa, Automações, Transmissões, atendente virtual, dados extraídos pela IA e integração. Anexar de novo uma etiqueta que o contato já tem não dispara.
  • data.tag é a etiqueta que entrou; data.contact.tags já traz a lista completa, com ela.
  • Exceção: ao mesclar dois contatos, as etiquetas herdadas do contato absorvido não geram este evento. Tirar etiqueta não tem evento.

Vendas

order.createdVenda registrada

Disparado a cada pedido registrado, por qualquer caminho: negócio ganho no funil, proposta aceita, venda pela conversa, pedido manual, API, integração ou recorrência.

  • Sai para todo pedido, de qualquer um dos 7 caminhos. Os campos vêm na raiz de data, sem objeto em volta.
  • Hoje este evento é montado no instante em que o pedido nasce, antes de as linhas serem gravadas: total_cents, items_count e delivery_status chegam null, e currency também pode chegar null. Para o valor, busque GET /v1/orders/{id} ao receber o evento. order.status_changed e order.delivered já trazem os totais.
order.status_changedStatus do pedido alterado

Disparado quando o pedido passa a contar como venda (confirmado) ou deixa de contar (cancelado ou de volta a rascunho).

  • Sai quando o pedido passa a contar como venda ou deixa de contar: rascunho para confirmado, confirmado para cancelado e confirmado de volta para rascunho. De rascunho para cancelado não dispara.
  • Mudar a etapa do pedido no quadro não dispara este evento. Cancelar pela tela também marca a entrega como cancelled.
order.deliveredPedido entregue

Disparado quando a entrega do pedido passa a Entregue, inclusive na retirada pelo cliente.

  • Sai quando delivery_status passa a delivered, por qualquer caminho: a tela do pedido, uma etapa que marca a entrega, a ação Marcar entrega das Automações ou uma integração. Na retirada pelo cliente, também.
  • Se a entrega sair de delivered e voltar, sai de novo.
quote.acceptedOrçamento aceito

Disparado quando a proposta é aceita, pelo cliente no link ou registrada pela equipe como aceita por fora.

  • Sai tanto no aceite pelo cliente, no link da proposta, quanto no aceite registrado pela equipe. O corpo não diz por qual dos dois caminhos veio.
  • O pedido gerado chega também como order.created, com source igual a quote. Aceitar de novo não gera outro pedido nem outro evento.

Campos de data

O que cada objeto traz e os valores possíveis de cada campo.

Conversa data.conversation

Nos 4 eventos conversation.*.

id
Número da conversa.
status
open, pending, in_progress, resolved, snoozed ou spam.
priority
low, medium, high ou urgent.
channel_type
Canal mais recente da conversa: whatsapp (API oficial), whatsapp_web (conexão por QR Code), instagram, facebook (Messenger), telegram, email, webchat ou sms.
contact
id, name, email e phone do contato, ou null.
assigned_to
id, name e type (user, team ou ai_agent), ou null quando ninguém é responsável.
created_at, last_message_at, resolved_at
Datas em ISO 8601. resolved_at fica null enquanto a conversa não é resolvida.

Contato data.contact

Nos 3 eventos contact.*, com changed_fields no contact.updated e tag no contact.tag_added.

id
Número do contato.
name, email, phone
Como estão no cadastro. E-mail e telefone podem vir null.
company_name
Empresa do contato, ou null.
status
active, inactive ou blocked. Pode chegar null no contact.created (veja o evento acima).
source
Texto de origem do cadastro. Quando o contato nasce de uma mensagem, é o tipo do canal (whatsapp, instagram...); pela API, api ou o valor que a sua integração mandar; na ficha de pessoa jurídica, o campo "Origem do Lead" grava valores como site e indicacao. A origem medida pela atribuição fica em GET /v1/contacts/{id}.
tags
Todas as etiquetas atuais, como lista de {id, name, color}.
created_at, updated_at
Datas em ISO 8601.

Pedido data

Nos 3 eventos order.*, direto na raiz de data.

id
Número interno do pedido (o mesmo de GET /v1/orders/{id}).
number
Número exibido na tela, como PED-2026-0137.
status
draft (rascunho), confirmed (conta como venda) ou cancelled.
source
Caminho de origem: manual (pedido criado na tela), conversation (venda pela conversa), crm (negócio ganho no funil), quote (proposta aceita), integration (API ou integração) ou recurrence (ciclo de recorrência).
sold_at
Data da venda, em ISO 8601.
total_cents
Total em centavos, número inteiro: 248000 é R$ 2.480,00. Chega null no order.created.
currency
BRL. Pode chegar null no order.created.
items_count
Quantidade de linhas do pedido. Chega null no order.created.
contact_id
Contato da venda, ou null em venda sem cliente identificado.
delivery_status
pending, preparing, ready, shipped, in_transit, delivered ou cancelled. Chega null no order.created.

Proposta data

No quote.accepted, direto na raiz de data.

quote_id
Número interno da proposta.
number
Número exibido, como ORC-00042.
version
Versão aceita. Revisar uma proposta cria uma versão nova com o mesmo número.
order_id
Pedido gerado pelo aceite.
total_cents
Total aceito, em centavos, já com as escolhas do cliente nos itens opcionais e nos grupos "escolha uma".
contact_id
Contato da proposta.
accepted_by
Nome digitado pelo cliente no link ou informado pela equipe. Pode vir null.
O que não vem no webhook
  • Linhas do pedido: estão em GET /v1/orders/{id}. Pagamentos e parcelas não estão no webhook nem na API.
  • Dono da carteira, foto e origem de marketing do contato: estão em GET /v1/contacts/{id}.
  • Campos personalizados do contato: não estão no webhook nem na API.

Cabeçalhos

Em destaque, os três que importam para a implementação: X-Codewo-Event para rotear, X-Codewo-Delivery para deduplicar e X-Codewo-Signature para autenticar.

CabeçalhoExemploPara que serve
Content-Typeapplication/jsonCorpo em JSON, UTF-8.
User-AgentCodewo-Webhooks/1.0Identifica o Codewo no seu log.
X-Codewo-Eventorder.createdTipo do evento, igual ao type do corpo. Dá para rotear antes de ler o JSON.
X-Codewo-Delivery01M3MA8R8GH4J9K2M7N1P5Q8RSId do evento, igual ao id do corpo. É o mesmo em todas as tentativas automáticas: use como chave para não processar duas vezes.
X-Codewo-Signaturet=<unix>,v1=<hex>[,v1=<hex>]HMAC-SHA256 de {t}.{corpo}. Nas 24 h depois de trocar o segredo, chegam dois v1. Valide antes de processar.

Verificar a assinatura

Valide a assinatura antes de processar qualquer evento. Sem ela, qualquer pessoa que descubra o seu endereço consegue mandar um evento falso.

  • Calcule o HMAC-SHA256 de "{t}.{corpo}" com o segredo inteiro, incluindo o prefixo whsec_, e compare em hexadecimal.
  • Use o corpo cru, byte a byte. Não interprete e remonte o JSON antes: o Codewo envia acentos e barras sem escape, e qualquer mudança de formatação muda a assinatura.
  • Compare em tempo constante (hash_equals, timingSafeEqual, compare_digest), nunca com ==: a comparação comum vaza informação pelo tempo de resposta.
  • Recuse t com mais de 5 minutos de diferença. A assinatura prova a origem; o t impede que alguém reenvie um corpo antigo capturado. Cada tentativa é assinada de novo, com t novo, então a janela não recusa retentativas.
  • Aceite qualquer v1 que bater. Nas 24 h depois de trocar o segredo, o cabeçalho traz dois. Os trechos abaixo já tratam isso.
// Segredo completo, com o prefixo whsec_, lido de variável de ambiente
$secret = getenv('CODEWO_WEBHOOK_SECRET');
$header = $_SERVER['HTTP_X_CODEWO_SIGNATURE'] ?? '';
$body = file_get_contents('php://input'); // corpo cru, byte a byte

// Durante a rotação o cabeçalho traz vários "v1=": "t=<unix>,v1=<novo>,v1=<antigo>".
$timestamp = null;
$signatures = [];
foreach (explode(',', $header) as $part) {
    [$key, $value] = array_pad(explode('=', $part, 2), 2, null);
    if ($key === 't') {
        $timestamp = $value;
    } elseif ($key === 'v1' && $value !== null) {
        $signatures[] = $value;
    }
}

// Janela de 5 minutos contra o reenvio de um corpo antigo capturado
if ($timestamp === null || abs(time() - (int) $timestamp) > 300) {
    http_response_code(403);
    exit;
}

$expected = hash_hmac('sha256', $timestamp . '.' . $body, $secret);

// Aceita se QUALQUER "v1" bater (comparação em tempo constante)
$valid = false;
foreach ($signatures as $signature) {
    if (hash_equals($expected, $signature)) {
        $valid = true;
        break;
    }
}
if (! $valid) {
    http_response_code(403);
    exit;
}

// Idempotência: X-Codewo-Delivery (igual ao "id" do corpo) é o mesmo em todas
// as tentativas automáticas do evento. É a chave para deduplicar.
$deliveryId = $_SERVER['HTTP_X_CODEWO_DELIVERY'] ?? null;
// ... se já processou $deliveryId, responda 200 e pare aqui

$event = json_decode($body, true);
// ... guarde o evento numa fila e responda logo (limite de 10 s)
http_response_code(200);

Tentativas e pausa automática

Só conta como entregue a resposta 2xx em até 10 segundos. Qualquer outra resposta (4xx, 5xx), tempo esgotado ou falha de conexão gera nova tentativa, até 5 no total.

TentativaQuando
1Imediata
230 segundos depois da 1ª falha
35 minutos depois da 2ª falha
430 minutos depois da 3ª falha
52 horas depois da 4ª falha

A pausa conta as falhas seguidas do endereço, somando todos os eventos. Na 5ª, o endereço é pausado e quem o criou recebe um aviso no Codewo. Um evento que falha nas 5 tentativas, sem nenhuma entrega com sucesso no meio, pausa o endereço; com a conta movimentada, 5 eventos diferentes falhando em sequência também pausam, às vezes em poucos minutos. Qualquer entrega com sucesso zera a contagem.

Se as 5 tentativas de um evento falharem sem pausar o endereço (porque outras entregas deram certo no meio), o Codewo para de tentar aquele evento. Ele fica no histórico como falha, pronto para o botão Reenviar.

Enquanto está pausado, nada fica guardado para depois: eventos novos não são enfileirados e as retentativas pendentes são descartadas. Reativar zera a contagem, mas não reenvia o que se perdeu. Para recuperar, reative o endereço, use o botão Reenviar no histórico de entregas (elas ficam guardadas por 30 dias) e consulte pela API o que aconteceu durante a pausa.

Evento de teste

O botão Testar manda um corpo fictício, com ids 0 e _test: true dentro de data. Para os 4 eventos de venda, o teste ainda usa o formato de contato, e o contato de teste vem com status igual a lead, que não existe nos contatos reais: valide o seu leitor com os exemplos desta página ou com um pedido de verdade.

Repetições e ordem

O mesmo evento pode chegar mais de uma vez: basta o seu servidor processar e responder depois de 10 segundos, ou a conexão cair antes de a resposta chegar. O seu código precisa aguentar isso.

Deduplique pelo id

O id do corpo (igual a X-Codewo-Delivery) é o mesmo em todas as tentativas automáticas. Guardar os ids por 7 dias dá folga: as retentativas terminam cerca de duas horas e meia depois da primeira.

Reenviar gera id novo

Reenviar pelo histórico e Testar criam entregas novas, com id novo, de propósito. Se um processamento não pode rodar duas vezes nem nesse caso, combine também uma chave do próprio dado, como type e data.id do pedido.

Responda rápido

Confira a assinatura, guarde o evento numa fila e responda 2xx. Processamento pesado dentro da requisição estoura os 10 segundos e vira retentativa.

A ordem não é garantida

As entregas correm em paralelo, e uma retentativa pode chegar depois de um evento mais novo. Para saber o estado atual, compare as datas do próprio objeto (updated_at do contato, last_message_at da conversa) ou consulte a API.

Troca do segredo

Cada endereço tem o próprio segredo, whsec_ seguido de 48 caracteres, mostrado uma vez só. O Codewo o guarda cifrado, e não como hash, porque precisa dele para assinar cada envio.

Ao clicar em Rotacionar secret, um segredo novo é gerado e, por 24 horas, cada entrega sai assinada com os dois: t=<unix>,v1=<novo>,v1=<antigo>. É o tempo para trocar a variável de ambiente do seu servidor sem perder entregas. Depois disso, só o novo vale.

O segredo mora só no seu servidor

Nunca em página, aplicativo ou código que roda no navegador. Se ele aparecer em log, console, repositório ou chamado de suporte, rotacione na hora.

O que ainda não tem evento

Melhor saber agora do que procurar depois.

  • Mensagens. Não há evento de mensagem recebida nem enviada.
  • Negociações do funil. Entrar no funil, mudar de etapa, ganhar ou perder não geram webhook. Quando a negociação ganha vira pedido, chega o order.created.
  • Proposta, além do aceite. Proposta enviada, aberta, recusada ou vencendo existe como gatilho de Automações. Webhook, só o aceite.
  • Pagamentos e parcelas. Pagamento recebido, pedido quitado e parcela vencendo ou vencida existem como gatilho de Automações, não como webhook.
  • Etiqueta removida. Só a entrada de etiqueta tem evento.
  • Histórico importado de ERP. Na primeira carga de uma integração, pedidos e contatos antigos entram sem disparar webhook. O que chega depois, ao vivo, dispara normalmente.
  • Endereço sem HTTPS. Só endereços que começam com https:// são aceitos.
Para o que não tem evento, uma automação

Monte uma automação com o gatilho do fato (por exemplo "Pagamento recebido" ou "Negociação ganha") e o passo "Chamar um sistema externo", que envia a requisição que você montar, com método, cabeçalhos e corpo com variáveis. Esse passo não tem a assinatura nem as retentativas dos webhooks.

Ver as Automações

Perguntas frequentes sobre webhooks

Ligue o Codewo ao sistema que você já usa.

Teste grátis por 7 dias com webhooks e API liberados, sem cartão e sem cobrança automática no fim do teste.