Recadu Guia da API

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

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.