Tema
Webhooks de eventos
O WayChat manda um POST para a sua URL quando algo acontece: mensagem recebida, conversa encerrada, contato criado, lead que marcou visita, WhatsApp que caiu e muitos outros. Configure em Configurações → API → Criar webhook. Os webhooks fazem parte do plano (módulo Webhooks).
Como é um envio
http
POST /sua-url
Content-Type: application/json
User-Agent: WayChat-Webhooks/1.0
X-WayChat-Evento: mensagem.recebida
X-WayChat-Entrega-Id: 3f2a9c1e-5b7d-4e2a-9c1f-8d6e4b2a7c10
X-WayChat-Tentativa: 1
X-WayChat-Timestamp: 1792761600
X-WayChat-Assinatura: sha256=5f1c…json
{
"id": "6b0c3f0e-8a51-4d1e-9f4e-0c2b7a9d1e55",
"evento": "mensagem.recebida",
"criado_em": "2026-10-30T14:02:12.000Z",
"dados": {
"conversa": { "id": 4821, "protocolo": 104821, "status": "aberta", "setor": { "id": 2, "nome": "Suporte técnico" } },
"contato": { "id": 917, "nome": "Maria Souza", "telefone": "+5544999998888" },
"mensagem": { "id": 99120, "direcao": "entrada", "texto": "Meu site está fora do ar" }
}
}| Cabeçalho | O que é |
|---|---|
X-WayChat-Evento | Nome do evento (o mesmo de evento no corpo) |
X-WayChat-Entrega-Id | Id desta entrega (evento + webhook). Igual em todas as tentativas e reenvios |
X-WayChat-Tentativa | 1 na primeira tentativa, 2 na segunda… |
X-WayChat-Timestamp | Hora do envio (segundos desde 1970, UTC) |
X-WayChat-Assinatura | sha256= + HMAC-SHA256 de timestamp + "." + corpo cru com o segredo do webhook |
X-WayChat-Assinatura-Anterior | Só nas 24 h depois de trocar o segredo: a mesma assinatura feita com o segredo antigo |
X-WayChat-Reenvio | 1 quando alguém da equipe reenviou a entrega pelo painel |
A lista completa de eventos, com um exemplo de corpo para cada um, está no Catálogo de eventos.
Conferir a assinatura
Cada webhook tem um segredo (whsec_...), mostrado uma vez ao criar. Para cada envio:
- Recuse se
X-WayChat-Timestampestiver a mais de 5 minutos da sua hora (evita repetição de um envio antigo). - Calcule
sha256=+ HMAC-SHA256 detimestamp + "." + corpo crucom o segredo. Use o corpo exatamente como chegou, antes de converter o JSON. - Aceite se for igual a
X-WayChat-Assinatura. Durante a troca de segredo, aceite também se for igual aX-WayChat-Assinatura-Anteriorcalculada com o segredo antigo (veja Trocar o segredo). - Compare em tempo constante.
js
import { createHmac, timingSafeEqual } from 'node:crypto'
// segredos: [segredoNovo, segredoAntigo] (o antigo só durante a troca; pode ser só [segredo])
export function assinaturaValida(headers, corpoCru, segredos) {
const ts = String(headers['x-waychat-timestamp'] || '')
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false
const recebidas = [headers['x-waychat-assinatura'], headers['x-waychat-assinatura-anterior']].filter(Boolean).map(String)
return segredos.filter(Boolean).some((segredo) => {
const esperada = Buffer.from('sha256=' + createHmac('sha256', segredo).update(`${ts}.${corpoCru}`).digest('hex'))
return recebidas.some((r) => r.length === esperada.length && timingSafeEqual(Buffer.from(r), esperada))
})
}
// Express: guarde o corpo cru
// app.post('/waychat', express.raw({ type: 'application/json' }), (req, res) => {
// if (!assinaturaValida(req.headers, req.body.toString('utf8'), [process.env.WAYCHAT_SEGREDO, process.env.WAYCHAT_SEGREDO_ANTIGO])) return res.sendStatus(401)
// const evento = JSON.parse(req.body)
// res.sendStatus(200)
// })php
<?php
// $segredos: [segredoNovo, segredoAntigo] (o antigo só durante a troca)
function assinaturaValida(array $segredos): bool {
$ts = $_SERVER['HTTP_X_WAYCHAT_TIMESTAMP'] ?? '';
if (!ctype_digit($ts) || abs(time() - (int) $ts) > 300) return false;
$corpo = file_get_contents('php://input');
$recebidas = array_filter([$_SERVER['HTTP_X_WAYCHAT_ASSINATURA'] ?? '', $_SERVER['HTTP_X_WAYCHAT_ASSINATURA_ANTERIOR'] ?? '']);
foreach (array_filter($segredos) as $segredo) {
$esperada = 'sha256=' . hash_hmac('sha256', $ts . '.' . $corpo, $segredo);
foreach ($recebidas as $r) if (hash_equals($esperada, $r)) return true;
}
return false;
}
if (!assinaturaValida([getenv('WAYCHAT_SEGREDO'), getenv('WAYCHAT_SEGREDO_ANTIGO')])) { http_response_code(401); exit; }
$evento = json_decode(file_get_contents('php://input'), true);
http_response_code(200);python
import hashlib, hmac, time
def assinatura_valida(headers, corpo_cru: bytes, segredos) -> bool:
"""segredos: [segredo_novo, segredo_antigo] (o antigo só durante a troca)"""
ts = headers.get('X-WayChat-Timestamp', '')
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
return False
recebidas = [h for h in (headers.get('X-WayChat-Assinatura'), headers.get('X-WayChat-Assinatura-Anterior')) if h]
for segredo in filter(None, segredos):
esperada = 'sha256=' + hmac.new(segredo.encode(), ts.encode() + b'.' + corpo_cru, hashlib.sha256).hexdigest()
if any(hmac.compare_digest(esperada, r) for r in recebidas):
return True
return False
# Flask:
# @app.post('/waychat')
# def waychat():
# if not assinatura_valida(request.headers, request.get_data(), [os.environ['WAYCHAT_SEGREDO'], os.environ.get('WAYCHAT_SEGREDO_ANTIGO')]):
# abort(401)
# evento = request.get_json()
# return '', 200Trocar o segredo
Em Editar → Trocar segredo, o WayChat gera um segredo novo e mostra uma vez.
- O novo passa a assinar na hora (
X-WayChat-Assinatura). - O antigo continua valendo por 24 horas: nesse período, cada envio leva também
X-WayChat-Assinatura-Anterior, feita com o segredo antigo. Assim você atualiza o sistema que recebe sem perder eventos. - Se o segredo vazou, marque Encerrar o segredo antigo agora: o antigo para de assinar na hora.
Entregas e novas tentativas
- Responda com qualquer status
2xxem até 10 segundos. Redirecionamentos (3xx) contam como falha: use o endereço final. - Se falhar (erro de rede, tempo esgotado ou status fora de
2xx), o WayChat tenta de novo até 6 vezes no total, com espera crescente: 30 s, 1 min, 2 min, 4 min e 8 min (cerca de 15 minutos no total). - Depois de 10 falhas seguidas, os admins recebem um e-mail. Depois de 50 falhas seguidas, o webhook é desligado por falhas. Corrija o endereço e clique em Reativar.
- Só endereços
httpspúblicos são aceitos (nada de rede interna).
Idempotência
O mesmo evento pode chegar mais de uma vez: a sua resposta se perdeu no caminho, demorou mais de 10 s, ou alguém reenviou pelo painel. Para processar cada evento uma vez só:
- Guarde o
X-WayChat-Entrega-Id(ou oiddo corpo) dos eventos que você já processou e ignore os repetidos. - Os dois são iguais em todas as tentativas e nos reenvios.
X-WayChat-Tentativamostra qual tentativa é. - A ordem de chegada não é garantida. Use
criado_emquando a ordem importar.
Filtros
Cada webhook pode receber só uma parte dos eventos:
| Filtro | Recebe só quando |
|---|---|
| Canais | o evento é de um dos canais escolhidos |
| Setores | a conversa está num dos setores escolhidos |
| Etiquetas | o contato (ou a conversa) tem pelo menos uma das etiquetas |
- Filtro vazio = tudo.
- O filtro só vale para eventos que têm aquela informação. Um
ticket.criadonão tem canal, então chega mesmo com filtro de canal. Uma conversa sem setor não passa num filtro de setor. - Conversas e mensagens têm canal, setor e etiquetas. Contatos e leads têm etiquetas. Campanhas e canais têm canal.
Cabeçalhos próprios
Até 10 cabeçalhos por webhook, por exemplo Authorization: Bearer … ou X-Api-Key, para o seu sistema reconhecer o WayChat.
- Os valores ficam cifrados no banco e não aparecem de novo na tela nem no registro de envios (aparecem como
••••). - Nomes com letras, números e hífen. Não dá para usar
Host,Content-Length,Content-Type,Connection,Transfer-Encoding,User-Agente outros de conexão, nem nada que comece comX-WayChat-. - Para trocar um valor, digite o novo. Deixar em branco mantém o atual.
Ver, reenviar, pausar e testar
- Envios mostra cada tentativa: evento, resultado, status, tempo, o corpo enviado, os cabeçalhos e o começo (2 KB) da resposta. Filtre por evento e por sucesso ou falha. Ficam os últimos 200 por webhook, por até 30 dias.
- Reenviar manda de novo uma entrega (mesmo corpo e mesmo
X-WayChat-Entrega-Id, comX-WayChat-Reenvio: 1). - Reenviar falhas reenvia, depois de confirmar, todas as entregas que falharam num período (até 31 dias), no máximo 500 por vez.
- Pausar para os envios sem apagar o webhook. Eventos que acontecem enquanto ele está pausado não ficam guardados. Retomar volta a enviar.
- Enviar teste manda na hora um exemplo realista do evento que você escolher, com
"teste": trueno corpo.
Receitas
n8n
- Crie um workflow com o nó Webhook (método
POST, Respond: Immediately). Copie a Production URL. - No WayChat, crie o webhook com essa URL e os eventos que quer. Clique em Enviar teste e veja o exemplo chegar no n8n.
- Para conferir a assinatura, ligue Raw Body no nó Webhook e use um nó Code com a função de Node.js acima (o segredo em uma credencial ou variável de ambiente).
- Use um nó Switch no campo
body.eventopara separar cada evento.
Make
- Crie um cenário com Webhooks → Custom webhook e copie o endereço.
- Crie o webhook no WayChat com esse endereço e clique em Enviar teste: o Make aprende a estrutura com o exemplo (clique em Redetermine data structure se precisar).
- Use um Router com filtros em
eventopara cada caminho. Para exigir uma chave, crie um cabeçalho próprio (ex.:X-Api-Key) e confira no primeiro módulo.
Zapier
- Crie um Zap com o gatilho Webhooks by Zapier → Catch Raw Hook (ou Catch Hook) e copie a URL.
- Crie o webhook no WayChat com essa URL e clique em Enviar teste. No Zapier, clique em Test trigger.
- Use Filter by Zapier em
eventopara seguir só com os eventos que interessam. Com Catch Raw Hook você recebe os cabeçalhos e pode conferir a assinatura num passo Code by Zapier.