Webhooks e Idempotação de eventos
TL; DR
Um bom webhook é um evento assinado (HMAC/mTLS), resumido e idumpotente, entregue por at-least-once, com backoff exponencial e dedução do destinatário. Negocie sobre um envelope ('event _ id', 'tipo', 'ts', 'version', 'attempt',' signatura '), uma janela de tempo (≤5 min), códigos de resposta, retais, DLQ e status-endpoint.
1) Papéis e modelo de entrega
Remetente (você/provedor): forma evento, assina, tenta entregar até 2xx, retraita a 3xx/4xx/5xx (exceto «não aceite» explícito), conduz DLQ, dá replay API.
Destinatário (parceiro/seu serviço): verifica a assinatura/janela temporária, faz o deduzimento e o processamento idumportante, atende com o código correto, fornece/status e/ack replay por 'event _ id'.
Garantia: at-least-once. O destinatário deve ser capaz de processar duplicados e mudar de ordem.
2) Envelope evento (envelope)
json
{
"event_id": "01HF7H9J9Q3E7DYT5Y6K3ZFD6M",
"type": "payout.processed",
"version": "2025-01-01",
"ts": "2025-11-03T12:34:56.789Z",
"attempt": 1,
"producer": "payments",
"tenant": "acme",
"data": {
"payout_id": "p_123",
"status": "processed",
"amount_minor": 10000,
"currency": "EUR"
}
}
Os campos obrigatórios são 'event _ id', 'tipo', 'version', 'ts', 'attempt'.
Regras de evolução: adicionando campos; remover/alterar tipos - apenas com o novo «versão».
3) Segurança: assinaturas e vinculação
3. 1 assinatura HMAC (recomendada por padrão)
Títulos:
X-Signature: v1=base64(hmac_sha256(<secret>, <canonical>))
X-Timestamp: 2025-11-03T12:34:56Z
X-Event-Id: 01HF7...
Linha canônica:
<timestamp>\n<method>\n<path>\n<sha256(body)>
Verificação do destinatário:
- abs(now − `X-Timestamp`) ≤ 300s
- 'X-Event-Id' não foi processado anteriormente (Dedup)
- 'X-Score' corresponde (com uma comparação de tempo seguro)
3. 2 Dopp. medidas
para webhooks altamente sensíveis.
IP/ASN allow-list.
DPoP (opcional) para sender-constrained se o webhook iniciar as chamadas invertidas.
4) Idempotidade e dedução
4. 1 Idempotidade do evento
O evento com o mesmo 'event _ id' não deve alterar novamente o status. Destinatário:- armazenando 'event _ id' em um cajo idumpotente (KV/Redis/BD) em TTL ≥ 24-72 h;
- salva o resultado do processamento (sucesso/erro, artefatos) para o retorno.
4. 2 Idempotidade dos comandos (chamadas de volta)
Se o webhook forçar o cliente a fazer API (por exemplo, «confirmar payout»), use 'Idempotency-Key' na chamada REST, armazene o resultado no lado do serviço (exactly-once outcome).
Modelo KV (mínimo):
key: idempotency:event:01HF7...
val: { status: "ok", processed_at: "...", handler_version: "..." }
TTL: 3d
5) Retraias e backoff
Horário recomendado (exponencial com jitter):- '5s, 15s, 30s, 1m, 2m, 5m, 10m, 30m, 1h, 3h, 6h, 12h, 24h' (seguir diárias para N dias)
- 2xx - sucesso, parar com o retrai.
- '400/ 401/403/404/422' não é retrátil se a assinatura/formato de ok (erro de cliente).
- '429' - Retraim por 'Retry-After' ou backoff.
- 5xx/rede - Retraim.
Os cabeçalhos do remetente são "Usuário-Agente", "X-Webhook-Producer", "X-Attempt'.
6) Processamento no lado do destinatário
Pseudopipline:pseudo verify_signature()
if abs(now - X-Timestamp) > 300s: return 401
if seen(event_id):
return 200 // идемпотентный ответ
begin transaction if seen(event_id): commit; return 200 handle(data) // доменная логика mark_seen(event_id) // запись в KV/DB commit return 200
Transacionalidade: O rótulo «seen» deve ser atômico com o efeito da operação (ou após a fixação do resultado) para evitar o duplo processamento em caso de falha.
7) Garantias de ordem e súmulos
A ordem não é garantida. Use «ts» e «seq »/« version» em «data» para verificar a relevância.
Para longas lajes/perdas - Adicione/replay junto ao remetente e/resync junto ao destinatário (obter snapshots e delta na janela de tempo/ID).
8) Status, replay e DLQ
8. 1 Endpointos do remetente
'POST/webhooks/replay' - na lista 'event _ id' ou na janela do tempo.
'GET/webhooks/events/id' - mostrar o pacote de origem e o histórico de tentativas.
DLQ: eventos «mortos» (limite de retais esgotado) → armazenamento separado, alertas.
8. 2 Endpointos do destinatário
`GET /webhooks/status/:event_id` — `seen=true/false`, `processed_at`, `handler_version`.
'POST/webhooks/ack' - (opcional) confirmação de processamento manual a partir do DLQ.
9) Contratos de erro (resposta do destinatário)
http
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
Retry-After: 120
X-Trace-Id: 4e3f...
{
"error": "invalid_state",
"error_description": "payout not found",
"trace_id": "4e3f..."
}
Recomendações: devolva sempre o código claro e, se possível, «Retry-After». Não devolva detalhes de segurança.
10) Monitoramento e SLO
Métricas (remetente):- delivery p50/p95, sucess rate, retrai/evento, drop-rate DLQ, share 2xx/4xx/5xx, janela de atraso de até 2xx.
- verify fail rate (assinatura/hora), dup-rate, latency handler p95, 5xx.
- Entrega: ≥ 99. 9% dos eventos recebem 2xx <3 c p95 (após a primeira tentativa bem sucedida).
- Kriptoprover, validação da assinatura ≤ 2-5 ms p95.
- Duplo: 0 efeitos repetidos (exactly-once outcome ao nível do domínio).
11) Segurança de dados e privacidade
Não transmita PAN/PII no corpo do webhook; use os ID e pulo subsequente para obter os detalhes da API autorizada.
Disfarce os campos sensíveis nos logs; guarde os corpos de eventos apenas pelo mínimo, com TTL.
Criptografe o armazenamento DLQ e a réplica.
12) Versionização e compatibilidade
Versão em 'version' (envelope) e no caminho: '/webhooks/v1/payments '.
Novos campos - opcionais; remoção - somente após o período 'Sunset'.
Documente as alterações em machine-readable changelog (para a produção automática).
13) Mala de teste (folha de cheque UAT)
- Enviar novamente o mesmo 'event _ id' → um efeito e '200' para duplicados.
- Assinatura: chave certa, chave errada, chave antiga (rotação), 'X-Timestamp' fora da janela.
- Backoff: o destinatário dá '429' s Retry-After '→ uma pausa correta.
- Ordem: eventos '... processed' vem antes de '... created' → processamento/espera correto.
- Falha de BD do destinatário entre o efeito e 'mark _ seen' → atômica/repetição.
- DLQ e replay manual → uma entrega bem sucedida.
- Tempestade em massa (o provedor envia pacotes) → sem perda, os limites não sufocam o crítico.
14) Mini-snippets
Assinatura do remetente (pseudo):pseudo body = json(event)
canonical = ts + "\n" + "POST" + "\n" + path + "\n" + sha256(body)
sig = base64(hmac_sha256(secret, canonical))
headers = {"X-Timestamp": ts, "X-Event-Id": event.event_id, "X-Signature": "v1="+sig}
POST(url, body, headers)
Verificação e dedução do destinatário (pseudo):
pseudo assert abs(now - X-Timestamp) <= 300 assert timingSafeEqual(hmac(secret, canonical), sig)
if kv.exists("idemp:"+event_id): return 200
begin tx if kv.exists("idemp:"+event_id): commit; return 200 handle(event.data) // доменная логика kv.set("idemp:"+event_id, "ok", ttl=259200)
commit return 200
15) Erros frequentes
Não há dedução → efeitos repetidos (refanda duplo/payout).
Uma assinatura sem timeling/janela → vulnerável a replay.
Armazenamento de um segredo HMAC em todos os parceiros.
Respostas de '200' antes de o resultado ser captado → perda de eventos de crash.
Limpar detalhes de segurança em respostas/logs.
Falta de DLQ/réplica - os incidentes estão pendentes.
16) Varejista de implementação
Segurança: HMAC v1 + 'X-Timestamp' + 'X-Event-Id', janela ≤ 5 min; mTLS/IP allow-list por necessidade.
Конверт: `event_id`, `type`, `version`, `ts`, `attempt`, `data`.
Entrega: at-least-once, backoff com jitter, 'Retry-After', DLQ + replay API.
Idempotidade: KV-kesh 24-72 h, atômico efeito de fixação + 'mark _ seen'.
Observabilidade: métricas de entrega, assinaturas, duplicados; traçado 'trace _ id'.
Documentação: versão, códigos de resposta, exemplos, folha de cheque UAT.
Currículos
Os webhooks resistentes são construídos em três baleias: envelope assinado, at-least-once e processamento idepotente. Formalize o contrato, inclua a HMAC/mTLS e a janela do tempo, implemente retais + DLQ e réplicas, armazene as marcas idumpotentes e capte os efeitos atômicos. Assim, os eventos permanecem confiáveis, mesmo com falhas na rede, picos de carga e raras duplicações do destino.