Saltar al contenido
AX Principios
AX-SYS-36Intermedio

Firma cada webhook y dale a cada evento un ID estable

Sign every webhook and give each event a stable ID

#saas#fintech#e-commerce

Firma cada webhook sobre su cuerpo y una marca de tiempo, dale a cada evento un ID que no cambie entre reintentos y documenta que la entrega se reintenta y no llega en orden, para que el agente que lo recibe pueda rechazar falsificaciones y procesar cada evento una sola vez.

Por qué

AX-SYS-17 dice que envíes webhooks. Una vez que lo haces, quien los recibe actúa sobre ellos: un evento charge.refunded puede cerrar un caso o mandar un correo a una clienta. Cualquiera que encuentre la URL de un webhook puede llamarla, así que sin firma un evento falso se ve igual que uno real, y un evento real capturado se puede reenviar. GitHub pide validar la firma antes de seguir procesando una entrega, para saber que viene de GitHub y que nadie la alteró. Stripe incluye una marca de tiempo dentro de su cabecera Stripe-Signature, lo que permite rechazar un payload bien firmado pero demasiado viejo. La entrega es la otra mitad. Stripe reintenta las entregas fallidas hasta por tres días con backoff exponencial en modo live, advierte que un endpoint puede recibir el mismo evento más de una vez, pide registrar los IDs de evento ya procesados y saltarse los repetidos, y no garantiza el orden. CloudEvents hace que el ID sea parte del formato: los productores deben mantener único el par source más id para cada evento distinto, un reenvío puede llevar el mismo id y los consumidores pueden tratar un par repetido como duplicado. Sin un ID estable, un charge.refunded reintentado parece un segundo reembolso.

Haz

Firma el cuerpo sin procesar junto con una marca de tiempo, conserva el mismo ID de evento en cada intento de entrega y publica tu calendario de reintentos y el hecho de que el orden no está garantizado.

No hagas

Enviar webhooks sin firma, generar un ID nuevo en cada reintento, ni dar a entender que los eventos llegan una sola vez y en orden.

Artefacto

POST /hooks/acme HTTP/1.1
Content-Type: application/cloudevents+json
Acme-Signature: t=1791036000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e7...
Acme-Delivery-Attempt: 2

{
  "specversion": "1.0",
  "id": "evt_4410",
  "source": "https://api.acme.com/charges",
  "type": "com.acme.charge.refunded",
  "time": "2026-10-03T14:00:00Z",
  "data": { "charge_id": "ch_123", "amount": 4900 }
}

El receptor comprueba el HMAC sobre la marca de tiempo y el cuerpo sin procesar, rechaza las marcas de tiempo viejas y se salta cualquier id que ya procesó; el intento 2 lleva el mismo id que el intento 1.

Fuentes

  1. [01]
    Stripe Docs: Receive Stripe events in your webhook endpoint ↗

    The Stripe-Signature header in each signed event contains a timestamp and one or more signatures to verify; if the signature is valid but the timestamp is too old, the application can reject the payload. Stripe retries delivery for up to three days with exponential backoff in live mode, endpoints might receive the same event more than once (guard against it by logging processed event IDs), and Stripe doesn't guarantee delivery in the order events are generated

  2. [02]
    GitHub Docs: Validating webhook deliveries ↗

    To ensure your server only processes webhook deliveries that were sent by GitHub and were not tampered with, validate the webhook signature before processing the delivery further

  3. [03]
    CloudEvents Specification v1.0 (id attribute) ↗

    Producers MUST ensure that source + id is unique for each distinct event; a duplicate event re-sent (e.g. due to a network error) MAY have the same id, and consumers MAY assume that events with identical source and id are duplicates