CRPRO HubEntrar no painel

Como configurar um webhook do WhatsApp

Uma chamada cria o endpoint e devolve o segredo — uma única vez. O resto do trabalho é do seu lado: validar a assinatura do jeito certo. Este guia vai da criação até reenviar uma entrega que você perdeu.

O que você precisa

  • Uma chave de API (hub_pk_...) com o escopo webhooks:write para criar, testar, reenviar e rotacionar; webhooks:read para listar e consultar entregas. Crie em /painel/chaves.
  • Uma URL https pública. Loopback, link-local e faixas privadas de IP são recusados com 400 WEBHOOK_URL_NOT_ALLOWED — e um domínio que resolve para IP interno também. Para desenvolver na sua máquina, exponha a porta por um túnel com domínio público.
  • Um handler que responda 2xx em até 10 segundos e que consiga ler o corpo cru da requisição. Sem o corpo cru você não tem como verificar a assinatura — reserializar o JSON muda os bytes.

Criar o endpoint

Requisição
curl -X POST https://crprohub.com/api/v1/webhooks \
  -H "Authorization: Bearer hub_pk_EXEMPLO_NAO_REAL" \
  -H "Content-Type: application/json" \
  -d '{"name":"Endpoint principal","url":"https://exemplo.com/webhooks/crprohub","event_types":["message.received","message.status"]}'
Resposta
{
  "data": {
    "webhook": {
      "id": "b1c2d3e4-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
      "name": "Endpoint principal",
      "url": "https://exemplo.com/webhooks/crprohub",
      "events": ["message.received", "message.status"],
      "payload_mode": "normalized",
      "delivery_format": "native",
      "all_channels": true,
      "status": "active",
      "consecutive_failures": 0,
      "channel_ids": [],
      "created_at": "2026-08-21T12:00:00.000Z",
      "updated_at": "2026-08-21T12:00:00.000Z"
    },
    "secret": "whsec_EXEMPLO_NAO_REAL",
    "warning": "Guarde este segredo agora. Ele nao podera ser visualizado novamente."
  }
}

O campo event_types parece opcional e é o primeiro lugar onde as pessoas erram: o padrão é lista vazia. Um endpoint criado sem ele nasce ativo, aparece na listagem, passa no teste de URL — e recebe TODOS os eventos da organização. Assine pelo menos message.received e message.status. A lista completa de eventos está em webhooks.

Por padrão o endpoint recebe eventos de todos os canais da organização (all_channels: true). Para restringir, mande all_channels: false junto com channel_ids não vazio — a combinação inconsistente nos dois sentidos responde 400 INVALID_WEBHOOK.

O secret (whsec_...) só existe nesta resposta. Ele não é reemitido, não aparece no GET /api/v1/webhooks e não aparece no painel. Perdeu, o único caminho é rotacionar — o que invalida o anterior na hora. Grave em variável de ambiente ou cofre antes de fechar o terminal.

Dois campos definem o comportamento da entrega e vale entender a diferença agora, porque delivery_format não muda depois — para trocar, crie outro endpoint. payload_mode escolhe o que vai no corpo (normalized, o padrão, ou meta_raw com o payload bruto da Meta). delivery_format escolhe como esse corpo é assinado: native (padrão) ou evohub, este último só para quem está migrando do EvoHub e não pode mexer no handler.

Os headers de cada entrega

Headers
X-Hub-Delivery-Id: c2d3e4f5-6a7b-4c8d-9e0f-1a2b3c4d5e6f
X-Hub-Timestamp: 1766000000
X-Hub-Signature-Version: v2
X-Hub-Signature-256: sha256=9f8b1c2d3e4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f809a1b
  • X-Hub-Delivery-Id — UUID desta tentativa. Deduplique por ele: uma retentativa reenvia o mesmo id, e o seu handler vai ver o mesmo evento mais de uma vez.
  • X-Hub-Timestamp — o instante do envio, em segundos desde a época Unix.
  • X-Hub-Signature-Version — v2 no formato nativo, evohub-v1 no de compatibilidade. Leia este header antes de calcular o HMAC: os dois formatos assinam conteúdos diferentes.
  • X-Hub-Signature-256 — a assinatura HMAC-SHA256, no formato sha256=HEX.

