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.

Alcance de los eventos

Zelta Pay tiene dos destinos posibles de webhook:

  • Webhook de la cuenta — recibe los eventos generales de los links de pago de tu cuenta.
  • Webhook del proyecto — cada proyecto de la tiene exactamente un webhook, que recibe los eventos de cargos, suscripciones, facturas y clientes.

En la tabla resumen, la columna Alcance indica dónde se entrega cada evento:

  • cuenta/proyecto — puede entregarse al webhook de la cuenta o al del proyecto, según dónde se originó el link.
  • proyecto — solo se entrega al webhook del proyecto (eventos propios de la API de Integración).

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 fue declinadocuenta/proyecto
payment_link.rejectedEl link fue rechazado antes de cobrar (monto bajo el mínimo del procesador)proyecto
charge.succeededUn cargo por API fue aprobado y liquidadoproyecto
charge.failedUn cargo por API fue declinadoproyecto
charge.deferredUn addon menor a $1 en NMI se aprobó sin mover dinero; se difiere a la próxima facturaproyecto
charge.unknownLa reconciliación de un cargo expiró sin respuesta definitiva; requiere revisión manualproyecto
subscription.createdSe creó una suscripciónproyecto
subscription.resumedUna suscripción cancelada fue reactivadaproyecto
subscription.past_dueUna renovación falló y la suscripción entró en dunningproyecto
subscription.cancelledUna suscripción fue canceladaproyecto
subscription.billing_blockedEl cobro recurrente quedó bloqueado por mala configuración del procesadorproyecto
subscription.items_updatedSe reemplazó el conjunto de ítems de una suscripciónproyecto
subscription.customer_updatedLa suscripción se re-apuntó a otro cliente vaulteadoproyecto
invoice.paidUna factura recurrente fue liquidadaproyecto
invoice.payment_failedEl cobro de una factura recurrente fue declinadoproyecto
customer.createdSe creó un cliente (tarjeta vaulteada) desde un link de colecciónproyecto
receipt.createdSe generó el comprobante PDF de un pagocuenta/proyecto
webhook.pingEvento de prueba enviado desde el dashboardn/a

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. Úsalo para
typestringTipo del evento (ej. payment.success)
createdAtstringFecha y hora del evento en formato ISO 8601
accountIdstringCuenta propietaria del recurso
projectIdstring | nullProyecto asociado, o null cuando el recurso no es de un proyecto

Montos y estados

Todos los montos (amount, price, cycleTotal) van en centavos (enteros): divide entre 100 para obtener el valor en dólares. Los estados de transacción posibles en los payloads son: pending, processing, paid, failed, unknown y deferred.

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": "evt_1234567890abcdef",
  "type": "payment.success",
  "paymentLinkId": "pl_1234567890abcdef",
  "accountId": "acc_1234567890abcdef",
  "projectId": null,
  "createdAt": "2026-03-13T10:30:00.000Z",
  "transaction": {
    "externalPaymentId": "ext_abc123",
    "orderId": "ORD-12345-xyz",
    "amount": 2500,
    "currency": "USD",
    "paidAt": "2026-03-13T10:30:00.000Z",
    "paymentMethod": "credit_card",
    "paymentProvider": "nmi",
    "concept": "Servicio de consultoría",
    "paidTo": "Acme Inc",
    "isTest": false
  },
  "customer": {
    "customerName": "Maria Garcia",
    "customerEmail": "maria@ejemplo.com"
  },
  "metadata": {
    "orderId": "ORD-001",
    "service": "consulting"
  }
}

projectId está siempre presente en la raíz: lleva el ID del proyecto cuando el link se creó con una API key de proyecto, y es null en caso contrario. customer.customerEmail puede ser null si el email del cliente no se conoce.

payment.failed

Se envía cuando un intento de pago es declinado.

