Skip to content

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.

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

json
{
  "type": "webhook.ping",
  "timestamp": "2026-03-13T10:00:00.000Z"
}

Campos del evento

Campos comunes

Todos los eventos incluyen estos campos:

CampoTipoDescripción
typestringTipo del evento (ej. payment.success, webhook.ping)
timestampstringFecha y hora en formato ISO 8601 cuando se generó el evento

Campos de payment.success

CampoTipoDescripción
idstringIdentificador único del link de pago
hashUrlstringHash corto usado en la URL de pago
conceptstringDescripción del cobro
amountintegerMonto en centavos
statusstringEstado del link: completed
customerNamestringNombre del cliente
customerEmailstringEmail del cliente (si fue proporcionado)
isTestbooleantrue si es un link de prueba
paymentMethodsarrayMétodos de pago habilitados: "yappy", "card"
paymentLinkUrlstringURL completa de la página de pago
redirectUrlstringURL de redirección después del pago (si fue configurada)
requestCustomerEmailbooleanSi se solicita email al cliente en la página de pago
metadataobjectDatos personalizados asociados al link
createdAtstringFecha de creación en formato ISO 8601
updatedAtstringFecha de última actualización en formato ISO 8601
expiresAtstringFecha de expiración en formato ISO 8601

Objeto transaction

CampoTipoDescripción
idstringIdentificador único de la transacción
paymentMethodstringMétodo de pago utilizado: "card" o "yappy"
amountintegerMonto de la transacción en centavos
statusstringEstado de la transacción: completed
completedAtstringFecha de completación en formato ISO 8601

Objeto customer

CampoTipoDescripción
namestringNombre del cliente
emailstringEmail del cliente (si fue proporcionado)

Headers del evento

Cada entrega de webhook incluye los siguientes headers HTTP:

HeaderEjemploDescripción
Content-Typeapplication/jsonTipo de contenido del body
User-AgentZeltaPay-Webhooks/1.0Identificador del servicio de webhooks
Zeltapay-Event-Idevt_abc123def456ID único del evento, usar para
Zeltapay-Event-Typepayment.successTipo del evento
Zeltapay-Timestamp1710323400Timestamp Unix (segundos) de cuando se generó el evento
Zeltapay-Signaturesha256=a1b2c3...Firma HMAC-SHA256 para
Zeltapay-Delivery-Attempt1Número del intento de entrega (1-6)

Procesar eventos

Manejo de payment.success

Cuando recibes un evento payment.success, sigue estos pasos:

  1. Verifica la firma del webhook usando el header Zeltapay-Signature
  2. Verifica idempotencia usando el header Zeltapay-Event-Id
  3. Extrae los datos del payload (paymentLink, transaction, customer)
  4. Actualiza tu sistema (orden, suscripción, inventario, etc.)
  5. Notifica al cliente si es necesario (email de confirmación, recibo, etc.)
  6. Responde con 200 lo más rápido posible

Ejemplos de handlers

:: tab Node.js (Express)

javascript
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

javascript
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 200 incluso 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:

javascript
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:

javascript
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

ProblemaSolución
El payload no tiene el campo esperadoVerifica la estructura del evento en esta página. Los campos opcionales pueden no estar presentes
customerEmail es nullEl email es opcional. Solo está presente si se proporcionó al crear el link o si el cliente lo ingresó
El amount no coincideLos montos siempre están en centavos. Divide entre 100 para obtener el valor en dólares
metadata está vacíoLos 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

Documentación oficial de Zelta