Verificar a assinatura

Este é o ponto onde a integração quebra por inteiro, e o erro é sempre o mesmo. No formato nativo a assinatura não cobre só o corpo: ela cobre `${timestamp}.${delivery_id}.${corpo}`, com os valores exatos dos headers daquela entrega. Quem calcula HMAC(corpo) vê 100% das entregas falharem na verificação — não algumas, todas.

Amarrar a assinatura ao timestamp e ao delivery id é de propósito: impede que um corpo capturado seja transplantado para outra entrega.

verificar-assinatura.js
import { createHmac, timingSafeEqual } from 'node:crypto'
const timestamp = request.headers['x-hub-timestamp']
const deliveryId = request.headers['x-hub-delivery-id']
const signed = `${timestamp}.${deliveryId}.${rawBody}`
const expected = createHmac('sha256', process.env.WEBHOOK_SECRET).update(signed).digest('hex')
const received = request.headers['x-hub-signature-256']?.replace('sha256=', '')
const valid = !!received &&
  /^[0-9a-f]{64}$/.test(received) &&
  timingSafeEqual(Buffer.from(received, 'hex'), Buffer.from(expected, 'hex'))

Duas linhas desse trecho não são estilo, são requisito. A comparação usa timingSafeEqual e nunca === — uma comparação de string comum vaza o tempo de execução por posição de byte, o que dá a um atacante um caminho para descobrir a assinatura correta por tentativa e erro. Em Python o equivalente é hmac.compare_digest; em PHP, hash_equals.

E o /^[0-9a-f]{64}$/ vem antes do Buffer.from por um motivo prático: uma assinatura de 64 caracteres com conteúdo não-hexadecimal produz um buffer mais curto, e aí timingSafeEqual lança RangeError em vez de devolver false. Sem a validação, um header malformado derruba o seu handler em vez de ser rejeitado.

Num endpoint evohub (X-Hub-Signature-Version: evohub-v1) a assinatura cobre somente o corpo cru. Os headers de delivery id e timestamp continuam chegando e continuam valendo para deduplicar, mas ficam fora do HMAC. O trecho correspondente está em webhooks. Em integração nova, prefira native.

Testar a entrega

Não espere uma mensagem real chegar para descobrir que o handler está errado. O endpoint de teste enfileira um evento sintético webhook.test, que percorre o mesmo caminho de qualquer outra entrega e respeita o payload_mode configurado.

Requisição
curl -X POST https://crprohub.com/api/v1/webhooks/WEBHOOK_UUID/test \
  -H "Authorization: Bearer hub_pk_EXEMPLO_NAO_REAL"
Resposta
{
  "data": {
    "event_id": "d3e4f5a6-7b8c-4d9e-0f1a-2b3c4d5e6f7a",
    "delivery_id": "c2d3e4f5-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
    "status": "pending"
  }
}

A resposta é 202: o evento foi enfileirado, não entregue. Guarde o delivery_id e confira o desfecho em deliveries. São 10 testes por hora por endpoint.

Só endpoint active aceita teste — pausado ou desabilitado responde 404. Se você acabou de bater no limite de falhas e o endpoint pausou sozinho, reative primeiro com PATCH /api/v1/webhooks/{id} mandando {"status":"active"}.

Ver as tentativas

Requisição
curl https://crprohub.com/api/v1/webhooks/WEBHOOK_UUID/deliveries \
  -H "Authorization: Bearer hub_pk_EXEMPLO_NAO_REAL"
