Skip to Content
WebhooksResumen

Webhooks — Resumen

Los webhooks son la señal autoritativa. Los callbacks del navegador (onSuccess) y las vistas del dashboard son conveniencia; los webhooks son la verdad de base.

Garantías de entrega

  • Al menos una vez. Un solo evento puede entregarse hasta 6 veces si tu servidor no devuelve 2xx dentro del timeout. Haz tu handler idempotente: deduplica por X-Delivery (el payload no tiene un campo event_id; el UUID estable de la entrega es la clave de idempotencia).
  • Un evento por petición HTTP. Sin batching.
  • Aislamiento por endpoint. Si tienes múltiples endpoints registrados, cada uno obtiene su propia pista de entrega + reintento. Una URL de comerciante lenta no puede hambrear a las otras: cada host tiene su propio circuit breaker.
  • Firmado. Cada payload lleva una cabecera X-Signature (y durante la ventana de 24 horas tras una rotación, también una X-Signature-Prev). Verifica antes de hacer nada con el body. Consulta Verificación de firma.

Tipos de evento suscribibles

EventoSe dispara cuando…
payment.settledLa transferencia on-chain alcanzó el conteo de confirmaciones de la cadena. Usa esto para marcar órdenes como pagadas.
payment.failedUn pago fiat fue explícitamente rechazado por el proveedor (actualmente: webhook de Stripe señalando fallo). No se dispara para timeouts cripto: esos aparecen como checkout.expired, y los pagos cripto cortos aparecen como payment.underpaid.
payment.underpaidLos fondos llegaron pero por debajo del total de la orden (típico: tarifa de transferencia de stablecoin tomada del importe).
payment.overpaidLos fondos llegaron en exceso del total de la orden. El sobrante se registra pero no se auto-reembolsa.
order.createdSe abrió una nueva orden: o por tu llamada a la API B2B o por una conversión de sesión de checkout.
order.canceledUna orden se movió a cancelada. El data.reason del payload distingue cancelación manual de payment_timeout (orden no pagada obsoleta barrida por el worker).
order.resolvedUna orden PARTIAL_PAID se resolvió a PAID: el comerciante aceptó el déficit.
order.reopenedUna orden previamente autocancelada (canceled_reason=payment_timeout) fue reabierta por el comerciante.
checkout.createdUn comprador abrió el checkout para una orden.
checkout.completedEl flujo del lado comprador terminó (no implica liquidación on-chain: usa payment.settled para eso).
checkout.expiredEl comprador abandonó y el TTL de la sesión se agotó.
payment.refund.requestedSe creó un registro de reembolso: o desde una llamada a la API iniciada por el comerciante o desde un formulario de solicitud de reembolso enviado por el cliente.
payment.refund.approvedUn reembolso pendiente pasó tu flujo de aprobación.
payment.refund.rejectedUn reembolso pendiente fue denegado.
payment.refund.executedLa transferencia on-chain del reembolso se confirmó y el registro se movió a executed terminal.
refund_request.createdSe emitió un token de solicitud de reembolso. data.source es b2b / dashboard / renewal. Opcional para suscribirse: útil para pipelines de auditoría que rastrean qué token está actualmente activo por orden.
refund_request.renewal_requestedUn comprador hizo clic en “Solicitar nuevo enlace” tras expirar su token. Fuertemente recomendado para suscribirse: esta es la señal para el comerciante de que el widget de renovaciones tiene un nuevo item sobre el que actuar.
refund_request.renewedUna renovación fue aprobada y un nuevo token reemplazó al antiguo. data.old_token / data.new_token forman la cadena de auditoría.
refund_request.canceledUn comerciante cambió un token a CANCELED desde el dashboard (p. ej. denegó una solicitud de renovación, mató un enlace activo). Idempotente: solo la primera transición emite. data.reason es la nota opcional del comerciante.

El dashboard obtiene esta lista de GET /v1/webhooks/event-types para que el formulario de creación / edición de endpoints siempre coincida con lo que la plataforma realmente emite. Suscribirse a un evento que no enviamos será rechazado en el momento de la creación con un error claro.

Los eventos de prueba no son suscribibles. El botón Enviar prueba por endpoint del dashboard hace POST de un envelope webhook.test.ping síncronamente a ese único endpoint (saltándose el pipeline de reintentos), y la ruta heredada “Enviar evento de prueba” a nivel de comerciante hace fan-out de un envelope webhook.test a cada endpoint activo independientemente de su filtro. Ninguno aparece en el catálogo de arriba: los recibes por tener un endpoint registrado, no por suscribirte.

Suscríbete solo a los eventos que manejas. Cada endpoint tiene su propio filtro de eventos; el wildcard "*" significa “cada evento, incluyendo los añadidos en el futuro”. Suscribirse a menos eventos mantiene tu handler más limpio y reduce la superficie sobre la que tenemos que reintentar en errores.

Payload + cabeceras

El body HTTP es el objeto data específico del evento directamente. Sin envelope externo estilo Stripe: campos como el tipo de evento, delivery ID y timestamp de emisión viven en cabeceras en su lugar. Para payment.settled el body se ve así:

{ "receipt_id": "rcp_…", "order_id": "ord_…", "payment_intent_id": "pin_…", "checkout_session_id": "cst_…", "merchant_id": "mer_…", "customer_id": "cus_…", "total": "49.00", "currency": "USD", "payment_method": "crypto", "token": "USDC", "network": "polygon", "tx_hash": "0x…", "deposit_address": "0x…", "treasury_address": "0x…", "amount_received": "49.00", "confirmations": 5, "metadata": { /* per-event */ } }

