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 campoevent_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 y reintento. Un endpoint lento no retrasa a los otros.
- Firmado. Cada payload lleva una cabecera
X-Signature(y durante la ventana de 24 horas tras una rotación, también unaX-Signature-Prev). Verifica antes de hacer nada con el body. Consulta Verificación de firma.
Tipos de evento suscribibles
| Evento | Se dispara cuando… |
|---|---|
payment.settled | La transferencia on-chain alcanzó el conteo de confirmaciones de la cadena. Usa esto para marcar órdenes como pagadas. |
payment.failed | Un pago fiat fue rechazado por el proveedor de pagos. No se dispara para timeouts cripto: esos aparecen como checkout.expired, y los pagos cripto cortos aparecen como payment.underpaid. |
payment.underpaid | Los fondos llegaron pero por debajo del total de la orden (típico: tarifa de transferencia de stablecoin tomada del importe). |
payment.overpaid | Los fondos llegaron en exceso del total de la orden. El sobrante se registra pero no se auto-reembolsa. |
order.created | Se abrió una nueva orden: o por tu llamada a la API B2B o por una conversión de sesión de checkout. |
order.canceled | Una orden se movió a cancelada. El data.reason del payload distingue cancelación manual de payment_timeout (una orden no pagada expiró). |
order.resolved | Una orden PARTIAL_PAID se resolvió a PAID: el comerciante aceptó el déficit. |
order.reopened | Una orden previamente autocancelada (canceled_reason=payment_timeout) fue reabierta por el comerciante. |
checkout.created | Un comprador abrió el checkout para una orden. |
checkout.completed | El flujo del lado comprador terminó (no implica liquidación on-chain: usa payment.settled para eso). |
checkout.expired | El comprador abandonó y el TTL de la sesión se agotó. |
payment.refund.requested | Se 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.approved | Un reembolso pendiente pasó tu flujo de aprobación. |
payment.refund.rejected | Un reembolso pendiente fue denegado. |
payment.refund.executed | La transferencia on-chain del reembolso se confirmó y el registro se movió a executed terminal. |
refund_request.created | Se 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_requested | Un 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.renewed | Una 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.canceled | Un 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. |
Planificados (próximamente)
Próximamente. Estos eventos pertenecen a las facturas recurrentes y las suscripciones, que aún no están disponibles. No están en la tabla de eventos suscribibles de arriba y hoy no se pueden suscribir. Ver Facturas recurrentes.
| Evento planificado | Se dispara cuando… |
|---|---|
subscription.created | Se crea una suscripción. |
invoice.created | Se crea una factura para un ciclo de facturación. |
invoice.paid | Se paga una factura. |
subscription.past_due | Una factura sigue impaga pasada su fecha de vencimiento. |
subscription.canceled | Se cancela una suscripción. |
El formulario de endpoints del dashboard lista los mismos eventos. Suscribirse a un evento que no existe se rechaza al guardar el endpoint.
Los eventos de prueba no son suscribibles. El botón
Enviar prueba por endpoint del dashboard envía un evento
webhook.test.ping a ese único endpoint de inmediato, sin
reintentos. No aparece en el catálogo de arriba: lo 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 simple y significa menos reintentos
cuando tu endpoint tiene 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 sus propios campos. Los nombres
de campo son estables (lower snake_case); el tx hash on-chain es
siempre tx_hash.
tx_hash es el identificador de la transacción en el formato propio de la red (0x… en cadenas EVM; el hash o la firma nativos en TRON, Solana y TON). deposit_address puede estar ausente en TRON, Solana y TON, porque allí los compradores pagan directamente a tu wallet de Tesorería, y confirmations sigue Cadenas y activos.
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)| Cabecera | Qué es |
|---|---|
X-Event | El tipo de evento (p. ej. payment.settled). Enruta sobre esto en la capa proxy si quieres saltarte el parsing JSON. |
X-Delivery | UUID 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-Key | Refleja X-Delivery (mismo valor). Se establece en cada entrega. |
X-Timestamp | Unix-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-Signature | sha256=<hex> de HMAC-SHA256(secret, X-Timestamp + "." + raw_body). Consulta Verificación de firma. |
X-Signature-Prev | Mismo 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):
| Intento | Retraso | Acumulado |
|---|---|---|
| 1 | 0s | 0s |
| 2 | +1 min | 1m |
| 3 | +5 min | 6m |
| 4 | +15 min | 21m |
| 5 | +1 hora | 1h 21m |
| 6 | +6 horas | 7h 21m |
Tras fallar el intento 6, la entrega se marca como Failed y
se notifica al email de tu cuenta. Puedes reproducir los eventos
fallidos desde el panel Developers →
Webhooks → Delivery history del dashboard. Cada replay es una
nueva entrega con su propio X-Delivery.
Registra un endpoint
Desde el dashboard del comerciante :
- Developers → Webhooks → + Add endpoint
- Pega tu URL: solo
https://…(HTTP plano se rechaza; el formulario de creación también bloquealocalhost, rangos de IP privadas y URLs que lleven userinfo) - Elige eventos a los que suscribirte (o
*para todos) - Elige entorno: test o live (cada uno tiene su propio secreto; nunca se cruzan)
- 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 tiene su propio estado de reintento y secreto.
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.pingfirmado con tu secreto actual. El dashboard muestra el status HTTP, latencia y un fragmento de 512 bytes de tu respuesta. Los pings de prueba no se reintentan, así que la respuesta es inmediata. - Rotate Secret: genera un nuevo secreto. El anterior sigue
válido por 24 horas (las entregas llevan tanto
X-SignaturecomoX-Signature-Prevdurante 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: activa o desactiva el endpoint 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
- Devuelve 2xx rápido. Confirma con
200 OKantes de hacer trabajo pesado: pasa el fulfillment a un job 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. - Deduplica por
X-Delivery(oIdempotency-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. - Tolera tipos de evento desconocidos. Pueden aparecer nuevos eventos; devuelve 200 y no-op en lugar de 4xx, o esas entregas seguirán reintentándose.
- Loguea
X-Deliveryjunto a tu lógica de negocio. Cuando algo va mal, esa es la clave de join entre nuestro lado y el tuyo.
Qué sigue
- Verificación de firma: el algoritmo exacto + patrones de protección contra replay.
- Conceptos → Sesiones: en qué estado está una sesión cuando cada evento se dispara.