Pular para o conteúdo principal

Implementar um Webhook Receiver

Este guia descreve a implementação de um webhook receiver que:

  1. Verifica a signature HMAC.
  2. Faz deduplicação por event_id.
  3. Retorna 200 rapidamente (em menos de 10 segundos).
  4. Enfileira o trabalho pesado para processamento async.

Arquitetura

  1. 1Receber o POST

    O DVS envia um body JSON e os headers de signature ao endpoint.

  2. 2Verificar a signature HMAC

    Rejeite com 401 se inválida. Consulte Verificar Signatures HMAC.

  3. 3Conferir idempotency

    Consulte X-DVS-Event-Id na tabela processed_events. Se já existir, retorne 200 imediatamente sem reprocessar.

  4. 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.

  5. 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 statusComportamento do DVS
2xxEntregue. Sem retry.
3xxTratado como erro. Faz retry. O DVS NÃO segue redirecionamentos.
4xxTratado como erro. Faz retry até o número máximo de tentativas.
5xxTratado 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.