json
{
  "eventId": "evt_1234567890abcdef",
  "type": "payment.failed",
  "paymentLinkId": "pl_1234567890abcdef",
  "accountId": "acc_1234567890abcdef",
  "projectId": null,
  "createdAt": "2026-03-13T10:30:00.000Z",
  "transaction": {
    "orderId": "ORD-12345-xyz",
    "amount": 2500,
    "currency": "USD",
    "failedAt": "2026-03-13T10:30:00.000Z",
    "failureReason": "Duplicate transaction REFID:1010907601",
    "failureCode": "duplicate_transaction",
    "paymentMethod": "credit_card",
    "paymentProvider": "nmi",
    "concept": "Servicio de consultoría",
    "paidTo": "Acme Inc",
    "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.

Se envía al webhook del proyecto cuando un link es rechazado antes de intentar cobrar, porque el monto está por debajo del mínimo que acepta el procesador (por ejemplo, menos de $1.00 en NMI). No es una declinación de tarjeta: el cobro nunca llegó al gateway.

Payload descrito de forma conservadora

Este evento no forma parte todavía de la referencia pública detallada de payloads. Lleva el sobre común del evento junto con la identidad del link de pago (paymentLinkId) y una indicación del motivo del rechazo. Los nombres exactos de los campos adicionales pueden variar; enruta por type y trata este evento como "el link no se pudo cobrar por monto inválido".

json
{
  "eventId": "evt_1234567890abcdef",
  "type": "payment_link.rejected",
  "paymentLinkId": "pl_1234567890abcdef",
  "accountId": "acc_1234567890abcdef",
  "projectId": "prj_1234567890abcdef",
  "createdAt": "2026-03-13T10:30:00.000Z",
  "reason": "amount_below_minimum"
}

Cargos

Eventos generados por los cargos de la (POST /v1/charges). Se entregan siempre al webhook del proyecto.

charge.succeeded

Se envía cuando un cargo por API es aprobado, y también cuando un cliente paga un link de tipo customer_payment ligado a un producto (el pago resultante se registra como un cargo). Los cargos de addon incluyen el subscriptionId al que están ligados; los cargos independientes (ej. créditos) lo tienen en null.

json
{
  "eventId": "evt_1234567890abcdef",
  "type": "charge.succeeded",
  "accountId": "acc_1234567890abcdef",
  "projectId": "prj_1234567890abcdef",
  "createdAt": "2026-06-04T10:30:00.000Z",
  "charge": {
    "id": "txn_1234567890abcdef",
    "orderId": "ORD-12345-abc",
    "amount": 2000,
    "currency": "USD",
    "quantity": 2,
    "prorated": false,
    "productCode": "prod_credits100",
    "customerId": "cus_1234567890abcdef",
    "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.

charge.failed

Se envía cuando un cargo por API es declinado.

json
{
  "eventId": "evt_1234567890abcdef",
  "type": "charge.failed",
  "accountId": "acc_1234567890abcdef",
  "projectId": "prj_1234567890abcdef",
  "createdAt": "2026-06-04T10:30:00.000Z",
  "charge": {
    "id": "txn_1234567890abcdef",
    "orderId": "ORD-12345-abc",
    "amount": 1000,
    "currency": "USD",
    "quantity": 2,
    "prorated": true,
    "productCode": "prod_addon_seats",
    "customerId": "cus_1234567890abcdef",
    "subscriptionId": "sub_1234567890abcdef",
    "failureReason": "Duplicate transaction REFID:1010907601",
    "failureCode": "duplicate_transaction",
    "failedAt": "2026-06-04T10:30:00.000Z",
    "isTest": false
  },
  "metadata": {}
}

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_transactionLa ventana de transacciones duplicadas del gateway rechazó un cobro del mismo monto repetido en ~20 minutos. 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 addon que cuesta menos de $1.00 se procesa contra una suscripción en NMI, cuyo gateway rechaza cualquier cargo bajo su piso de $1.00. En lugar de cobrarlo por separado, Zelta Pay lo aprueba sin mover dinero y difiere su monto a la próxima factura de la suscripción como un recargo de un solo ciclo.

Payload descrito de forma conservadora

Este evento no forma parte todavía de la referencia pública detallada de payloads. Lleva el sobre común del evento y un objeto charge con el estado deferred, el monto diferido y la suscripción ligada. Los nombres exactos de los campos adicionales pueden variar; interpreta este evento como "el addon se registró pero se cobrará en la siguiente factura".

json
{
  "eventId": "evt_1234567890abcdef",
  "type": "charge.deferred",
  "accountId": "acc_1234567890abcdef",
  "projectId": "prj_1234567890abcdef",
  "createdAt": "2026-06-04T10:30:00.000Z",
  "charge": {
    "id": "txn_1234567890abcdef",
    "amount": 83,
    "currency": "USD",
    "status": "deferred",
    "productCode": "prod_addon_micro",
    "customerId": "cus_1234567890abcdef",
    "subscriptionId": "sub_1234567890abcdef",
    "isTest": false
  }
}

charge.unknown

Se envía cuando un cargo que devolvió 202 processing no pudo resolverse dentro de la ventana de reconciliación (~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 gateway, así que el cargo se marca para revisión manual; nunca lo trates como una declinación limpia.

json
{
  "eventId": "evt_1234567890abcdef",
  "type": "charge.unknown",
  "accountId": "acc_1234567890abcdef",
  "projectId": "prj_1234567890abcdef",
  "createdAt": "2026-06-04T10:30:00.000Z",
  "charge": {
    "id": "txn_1234567890abcdef",
    "orderId": "ORD-12345-abc",
    "amount": 2000,
    "currency": "USD",
    "quantity": 2,
    "prorated": false,
    "productCode": "prod_credits100",
    "customerId": "cus_1234567890abcdef",
    "subscriptionId": null,
    "failureReason": "Gateway outcome unknown after the reconciliation window; manual review required",
    "isTest": false
  },
  "metadata": {}
}

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): reconcilia contra el gateway o contacta a soporte. 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.

json
{
  "eventId": "evt_1234567890abcdef",
  "type": "subscription.created",
  "accountId": "acc_1234567890abcdef",
  "projectId": "prj_1234567890abcdef",
  "createdAt": "2026-06-04T10:30:00.000Z",
  "subscriptionId": "sub_1234567890abcdef",
  "customerId": "cus_1234567890abcdef",
  "productId": "111",
  "productCode": "pos-base",
  "items": [
    { "productCode": "pos-base", "name": "Plan base", "price": 3500, "quantity": 1 },
    { "productCode": "pos-sucursal", "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 una suscripción cancelled es reactivada (PATCH /v1/subscriptions/:id/reactivate) mientras aún está dentro de su período ya pagado. Vuelve a active y el cobro se re-arma en currentPeriodEnd.

json
{
  "eventId": "evt_1234567890abcdef",
  "type": "subscription.resumed",
  "accountId": "acc_1234567890abcdef",
  "projectId": "prj_1234567890abcdef",
  "createdAt": "2026-06-20T10:30:00.000Z",
  "subscriptionId": "sub_1234567890abcdef",
  "status": "active"
}

subscription.past_due

Se envía cuando una renovación falla y se agenda un reintento de dunning. attempts es el número de intentos fallidos hasta ahora; nextBillingAt es cuándo corre el siguiente reintento (días +1/+3/+5/+7 desde la fecha de vencimiento original).

json
{
  "eventId": "evt_1234567890abcdef",
  "type": "subscription.past_due",
  "accountId": "acc_1234567890abcdef",
  "projectId": "prj_1234567890abcdef",
  "createdAt": "2026-07-04T00:00:05.000Z",
  "subscriptionId": "sub_1234567890abcdef",
  "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 próximo no se cobra.

json
{
  "eventId": "evt_1234567890abcdef",
  "type": "subscription.cancelled",
  "accountId": "acc_1234567890abcdef",
  "projectId": "prj_1234567890abcdef",
  "createdAt": "2026-07-04T00:00:00.000Z",
  "subscriptionId": "sub_1234567890abcdef",
  "reason": "cancel_at_period_end"
}
reasonSignificado
cancelled_by_requestCancelada de inmediato vía PATCH /v1/subscriptions/:id/cancel
cancel_at_period_endCancelación programada que llegó al fin del período (sin cargo final)
dunning_exhaustedCancelada tras agotarse la ventana de dunning de 7 días; incluye attempts

subscription.billing_blocked

Se envía cuando una renovación no pudo cobrarse por una mala configuración del lado del comercio (el procesador de tarjetas está deshabilitado o no se pueden leer sus credenciales): el cargo nunca llegó al gateway. No es una declinación del cliente: no se inicia dunning y el estado de la suscripción no cambia. El cobro se difiere y se espera que el comercio corrija el procesador. code es el código de error interno que clasificó el fallo.

json
{
  "eventId": "evt_1234567890abcdef",
  "type": "subscription.billing_blocked",
  "accountId": "acc_1234567890abcdef",
  "projectId": "prj_1234567890abcdef",
  "createdAt": "2026-07-04T00:00:05.000Z",
  "subscriptionId": "sub_1234567890abcdef",
  "invoiceId": "inv_1234567890abcdef",
  "reason": "provider_misconfigured",
  "paymentProvider": "nmi",
  "code": "ERR_PAYMENT_PROVIDER_DISABLED"
}

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": "evt_1234567890abcdef",
  "type": "subscription.items_updated",
  "accountId": "acc_1234567890abcdef",
  "projectId": "prj_1234567890abcdef",
  "createdAt": "2026-06-04T10:30:00.000Z",
  "subscriptionId": "sub_1234567890abcdef",
  "items": [
    { "productCode": "pos-base", "name": "Plan base", "price": 3500, "quantity": 1 },
    { "productCode": "pos-sucursal", "name": "Sucursal adicional", "price": 2000, "quantity": 2 },
    { "productCode": "pos-usuario", "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 se re-apunta a otro cliente vaulteado (PATCH /v1/subscriptions/:id/customer), es decir, el software cambió el método de pago activo. Las facturas futuras y los reintentos de dunning cobran el vault del nuevo cliente; no se cobra nada al momento del cambio.

json
{
  "eventId": "evt_1234567890abcdef",
  "type": "subscription.customer_updated",
  "accountId": "acc_1234567890abcdef",
  "projectId": "prj_1234567890abcdef",
  "createdAt": "2026-06-04T10:30:00.000Z",
  "subscriptionId": "sub_1234567890abcdef",
  "customerId": "cus_new1234567890",
  "previousCustomerId": "cus_old1234567890"
}

Facturas

Eventos de las facturas recurrentes generadas por las suscripciones. Se entregan siempre al webhook del proyecto.

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 siempre coinciden con el amount cobrado (en centavos).

json
{
  "eventId": "evt_1234567890abcdef",
  "type": "invoice.paid",
  "accountId": "acc_1234567890abcdef",
  "projectId": "prj_1234567890abcdef",
  "createdAt": "2026-07-04T00:00:05.000Z",
  "subscriptionId": "sub_1234567890abcdef",
  "invoiceId": "inv_1234567890abcdef",
  "amount": 5500,
  "currency": "USD",
  "items": [
    { "productCode": "pos-base", "name": "Plan base", "price": 3500, "quantity": 1 },
    { "productCode": "pos-sucursal", "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 dunning reintente el mismo período. failureReason es el texto literal del procesador; failureCode es la causa normalizada opcional (ver ).

json
{
  "eventId": "evt_1234567890abcdef",
  "type": "invoice.payment_failed",
  "accountId": "acc_1234567890abcdef",
  "projectId": "prj_1234567890abcdef",
  "createdAt": "2026-07-04T00:00:05.000Z",
  "subscriptionId": "sub_1234567890abcdef",
  "invoiceId": "inv_1234567890abcdef",
  "amount": 5500,
  "currency": "USD",
  "items": [
    { "productCode": "pos-base", "name": "Plan base", "price": 3500, "quantity": 1 }
  ],
  "failureReason": "Card declined"
}

Clientes

customer.created

Se envía al webhook del proyecto cuando un cliente guarda su tarjeta a través de un link de colección (POST /v1/collections/links). El objeto card lleva metadata de facturación solo para mostrar, para que tu software la almacene; los cargos siempre usan la referencia del vault, nunca estos campos.

json
{
  "eventId": "evt_1234567890abcdef",
  "type": "customer.created",
  "accountId": "acc_1234567890abcdef",
  "projectId": "prj_1234567890abcdef",
  "createdAt": "2026-06-04T10:30:00.000Z",
  "customer": {
    "id": "cus_1234567890abcdef",
    "name": "John Doe",
    "email": "john@example.com",
    "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. paymentLinkId solo está presente cuando la transacción se originó desde un link de pago.

json
{
  "eventId": "evt_1234567890abcdef",
  "type": "receipt.created",
  "paymentTransactionId": "txn_1234567890abcdef",
  "paymentLinkId": "pl_1234567890abcdef",
  "accountId": "acc_1234567890abcdef",
  "orderId": "ORD-12345-xyz",
  "receipt": {
    "fileUrl": "https://files.zelta.dev/receipts/rcpt_abc123.pdf",
    "originalName": "receipt-ORD-12345-xyz.pdf",
    "mimeType": "application/pdf",
    "size": 24576,
    "createdAt": "2026-06-04T10:30:05.000Z"
  }
}

Prueba

webhook.ping

Se envía cuando pruebas tu endpoint desde el dashboard. Sirve para confirmar que tu URL es alcanzable y responde con un 2xx.

json
{
  "eventId": "evt_1234567890abcdef",
  "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-Idevt_1234567890abcdefID único del evento; úsalo para
Zeltapay-Event-Typepayment.successTipo del evento
Zeltapay-Timestamp1710323400Timestamp Unix (segundos) de cuando se generó el evento
Zeltapay-Signaturet=1710323400, v1=a1b2c3...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, DLQ y lógica de entrega
  • -- Evitar procesamiento duplicado

Documentación oficial de Zelta