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
| 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 fue declinado | cuenta/proyecto |
payment_link.rejected | El link fue rechazado antes de cobrar (monto bajo el mínimo del procesador) | proyecto |
charge.succeeded | Un cargo por API fue aprobado y liquidado | proyecto |
charge.failed | Un cargo por API fue declinado | proyecto |
charge.deferred | Un addon menor a $1 en NMI se aprobó sin mover dinero; se difiere a la próxima factura | proyecto |
charge.unknown | La reconciliación de un cargo expiró sin respuesta definitiva; requiere revisión manual | proyecto |
subscription.created | Se creó una suscripción | proyecto |
subscription.resumed | Una suscripción cancelada fue reactivada | proyecto |
subscription.past_due | Una renovación falló y la suscripción entró en dunning | proyecto |
subscription.cancelled | Una suscripción fue cancelada | proyecto |
subscription.billing_blocked | El cobro recurrente quedó bloqueado por mala 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 se re-apuntó a otro cliente vaulteado | 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 (tarjeta vaulteada) desde un link de colección | proyecto |
receipt.created | Se generó el comprobante PDF de un pago | cuenta/proyecto |
webhook.ping | Evento de prueba enviado desde el dashboard | n/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:
| Campo | Tipo | Descripción |
|---|---|---|
eventId | string | Identificador único del evento. Úsalo para |
type | string | Tipo del evento (ej. payment.success) |
createdAt | string | Fecha y hora del evento en formato ISO 8601 |
accountId | string | Cuenta propietaria del recurso |
projectId | string | null | Proyecto asociado, o null cuando el recurso no es de un proyecto |
Dos excepciones al sobre común: webhook.ping solo lleva eventId y type (sin accountId, projectId ni createdAt), y receipt.created no incluye createdAt en la raíz (usa receipt.createdAt). En ambos casos enruta siempre por el header Zeltapay-Event-Type o por el campo type.
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.
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": "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": "[email protected]"
},
"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.
{
"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.
payment_link.rejected
Se envía al webhook del proyecto cuando la creación de un link de pago se rechaza antes de que el link exista, porque el monto está por debajo del mínimo que acepta NMI ($1.00 / 100 centavos). No es una declinación de tarjeta: nunca se creó un link ni se llegó al gateway. Por eso el payload no lleva paymentLinkId — no hay link al que apuntar. La creación desde el dashboard reporta el mismo motivo de forma síncrona en el error de la API, así que este evento existe para que una integración por webhook también se entere de por qué no le devolvimos un link.
| 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 (p. ej. USD) |
concept | string | Concepto enviado al intentar crear el link (presente si se envió) |
metadata | object | Metadata enviada al intentar crear el link (presente si se envió) |
{
"eventId": "evt_1234567890abcdef",
"type": "payment_link.rejected",
"accountId": "acc_1234567890abcdef",
"projectId": "prj_1234567890abcdef",
"createdAt": "2026-03-13T10:30:00.000Z",
"reason": "NMI no procesa pagos menores a 1.00 USD (100 centavos); no se puede generar el link de pago",
"code": "ERR_AMOUNT_BELOW_NMI_MINIMUM",
"amount": 50,
"currency": "USD",
"concept": "Servicio de consultoría",
"metadata": {
"orderId": "ORD-001"
}
}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.
{
"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.
{
"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.
failureCode | Significado |
|---|---|
duplicate_transaction | La 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. El objeto charge no lleva campo status (el type del evento ya indica que fue diferido); en su lugar trae reason con la explicación y deferredQuantity con la cantidad extra que se sumará al ítem en la próxima factura para recuperar el monto.
| 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 absorbe el monto diferido en la próxima factura (amount ÷ precio del producto) |
charge.subscriptionId | string | Suscripción a cuya próxima factura se difiere el cargo |
{
"eventId": "evt_1234567890abcdef",
"type": "charge.deferred",
"accountId": "acc_1234567890abcdef",
"projectId": "prj_1234567890abcdef",
"createdAt": "2026-06-04T10:30:00.000Z",
"charge": {
"id": "txn_1234567890abcdef",
"orderId": "ORD-12345-abc",
"amount": 83,
"currency": "USD",
"quantity": 1,
"prorated": false,
"productCode": "prod_addon_micro",
"customerId": "cus_1234567890abcdef",
"subscriptionId": "sub_1234567890abcdef",
"reason": "El monto (0.83 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": 1,
"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.
{
"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
}
}A diferencia de charge.succeeded / charge.failed, este evento no 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): 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.
{
"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.
{
"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).
{
"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.
{
"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"
}reason | Significado |
|---|---|
cancelled_by_request | Cancelada de inmediato vía PATCH /v1/subscriptions/:id/cancel |
cancel_at_period_end | Cancelación programada que llegó al fin del período (sin cargo final) |
dunning_exhausted | Cancelada 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.
{
"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.
{
"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.
{
"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.
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 legacy de un solo producto que no tienen instantánea. 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 siempre coinciden con el amount cobrado (en centavos).
{
"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 ).
{
"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.
{
"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": "[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. paymentLinkId solo está presente cuando la transacción se originó desde un link de pago. subscriptionId e invoiceId se rellenan cuando el comprobante corresponde al cobro recurrente de una suscripción (permiten enlazarlo con el invoice.paid del mismo invoiceId); en un pago único o de link son null. Este evento no lleva createdAt en la raíz: usa receipt.createdAt como marca de tiempo de generación.
{
"eventId": "evt_1234567890abcdef",
"type": "receipt.created",
"paymentTransactionId": "txn_1234567890abcdef",
"paymentLinkId": "pl_1234567890abcdef",
"accountId": "acc_1234567890abcdef",
"projectId": null,
"subscriptionId": null,
"invoiceId": null,
"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.
{
"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:
| 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 | evt_1234567890abcdef | ID único del evento; úsalo para |
Zeltapay-Event-Type | payment.success | Tipo del evento |
Zeltapay-Timestamp | 1710323400 | Timestamp Unix (segundos) de cuando se generó el evento |
Zeltapay-Signature | t=1710323400, v1=a1b2c3... | 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, DLQ y lógica de entrega
- -- Evitar procesamiento duplicado