Webhooks e eventos
O Recadu avisa o seu sistema de tudo o que acontece (mensagem recebida, entregue, lida, instância caiu) com um POST assinado na URL que você cadastrar.
Cadastrar um destino
No painel, em Webhooks, ou pela API:
curl -X POST https://api.recadu.com.br/v1/webhook-endpoints \
-H "Authorization: Bearer $GW_API_KEY" -H "Content-Type: application/json" \
-d '{
"url": "https://meu-sistema.com.br/webhooks/whatsapp",
"event_types": ["message.received", "message.interactive_reply", "message.status_updated", "message.failed"],
"description": "Sistema de pedidos"
}'
A resposta traz o segredo (whsec_...) uma vez só. Guarde-o: é com ele que você confere a assinatura.
| Campo | |
|---|---|
url |
HTTPS, num endereço público (endereços internos são recusados) |
event_types |
Vazio: todos, menos instance.qr |
instance_ids |
Vazio: todas as instâncias da conta |
Até 10 destinos por conta. Para testar: POST /v1/webhook-endpoints/{id}/test manda um evento webhook.test.
O que chega
POST /webhooks/whatsapp HTTP/1.1
Content-Type: application/json
webhook-id: evt_0192c4...
webhook-timestamp: 1760364340
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{
"type": "message.received",
"timestamp": "2026-10-13T14:05:40Z",
"data": { ... }
}
Conferir a assinatura
O Recadu segue o padrão Standard Webhooks: webhook-signature é o HMAC-SHA256 de {webhook-id}.{webhook-timestamp}.{corpo}, em base64, com o prefixo v1,. Use a biblioteca oficial do padrão e confira sobre o corpo cru, antes de converter o JSON.
Python
# pip install standardwebhooks
import os
from standardwebhooks.webhooks import Webhook
wh = Webhook(os.environ["GW_WEBHOOK_SECRET"]) # whsec_...
@app.post("/webhooks/whatsapp")
async def receber(request: Request):
corpo = await request.body()
evento = wh.verify(corpo, dict(request.headers)) # erro se a assinatura não confere
fila.enqueue(evento) # processe depois
return {"ok": True}
Node.js
// npm install standardwebhooks
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.GW_WEBHOOK_SECRET); // whsec_...
app.post("/webhooks/whatsapp", express.raw({ type: "application/json" }), (req, res) => {
const evento = wh.verify(req.body, req.headers); // erro se a assinatura não confere
fila.add(evento);
res.sendStatus(204);
});
PHP
// composer require standard-webhooks/standard-webhooks
$wh = new \StandardWebhooks\Webhook(getenv('GW_WEBHOOK_SECRET'));
$evento = $wh->verify(file_get_contents('php://input'), getallheaders());
A biblioteca também recusa entregas com webhook-timestamp muito antigo, o que impede alguém de reenviar um evento capturado.
Trocar o segredo: POST /v1/webhook-endpoints/{id}/rotate-secret devolve um segredo novo. Por 24 h as entregas saem assinadas com os dois (separados por espaço em webhook-signature), então dá tempo de atualizar o seu sistema sem perder nada.
Entrega, ordem e repetição
- Responda
2xxem até 10 segundos. Grave o evento numa fila e processe depois; qualquer outra resposta, ou demora, conta como falha. - Novas tentativas depois de 10 s, 1 min, 5 min, 30 min, 2 h, 6 h e 12 h (cerca de 21 h no total). Depois disso a entrega fica como
failed. - Ordem por conversa: eventos da mesma conversa chegam na ordem. Enquanto um falha, os seguintes daquela conversa esperam; as outras conversas seguem.
- Pode chegar repetido (por exemplo, se a sua resposta se perdeu na rede). Use o
webhook-idpara ignorar o que você já processou. - Um destino que falha sem parar por 24 h é desativado. Depois de corrigir, reative com
PATCH /v1/webhook-endpoints/{id}{"active": true}.
Reenviar: veja o histórico em GET /v1/webhook-endpoints/{id}/deliveries?status=failed (com o código e o erro de cada tentativa) e reenvie uma entrega (POST /v1/webhook-deliveries/{id}/retry) ou todas as que falharam num período:
curl -X POST https://api.recadu.com.br/v1/webhook-endpoints/whe_.../retry-failed \
-H "Authorization: Bearer $GW_API_KEY" -H "Content-Type: application/json" \
-d '{"since": "2026-10-13T00:00:00Z"}'
Sem webhook, ou para conferir o que perdeu: GET /v1/events?type=message.received&since=... lista os eventos dos últimos 90 dias.
Eventos
Mensagens
| Evento | Quando |
|---|---|
message.received |
O contato mandou uma mensagem |
message.interactive_reply |
O contato respondeu a botões, lista, enquete ou com o número da opção (interativos) |
message.sent_from_device |
Alguém da equipe mandou uma mensagem pelo celular ou pelo WhatsApp Web do número |
message.sent |
Uma mensagem da API saiu (wa_message_id, rendered_as) |
message.failed |
Uma mensagem da API não saiu (error.code, veja mensagens) |
message.status_updated |
delivered ou read (played: true quando um áudio de voz foi ouvido) |
message.edited |
Uma mensagem foi editada (text novo) |
message.deleted |
Uma mensagem foi apagada para todos |
message.reaction |
Reação (emoji; vazio quando foi removida) |
message.received completo:
{
"type": "message.received",
"timestamp": "2026-10-13T14:05:40Z",
"data": {
"instance_id": "ins_...",
"message_id": "msg_...",
"wa_message_id": "3EB0C4...",
"chat": {"id": "5537999990000@s.whatsapp.net", "is_group": false, "phone": "5537999990000"},
"from": {"id": "5537999990000@s.whatsapp.net", "phone": "5537999990000", "name": "Maria"},
"from_me": false,
"timestamp": "2026-10-13T14:05:39Z",
"type": "text",
"text": "Oi, meu pedido ainda não chegou"
}
}
type pode ser text, image, video, audio, document, sticker, location, contact, poll, interactive ou other. Campos que aparecem conforme o caso: text (texto ou legenda), media, location, contacts, quoted_wa_message_id (a mensagem citada), mentions, is_forwarded.
Mídia recebida:
"media": {
"media_id": "med_...",
"mimetype": "audio/ogg; codecs=opus",
"size": 18234,
"sha256": "9f2c...",
"duration_s": 7,
"voice": true,
"url": "https://midia.recadu.com.br/..."
}
media.url é um link temporário (vale 24 h), gerado a cada entrega: baixe o arquivo logo ao receber o evento. Depois, GET /v1/media/{media_id} gera um link novo (até a mídia vencer, em 30 dias). Se o download do WhatsApp falhou, vem media.error no lugar do media_id.
Contatos com número escondido (@lid). O WhatsApp às vezes identifica o contato por um código (...@lid) em vez do telefone. Nesse caso chat.id e from.id terminam em @lid, e phone vem quando é conhecido. Para responder, use o chat.id como to.
Instâncias
| Evento | Quando | data |
|---|---|---|
instance.qr |
Um QR novo durante o pareamento (só se você assinar) | code, expires_in_s |
instance.paired |
O celular leu o QR ou digitou o código | jid, business_name, platform |
instance.connected |
Conectada | jid |
instance.disconnected |
Caiu (reconecta sozinha) | reason |
instance.logged_out |
O aparelho foi desconectado no celular; precisa parear de novo | reason |
instance.banned |
O WhatsApp bloqueou o número | code, expires_at (bloqueio temporário) |
Todos trazem instance_id.
Contatos
| Evento | Quando | data |
|---|---|---|
contact.opted_out |
O contato mandou SAIR (ou outra palavra de descadastro) | contact, keyword, message_id |
contact.opted_in |
O contato mandou VOLTAR | contact, keyword, message_id |
Veja descadastro.
Teste
webhook.test: mandado por POST /v1/webhook-endpoints/{id}/test ou pelo botão no painel.