Eventos de Webhook
Un evento de webhook es una notificación que Zelta Pay envía a tu endpoint cuando ocurre algo relevante en tu cuenta o proyecto: un pago se completa, un cargo se declina, una suscripción se renueva, etc. Cada entrega es un POST con un cuerpo JSON, y el campo type identifica de qué evento se trata. Tu integración debe enrutar la lógica según ese type.
Esta página documenta los 19 tipos de evento que puede emitir Zelta Pay, con la estructura de su payload. No hay selección de eventos: tu webhook recibe todos los que le corresponden, así que ignora (respondiendo 2xx) los que no manejes.
Alcance de los eventos
Zelta Pay tiene dos destinos posibles de webhook:
- Webhook de la cuenta — recibe los eventos de los links de pago creados desde el dashboard o con la API key de la cuenta, y sirve de respaldo para algunos eventos de proyecto.
- Webhook del proyecto — cada proyecto de la puede tener un webhook, que recibe los eventos de sus links, cargos, suscripciones, facturas y clientes.
En la tabla resumen, la columna Alcance indica dónde se entrega cada evento:
- cuenta/proyecto — si el evento pertenece a un proyecto, va al webhook del proyecto; si el proyecto no tiene un webhook activo (o el evento no es de un proyecto), va al webhook de la cuenta.
- proyecto — solo se entrega al webhook del proyecto. Si el proyecto no tiene un webhook activo, el evento no se envía.
Resumen de eventos
| Evento | Descripción | Alcance |
|---|---|---|
payment.success | Un pago de un link de pago se completó exitosamente | cuenta/proyecto |
payment.failed | Un intento de pago de un link terminó sin pago | cuenta/proyecto |
payment_link.rejected | No se creó un link porque su monto es menor al mínimo de 1.00 USD | proyecto |
charge.succeeded | Un cargo fue aprobado y liquidado | proyecto |
charge.failed | Un cargo fue declinado | proyecto |
charge.deferred | Un cargo de addon menor a $1.00 se aprobó sin mover dinero y se difiere a la próxima factura | proyecto |
charge.unknown | Un cargo no pudo confirmarse dentro de la ventana de conciliación; requiere revisión manual | proyecto |
subscription.created | Se creó una suscripción | proyecto |
subscription.resumed | Se reactivó una suscripción o se quitó su cancelación programada | proyecto |
subscription.past_due | Una renovación falló y se programó un reintento | proyecto |
subscription.cancelled | Una suscripción fue cancelada | proyecto |
subscription.billing_blocked | El cobro recurrente quedó bloqueado por una configuración del procesador | proyecto |
subscription.items_updated | Se reemplazó el conjunto de ítems de una suscripción | proyecto |
subscription.customer_updated | La suscripción pasó a cobrarse a otro cliente | proyecto |
invoice.paid | Una factura recurrente fue liquidada | proyecto |
invoice.payment_failed | El cobro de una factura recurrente fue declinado | proyecto |
customer.created | Se creó un cliente con su tarjeta guardada | proyecto |
receipt.created | Se generó el comprobante PDF de un pago | cuenta/proyecto |
webhook.ping | Evento de prueba enviado con Enviar ping desde el dashboard | el webhook que pruebas |
TIP
Enruta siempre tu lógica por el campo type del payload (o por el header Zeltapay-Event-Type). Si recibes un type que aún no manejas, responde 200 e ignóralo: así tu endpoint sigue siendo compatible cuando se agreguen nuevos eventos.
Campos comunes
La mayoría de los eventos comparten un sobre común en la raíz del payload:
| Campo | Tipo | Descripción |
|---|---|---|
eventId | string | Identificador único del evento: un UUID, igual al header Zeltapay-Event-Id. Úsalo para |
type | string | Tipo del evento (ej. payment.success) |
createdAt | string | Fecha y hora del evento en formato ISO 8601. En payment.success y payment.failed es el momento del pago o del rechazo |
accountId | string | Cuenta propietaria del recurso |
projectId | string | null | Proyecto asociado. Solo puede ser null en payment.success, payment.failed y receipt.created, cuando el recurso no pertenece a un proyecto |
Dos excepciones al sobre común: webhook.ping no lleva accountId, projectId ni createdAt (usa sentAt), y receipt.created no incluye createdAt en la raíz (usa receipt.createdAt).
Los IDs (paymentLinkId, accountId, projectId, subscriptionId, customerId, invoiceId, etc.) son cadenas numéricas sin prefijo.
Sobre el eventId
payment.successtiene uneventIdestable por transacción: si Zelta Pay vuelve a enviar elpayment.successdel mismo pago, llega con el mismoeventId.- Los demás eventos reciben un
eventIdaleatorio cada vez que se generan. - Los reintentos automáticos de una entrega conservan siempre el mismo
eventId.
Montos
Todos los montos (amount, price, cycleTotal) van en centavos (enteros): divide entre 100 para obtener el valor en dólares. quantity y deferredQuantity pueden tener decimales en los casos que se indican más abajo.
Pagos de links
Eventos generados por los hospedados.
payment.success
Se envía cuando un cliente completa un pago exitosamente. Es el evento principal para actualizar el estado de tus órdenes.
{
"eventId": "9b2f6c1e-3d4a-8f8b-9c2d-1a2b3c4d5e6f",
"type": "payment.success",
"paymentLinkId": "7312345678901234567",
"accountId": "7298765432109876543",
"projectId": null,
"createdAt": "2026-03-13T10:30:00.000Z",
"transaction": {
"externalPaymentId": "ext_abc123",
"orderId": "7K2M9QX4LB-543",
"amount": 2500,
"currency": "USD",
"paidAt": "2026-03-13T10:30:00.000Z",
"concept": "Servicio de consultoría",
"paidTo": "Acme Inc",
"paymentMethod": "credit_card",
"paymentProvider": "nmi",
"isTest": false
},
"customer": {
"customerName": "Maria Garcia",
"customerEmail": "[email protected]"
},
"metadata": {
"orderId": "ORD-001",
"service": "consulting"
}
}| Campo | Descripción |
|---|---|
paymentLinkId | ID del link que se pagó |
transaction.externalPaymentId | ID del pago en el procesador (con PayPal, el ID de la captura) |
transaction.orderId | ID de la orden de Zelta Pay para este pago. No es el orderId que pongas en tu metadata |
transaction.amount / currency | Monto cobrado en centavos y moneda (USD) |
transaction.paidAt | Momento en que se completó el pago |
transaction.concept / paidTo | Concepto del link y nombre del comercio |
transaction.paymentMethod | credit_card, yappy o paypal |
transaction.paymentProvider | nmi, emetec, yappy o paypal |
transaction.isTest | Si el pago se hizo en modo de prueba |
customer.customerName | Nombre del cliente del link |
customer.customerEmail | Email del cliente, o null si no se conoce |
metadata | Metadata del link. Se omite si el link no tiene metadata: accede con cuidado (payload.metadata?.orderId) |
Pagos con PayPal
Cuando el cliente paga con , transaction.paymentProvider es "paypal" y:
transaction.paymentMethodes"paypal"si el cliente pagó con su cuenta de PayPal, o"credit_card"si pagó con tarjeta sin cuenta de PayPaltransaction.externalPaymentIdes el ID de la captura de PayPal: el ID del pago que ves en tu cuenta de PayPal y que usas para reembolsostransaction.paidAtes el momento en que PayPal completó la capturacustomer.customerEmailes el email fijado en el link; si no hay, el que el cliente escribió en la página de pago; si tampoco, el email de su cuenta de PayPal
{
"eventId": "6a1d0e7c-2b9f-8c3e-a4d5-7e8f9a0b1c2d",
"type": "payment.success",
"paymentLinkId": "7312345678901234567",
"accountId": "7298765432109876543",
"projectId": null,
"createdAt": "2026-09-28T15:04:10.000Z",
"transaction": {
"externalPaymentId": "3C679366HH908993F",
"orderId": "Q8W3N5ZR2T-543",
"amount": 4999,
"currency": "USD",
"paidAt": "2026-09-28T15:04:10.000Z",
"concept": "Orden #12345",
"paidTo": "Acme Inc",
"paymentMethod": "paypal",
"paymentProvider": "paypal",
"isTest": false
},
"customer": {
"customerName": "Maria Lopez",
"customerEmail": "[email protected]"
},
"metadata": {
"orderId": "ORD-12345"
}
}PayPal retiene algunos pagos para revisión antes de completarlos (desde minutos hasta varios días). Mientras tanto el link aparece en estado processing, y payment.success se envía cuando PayPal completa el pago.
payment.failed
Se envía cuando un intento de pago de un link termina sin pago:
- Tarjeta (NMI o Emetec): el pago fue rechazado, dio error o su resultado no pudo confirmarse; con Emetec, también cuando el checkout vence
- Yappy: el pago fue cancelado, venció o fue rechazado (
failureReason"Payment cancelled","Payment expired"o"Payment rejected") - PayPal: solo rechazos reales, con
failureReasonen el formato"PayPal: <motivo>"(por ejemplo"PayPal: INSTRUMENT_DECLINED"). No se envía cuando el cliente cierra la ventana de PayPal o abandona el pago: el link simplemente sigue disponible
{
"eventId": "2c4e6a8b-0d1f-4a3b-8c5d-7e9f1a2b3c4d",
"type": "payment.failed",
"paymentLinkId": "7312345678901234567",
"accountId": "7298765432109876543",
"projectId": null,
"createdAt": "2026-03-13T10:30:00.000Z",
"transaction": {
"orderId": "7K2M9QX4LB-543",
"amount": 2500,
"currency": "USD",
"failedAt": "2026-03-13T10:30:00.000Z",
"failureReason": "Duplicate transaction REFID:1010907601",
"failureCode": "duplicate_transaction",
"concept": "Servicio de consultoría",
"paidTo": "Acme Inc",
"paymentMethod": "credit_card",
"paymentProvider": "nmi",
"isTest": false
},
"customer": {
"customerName": "Maria Garcia",
"customerEmail": null
},
"metadata": {
"orderId": "ORD-001"
}
}El objeto transaction lleva failureReason (el texto literal del procesador) y, cuando el rechazo se mapea a una causa conocida, un failureCode normalizado (ver la ). No incluye externalPaymentId ni paidAt porque no se completó ningún pago. metadata se omite si el link no tiene metadata. Después de un payment.failed, el link sigue disponible para que el cliente lo intente de nuevo.
payment_link.rejected
Se envía al webhook del proyecto cuando la creación de un link de pago con la API key de un proyecto (POST /v1/payment-links o POST /v1/collections/payment-links) se rechaza antes de que el link exista, porque el monto es menor al mínimo de 1.00 USD (100 centavos), que aplica a todos los métodos de pago. No es una declinación de tarjeta: nunca se creó un link ni se intentó cobrar, por eso el payload no lleva paymentLinkId.
La propia solicitud también falla de inmediato con 400 y el código ERR_AMOUNT_BELOW_NMI_MINIMUM; el evento existe para que una integración que trabaja por webhooks también se entere de por qué no recibió un link. Con la API key de la cuenta no se envía este evento.
| Campo | Tipo | Descripción |
|---|---|---|
reason | string | Motivo legible del rechazo |
code | string | Código de error normalizado — actualmente ERR_AMOUNT_BELOW_NMI_MINIMUM |
amount | integer | Monto rechazado en centavos |
currency | string | Moneda del monto (USD) |
concept | string | Concepto enviado al intentar crear el link |
metadata | object | Metadata enviada al intentar crear el link. Se omite si no se envió |
{
"eventId": "5e7a9c1b-3d5f-4b7d-9e1f-3a5b7c9d1e2f",
"type": "payment_link.rejected",
"accountId": "7298765432109876543",
"projectId": "7301234567890123456",
"createdAt": "2026-03-13T10:30:00.000Z",
"reason": "El monto mínimo para un link de pago es 1.00 USD (100 centavos); no se puede generar el link de pago",
"code": "ERR_AMOUNT_BELOW_NMI_MINIMUM",
"amount": 50,
"currency": "USD",
"concept": "Recarga de créditos",
"metadata": {
"orderId": "ORD-001"
}
}Cargos
Eventos de los cargos de la . Se entregan siempre al webhook del proyecto.
charge.succeeded
Se envía cuando un cargo es aprobado:
- Un cargo por API (
POST /v1/charges), tanto si se aprobó en la respuesta como si se confirmó después de un202 processing - El pago de un link de tipo
customer_payment(el pago se registra como un cargo del producto del link) - Cada renovación de una suscripción. En ese caso
amountes el total de la factura,quantityes1yproductCodees el producto principal de la suscripción; usa para el desglose
Los cargos de addon incluyen el subscriptionId al que están ligados; los cargos independientes (ej. créditos) lo tienen en null.
{
"eventId": "8d0f2b4c-6e8a-4c1e-9f3b-5d7e9a1c3e5f",
"type": "charge.succeeded",
"accountId": "7298765432109876543",
"projectId": "7301234567890123456",
"createdAt": "2026-06-04T10:30:00.000Z",
"charge": {
"id": "7318888999900001111",
"orderId": "7K2M9QX4LB-543",
"amount": 2000,
"currency": "USD",
"quantity": 2,
"prorated": false,
"productCode": "prod_CREDITS100",
"customerId": "7315555666677778888",
"subscriptionId": null,
"externalChargeId": "ext_456",
"paidAt": "2026-06-04T10:30:00.000Z",
"isTest": false
},
"metadata": {
"orderId": "order-789"
}
}Cuando el cargo se creó con prorate: true, amount es el monto realmente cobrado (la fracción restante del período) y prorated es true. metadata se incluye solo cuando el cargo la llevó; puede faltar en un cargo que se confirmó después de responder 202.
charge.failed
Se envía cuando un cargo es declinado.
{
"eventId": "1a3c5e7a-9b1d-4f3a-8c5e-7a9b1c3d5e7f",
"type": "charge.failed",
"accountId": "7298765432109876543",
"projectId": "7301234567890123456",
"createdAt": "2026-06-04T10:30:00.000Z",
"charge": {
"id": "7318888999900001111",
"orderId": "7K2M9QX4LB-543",
"amount": 1000,
"currency": "USD",
"quantity": 2,
"prorated": true,
"productCode": "prod_ADDONSEAT1",
"customerId": "7315555666677778888",
"subscriptionId": "7316666777788889999",
"failureReason": "Duplicate transaction REFID:1010907601",
"failureCode": "duplicate_transaction",
"failedAt": "2026-06-04T10:30:00.000Z",
"isTest": false
}
}metadata se incluye en la raíz solo cuando el cargo la llevó.
Códigos de fallo
failureReason es el texto literal del procesador. Cuando la declinación se mapea a una causa conocida, el payload también trae un failureCode normalizado para que tu software reaccione de forma programática. El mismo campo aparece en invoice.payment_failed y payment.failed. Hoy solo las declinaciones de NMI se clasifican en un failureCode; las de otros procesadores llevan solo failureReason.
failureCode | Significado |
|---|---|
duplicate_transaction | El procesador rechazó el cobro por considerarlo un duplicado de uno reciente del mismo monto. La tarjeta no fue declinada: reintenta más tarde o trátalo como duplicado del intento anterior. |
Las declinaciones ordinarias de tarjeta no traen failureCode: usa failureReason para mostrar el motivo.
charge.deferred
Se envía cuando un POST /v1/charges de un addon da un monto final menor a $1.00 (precio × cantidad, después de la proración) para un cliente cuya tarjeta está guardada con NMI, que no procesa cargos por debajo de $1.00. Solo aplica a un addon que ya es un ítem de la suscripción activa del cliente. En lugar de cobrarlo por separado, Zelta Pay lo aprueba sin mover dinero y lo suma a la próxima factura de la suscripción. La respuesta de POST /v1/charges es 201 con status: "deferred", y no se genera comprobante.
El objeto charge es igual al de charge.succeeded pero sin externalChargeId ni paidAt, y con:
| Campo | Tipo | Descripción |
|---|---|---|
charge.amount | integer | Monto diferido en centavos |
charge.reason | string | Explicación de por qué se aprobó sin cobrar y se difirió |
charge.deferredQuantity | number | Cantidad extra que se suma al ítem del addon en la próxima factura (amount ÷ precio del addon); puede tener decimales |
charge.subscriptionId | string | Suscripción a cuya próxima factura se difiere el cargo |
{
"eventId": "3b5d7f9a-1c3e-4a5c-8e7f-9a1b3c5d7e9f",
"type": "charge.deferred",
"accountId": "7298765432109876543",
"projectId": "7301234567890123456",
"createdAt": "2026-06-28T10:30:00.000Z",
"charge": {
"id": "7318888999900001111",
"orderId": "7K2M9QX4LB-543",
"amount": 80,
"currency": "USD",
"quantity": 1,
"prorated": true,
"productCode": "prod_ADDONUSER1",
"customerId": "7315555666677778888",
"subscriptionId": "7316666777788889999",
"reason": "El monto (0.80 USD) es menor al mínimo de NMI (1.00); se aprobó sin cobrar y se difirió al próximo cobro de la suscripción.",
"deferredQuantity": 0.16,
"isTest": false
}
}En este ejemplo el addon cuesta $5.00 (500 centavos): 80 ÷ 500 = 0.16. metadata se incluye solo cuando el cargo la llevó.
charge.unknown
Se envía cuando un cargo que devolvió 202 processing no pudo resolverse dentro de la ventana de conciliación (unos 2 minutos). Es la señal de que no llegará ningún resultado adicional para este cargo: no seguirá un charge.succeeded ni un charge.failed. El pago puede o no haberse capturado en el procesador, así que el cargo se marca para revisión manual; nunca lo trates como una declinación limpia.
{
"eventId": "7f9b1d3e-5a7c-4e9b-8d1f-3e5a7c9b1d3f",
"type": "charge.unknown",
"accountId": "7298765432109876543",
"projectId": "7301234567890123456",
"createdAt": "2026-06-04T10:30:00.000Z",
"charge": {
"id": "7318888999900001111",
"orderId": "7K2M9QX4LB-543",
"amount": 2000,
"currency": "USD",
"quantity": 2,
"prorated": false,
"productCode": "prod_CREDITS100",
"customerId": "7315555666677778888",
"subscriptionId": null,
"failureReason": "Gateway outcome unknown after the reconciliation window; manual review required",
"isTest": false
}
}A diferencia de charge.succeeded / charge.failed, este evento nunca incluye metadata en la raíz.
WARNING
Al recibir charge.unknown, deja de esperar un veredicto para ese orderId / id. No reintentes ciegamente con el mismo idempotencyKey (el cargo original pudo haberse liquidado de verdad): contacta a soporte o revisa el cargo manualmente. El cargo también aparece como unknown en GET /v1/charges/{id}.
Suscripciones
Eventos del ciclo de vida de las suscripciones de la API de Integración. Se entregan siempre al webhook del proyecto.
subscription.created
Se envía cuando se crea una suscripción (POST /v1/subscriptions). No hay cobro al crearla: el primer cargo recurrente corre en currentPeriodEnd. items lleva el desglose con precios y cycleTotal es el monto por ciclo en centavos. productId y productCode corresponden al primer ítem.
{
"eventId": "0c2e4a6c-8e0a-4c2e-9a4c-6e8a0c2e4a6c",
"type": "subscription.created",
"accountId": "7298765432109876543",
"projectId": "7301234567890123456",
"createdAt": "2026-06-04T10:30:00.000Z",
"subscriptionId": "7316666777788889999",
"customerId": "7315555666677778888",
"productId": "7314444555566667777",
"productCode": "prod_PLANBASE01",
"items": [
{ "productCode": "prod_PLANBASE01", "name": "Plan base", "price": 3500, "quantity": 1 },
{ "productCode": "prod_SUCURSAL01", "name": "Sucursal adicional", "price": 2000, "quantity": 1 }
],
"cycleTotal": 5500,
"status": "active",
"currentPeriodStart": "2026-06-04T00:00:00.000Z",
"currentPeriodEnd": "2026-07-04T00:00:00.000Z",
"nextBillingAt": "2026-07-04T00:00:00.000Z"
}subscription.resumed
Se envía cuando PATCH /v1/subscriptions/{id}/reactivate tiene éxito, en dos casos:
- Se quita una cancelación programada (
cancelAtPeriodEnd): la suscripción sigue corriendo ystatuses su estado actual (activeopast_due) - Se reactiva una suscripción
cancelledmientras aún está dentro de su período ya pagado: vuelve aactive(status: "active") y el cobro se re-arma encurrentPeriodEnd
{
"eventId": "4d6f8b0d-2f4b-4d6f-8b0d-2f4b6d8f0b2d",
"type": "subscription.resumed",
"accountId": "7298765432109876543",
"projectId": "7301234567890123456",
"createdAt": "2026-06-20T10:30:00.000Z",
"subscriptionId": "7316666777788889999",
"status": "active"
}subscription.past_due
Se envía cuando una renovación falla y se agenda un reintento. attempts es el número de intentos fallidos hasta ahora (de 1 a 4); nextBillingAt es cuándo corre el siguiente reintento (días +1/+3/+5/+7 desde la fecha de vencimiento original). Si falla el quinto intento, la suscripción se cancela en lugar de enviar este evento.
{
"eventId": "6e8a0c2e-4a6c-4e8a-9c2e-4a6c8e0a2c4e",
"type": "subscription.past_due",
"accountId": "7298765432109876543",
"projectId": "7301234567890123456",
"createdAt": "2026-07-04T00:00:05.000Z",
"subscriptionId": "7316666777788889999",
"attempts": 1,
"nextBillingAt": "2026-07-05T00:00:00.000Z"
}subscription.cancelled
Se envía cuando una suscripción es cancelada. reason distingue cómo terminó; attempts solo está presente para dunning_exhausted. Una cancelación cancel_at_period_end no produce un cargo final: el período siguiente no se cobra. Programar la cancelación al fin del período no envía ningún evento en ese momento; subscription.cancelled llega cuando el período termina.
{
"eventId": "8f0b2d4f-6b8d-4f0b-8d2f-6b8d0f2b4d6f",
"type": "subscription.cancelled",
"accountId": "7298765432109876543",
"projectId": "7301234567890123456",
"createdAt": "2026-07-04T00:00:00.000Z",
"subscriptionId": "7316666777788889999",
"reason": "cancel_at_period_end"
}reason | Significado |
|---|---|
cancelled_by_request | Cancelada de inmediato (vía PATCH /v1/subscriptions/{id}/cancel o desde el dashboard) |
cancel_at_period_end | Cancelación programada que llegó al fin del período (sin cargo final) |
dunning_exhausted | Cancelada tras agotarse los reintentos de cobro (7 días); incluye attempts |
subscription.billing_blocked
Se envía cuando una renovación no pudo cobrarse por una configuración del lado del comercio: el procesador que guarda la tarjeta del cliente está deshabilitado o fue eliminado, o no se pueden leer sus credenciales. El cargo nunca llegó al procesador. No es una declinación del cliente: no se inician reintentos de cobro y el estado de la suscripción no cambia. El cobro se pospone y se espera que el comercio corrija el procesador; mientras el problema persista, el evento se repite periódicamente, con un eventId nuevo cada vez.
Usa reason: "provider_misconfigured" para identificarlo. paymentProvider es nmi o emetec. code es el código de error que clasificó el fallo (trátalo como diagnóstico):
code | Causa |
|---|---|
ERR_NO_ACTIVE_<PROVIDER>_<SANDBOX|PRODUCTION>_PAYMENT_PROVIDER | El procesador que guarda la tarjeta no tiene credenciales utilizables en el entorno de la suscripción. <PROVIDER> es NMI o EMETEC, por ejemplo ERR_NO_ACTIVE_NMI_PRODUCTION_PAYMENT_PROVIDER |
ERR_CREDENTIALS_DECRYPT_PARSE_FAILED | No se pueden leer las credenciales guardadas del procesador |
ERR_INVALID_PAYMENT_PROVIDER, ERR_SANDBOX_PAYMENT_PROVIDER_NOT_FOUND, ERR_PRODUCTION_PAYMENT_PROVIDER_NOT_FOUND | El procesador o sus credenciales se eliminaron mientras se preparaba el cobro |
{
"eventId": "1b3d5f7b-9d1f-4b3d-8f5b-9d1f3b5d7f9b",
"type": "subscription.billing_blocked",
"accountId": "7298765432109876543",
"projectId": "7301234567890123456",
"createdAt": "2026-07-04T00:00:05.000Z",
"subscriptionId": "7316666777788889999",
"invoiceId": "7317777888899990000",
"reason": "provider_misconfigured",
"paymentProvider": "nmi",
"code": "ERR_NO_ACTIVE_NMI_PRODUCTION_PAYMENT_PROVIDER"
}subscription.items_updated
Se envía cuando se reemplaza el conjunto de ítems de una suscripción (PATCH /v1/subscriptions/{id}/items). La nueva composición toma efecto en effectiveAt (el próximo ciclo de cobro); cycleTotal es el nuevo monto por ciclo en centavos.
{
"eventId": "3c5e7a9c-1e3a-4c5e-9a7c-1e3a5c7e9a1c",
"type": "subscription.items_updated",
"accountId": "7298765432109876543",
"projectId": "7301234567890123456",
"createdAt": "2026-06-04T10:30:00.000Z",
"subscriptionId": "7316666777788889999",
"items": [
{ "productCode": "prod_PLANBASE01", "name": "Plan base", "price": 3500, "quantity": 1 },
{ "productCode": "prod_SUCURSAL01", "name": "Sucursal adicional", "price": 2000, "quantity": 2 },
{ "productCode": "prod_USUARIO001", "name": "Usuario adicional", "price": 500, "quantity": 2 }
],
"cycleTotal": 8500,
"effectiveAt": "2026-07-04T00:00:00.000Z"
}subscription.customer_updated
Se envía cuando una suscripción pasa a cobrarse a otro cliente con tarjeta guardada (PATCH /v1/subscriptions/{id}/customer), es decir, el software cambió el método de pago activo. Las facturas futuras y los reintentos de cobro usan la tarjeta del nuevo cliente; no se cobra nada al momento del cambio. No se envía si el cliente no cambia.
{
"eventId": "5d7f9b1d-3f5b-4d7f-8b1d-3f5b7d9f1b3d",
"type": "subscription.customer_updated",
"accountId": "7298765432109876543",
"projectId": "7301234567890123456",
"createdAt": "2026-06-04T10:30:00.000Z",
"subscriptionId": "7316666777788889999",
"customerId": "7319999000011112222",
"previousCustomerId": "7315555666677778888"
}Facturas
Eventos de las facturas recurrentes generadas por las suscripciones. Se entregan siempre al webhook del proyecto.
Campo items
En ambos eventos de factura, items lleva la instantánea congelada de la factura ([{ productCode, name, price, quantity }]). El campo es condicional: se omite en facturas antiguas de un solo producto que no tienen instantánea. quantity puede tener decimales cuando la factura incluye un . En invoice.payment_failed, failureCode también es opcional (solo lo emite NMI; ver ).
invoice.paid
Se envía cuando un cargo recurrente tiene éxito y su factura queda liquidada. items refleja la instantánea congelada de la factura, por lo que las líneas coinciden con el amount cobrado (en centavos).
{
"eventId": "7e9a1c3e-5a7c-4e9a-8c3e-5a7c9e1a3c5e",
"type": "invoice.paid",
"accountId": "7298765432109876543",
"projectId": "7301234567890123456",
"createdAt": "2026-07-04T00:00:05.000Z",
"subscriptionId": "7316666777788889999",
"invoiceId": "7317777888899990000",
"amount": 5500,
"currency": "USD",
"items": [
{ "productCode": "prod_PLANBASE01", "name": "Plan base", "price": 3500, "quantity": 1 },
{ "productCode": "prod_SUCURSAL01", "name": "Sucursal adicional", "price": 2000, "quantity": 1 }
],
"externalChargeId": "ext_456"
}TIP
Un cargo de suscripción también emite un evento charge.succeeded (con el mismo subscriptionId), de modo que las integraciones que registran cargos o comprobantes a partir de charge.succeeded también capturan el cobro recurrente.
invoice.payment_failed
Se envía cuando un cargo recurrente es declinado. La factura queda en open para que el reintento cobre el mismo período; en el último intento, la factura se marca como fallida y le sigue un subscription.cancelled. failureReason es el texto literal del procesador; failureCode es la causa normalizada opcional (ver ).
{
"eventId": "9f1b3d5f-7b9d-4f1b-8d5f-7b9d1f3b5d7f",
"type": "invoice.payment_failed",
"accountId": "7298765432109876543",
"projectId": "7301234567890123456",
"createdAt": "2026-07-04T00:00:05.000Z",
"subscriptionId": "7316666777788889999",
"invoiceId": "7317777888899990000",
"amount": 5500,
"currency": "USD",
"items": [
{ "productCode": "prod_PLANBASE01", "name": "Plan base", "price": 3500, "quantity": 1 }
],
"failureReason": "Card declined"
}Clientes
customer.created
Se envía al webhook del proyecto cuando se crea un cliente con su tarjeta guardada:
- Cuando un cliente guarda su tarjeta a través de un link de colección (
POST /v1/collections/links) - Cuando un cliente paga un link de tipo
customer_payment(POST /v1/collections/payment-links). En ese caso llega después depayment.successy antes decharge.succeeded
El objeto card lleva datos de la tarjeta solo para mostrar, para que tu software los almacene; los cargos nunca usan estos campos. email y cada campo de card pueden ser null. paymentProvider es el procesador que guarda la tarjeta (nmi o emetec).
{
"eventId": "2a4c6e8a-0c2e-4a4c-9e8a-0c2e4a6c8e0a",
"type": "customer.created",
"accountId": "7298765432109876543",
"projectId": "7301234567890123456",
"createdAt": "2026-06-04T10:30:00.000Z",
"customer": {
"id": "7315555666677778888",
"name": "John Doe",
"email": "[email protected]",
"isTest": false,
"paymentProvider": "nmi",
"card": {
"bin": "411111",
"exp": "1028",
"last4": "1111",
"brand": "visa"
}
},
"metadata": {
"userId": "usr_123"
}
}Comprobantes
receipt.created
Se envía cuando se genera el comprobante PDF de un pago completado. Cada pago exitoso produce un comprobante (tenga o no email el cliente):
- Pagos de links:
paymentLinkIdestá presente;subscriptionIdeinvoiceIdsonnull. Se genera después depayment.success, pero el orden de llegada no está garantizado (puede llegar antes sipayment.successse reintenta) - Cargos por API (
POST /v1/charges): no llevapaymentLinkId;subscriptionIdeinvoiceIdsonnull - Cobros recurrentes de suscripciones: no lleva
paymentLinkId;subscriptionIdeinvoiceIdestán presentes y coinciden con elinvoice.paid
Los cargos diferidos (charge.deferred) no generan comprobante. projectId, subscriptionId e invoiceId siempre están presentes (null cuando no aplican); paymentLinkId se omite cuando el comprobante no viene de un link. Este evento no lleva createdAt en la raíz: usa receipt.createdAt.
{
"eventId": "4b6d8f0b-2d4f-4b6d-8f0b-2d4f6b8d0f2b",
"type": "receipt.created",
"paymentTransactionId": "7318888999900001111",
"paymentLinkId": "7312345678901234567",
"accountId": "7298765432109876543",
"projectId": null,
"subscriptionId": null,
"invoiceId": null,
"orderId": "7K2M9QX4LB-543",
"receipt": {
"fileUrl": "https://cdn-zeltapay.zelta.dev/receipts/7298765432109876543/2026/06/1780569005123_V1StGXR8Z5jdHi6BmyTkq3LpN0aWcF9u.pdf",
"originalName": "zeltapay-comprobante-7K2M9QX4LB-543.pdf",
"mimeType": "application/pdf",
"size": 24576,
"createdAt": "2026-06-04T10:30:05.000Z"
}
}receipt.fileUrl es la URL pública del PDF y receipt.size es su tamaño en bytes.
Prueba
webhook.ping
Se envía cuando haces clic en Enviar ping en la página Webhooks del dashboard. Sirve para confirmar que tu URL es alcanzable y responde con un 2xx. Se firma y se reintenta igual que cualquier otro evento, y solo se puede enviar a un webhook activo. Lleva sentAt en lugar de createdAt.
{
"eventId": "6c8e0a2c-4e6a-4c8e-9a2c-4e6a8c0e2a4c",
"type": "webhook.ping",
"ok": true,
"message": "Hello from Zeltapay! Your endpoint is reachable.",
"sentAt": "2026-03-13T10:00:00.000Z",
"description": "This is a ping payload sent to verify your webhook endpoint. If you received this with a 2xx status, everything is working."
}Headers de entrega
Cada entrega de webhook incluye estos headers HTTP:
| Header | Ejemplo | Descripción |
|---|---|---|
Content-Type | application/json | Tipo de contenido del body |
User-Agent | zeltapay-webhook/1.0 | Identificador del servicio de webhooks |
Zeltapay-Event-Id | 9b2f6c1e-3d4a-8f8b-9c2d-1a2b3c4d5e6f | ID del evento (igual al eventId del cuerpo); úsalo para |
Zeltapay-Event-Type | payment.success | Tipo del evento |
Zeltapay-Timestamp | 1710323400 | Timestamp Unix (segundos) del momento en que se firmó este intento de entrega; cambia en cada reintento |
Zeltapay-Signature | t=1710323400, v1=5f2b... | Firma HMAC-SHA256 para |
Zeltapay-Delivery-Attempt | 1 | Número del intento de entrega |
Verifica siempre la firma
La firma HMAC-SHA256 se calcula sobre el string t=<timestamp>.<body>, donde el prefijo t= forma parte del contenido firmado y <body> es el cuerpo JSON crudo tal como se recibió. Verifica la firma antes de procesar cualquier evento. Consulta la para la implementación completa.
Siguientes pasos
- -- Guía general para configurar webhooks
- -- Verificar la autenticidad de cada entrega
- -- Reintentos, timeouts y entregas descartadas
- -- Evitar procesamiento duplicado