Resposta
{
  "data": {
    "deliveries": [
      {
        "id": "c2d3e4f5-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
        "event": "message.status",
        "status": "succeeded",
        "attempts": 1,
        "next_attempt_at": "2026-08-21T12:00:05.000Z",
        "last_http_status": 200,
        "last_error_code": null,
        "delivered_at": "2026-08-21T12:00:05.000Z",
        "created_at": "2026-08-21T12:00:00.000Z"
      }
    ]
  }
}

Traz as 50 entregas mais recentes do endpoint, mais recente primeiro. A rota não tem paginação — é uma janela de diagnóstico, não um histórico completo. Para investigar mais fundo, cruze com /painel/logs.

O status de cada entrega é pending, processing, retry, succeeded, dead ou canceled. Quando algo está errado, os campos que respondem “por quê” são last_http_status e last_error_code: um 401 ali costuma ser assinatura calculada errado, e um attempts alto com last_http_status: null costuma ser timeout ou DNS.

Reenviar um evento perdido

Seu servidor caiu, ou você corrigiu a verificação depois de rejeitar entregas válidas. O replay reenvia uma entrega específica pelo id que veio de deliveries.

Requisição
curl -X POST https://crprohub.com/api/v1/webhooks/WEBHOOK_UUID/replay/DELIVERY_UUID \
  -H "Authorization: Bearer hub_pk_EXEMPLO_NAO_REAL"
Resposta
{
  "data": {
    "delivery_id": "c2d3e4f5-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
    "status": "pending"
  }
}

Só entregas já finalizadas podem ser reenviadas — succeeded, dead ou canceled. Uma entrega ainda em pending, processing ou retry responde 404, e isso não é bug: ela já está na fila, e reenviar seria duplicar. O replay devolve a entrega para pending; o resultado real aparece depois em deliveries.

Um evento reenviado chega com o mesmo X-Hub-Delivery-Id da entrega original. Se o seu handler deduplica por esse id — e ele deve — um replay de entrega succeeded vai ser descartado do seu lado. Reenvie o que ficou dead, ou limpe o registro de deduplicação antes.

Retentativa e pausa automática

Uma entrega que falha — timeout, resposta fora de 2xx, erro de conexão — é reenviada até nove vezes. Depois de 20 falhas consecutivas o endpoint pausa sozinho.

A pausa é uma proteção, não um castigo: um endpoint quebrado que continuasse recebendo acumularia fila indefinidamente e atrasaria as entregas dos endpoints saudáveis. O campo consecutive_failures no GET /api/v1/webhooks/{id} é o contador — vale monitorar antes de ele chegar a 20, porque uma vez pausado nada chega até alguém reativar.

Reative com PATCH /api/v1/webhooks/{id} mandando {"status":"active"}, ou em /painel/webhooks. Corrija o que impedia o recebimento primeiro: reativar sem corrigir só gasta as 20 falhas de novo. O mesmo PATCH serve para pausar de propósito durante uma janela de manutenção, com {"status":"paused"}.

Rotacionar o segredo

Requisição
curl -X POST https://crprohub.com/api/v1/webhooks/WEBHOOK_UUID/rotate-secret \
  -H "Authorization: Bearer hub_pk_EXEMPLO_NAO_REAL"
Resposta
{
  "data": {
    "secret": "whsec_EXEMPLO_NAO_REAL",
    "warning": "O segredo anterior foi invalidado. Guarde este valor agora."
  }
}

Não há período de convivência entre os dois segredos: o anterior é invalidado imediatamente, e toda assinatura calculada com ele para de bater a partir desta chamada. Na prática isso significa uma janela de falhas entre a rotação e o deploy do novo valor — planeje a ordem, ou aceite reenviar as entregas perdidas depois com replay.

O novo secret aparece só nesta resposta, pela mesma razão do segredo original. Se você perder o valor, o único caminho é rotacionar outra vez.

Um id de webhook de outra organização responde 404, nunca 403 — 403 confirmaria que o recurso existe. Toda resposta traz x-request-id; guarde no seu log. O contrato completo dos endpoints está em referência de webhooks, e a tabela de códigos em erros.