Eventos de Webhook
Referencia completa de todos los tipos de eventos que Zelta Pay envía a través de webhooks, incluyendo la estructura de los payloads y sus campos.
Tipos de eventos
payment.success
Se envía cuando un pago se completa exitosamente. Este es el evento principal que debes manejar para actualizar el estado de tus órdenes.
{
"type": "payment.success",
"timestamp": "2026-03-13T10:30:00.000Z",
"paymentLink": {
"id": "pl_abc123def456",
"hashUrl": "a1b2c3d4e5",
"concept": "Pago de servicio mensual",
"amount": 5000,
"status": "completed",
"customerName": "Maria Garcia",
"customerEmail": "[email protected]",
"isTest": false,
"paymentMethods": ["card", "yappy"],
"paymentLinkUrl": "https://pay.zelta.dev/a1b2c3d4e5",
"redirectUrl": "https://mitienda.com/gracias",
"requestCustomerEmail": false,
"metadata": {
"orderId": "ord_001",
"productName": "Plan Premium",
"type": "subscription-renewal"
},
"createdAt": "2026-03-13T09:00:00.000Z",
"updatedAt": "2026-03-13T10:30:00.000Z",
"expiresAt": "2026-03-14T09:00:00.000Z"
},
"transaction": {
"id": "txn_xyz789abc012",
"paymentMethod": "card",
"amount": 5000,
"status": "completed",
"completedAt": "2026-03-13T10:30:00.000Z"
},
"customer": {
"name": "Maria Garcia",
"email": "[email protected]"
}
}webhook.ping
Evento de prueba enviado cuando configuras un nuevo webhook o cuando solicitas un ping de verificación desde el dashboard.
{
"type": "webhook.ping",
"timestamp": "2026-03-13T10:00:00.000Z"
}Campos del evento
Campos comunes
Todos los eventos incluyen estos campos:
| Campo | Tipo | Descripción |
|---|---|---|
type | string | Tipo del evento (ej. payment.success, webhook.ping) |
timestamp | string | Fecha y hora en formato ISO 8601 cuando se generó el evento |
Campos de payment.success
Objeto paymentLink
| Campo | Tipo | Descripción |
|---|---|---|
id | string | Identificador único del link de pago |
hashUrl | string | Hash corto usado en la URL de pago |
concept | string | Descripción del cobro |
amount | integer | Monto en centavos |
status | string | Estado del link: completed |
customerName | string | Nombre del cliente |
customerEmail | string | Email del cliente (si fue proporcionado) |
isTest | boolean | true si es un link de prueba |
paymentMethods | array | Métodos de pago habilitados: "yappy", "card" |
paymentLinkUrl | string | URL completa de la página de pago |
redirectUrl | string | URL de redirección después del pago (si fue configurada) |
requestCustomerEmail | boolean | Si se solicita email al cliente en la página de pago |
metadata | object | Datos personalizados asociados al link |
createdAt | string | Fecha de creación en formato ISO 8601 |
updatedAt | string | Fecha de última actualización en formato ISO 8601 |
expiresAt | string | Fecha de expiración en formato ISO 8601 |
Objeto transaction
| Campo | Tipo | Descripción |
|---|---|---|
id | string | Identificador único de la transacción |
paymentMethod | string | Método de pago utilizado: "card" o "yappy" |
amount | integer | Monto de la transacción en centavos |
status | string | Estado de la transacción: completed |
completedAt | string | Fecha de completación en formato ISO 8601 |
Objeto customer
| Campo | Tipo | Descripción |
|---|---|---|
name | string | Nombre del cliente |
email | string | Email del cliente (si fue proporcionado) |
Headers del evento
Cada entrega de webhook incluye los siguientes headers HTTP:
| Header | Ejemplo | Descripción |
|---|---|---|
Content-Type | application/json | Tipo de contenido del body |
User-Agent | ZeltaPay-Webhooks/1.0 | Identificador del servicio de webhooks |
Zeltapay-Event-Id | evt_abc123def456 | ID único del evento, usar para |
Zeltapay-Event-Type | payment.success | Tipo del evento |
Zeltapay-Timestamp | 1710323400 | Timestamp Unix (segundos) de cuando se generó el evento |
Zeltapay-Signature | sha256=a1b2c3... | Firma HMAC-SHA256 para |
Zeltapay-Delivery-Attempt | 1 | Número del intento de entrega (1-6) |
Procesar eventos
Manejo de payment.success
Cuando recibes un evento payment.success, sigue estos pasos:
- Verifica la firma del webhook usando el header
Zeltapay-Signature - Verifica idempotencia usando el header
Zeltapay-Event-Id - Extrae los datos del payload (
paymentLink,transaction,customer) - Actualiza tu sistema (orden, suscripción, inventario, etc.)
- Notifica al cliente si es necesario (email de confirmación, recibo, etc.)
- Responde con
200lo más rápido posible
Ejemplos de handlers
:: tab Node.js (Express)
import express from 'express';
const app = express();
app.use(express.json());
app.post('/webhooks/zelta-pay', async (req, res) => {
const eventId = req.headers['zeltapay-event-id'];
const eventType = req.headers['zeltapay-event-type'];
const event = req.body;
// Verificar idempotencia
const alreadyProcessed = await checkIfProcessed(eventId);
if (alreadyProcessed) {
return res.status(200).json({ received: true, duplicate: true });
}
// Procesar según el tipo de evento
if (event.type === 'payment.success') {
const { paymentLink, transaction, customer } = event;
console.log(`Pago completado: ${paymentLink.id}`);
console.log(`Monto: $${(paymentLink.amount / 100).toFixed(2)}`);
console.log(`Método: ${transaction.paymentMethod}`);
console.log(`Cliente: ${customer.name} (${customer.email})`);
// Actualizar la orden en la base de datos
await updateOrderStatus(paymentLink.metadata.orderId, 'paid', {
transactionId: transaction.id,
paymentMethod: transaction.paymentMethod,
paidAt: transaction.completedAt
});
// Enviar email de confirmación
if (customer.email) {
await sendConfirmationEmail(customer.email, {
concept: paymentLink.concept,
amount: paymentLink.amount,
transactionId: transaction.id
});
}
}
// Marcar como procesado
await markAsProcessed(eventId, eventType);
res.status(200).json({ received: true });
});::
:: tab Cloudflare Workers
export default {
async fetch(request, env, ctx) {
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
const eventId = request.headers.get('zeltapay-event-id');
const event = await request.json();
// Verificar idempotencia con KV
const existing = await env.WEBHOOK_KV.get(eventId);
if (existing) {
return Response.json({ received: true, duplicate: true }, { status: 200 });
}
if (event.type === 'payment.success') {
const { paymentLink, transaction, customer } = event;
console.log(`Pago completado: ${paymentLink.id}`);
console.log(`Monto: $${(paymentLink.amount / 100).toFixed(2)}`);
console.log(`Método: ${transaction.paymentMethod}`);
// Tu lógica de negocio aquí
// await processPayment(paymentLink, transaction, customer);
}
// Marcar como procesado (TTL de 30 días)
await env.WEBHOOK_KV.put(eventId, JSON.stringify({ processedAt: new Date().toISOString() }), {
expirationTtl: 60 * 60 * 24 * 30
});
return Response.json({ received: true }, { status: 200 });
}
};::
Buenas prácticas
Idempotencia
Siempre implementa idempotencia usando el header Zeltapay-Event-Id. Zelta Pay puede enviar el mismo evento más de una vez durante reintentos. Consulta la .
Manejo de errores
- Envuelve tu lógica de procesamiento en bloques
try/catch - Registra los errores para depuración posterior
- Responde con
200incluso si tu procesamiento falla (evita reintentos innecesarios) - Implementa una cola interna de reintentos para errores en tu lógica de negocio
Logging
Registra información relevante de cada evento para facilitar la depuración:
function logWebhookEvent(event, headers) {
console.log(JSON.stringify({
type: 'webhook_received',
eventId: headers['zeltapay-event-id'],
eventType: event.type,
deliveryAttempt: headers['zeltapay-delivery-attempt'],
paymentLinkId: event.paymentLink?.id,
amount: event.paymentLink?.amount,
timestamp: new Date().toISOString()
}));
}Integración con base de datos
Al procesar un payment.success, actualiza tu base de datos de forma atómica:
async function processPaymentSuccess(event, eventId) {
await db.transaction(async (tx) => {
// Insertar registro del evento (idempotencia)
await tx.insert('webhook_events', {
event_id: eventId,
event_type: event.type,
payload: JSON.stringify(event)
});
// Actualizar la orden
await tx.update('orders', {
status: 'paid',
paid_at: event.transaction.completedAt,
transaction_id: event.transaction.id
}, {
where: { id: event.paymentLink.metadata.orderId }
});
});
}Solución de problemas
| Problema | Solución |
|---|---|
| El payload no tiene el campo esperado | Verifica la estructura del evento en esta página. Los campos opcionales pueden no estar presentes |
customerEmail es null | El email es opcional. Solo está presente si se proporcionó al crear el link o si el cliente lo ingresó |
El amount no coincide | Los montos siempre están en centavos. Divide entre 100 para obtener el valor en dólares |
metadata está vacío | Los metadatos son opcionales. Solo están presentes si se incluyeron al crear el link de pago |
Siguientes pasos
- -- Guía general de webhooks
- -- Reintentos, DLQ y lógica de entrega
- -- Evitar procesamiento duplicado
- -- Verificar autenticidad de los webhooks