Otros eventos llevan su propio set de campos: consulta los structs de publisher en payment-service/internal/domain/events.go para la forma canónica hasta que aterricen los docs por evento. Los nombres de campo son estables (lower snake_case); el tx hash on-chain es siempre tx_hash (no transaction_hash).

Cabeceras en la petición entrante

Content-Type: application/json X-Event: payment.settled X-Delivery: 7c9e6679-7425-40de-944b-e07fc1f90ae7 Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7 X-Timestamp: 1729536000 X-Signature: sha256=9a8b7c… X-Signature-Prev: sha256=fa31b2… (solo durante la ventana de gracia de rotación)
CabeceraQué es
X-EventEl tipo de evento (p. ej. payment.settled). Enruta sobre esto en la capa proxy si quieres saltarte el parsing JSON.
X-DeliveryUUID que identifica la fila de entrega. Estable a través de todos los reintentos del mismo par (evento, endpoint): úsalo como tu clave de idempotencia.
Idempotency-KeyRefleja X-Delivery (mismo valor). Se establece en cada entrega: toma convención de Stripe / GitHub.
X-TimestampUnix-segundos cuando se envió el intento. Firmado en el payload para que un par (body, X-Signature) capturado no pueda repetirse indefinidamente: rechaza entregas cuyo timestamp esté fuera de tu ventana de tolerancia.
X-Signaturesha256=<hex> de HMAC-SHA256(secret, X-Timestamp + "." + raw_body). Consulta Verificación de firma.
X-Signature-PrevMismo algoritmo con el secreto anterior. Presente solo en la ventana de 24 horas tras rotar: deja que los verificadores corriendo cualquiera de las claves sigan aceptando entregas durante el corte. Tras cerrar la ventana, la cabecera deja de enviarse.

Calendario de reintentos

Si tu endpoint no devuelve 2xx dentro del timeout, reintentamos en este calendario (timestamps relativos al primer intento):

IntentoRetrasoAcumulado
10s0s
2+1 min1m
3+5 min6m
4+15 min21m
5+1 hora1h 21m
6+6 horas7h 21m

Tras fallar el intento 6, la entrega se mueve a dead letter y se notifica al email de la cuenta del comerciante. Los eventos dead-lettered pueden reproducirse desde el panel Developers → Webhooks → Delivery history del dashboard, o directamente vía POST /v1/webhooks/deliveries/:id/replay. Cada replay crea una nueva fila de entrega con su propio X-Delivery: la cadena de auditoría se vincula de vuelta al original vía parent_delivery_id para que los reintentos-de-replays no ensombrezcan el evento fuente.

Registra un endpoint

Desde el dashboard del comerciante :

  1. Developers → Webhooks+ Add endpoint
  2. Pega tu URL: solo https://… (HTTP plano se rechaza; el formulario de creación también bloquea localhost, rangos de IP privadas y URLs que lleven userinfo)
  3. Elige eventos a los que suscribirte (o * para todos)
  4. Elige entorno: test o live (cada uno tiene su propio secreto; nunca se cruzan)
  5. Guarda → el dashboard muestra el secreto de firma (whsec_…) una vez. Guárdalo en el lado del servidor; lo necesitarás para las próximas dos funciones.

Puedes registrar hasta 10 endpoints por entorno por comerciante (p. ej., uno para fulfillment de producción, uno para staging mirroring, uno para un notificador de Slack). Cada uno mantiene su propio estado de reintento, secreto y circuit breaker por host.

Acciones de ciclo de vida en cada endpoint

El menú ⋮ en cada tarjeta de endpoint muestra:

  • Edit: cambia la URL, descripción o lista de suscripciones. La nueva URL se revalida con las mismas reglas de https:///SSRF que en la creación.
  • Send Test: POSTea síncronamente un envelope webhook.test.ping firmado con tu secreto actual. El dashboard muestra el status HTTP, latencia y un fragmento de 512 bytes de tu respuesta. Salta el pipeline de RMQ para que la respuesta sea inmediata.
  • Rotate Secret: genera un nuevo secreto. El anterior sigue válido por 24 horas (las entregas llevan tanto X-Signature como X-Signature-Prev durante la ventana para que los verificadores corriendo cualquiera de las claves sigan aceptando eventos mientras redespliegas).
  • Reveal Secret: muestra de nuevo el secreto existente. Protegido por verificación 2FA reciente y registrado en el log de auditoría; úsalo solo cuando hayas perdido tu copia y Rotate no sea aceptable.
  • Enable / Disable: cambia is_active sin perder el historial de entregas. Los endpoints deshabilitados permanecen en el dashboard pero no reciben nuevas entregas.
  • Delete: permanente. Usa Disable si podrías re-habilitar más tarde.

Consejos para handlers

  1. Devuelve 2xx rápido. Confirma con 200 OK antes de hacer trabajo pesado: lanza el fulfillment a una cola en background. El timeout por intento es de 10 segundos; retener la respuesta más allá de eso dispara un reintento. El timeout es del lado de la plataforma y no es configurable por comerciante: contacta con soporte si tu handler genuinamente necesita más tiempo.
  2. Deduplica por X-Delivery (o Idempotency-Key: mismo valor). Aunque devuelvas 2xx, un proxy aguas arriba podría soltar la conexión y disparar un reintento; el delivery ID es estable a través de cada reintento de la misma fila de entrega, así que es la clave correcta.
  3. Tolera tipos de evento desconocidos. Pueden aparecer nuevos eventos; devuelve 200 y no-op en lugar de 4xx, o llenarás la cola de reintentos.
  4. Loguea X-Delivery junto a tu lógica de negocio. Cuando algo va mal, esa es la clave de join entre nuestro lado y el tuyo.

Qué sigue