Receber webhooks
Requisição, assinatura HMAC, deduplicação, reentrega e reenvio manual.
Requisição
O hub faz POST na URL cadastrada, com content-type: application/json e estes headers:
| Header | Conteúdo |
|---|---|
x-hub-event-id |
Id do evento (igual ao id do envelope) |
x-hub-timestamp |
Unix timestamp em segundos do momento do envio desta tentativa |
x-hub-signature |
sha256= seguido do HMAC-SHA256 em hex |
user-agent |
MatraMetaHub/1.0 |
Corpo (envelope):
{
"id": "cm...",
"type": "whatsapp.message.received",
"createdAt": "2026-10-22T15:00:00.000Z",
"workspace": { "id": "...", "externalId": "cliente-123" },
"connectionId": null,
"data": {},
"meta": null
}
connectionId e meta podem ser null. O formato de data depende do type: ver Eventos de conexão e Eventos do WhatsApp.
Assinatura
A assinatura cobre o timestamp e o corpo exatamente como chegaram:
x-hub-signature = "sha256=" + hex(HMAC_SHA256(secret, `${x-hub-timestamp}.${rawBody}`))
Calcule sobre o corpo cru (os bytes recebidos), antes de qualquer JSON.parse: reserializar muda espaços e ordem de chaves e a assinatura deixa de bater.
import { createHmac, timingSafeEqual } from 'node:crypto'
const TOLERANCE_SECONDS = 5 * 60
// rawBody: o corpo cru da requisição, como string
export const verifyHubWebhook = (
secret: string,
headers: { timestamp: string; signature: string },
rawBody: string,
nowSeconds = Math.floor(Date.now() / 1000),
): boolean => {
const timestamp = Number(headers.timestamp)
if (!Number.isInteger(timestamp)) return false
// rejeita replay: timestamp fora de 5 minutos, para mais ou para menos
if (Math.abs(nowSeconds - timestamp) > TOLERANCE_SECONDS) return false
const expected = `sha256=${createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')}`
const received = Buffer.from(headers.signature)
const computed = Buffer.from(expected)
// timingSafeEqual lança se os tamanhos diferem
return (
received.length === computed.length && timingSafeEqual(received, computed)
)
}
Passe headers['x-hub-timestamp'] e headers['x-hub-signature'] para a função. Assinatura inválida: responda 401 e não processe.
O que o seu receptor precisa tratar
- Responda 2xx rápido e processe depois. O hub espera no máximo 10 segundos por tentativa; estourou, conta como falha (
timeout) e a entrega será repetida. Grave o evento numa fila ou tabela e devolva 200 antes de qualquer trabalho pesado. - Não use redirect. O hub não segue redirecionamentos: um 3xx conta como falha. Cadastre a URL final.
- Deduplique pelo
iddo envelope. A entrega é "pelo menos uma vez": se o seu 2xx se perder no caminho, o hub reenvia o mesmo evento, e o reenvio manual também repete oid. Guarde os ids processados e ignore repetidos. - A ordem não é garantida. Eventos chegam em tentativas independentes: um evento antigo em retry pode chegar depois de um mais novo. Use
createdAtdo envelope para decidir qual estado é mais recente, com uma exceção: num envio, oacceptedcommeta.overridesAdminResolution: truevence oabandonedmesmo que este tenhacreatedAtmaior. - Qualquer status 2xx é sucesso. Qualquer outro status, erro de rede ou timeout agenda nova tentativa. O corpo da sua resposta é descartado sem ser lido.
Reentrega automática
Falhou, o hub tenta de novo com estes intervalos após a falha anterior:
| Tentativa | Espera antes |
|---|---|
| 2 | 1 min |
| 3 | 5 min |
| 4 | 15 min |
| 5 | 1 h |
| 6 | 3 h |
| 7 | 6 h |
| 8 | 12 h |
São 8 tentativas ao todo (a inicial e 7 retries), cerca de 22 horas. Esgotadas, a entrega fica failed e deixa de ser tentada; reenvie por POST /v1/events/:deliveryId/redeliver depois de corrigir o receptor. Se o endpoint for desativado depois da criação do evento, a entrega falha de imediato com endpoint_inactive, sem tentativa.
Os eventos e as entregas são apagados do hub 30 dias após a criação (exceto os que ainda têm entrega pending). Reenvie antes disso.
URL do endpoint
A equipe Matra valida a URL ao cadastrá-la e o hub confere de novo a cada envio:
- só
https://(http://apenas em desenvolvimento local, nunca em produção); - sem credenciais na URL (
https://user:senha@...); - o endereço resolvido não pode ser privado, link-local, CGNAT, "qualquer endereço", reservado ou multicast (
10/8,172.16/12,192.168/16,169.254/16,100.64/10,0/8,192.0.0/24,198.18/15,224/4,240/4,fc00::/7,fe80::/10,ff00::/8,100::/64,2001:db8::/32), nem um prefixo IPv6 que embute IPv4 (64:ff9b::/96,2002::/16). Loopback só é aceito em desenvolvimento.
O receptor precisa estar acessível pela internet pública.
Entregas e reenvio
O hub registra uma entrega para cada evento emitido para o seu app, desde que o app tenha um endpoint de webhook ativo. A URL do endpoint e o segredo de assinatura são cadastrados pela equipe Matra (o segredo aparece em claro só na criação ou na rotação).
| Método e rota | O que faz | Respostas |
|---|---|---|
GET /v1/events |
Lista as entregas do app. Filtros: status (pending, delivered, failed) e type |
200 |
POST /v1/events/:deliveryId/redeliver |
Reenvia uma entrega | 202, 404 |
Cada item da listagem:
{
"id": "...",
"eventId": "...",
"type": "connection.created",
"workspaceId": "...",
"status": "failed",
"attempts": 8,
"lastStatusCode": 500,
"nextAttemptAt": null,
"deliveredAt": null,
"createdAt": "2026-10-22T15:00:00.000Z"
}
id é o da entrega; eventId é o do evento (o mesmo id do envelope e do header x-hub-event-id).
O reenvio zera as tentativas, volta a entrega para pending e a enfileira de novo, mesmo se já estava delivered. Responde 202 { "id": "...", "status": "pending" }. Entrega de outro app responde 404.