Saltar al contenido
Payment Emulator LabDocumentaciónEntrar a la consola

Funcionamiento interno

Webhooks de pago: emisión y entrega

La entrega de webhooks usa un outbox transaccional en PostgreSQL y untransporte configurable. La API escribe obligaciones; sólo el worker mandaHTTP.

5 min de lectura

Índice / Index · English

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

  1. PaymentsService llama a WebhooksService.schedule() dentro de la transacción de base de datos del pago.
  2. Una o más filas de webhook_events se 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.
  3. OutboxDispatcher reclama las filas que tocan con FOR UPDATE SKIP LOCKED, sella queued_at y se las entrega al transporte. Con el driver postgres no corre en absoluto: ese driver lee esas mismas filas, y dos reclamadores sobre una tabla es una carrera, no una redundancia.
  4. El worker recarga la fila durable, le pide al plugin del proveedor que la firme y hace POST al callback de la aplicación.
  5. Cada intento crea un webhook_attempt; el evento pasa a delivered o a failed.

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: o https:;
  • 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.

Payment Emulator Lab · RonuSoftwareMIT