Skip to content

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 ​

EventoDescripciónAlcance
payment.successUn pago de un link de pago se completó exitosamentecuenta/proyecto
payment.failedUn intento de pago de un link terminó sin pagocuenta/proyecto
payment_link.rejectedNo se creó un link porque su monto es menor al mínimo de 1.00 USDproyecto
charge.succeededUn cargo fue aprobado y liquidadoproyecto
charge.failedUn cargo fue declinadoproyecto
charge.deferredUn cargo de addon menor a $1.00 se aprobó sin mover dinero y se difiere a la próxima facturaproyecto
charge.unknownUn cargo no pudo confirmarse dentro de la ventana de conciliación; requiere revisión manualproyecto
subscription.createdSe creó una suscripciónproyecto
subscription.resumedSe reactivó una suscripción o se quitó su cancelación programadaproyecto
subscription.past_dueUna renovación falló y se programó un reintentoproyecto
subscription.cancelledUna suscripción fue canceladaproyecto
subscription.billing_blockedEl cobro recurrente quedó bloqueado por una configuración del procesadorproyecto
subscription.items_updatedSe reemplazó el conjunto de ítems de una suscripciónproyecto
subscription.customer_updatedLa suscripción pasó a cobrarse a otro clienteproyecto
invoice.paidUna factura recurrente fue liquidadaproyecto
invoice.payment_failedEl cobro de una factura recurrente fue declinadoproyecto
customer.createdSe creó un cliente con su tarjeta guardadaproyecto
receipt.createdSe generó el comprobante PDF de un pagocuenta/proyecto
webhook.pingEvento de prueba enviado con Enviar ping desde el dashboardel 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:

CampoTipoDescripción
eventIdstringIdentificador único del evento: un UUID, igual al header Zeltapay-Event-Id. Úsalo para
typestringTipo del evento (ej. payment.success)
createdAtstringFecha y hora del evento en formato ISO 8601. En payment.success y payment.failed es el momento del pago o del rechazo
accountIdstringCuenta propietaria del recurso
projectIdstring | nullProyecto 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.success tiene un eventId estable por transacción: si Zelta Pay vuelve a enviar el payment.success del mismo pago, llega con el mismo eventId.
  • Los demás eventos reciben un eventId aleatorio 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.

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.

json
{
  "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"
  }
}
CampoDescripción
paymentLinkIdID del link que se pagó
transaction.externalPaymentIdID del pago en el procesador (con PayPal, el ID de la captura)
transaction.orderIdID de la orden de Zelta Pay para este pago. No es el orderId que pongas en tu metadata
transaction.amount / currencyMonto cobrado en centavos y moneda (USD)
transaction.paidAtMomento en que se completó el pago
transaction.concept / paidToConcepto del link y nombre del comercio
transaction.paymentMethodcredit_card, yappy o paypal
transaction.paymentProvidernmi, emetec, yappy o paypal
transaction.isTestSi el pago se hizo en modo de prueba
customer.customerNameNombre del cliente del link
customer.customerEmailEmail del cliente, o null si no se conoce
metadataMetadata 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.paymentMethod es "paypal" si el cliente pagó con su cuenta de PayPal, o "credit_card" si pagó con tarjeta sin cuenta de PayPal
  • transaction.externalPaymentId es el ID de la captura de PayPal: el ID del pago que ves en tu cuenta de PayPal y que usas para reembolsos
  • transaction.paidAt es el momento en que PayPal completó la captura
  • customer.customerEmail es 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
json
{
  "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 failureReason en 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
json
{
  "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.

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.

CampoTipoDescripción
reasonstringMotivo legible del rechazo
codestringCódigo de error normalizado — actualmente ERR_AMOUNT_BELOW_NMI_MINIMUM
amountintegerMonto rechazado en centavos
currencystringMoneda del monto (USD)
conceptstringConcepto enviado al intentar crear el link
metadataobjectMetadata enviada al intentar crear el link. Se omite si no se envió
json
{
  "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 un 202 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 amount es el total de la factura, quantity es 1 y productCode es 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.

json
{
  "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.

json
{
  "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.

failureCodeSignificado
duplicate_transactionEl 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:

CampoTipoDescripción
charge.amountintegerMonto diferido en centavos
charge.reasonstringExplicación de por qué se aprobó sin cobrar y se difirió
charge.deferredQuantitynumberCantidad extra que se suma al ítem del addon en la próxima factura (amount ÷ precio del addon); puede tener decimales
charge.subscriptionIdstringSuscripción a cuya próxima factura se difiere el cargo
json
{
  "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.

json
{
  "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.

json
{
  "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 y status es su estado actual (active o past_due)
  • Se reactiva una suscripción cancelled mientras aún está dentro de su período ya pagado: vuelve a active (status: "active") y el cobro se re-arma en currentPeriodEnd
json
{
  "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.

json
{
  "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.

json
{
  "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"
}
reasonSignificado
cancelled_by_requestCancelada de inmediato (vía PATCH /v1/subscriptions/{id}/cancel o desde el dashboard)
cancel_at_period_endCancelación programada que llegó al fin del período (sin cargo final)
dunning_exhaustedCancelada 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):

codeCausa
ERR_NO_ACTIVE_<PROVIDER>_<SANDBOX|PRODUCTION>_PAYMENT_PROVIDEREl 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_FAILEDNo se pueden leer las credenciales guardadas del procesador
ERR_INVALID_PAYMENT_PROVIDER, ERR_SANDBOX_PAYMENT_PROVIDER_NOT_FOUND, ERR_PRODUCTION_PAYMENT_PROVIDER_NOT_FOUNDEl procesador o sus credenciales se eliminaron mientras se preparaba el cobro
json
{
  "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.

json
{
  "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.

json
{
  "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).

json
{
  "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 ).

json
{
  "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 de payment.success y antes de charge.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).

json
{
  "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: paymentLinkId está presente; subscriptionId e invoiceId son null. Se genera después de payment.success, pero el orden de llegada no está garantizado (puede llegar antes si payment.success se reintenta)
  • Cargos por API (POST /v1/charges): no lleva paymentLinkId; subscriptionId e invoiceId son null
  • Cobros recurrentes de suscripciones: no lleva paymentLinkId; subscriptionId e invoiceId están presentes y coinciden con el invoice.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.

json
{
  "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.

json
{
  "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:

HeaderEjemploDescripción
Content-Typeapplication/jsonTipo de contenido del body
User-Agentzeltapay-webhook/1.0Identificador del servicio de webhooks
Zeltapay-Event-Id9b2f6c1e-3d4a-8f8b-9c2d-1a2b3c4d5e6fID del evento (igual al eventId del cuerpo); úsalo para
Zeltapay-Event-Typepayment.successTipo del evento
Zeltapay-Timestamp1710323400Timestamp Unix (segundos) del momento en que se firmó este intento de entrega; cambia en cada reintento
Zeltapay-Signaturet=1710323400, v1=5f2b...Firma HMAC-SHA256 para
Zeltapay-Delivery-Attempt1Nú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

Documentación oficial de Zelta