Pular para o conteúdo
Páginas da documentação

Receber webhooks

Requisição, assinatura HMAC, deduplicação, reentrega e reenvio manual.

Abrir o Markdown

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 id do 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 o id. 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 createdAt do envelope para decidir qual estado é mais recente, com uma exceção: num envio, o accepted com meta.overridesAdminResolution: true vence o abandoned mesmo que este tenha createdAt maior.
  • 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.