Webhooks de saída (integrar seu sistema)
Um webhook de saída é um endereço do seu sistema que a plataforma chama toda vez que algo acontece na conversa. É assim que você grava o atendimento inteiro no seu próprio banco de dados — por exemplo, num SaaS de Ordem de Serviço.
Cadastrar o destino
Abra a seção Webhooks
No agent, clique em Webhooks no menu lateral (“Envie as conversas para seu sistema”).
Escolha o que a conexão faz
- Espelho de eventos — o que esta página descreve: uma cópia de cada mensagem vai para o seu sistema, que não precisa responder nada.
- Ação na conversa — o agent chama o seu sistema durante o atendimento e usa a resposta. Está em Agents trabalhando juntos.
O tipo não muda depois de criado.
Preencha o destino
- Nome — como você quer chamar esse destino (ex.:
SaaS de OS). - URL — o endereço HTTPS do seu sistema que vai receber os avisos.
- Secret — uma senha com pelo menos 16 caracteres. É com ela que você confirma que o aviso veio mesmo da plataforma.
Escolha os eventos
Os dois eventos vêm marcados por padrão:
- Mensagens do cliente (
conversation.inbound) - Respostas do agent (
hermes.output)
Salve
O destino aparece na lista com a etiqueta Enviando. Você pode editar, pausar e remover depois.
O secret é gravado e nunca mais exibido. Ao editar o destino, o campo aparece como
•••••••• (manter atual): deixar assim mantém o secret atual, e preencher troca por um
novo. Guarde a sua cópia no cofre de senhas do seu sistema.
Os eventos
| Evento | Quando acontece | O que vem |
|---|---|---|
conversation.inbound | Quando o lote de mensagens do cliente é levado para o agent responder | provider e a lista de mensagens do cliente, cada uma com type, content e received_at |
hermes.output | Quando o agent produz uma resposta | O conteúdo da resposta produzida pelo agent |
conversation.operator | Quando um atendente humano digita no número — nos dois canais (no oficial/Datafy, depende da assinatura smb_message_echoes no painel) | A fala do atendente, para o seu sistema registrar o atendimento humano |
conversation.handoff | Quando a conversa é entregue a uma pessoa — pelo próprio agent ou pela API de pausa | Quem entregou, o canal, a etiqueta aplicada e o motivo |
Os checkboxes da tela cobrem os dois primeiros. O conversation.operator e o
conversation.handoff existem e são disparados, mas hoje só podem ser assinados pela API
(incluindo o nome do evento na lista do destino, ou usando * para receber tudo).
Destinos criados antes de um evento novo existir não passam a receber esse evento sozinhos. A assinatura é sempre explícita: edite o destino e marque o evento.
O que chega no seu sistema
A plataforma faz um POST com JSON e três cabeçalhos:
POST /seu/endpoint HTTP/1.1
Content-Type: application/json
X-Gateway-Event: conversation.inbound
X-Gateway-Event-Id: 5b7c1f8d-8c3f-4a2c-8a1b-123456789abc
X-Gateway-Signature: sha256=ASSINATURA_EM_HEXADECIMALO corpo sempre traz o mesmo envelope: event_id, event_type, source, org_id,
agent_id, conversation_id, created_at e o payload do evento.
Exemplo de conversation.handoff (a conversa acabou de ser entregue a uma pessoa):
{
"event_id": "5b7c1f8d-8c3f-4a2c-8a1b-123456789abc",
"event_type": "conversation.handoff",
"source": "hermes",
"org_id": "<ID_DA_ORG>",
"agent_id": "<ID_DO_AGENT>",
"conversation_id": "5511999999999@s.whatsapp.net",
"created_at": "2026-09-10T13:45:10.000Z",
"payload": {
"by": "agent:<ID_DO_AGENT>",
"provider": "uazapi",
"label": "atendimento humano",
"label_status": "applied",
"reason": "cliente pediu para falar com uma pessoa"
}
}by diz quem entregou: agent:<id> quando foi o próprio agent, ou a credencial que chamou
a API de pausa. label_status vale applied, not_found (a etiqueta não existe na conta),
not_supported (canal sem etiqueta, ou nenhuma pedida) ou failed. Numa pausa pela API,
label e reason vêm null.
Validar a assinatura (obrigatório)
A assinatura é um HMAC SHA-256 do corpo bruto da requisição, usando o seu secret. Valide antes de interpretar ou salvar qualquer coisa — sem isso, qualquer um que descubra a sua URL consegue gravar dados falsos no seu sistema.
Exemplo genérico em Node.js:
import { createHmac, timingSafeEqual } from "node:crypto";
// SEU_SECRET vem do seu cofre de segredos — nunca escrito no código.
const secret = process.env.GATEWAY_WEBHOOK_SECRET;
export async function receberWebhook(request) {
const corpoBruto = await request.text(); // o corpo CRU, antes de qualquer parse
const recebida = request.headers.get("x-gateway-signature") ?? "";
const esperada =
"sha256=" + createHmac("sha256", secret).update(corpoBruto).digest("hex");
const valida =
recebida.length === esperada.length &&
timingSafeEqual(Buffer.from(recebida), Buffer.from(esperada));
if (!valida) return new Response("assinatura inválida", { status: 401 });
const evento = JSON.parse(corpoBruto);
// ... aqui você grava no seu banco
return new Response("ok", { status: 200 });
}Três detalhes que costumam quebrar a validação:
- Use o corpo bruto. Se o seu framework já converteu para objeto e você reserializar, um único espaço diferente muda a assinatura.
- Compare em tempo constante (
timingSafeEqual), não com===. - Responda
2xxsó depois de aceitar o evento.
Caso de uso: gravar a conversa no seu SaaS
Assinando os dois eventos, o seu sistema recebe o diálogo completo — o que o cliente
falou (conversation.inbound) e o que o agent respondeu (hermes.output) — e pode
montar a timeline do atendimento na ficha da Ordem de Serviço.
Roteiro sugerido para o seu endpoint:
- Validar a assinatura.
- Ignorar o evento se o
event_idjá tiver sido processado (idempotência). - Localizar/criar o atendimento pelo
conversation_id. - Gravar a mensagem com a data de
created_at. - Responder
200.
Guarde o event_id de tudo que você processar. A plataforma tenta entregar duas
vezes, com 10 segundos de limite por tentativa — se a primeira demorar demais e a
segunda chegar, sem idempotência você grava a mesma mensagem duas vezes.
Problemas comuns
| Sintoma | Causa provável | O que fazer |
|---|---|---|
| “Preencha o nome, a URL HTTPS, um secret de 16 caracteres e ao menos um evento.” | Algum campo faltando ou secret curto demais | Complete o formulário — a mensagem aparece no próprio card |
| O destino não recebe nada | Está pausado, ou o evento não está marcado | Confira a etiqueta (Enviando × Pausado) e os eventos do destino |
| Recebo só metade da conversa | Só um dos dois eventos está assinado | Marque Mensagens do cliente e Respostas do agent |
| Assinatura sempre inválida | Você validou o corpo já convertido, e não o corpo bruto | Valide sobre o texto cru da requisição |
| Mensagens duplicadas no meu banco | Sem controle de idempotência | Guarde e confira o event_id |
| Perdi o secret | Ele nunca é exibido de novo | Edite o destino e grave um secret novo dos dois lados |