Webhooks e idempotencia de eventos
TL; DR
Un buen webhook es un evento firmado (HMAC/mTLS), resumible e idempotente, entregado a través del modelo at-least-once con retroceso exponencial y deduplicación en el destinatario. Negocia sobre ('event _ id', 'type', 'ts',' version ',' attempt', 'signature'), ventana de tiempo (≤5 min), códigos de respuesta, retratos, DLQ y status endpoint.
1) Roles y modelo de entrega
Remitente (usted/proveedor): forma un evento, firma, intenta entregar hasta 2xx, retira a 3xx/4xx/5xx (excepto explícitamente «no aceptes»), conduce DLQ, da replay API.
Destinatario (socio/su servicio): verifica la firma/ventana temporal, hace el dedoup y el procesamiento idempotente, responde con el código correcto, proporciona/status y/ack replay por 'event _ id'.
Garantías: at-least-once. El destinatario debe ser capaz de manejar duplicados y cambios de orden.
2) Sobre del 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"
}
}
Campos obligatorios: 'event _ id', 'type', 'version', 'ts',' attempt'.
Reglas de evolución: agregamos campos; eliminación/cambio de tipos - sólo con la nueva 'versión'.
3) Seguridad: firma y enlace
3. 1 firma HMAC (recomendada por defecto)
Encabezados:
X-Signature: v1=base64(hmac_sha256(<secret>, <canonical>))
X-Timestamp: 2025-11-03T12:34:56Z
X-Event-Id: 01HF7...
Cadena canónica:
<timestamp>\n<method>\n<path>\n<sha256(body)>
Comprobación del destinatario:
- abs(now − `X-Timestamp`) ≤ 300s
- 'X-Event-Id' no ha sido procesado previamente (dedoup)
- 'X-Signature' coincide (con una comparación segura en el tiempo)
3. 2 Dop. Medidas
mTLS para webhooks altamente sensibles.
IP/ASN allow-list.
DPoP (opcional) para sender-constrained si el webhook inicia las devoluciones de llamada.
4) Idempotencia y deduplicación
4. 1 Idempotencia del evento
Un evento con el mismo 'event _ id' no debe cambiar el estado de nuevo. Destinatario:- almacena 'event _ id' en caché idempotente (KV/Redis/BD) en TTL ≥ 24-72 h;
- conserva el resultado del procesamiento (éxito/error, artefactos) para volver a devolverlo.
4. 2 Idempotencia de comandos (devoluciones de llamadas)
Si el webhook hace que el cliente tire de la API (por ejemplo, «confirmar payout»), use 'Idempotency-Key' en el volumen de la llamada NAT, guarde el resultado en el lado del servicio (exactly-once outcome).
Modelo KV (mínimo):
key: idempotency:event:01HF7...
val: { status: "ok", processed_at: "...", handler_version: "..." }
TTL: 3d
5) Retrai y backoff
Gráfico recomendado (exponencial con jitter):- '5s, 15s, 30s, 1m, 2m, 5m, 10m, 30m, 1h, 3h, 6h, 12h, 24h' (más adelante diario a N días)
- 2xx - éxito, dejar de retraer.
- '400/ 401/403/404/422' - no retraemos si la firma/formato es aprox (error del cliente).
- '429' es un retraim por 'Retry-After' o backoff.
- 5xx/red - retraim.
Los encabezados del remitente son: 'User-Agent', 'X-Webhook-Producer', 'X-Attempt'.
6) Tratamiento en el lado del destinatario
Pseudopapline: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
Transaccionalidad: la etiqueta «seen» debe colocarse atomicamente con el efecto de la operación (o después de fijar el resultado) para evitar el doble procesamiento cuando falla.
7) Garantías de orden y tapones
El orden no está garantizado. Utilice 'ts' y dominios' seq '/' version 'en' data 'para conciliar la relevancia.
Para largos retrasos/pérdidas: agregue/replay del remitente y/resync del destinatario (obtenga snapshot y delta por la ventana de tiempo/ID).
8) Estado, Replay y DLQ
8. 1 Endpoints del remitente
'POST/webhooks/replay' - por la lista 'event _ id' o por la ventana de tiempo.
'GET/webhooks/events/: id' - mostrar el paquete original y el historial de intentos.
DLQ: eventos «muertos» (se ha agotado el límite de retraídos) → almacenamiento separado, alertas.
8. 2 Endpoints del receptor
`GET /webhooks/status/:event_id` — `seen=true/false`, `processed_at`, `handler_version`.
'POST/webhooks/ack' - (opcional) confirmación de mecanizado manual desde DLQ.
9) Contratos de error (respuesta del destinatario)
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..."
}
Recomendaciones: siempre devuelve un código claro y, si puedes, 'Retry-After'. No devuelva detalles de seguridad detallados.
10) Monitoreo y SLO
Métricas (remitente):- delivery p50/p95, tasa de éxito, retray/evento, drop-rate DLQ, compartir 2xx/4xx/5xx, ventana de latencia hasta 2xx.
- verify fail rate (firma/tiempo), dup-rate, latency handler p95, 5xx.
- Entrega: ≥ 99. El 9% de los eventos reciben 2xx <3 c p95 (después del primer intento exitoso).
- Cryptoproverk: validación de firma ≤ 2-5 ms p95.
- Dedoup: 0 efectos repetidos (exactly-once outcome a nivel de dominio).
11) Seguridad de datos y privacidad
No transmita PAN/PII en el cuerpo del webhook; utilice los identificadores y el pull subsiguiente para las piezas a través de la API autorizada.
Enmascarar campos sensibles en los logs; almacenar los cuerpos de eventos sólo al mínimo, con TTL.
Cifre los repositorios DLQ y Replay.
12) Versificación e interoperabilidad
Versión en 'versión' (sobre) y en ruta: '/webhooks/v1/payments '.
Nuevos campos: opcionales; eliminación: sólo después del período 'Sunset'.
Documente los cambios en el changelog machine-readable (para verificaciones automáticas).
13) Casos de prueba (lista de cheques UAT)
- Volver a enviar el mismo 'event _ id' → un efecto y '200' por duplicado.
- Firma: clave fiel, clave incorrecta, clave vieja (rotación), 'X-Timestamp' fuera de la ventana.
- Backoff: el receptor da un '429' de 'Retry-After' → una pausa correcta.
- Orden: eventos '... processed' viene antes '... created' → procesamiento/espera correctos.
- Fallo de la DB en el receptor entre el efecto y 'mark _ seen' → atomicidad/repetición.
- DLQ y replay manual → entrega exitosa.
- La «tormenta» masiva (proveedor de la manada) → sin pérdida, los límites no estrangulan lo crítico.
14) Mini-snippets
Firma del remitente (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)
Verificación y dedo del destinatario (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) Errores frecuentes
No hay dedoop → efectos repetidos (refandos/peyouts dobles).
Firma sin tiempo de espera/ventana → vulnerabilidad a replay.
Almacena un único secreto HMAC en todos los socios.
Respuestas '200' antes de fijar el resultado → pérdida de eventos en crash.
«Lavado» de detalles de seguridad en respuestas/registros.
Falta de DLQ/replay: los incidentes son irresolubles.
16) Introducción de Spark
Seguridad: HMAC v1 + 'X-Timestamp' + 'X-Event-Id', ventana ≤ 5 min; mTLS/IP allow-list por necesidad.
Конверт: `event_id`, `type`, `version`, `ts`, `attempt`, `data`.
Entrega: at-least-once, backoff con jitter, 'Retry-After', DLQ + replay API.
Idempotencia: caché KV 24-72 h, fijación atómica del efecto + 'mark _ seen'.
Observabilidad: métricas de entrega, firmas, duplicados; seguimiento 'trace _ id'.
Documentación: versión, códigos de respuesta, ejemplos, lista de comprobación UAT.
Resumen
Los webhooks resistentes se construyen sobre tres ballenas: un sobre firmado, entrega en hoja y tratamiento idempotente. Formaliza el contrato, habilita HMAC/mTLS y la ventana de tiempo, implementa retraídas + DLQ y réplicas, almacena marcas idempotentes y captura los efectos de forma atómica. Entonces, los eventos permanecen confiables incluso con fallas en la red, picos de carga y raros «duplicados del destino».