Webhook contract
Hub envia webhooks pra webhook_url da app sempre que algo relevante acontece (mensagem chega, chip cai, etc). Esta pagina documenta o contrato exato — headers, payload, seguranca, retry.
Anatomia de uma delivery
Method e URL
POST {webhook_url da app}webhook_url configurada em POST /v1/apps ou via PATCH /v1/apps/:id. Hub nao segue redirects (3xx) — defina a URL final.
Headers
Content-Type: application/jsonUser-Agent: WppHub/1.0X-Hub-Event: message.receivedX-Hub-Delivery: 01HZTQDELIVERY01...X-Hub-Timestamp: 1714200000X-Hub-Signature: sha256=<hex>| Header | Notas |
|---|---|
User-Agent | fixo WppHub/1.0 (override via env WEBHOOK_USER_AGENT) |
X-Hub-Event | nome do evento — ver Eventos |
X-Hub-Delivery | ULID unico por delivery — usar pra idempotency no consumer |
X-Hub-Timestamp | Unix seconds do envio |
X-Hub-Signature | HMAC SHA-256 do body bruto com webhook_secret da app |
Body
{ "event": "message.received", "timestamp": "2026-04-30T12:34:56.789Z", "data": { ... } // varia por evento}event repete o que vai no header X-Hub-Event (conveniencia pra parser unico). timestamp e o instante do envio (pode diferir do timestamp do recurso — ex: data.timestamp da mensagem original).
data esta documentado em Eventos.
Seguranca — validacao HMAC
Por que assinar
Sem signature, qualquer ator que descubra a webhook_url pode injetar payloads. HMAC com secret compartilhado garante que o webhook veio do hub.
Algoritmo
HMAC-SHA256(key=webhook_secret, message=request_body_bytes)Resultado vai como hex no header X-Hub-Signature, prefixado por sha256=.
Validacao em Node.js
import crypto from 'node:crypto'import express from 'express'
const app = express()const SECRET = process.env.WPPHUB_WEBHOOK_SECRET
// CRITICO: precisa do body BRUTO (Buffer). Nao use express.json() antes.app.post( '/webhook', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.header('x-hub-signature') || '' const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex')
if ( sig.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)) ) { return res.status(401).send('invalid signature') }
const payload = JSON.parse(req.body.toString()) // processar payload.event + payload.data res.sendStatus(200) },)Validacao em Python (FastAPI)
import hmacimport hashlibimport osfrom fastapi import FastAPI, Request, HTTPException
SECRET = os.getenv('WPPHUB_WEBHOOK_SECRET').encode()app = FastAPI()
@app.post('/webhook')async def webhook(request: Request): body = await request.body() sig = request.headers.get('x-hub-signature', '') expected = 'sha256=' + hmac.new(SECRET, body, hashlib.sha256).hexdigest() if not hmac.compare_digest(sig, expected): raise HTTPException(401, 'invalid signature') payload = await request.json() # processar payload['event'] + payload['data'] return {'ok': True}Validacao em PHP
$body = file_get_contents('php://input');$sig = $_SERVER['HTTP_X_HUB_SIGNATURE'] ?? '';$expected = 'sha256=' . hash_hmac('sha256', $body, $_ENV['WPPHUB_WEBHOOK_SECRET']);
if (!hash_equals($sig, $expected)) { http_response_code(401); exit('invalid signature');}
$payload = json_decode($body, true);// processar $payload['event'] + $payload['data']http_response_code(200);Anti-padroes (NAO faça)
- ❌
JSON.stringify(JSON.parse(body))antes de calcular HMAC — Postgres JSONB nao preserva ordem de keys, qualquer reformatacao quebra - ❌ Comparar com
==— vulneravel a timing attack. SempretimingSafeEqual/hash_equals/compare_digest - ❌ Logar a
webhook_secretou a signature inteira em log de erro - ❌ Aceitar webhook sem validacao em prod (“vou ligar HMAC depois”)
Idempotency no consumer
X-Hub-Delivery e ULID unico por entrega. Hub pode reentregar (retry) — o X-Hub-Delivery se mantem dentro do retry, mas voce deve dedupar caso a caso:
const deliveryId = req.header('x-hub-delivery')
if (await alreadyProcessed(deliveryId)) { return res.sendStatus(200)}
// processa idempotenteawait markProcessed(deliveryId)Tambem cada data.message_id (em message.received) e UUID/ULID unico. Voce pode dedupar por ele se preferir.
Retry policy
Hub considera “sucesso” qualquer status 2xx retornado pela webhook_url. Caso contrario, retenta com backoff exponencial:
| Tentativa | Delay desde envio anterior |
|---|---|
| 1 | imediata |
| 2 | 30s |
| 3 | 2min |
| 4 | 10min |
| 5 | 1h |
| 6 | 6h |
| 7 | 24h |
Apos a 7a falha → status failed. Nao retenta automaticamente. Voce pode forcar via POST /v1/webhook-deliveries/:id/replay.
Quando hub PARA de retentar (vai pra failed na hora)
- HTTP 4xx exceto 408/429 — payload errado, retry inutil
- Erro de DNS persistente
Quando hub continua retentando
- HTTP 408, 429, 5xx
- Timeout de rede (default
WEBHOOK_TIMEOUT_MS=10000ms)
Implicacao pro consumer
- Responda 2xx rapido (< 10s) e processe async se necessario. Demorar = timeout = retry desnecessario.
- Em 5xx temporario do seu lado (deploy, etc), retorna 503 pra dar replays.
- Em 4xx (payload invalido), retorna 400 pra hub parar.
Ordem de eventos
Hub nao garante ordem absoluta de eventos. Em alta concorrencia voce pode receber message.received antes do chip.connected se a janela for muito curta. Use data.timestamp ou X-Hub-Timestamp pra ordenar do seu lado se precisar.
Eventos do mesmo recurso (ex: 2 message.status consecutivos da mesma mensagem: delivered → read) tambem podem chegar fora de ordem em situacoes raras. Sempre olhe o status mais “avancado” como verdade.
Throttling
Hub usa 1 worker por app com sua propria fila. Se sua webhook_url esta lenta, isso atrasa so as deliveries dessa app — outras apps nao sofrem.
Pra escalar throughput se voce ficar gargalo: 503 em pico → hub retenta → hora pra deploy/scale.
Como testar localmente
- Use webhook.site ou ngrok pra expor sua maquina
- Crie app com
webhook_urlapontando pra la - Mande mensagem teste — webhook chega
- Use
GET /v1/webhook-deliveries/:id/payloadpra ver exatamente o que foi enviado e validar HMAC do seu lado
Receiver dummy embutido (DEV-only)
Em ambiente DEV (NODE_ENV != production), hub expoe POST /admin/webhook-receiver?app_id=...&fail=N. Pode usar como webhook_url da sua app pra debug — receiver loga o payload, valida HMAC e suporta forced fail (?fail=500).