Implementar um Webhook Receiver
Este guia descreve a implementação de um webhook receiver que:
- Verifica a signature HMAC.
- Faz deduplicação por
event_id. - Retorna 200 rapidamente (em menos de 10 segundos).
- Enfileira o trabalho pesado para processamento async.
Arquitetura
- 1Receber o POST
O DVS envia um body JSON e os headers de signature ao endpoint.
- 2Verificar a signature HMAC
Rejeite com 401 se inválida. Consulte Verificar Signatures HMAC.
- 3Conferir idempotency
Consulte
X-DVS-Event-Idna tabelaprocessed_events. Se já existir, retorne 200 imediatamente sem reprocessar. - 4Inserir registro de idempotency + enfileirar
Insert atômico em
processed_events(unique constraint em event_id). Em seguida, publique o evento na fila/worker interna. - 5Retornar 200 OK
Em até 10 segundos. Não execute lógica de negócio de forma síncrona.
Por que desacoplar da lógica de negócio
O DVS aplica timeout de 10 segundos na entrega do webhook. Se o receiver realizar trabalho pesado de forma síncrona (consultas a banco, notificação de sistemas downstream, envio de e-mails), há risco de timeout e o DVS fará retry — levando a processamento duplicado.
O padrão é: receber, verificar, enfileirar, responder rápido. Um worker separado faz dequeue e processa em seu próprio ritmo.
Tabela de idempotency
CREATE TABLE processed_webhook_events (
event_id VARCHAR(255) PRIMARY KEY,
event_type VARCHAR(50) NOT NULL,
processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Faça insert com semântica ON CONFLICT DO NOTHING. Se o insert retornar 0 linhas, o evento já foi processado — responda 200 sem reenfileirar.
Limpe registros com mais de 30 dias por meio de um cron diário — o DVS não faz retry de eventos com mais de 30 minutos de qualquer forma.
Receiver completo — Python (FastAPI)
import hmac
import hashlib
import time
from fastapi import FastAPI, Request, HTTPException, Header
from sqlalchemy import insert
from sqlalchemy.dialects.postgresql import insert as pg_insert
# Assume que `db`, `events_table`, `internal_queue` já estão configurados.
DVS_WEBHOOK_SECRET = os.environ["DVS_WEBHOOK_SECRET"]
SIGNATURE_TOLERANCE_SECONDS = 300
def verify_signature(raw_body: bytes, sig: str, ts: str, secret: str) -> bool:
if not sig or not ts: return False
try: ts_int = int(ts)
except ValueError: return False
if abs(time.time() - ts_int) > SIGNATURE_TOLERANCE_SECONDS: return False
parts = {k: v for k, v in (kv.split("=", 1) for kv in sig.split(","))}
if "v1" not in parts: return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(parts["v1"], expected)
def mark_processed(event_id: str, event_type: str) -> bool:
"""Retorna True se inserido pela primeira vez, False se já existia."""
stmt = pg_insert(events_table).values(event_id=event_id, event_type=event_type).on_conflict_do_nothing()
result = db.execute(stmt)
return result.rowcount == 1
app = FastAPI()
@app.post("/integrations/dvs/webhooks")
async def receive(request: Request,
x_dvs_signature: str = Header(None),
x_dvs_signature_timestamp: str = Header(None),
x_dvs_event_id: str = Header(None),
x_dvs_event_type: str = Header(None)):
raw_body = await request.body()
if not verify_signature(raw_body, x_dvs_signature, x_dvs_signature_timestamp, DVS_WEBHOOK_SECRET):
raise HTTPException(401, "Invalid signature")
if not mark_processed(x_dvs_event_id, x_dvs_event_type):
return {"status": "duplicate_ignored"}
internal_queue.send_message(MessageBody=raw_body.decode(), MessageAttributes={
"event_type": {"StringValue": x_dvs_event_type, "DataType": "String"},
"event_id": {"StringValue": x_dvs_event_id, "DataType": "String"},
})
return {"status": "received"}
O worker (processo separado) lê de internal_queue, faz parse do JSON e executa a lógica de negócio.
Semântica dos response codes (o que o DVS faz com a resposta)
| Seu status | Comportamento do DVS |
|---|---|
2xx | Entregue. Sem retry. |
3xx | Tratado como erro. Faz retry. O DVS NÃO segue redirecionamentos. |
4xx | Tratado como erro. Faz retry até o número máximo de tentativas. |
5xx | Tratado como erro. Faz retry. |
| Timeout (>10s) | Tratado como falha. Faz retry. |
Retornar 200 para eventos duplicados mantém o DVS satisfeito e evita retries desnecessários.
O que fazer se o endpoint estiver fora do ar
O DVS faz retry até 3 vezes (imediato, +60s, +300s). Após 3 falhas, o evento vai para a dead-letter queue do DVS e não é recuperado automaticamente.
Fallback: faça polling em GET /v1/classification-requests/{id} (ou validation-requests) para recursos cujo webhook foi perdido. O id também está na response do POST original.
Após 10 falhas consecutivas, o DVS desabilita automaticamente o endpoint. Entre em contato com a OSIGU para reativar.
Testes locais
Use o ngrok para expor o servidor local. Consulte Testando com ngrok.