Traducción de
docs/en/runtime/webhooks.md, que es el original. Si discrepan, manda el inglés.
La entrega de webhooks usa un outbox transaccional en PostgreSQL y un transporte configurable. La API escribe obligaciones; sólo el worker manda HTTP.
WEBHOOK_QUEUE_DRIVER |
Los trabajos viven en | Necesita en marcha |
|---|---|---|
postgres (por defecto) |
el propio webhook_events |
nada más |
mongo |
un documento de webhook_jobs |
mongod, suelto |
bullmq |
Redis | Redis |
Ningún driver guarda verdad de pago — un identificador de evento, un contador de intentos y una hora, y nada más. Ver ADR 011.
#Ciclo de vida
PaymentsServicellama aWebhooksService.schedule()dentro de la transacción de base de datos del pago.- Una o más filas de
webhook_eventsse confirman con el pago. El retraso del escenario, el número de duplicados y la intención de firmar mal se guardan en esas filas. OutboxDispatcherreclama las filas que tocan conFOR UPDATE SKIP LOCKED, sellaqueued_aty se las entrega al transporte. Con el driverpostgresno corre en absoluto: ese driver lee esas mismas filas, y dos reclamadores sobre una tabla es una carrera, no una redundancia.- El worker recarga la fila durable, le pide al plugin del proveedor que la
firme y hace
POSTal callback de la aplicación. - Cada intento crea un
webhook_attempt; el evento pasa adeliveredo afailed.
La fila es el compromiso y la cola es transporte sustituible — literalmente desde el ADR 011. Si el transporte no está disponible un rato, la API sigue pudiendo confirmar el pago y su fila de outbox. El barrido de lo rancio recrea después el trabajo que falta.
#Semántica de entrega
La entrega es al menos una vez. Cuatro intentos con retroceso exponencial
empezando en 250 ms, declarados una sola vez en RETRY y aplicados por todos los
drivers — BullMQ por dentro, los otros dos con aritmética en su bucle de reclamo.
test/integration/webhook-queue.spec.ts corre una sola suite de contrato contra
cada driver configurado, y eso es lo que impide que se desvíen. Una caída entre
reclamar y encolar la repara el barrido de lo rancio; un escenario también puede
crear duplicados a propósito. Por tanto, quien recibe tiene que procesar los
identificadores de evento de forma idempotente.
#Responsabilidad del proveedor
El plugin aporta:
- qué estados canónicos producen un asunto;
- el cuerpo con la forma del proveedor;
- las cabeceras de firma y su manifiesto;
- las reglas de verificación, incluida la tolerancia de la marca de tiempo.
Los formatos de Mercado Pago y de Stripe están en sus documentos de fidelidad. La conformidad estática exige un esquema total y reproducible, y las pruebas con los SDK oficiales verifican las partes que sus librerías exponen.
#Seguridad de red
Antes de mandar, apps/api/src/worker.ts exige:
RUNTIME_OUTBOUND_ENABLED=true;- un callback
http:ohttps:; - que el host resuelva a una dirección pública.
La última es la que importa, y se comprueba por dirección y no por nombre: la URL
del webhook la elige quien registra la aplicación, así que un nombre no prueba
nada — cualquiera puede apuntar su propio dominio a 192.168.1.50. Sin esa
comprobación, el emulador es un mando a distancia para hacer peticiones desde
dentro de la red donde está desplegado, y el estado y la duración de cada
intento quedan visibles en la consola de quien registró la aplicación.
Se rechaza todo el espacio de uso especial de IANA: privadas, loopback,
link-local —incluido el 169.254.169.254 de los metadatos de nube—, CGNAT,
multicast y reservado, más sus equivalentes IPv6 y las direcciones IPv4
mapeadas. Se miran todas las direcciones que devuelve el DNS, no la primera:
un nombre que contesta con una pública y una privada es la forma más vieja de
saltarse un control así.
WEBHOOK_ALLOWED_HOSTS es la excepción a esa regla, no la regla: la lista de
hosts permitidos a pesar de resolver a una dirección interna. Un dominio
público de cliente no necesita estar ahí; localhost sí, y viene por defecto
para que una instalación local siga funcionando.
Lo que esto no cierra es el DNS rebinding: el nombre se resuelve aquí y lo
vuelve a resolver fetch, y un dominio con un TTL de un segundo podría contestar
distinto la segunda vez. Cerrarlo exige conectar contra la dirección ya
comprobada, y eso cuesta la verificación del certificado si no se hace con mucho
cuidado. La ventana es estrecha y hay que ganar una carrera; queda escrito aquí
en vez de dejarlo para que alguien lo descubra.
No existen llamadas a producción de ningún proveedor. El único HTTP saliente es la entrega de webhooks.
#Inspeccionar la entrega
El plano de control expone eventos e intentos por
apps/api/src/modules/webhooks/webhooks.controller.ts; la consola se los pinta a
administradoras y comercios autorizados. Los intentos anotan estado de respuesta,
duración, error y el manifiesto de firma — no una segunda copia de la verdad del
pago.
#Carencia de alcance conocida
El listado de eventos se filtra por applicationId, pero el manejador actual de
GET /applications/:applicationId/webhooks/:eventId/attempts consulta el
identificador del evento sin restringirlo además a la aplicación de la ruta.
La ruta sigue exigiendo acceso a una aplicación, pero no demuestra que el evento
pertenezca a esa misma aplicación. Mantén el emulador en una red de desarrollo de
confianza hasta que se añadan esa comprobación y una prueba de regresión entre
inquilinos.
Ver Flujos de petición críticos para el diagrama de secuencia, y Diagnóstico cuando los eventos no salgan del outbox.