Skip to content

Casos de Uso ​

Esta guía presenta ejemplos prácticos de cómo integrar Zelta Pay en diferentes escenarios de negocio. Cada caso de uso incluye código funcional que puedes adaptar a tu aplicación.

E-commerce ​

Crea links de pago vinculados a productos de tu catálogo:

javascript
async function createProductPaymentLink(product, customer) {
  const response = await fetch('https://api-pay.zelta.dev/v1/payment-links', {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.ZELTA_API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      concept: `Compra: ${product.name}`,
      amount: product.priceInCents,
      customerName: customer.name,
      customerEmail: customer.email,
      isTest: false,
      redirectUrl: `https://mitienda.com/compra/gracias?product=${product.id}`,
      paymentMethods: ['card', 'yappy'],
      metadata: {
        productId: product.id,
        productName: product.name,
        sku: product.sku,
        customerId: customer.id,
        type: 'product-purchase'
      }
    })
  });

  return await response.json();
}

// Uso
const paymentLink = await createProductPaymentLink(
  { id: 'prod_001', name: 'Audífonos Bluetooth', priceInCents: 4500, sku: 'AUD-BT-001' },
  { id: 'cust_123', name: 'Ana Martinez', email: '[email protected]' }
);

Checkout de carrito de compras ​

Genera un link de pago consolidado para un carrito completo:

javascript
async function createCartCheckout(cart, customer) {
  const itemsSummary = cart.items
    .map(item => `${item.name} x${item.quantity}`)
    .join(', ');

  const totalCents = cart.items.reduce(
    (sum, item) => sum + (item.priceInCents * item.quantity), 0
  );

  const response = await fetch('https://api-pay.zelta.dev/v1/payment-links', {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.ZELTA_API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      concept: `Orden - ${cart.items.length} productos`,
      amount: totalCents,
      customerName: customer.name,
      customerEmail: customer.email,
      isTest: false,
      redirectUrl: `https://mitienda.com/ordenes/${cart.id}/confirmacion`,
      metadata: {
        cartId: cart.id,
        itemCount: String(cart.items.length),
        itemsSummary: itemsSummary.substring(0, 200),
        customerId: customer.id,
        type: 'cart-checkout'
      }
    })
  });

  return await response.json();
}

Seguimiento de estado de órdenes ​

Consulta el estado de un link de pago para actualizar tu sistema. Usa el hashUrl del link (el último segmento de paymentLinkUrl), y recuerda que un link vencido puede seguir apareciendo como pending hasta que un listado lo marque como expired:

javascript
async function checkOrderPaymentStatus(hashUrl) {
  const response = await fetch(
    `https://api-pay.zelta.dev/v1/payment-links/${hashUrl}`,
    {
      headers: {
        'X-API-Key': process.env.ZELTA_API_KEY,
        'Content-Type': 'application/json'
      }
    }
  );

  const result = await response.json();

  if (!result.success) {
    throw new Error(`Error al consultar link: ${result.error.code}`);
  }

  const paymentLink = result.data.paymentLink;
  const isPastExpiry = new Date(paymentLink.expiresAt) <= new Date();
  const status = paymentLink.status === 'pending' && isPastExpiry ? 'expired' : paymentLink.status;

  switch (status) {
    case 'pending':
      console.log('Pago pendiente - el cliente aún no ha pagado');
      break;
    case 'processing':
      console.log('Pago en confirmación - espera el webhook payment.success');
      break;
    case 'completed':
      console.log('Pago completado - actualizar orden como pagada');
      await updateOrderStatus(paymentLink.metadata?.orderId, 'paid');
      break;
    case 'expired':
      console.log('Link expirado - notificar al cliente');
      await notifyCustomerExpiredLink(paymentLink.metadata?.customerId);
      break;
    case 'cancelled':
      console.log('Link cancelado');
      break;
  }

  return paymentLink;
}

Suscripciones ​

Pagos mensuales ​

Genera links de pago mensuales para tus suscriptores. Si quieres que el cobro se haga automáticamente con la tarjeta guardada del cliente, usa en su lugar las .

javascript
async function createMonthlySubscriptionPayment(subscription) {
  const now = new Date();
  const monthNames = [
    'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
    'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'
  ];
  const period = `${monthNames[now.getMonth()]} ${now.getFullYear()}`;

  const response = await fetch('https://api-pay.zelta.dev/v1/payment-links', {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.ZELTA_API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      concept: `${subscription.planName} - ${period}`,
      amount: subscription.amountInCents,
      customerName: subscription.customerName,
      customerEmail: subscription.customerEmail,
      isTest: false,
      redirectUrl: `https://miapp.com/suscripciones/${subscription.id}`,
      metadata: {
        subscriptionId: subscription.id,
        planName: subscription.planName,
        billingPeriod: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`,
        type: 'subscription-renewal'
      }
    })
  });

  return await response.json();
}

// Generar links para todas las suscripciones activas
async function generateMonthlyBilling(subscriptions) {
  const results = [];

  for (const sub of subscriptions) {
    const result = await createMonthlySubscriptionPayment(sub);
    results.push({
      subscriptionId: sub.id,
      paymentLinkUrl: result.data?.paymentLink?.paymentLinkUrl,
      success: result.success
    });

    // Respetar rate limit (60 req/min)
    await new Promise(resolve => setTimeout(resolve, 1100));
  }

  return results;
}

Seguimiento de renovaciones ​

Lleva un control de qué suscripciones han pagado y cuáles necesitan seguimiento:

javascript
async function trackSubscriptionRenewals(subscriptions, paymentLinks) {
  const renewalStatus = [];

  for (const sub of subscriptions) {
    const link = paymentLinks.find(
      pl => pl.metadata?.subscriptionId === sub.id
    );

    if (!link) {
      renewalStatus.push({
        subscriptionId: sub.id,
        status: 'no-link',
        action: 'Crear link de pago'
      });
      continue;
    }

    const isPastExpiry = new Date(link.expiresAt) <= new Date();
    const status = link.status === 'pending' && isPastExpiry ? 'expired' : link.status;

    switch (status) {
      case 'completed':
        renewalStatus.push({
          subscriptionId: sub.id,
          status: 'paid',
          action: 'Ninguna - renovación exitosa'
        });
        break;

      case 'pending':
        const expiresAt = new Date(link.expiresAt);
        const hoursLeft = (expiresAt - new Date()) / (1000 * 60 * 60);

        renewalStatus.push({
          subscriptionId: sub.id,
          status: 'pending',
          action: hoursLeft < 24
            ? 'Enviar recordatorio urgente'
            : 'Esperar pago del cliente'
        });
        break;

      case 'expired':
        renewalStatus.push({
          subscriptionId: sub.id,
          status: 'expired',
          action: 'Generar nuevo link y notificar al cliente'
        });
        break;
    }
  }

  return renewalStatus;
}

Cobro de servicios ​

Consultorías y servicios profesionales ​

javascript
async function createServicePaymentLink(service) {
  const response = await fetch('https://api-pay.zelta.dev/v1/payment-links', {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.ZELTA_API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      concept: `${service.type}: ${service.description}`,
      amount: service.amountInCents,
      customerName: service.clientName,
      customerEmail: service.clientEmail,
      isTest: false,
      redirectUrl: service.confirmationUrl,
      metadata: {
        serviceId: service.id,
        serviceType: service.type,
        professionalId: service.professionalId,
        scheduledDate: service.date,
        type: 'service-payment'
      }
    })
  });

  return await response.json();
}

// Ejemplo: cobro de consultoría
const result = await createServicePaymentLink({
  id: 'srv_001',
  type: 'Consultoría legal',
  description: 'Sesión de 1 hora - Derecho corporativo',
  amountInCents: 150000,
  clientName: 'Roberto Diaz',
  clientEmail: '[email protected]',
  professionalId: 'prof_045',
  date: '2026-03-20',
  confirmationUrl: 'https://miconsultorio.com/citas/confirmacion'
});

Registro a eventos ​

javascript
async function createEventRegistrationLink(event, attendee) {
  const response = await fetch('https://api-pay.zelta.dev/v1/payment-links', {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.ZELTA_API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      concept: `Registro: ${event.name}`,
      amount: event.ticketPriceInCents,
      customerName: attendee.name,
      customerEmail: attendee.email,
      isTest: false,
      redirectUrl: `https://mieventos.com/registro/${event.id}/confirmacion`,
      paymentMethods: ['card', 'yappy'],
      metadata: {
        eventId: event.id,
        eventName: event.name,
        eventDate: event.date,
        ticketType: attendee.ticketType,
        attendeeId: attendee.id,
        type: 'event-registration'
      }
    })
  });

  return await response.json();
}

Integración con webhooks ​

Combina la creación de links de pago con para automatizar tu flujo completo.

Handler de pago exitoso ​

javascript
// Procesar webhook de pago completado
app.post('/webhook/zelta-pay', express.raw({ type: 'application/json' }), async (req, res) => {
  const payload = JSON.parse(req.body);

  if (payload.type === 'payment.success') {
    const { metadata } = payload; // se omite si el link no tiene metadata

    switch (metadata?.type) {
      case 'product-purchase':
        await fulfillProductOrder(metadata.productId, metadata.customerId);
        await sendOrderConfirmationEmail(metadata.customerId);
        break;

      case 'cart-checkout':
        await processCartOrder(metadata.cartId);
        await updateInventory(metadata.cartId);
        await sendOrderConfirmationEmail(metadata.customerId);
        break;

      case 'subscription-renewal':
        await renewSubscription(metadata.subscriptionId, metadata.billingPeriod);
        await sendRenewalConfirmationEmail(metadata.subscriptionId);
        break;

      case 'service-payment':
        await confirmServiceAppointment(metadata.serviceId);
        await notifyProfessional(metadata.professionalId);
        break;

      case 'event-registration':
        await confirmEventRegistration(metadata.eventId, metadata.attendeeId);
        await sendEventTicket(metadata.attendeeId, metadata.eventId);
        break;

      default:
        console.log('Tipo de pago no reconocido:', metadata?.type);
    }
  }

  res.status(200).json({ received: true });
});

Handler de renovación de suscripción ​

javascript
async function handleSubscriptionRenewalWebhook(payload) {
  const { metadata, transaction, customer } = payload;

  if (metadata?.type !== 'subscription-renewal') return;

  const subscriptionId = metadata.subscriptionId;
  const billingPeriod = metadata.billingPeriod;

  // 1. Extender la suscripción
  await db.subscription.update({
    where: { id: subscriptionId },
    data: {
      status: 'active',
      currentPeriodEnd: getNextPeriodEnd(billingPeriod),
      lastPaymentDate: new Date()
    }
  });

  // 2. Registrar el pago
  await db.payment.create({
    data: {
      subscriptionId,
      amount: transaction.amount,
      billingPeriod,
      paymentLinkId: payload.paymentLinkId,
      zeltaOrderId: transaction.orderId,
      paidAt: new Date(transaction.paidAt)
    }
  });

  // 3. Notificar al cliente
  await sendEmail({
    to: customer.customerEmail,
    subject: 'Renovación exitosa',
    body: `Tu suscripción ha sido renovada para ${billingPeriod}.`
  });
}

Analítica ​

Analítica de pagos ​

Consulta tus links de pago para generar reportes. La API no filtra por fecha, así que recorre las páginas y filtra por createdAt en tu aplicación (ver ):

javascript
async function getPaymentAnalytics(startDate, endDate) {
  const from = new Date(startDate);
  const to = new Date(endDate);
  const now = new Date();

  // getAllPaymentLinks: recorre todas las páginas de GET /v1/payment-links
  const links = (await getAllPaymentLinks()).filter(l => {
    const createdAt = new Date(l.createdAt);
    return !l.isTest && createdAt >= from && createdAt <= to;
  });

  const isExpired = l =>
    l.status === 'expired' || (l.status === 'pending' && new Date(l.expiresAt) <= now);

  const analytics = {
    total: links.length,
    completed: links.filter(l => l.status === 'completed').length,
    pending: links.filter(l => l.status === 'pending' && !isExpired(l)).length,
    expired: links.filter(isExpired).length,
    totalAmountCollected: links
      .filter(l => l.status === 'completed')
      .reduce((sum, l) => sum + l.amount, 0),
    conversionRate: 0,
    averageAmount: 0
  };

  if (analytics.total > 0) {
    analytics.conversionRate = (analytics.completed / analytics.total * 100).toFixed(2);
  }

  if (analytics.completed > 0) {
    analytics.averageAmount = Math.round(analytics.totalAmountCollected / analytics.completed);
  }

  // El método con que se pagó cada link llega en el webhook payment.success
  // (transaction.paymentMethod y transaction.paymentProvider); guárdalo ahí
  // si necesitas un desglose por método.

  return analytics;
}

Historial de pagos por cliente ​

El parámetro search busca por una parte del nombre del cliente. Si guardas tu propio ID de cliente en la metadata, filtra además por ese valor:

javascript
async function getCustomerPaymentHistory(customerName, customerId) {
  const response = await fetch(
    `https://api-pay.zelta.dev/v1/payment-links?search=${encodeURIComponent(customerName)}&lim=100`,
    {
      headers: {
        'X-API-Key': process.env.ZELTA_API_KEY,
        'Content-Type': 'application/json'
      }
    }
  );

  const result = await response.json();

  return result.data.paymentLinks
    .filter(link => !customerId || link.metadata?.customerId === customerId)
    .map(link => ({
    id: link.id,
    concept: link.concept,
    amount: link.amount,
    status: link.status,
    createdAt: link.createdAt,
    metadata: link.metadata
  }));
}

Buenas prácticas ​

Manejo de errores ​

Centraliza el manejo de errores en una función reutilizable:

javascript
class ZeltaPayError extends Error {
  constructor(status, code, message, issues = []) {
    super(message);
    this.status = status;
    this.code = code;
    this.issues = issues;
  }
}

async function zeltaPayRequest(endpoint, options = {}) {
  const response = await fetch(`https://api-pay.zelta.dev${endpoint}`, {
    ...options,
    headers: {
      'X-API-Key': process.env.ZELTA_API_KEY,
      'Content-Type': 'application/json',
      ...options.headers
    }
  });

  if (response.status === 429) {
    // Rate limit: cuerpo en texto plano, espera Retry-After segundos
    throw new ZeltaPayError(429, 'RATE_LIMITED', `Reintenta en ${response.headers.get('Retry-After')}s`);
  }

  const result = await response.json();

  if (!result.success) {
    // Errores de negocio: error.code + message. Errores de validación: error.issues
    throw new ZeltaPayError(
      response.status,
      result.error?.code ?? 'VALIDATION_ERROR',
      result.message ?? 'Datos inválidos',
      result.error?.issues ?? []
    );
  }

  return result.data;
}

Rate limiting ​

Respeta el límite de 60 solicitudes por ventana de 60 segundos de cada API key implementando un control de tasa en tu aplicación:

javascript
class RateLimiter {
  constructor(maxRequests = 60, windowMs = 60000) {
    this.maxRequests = maxRequests;
    this.windowMs = windowMs;
    this.requests = [];
  }

  async waitForSlot() {
    const now = Date.now();
    this.requests = this.requests.filter(t => now - t < this.windowMs);

    if (this.requests.length >= this.maxRequests) {
      const oldestRequest = this.requests[0];
      const waitTime = this.windowMs - (now - oldestRequest) + 100;
      await new Promise(resolve => setTimeout(resolve, waitTime));
    }

    this.requests.push(Date.now());
  }
}

const limiter = new RateLimiter();

async function createPaymentLinkWithRateLimit(data) {
  await limiter.waitForSlot();

  return await zeltaPayRequest('/v1/payment-links', {
    method: 'POST',
    body: JSON.stringify(data)
  });
}

Caché ​

Si consultas frecuentemente el estado de los links, implementa un caché para reducir llamadas a la API:

javascript
const cache = new Map();
const CACHE_TTL = 30000; // 30 segundos

async function getPaymentLinkCached(hashUrl) {
  const cacheKey = `link:${hashUrl}`;
  const cached = cache.get(cacheKey);

  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return cached.data;
  }

  const data = await zeltaPayRequest(`/v1/payment-links/${hashUrl}`);

  cache.set(cacheKey, { data, timestamp: Date.now() });
  return data;
}

Caché y estados de pago

No confíes solo en el caché para determinar si un pago fue completado. Usa para recibir notificaciones en tiempo real cuando cambie el estado de un pago.

Logging ​

Registra las interacciones con la API para facilitar la depuración:

javascript
async function zeltaPayRequestWithLogging(endpoint, options = {}) {
  const requestId = crypto.randomUUID();
  const startTime = Date.now();

  console.log(JSON.stringify({
    type: 'zelta_pay_request',
    requestId,
    endpoint,
    method: options.method || 'GET',
    timestamp: new Date().toISOString()
  }));

  try {
    const result = await zeltaPayRequest(endpoint, options);

    console.log(JSON.stringify({
      type: 'zelta_pay_response',
      requestId,
      success: true,
      durationMs: Date.now() - startTime
    }));

    return result;
  } catch (error) {
    console.error(JSON.stringify({
      type: 'zelta_pay_error',
      requestId,
      code: error.code,
      message: error.message,
      durationMs: Date.now() - startTime
    }));

    throw error;
  }
}

Testing ​

Tests unitarios ​

Verifica la lógica de tu integración con tests unitarios:

javascript
import { describe, it, expect, vi } from 'vitest';

describe('createProductPaymentLink', () => {
  it('debe crear un link con los datos correctos del producto', async () => {
    const mockFetch = vi.fn().mockResolvedValue({
      json: () => Promise.resolve({
        success: true,
        data: {
          paymentLink: {
            id: '7312345678901234567',
            paymentLinkUrl: 'https://pay.zelta.dev/Xk3v9Q_aB7mN2pL8rT4wYz1c',
            status: 'pending'
          }
        }
      })
    });

    global.fetch = mockFetch;

    const product = { id: 'prod_001', name: 'Test Product', priceInCents: 5000, sku: 'TST-001' };
    const customer = { id: 'cust_001', name: 'Test User', email: '[email protected]' };

    const result = await createProductPaymentLink(product, customer);

    expect(result.success).toBe(true);
    expect(result.data.paymentLink.status).toBe('pending');

    const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body);
    expect(requestBody.amount).toBe(5000);
    expect(requestBody.metadata.productId).toBe('prod_001');
  });

  it('debe manejar un monto por debajo del mínimo', async () => {
    global.fetch = vi.fn().mockResolvedValue({
      json: () => Promise.resolve({
        success: false,
        message: 'El monto mínimo para un link de pago es 1.00 USD (100 centavos); no se puede generar el link de pago',
        error: { code: 'ERR_AMOUNT_BELOW_NMI_MINIMUM' }
      })
    });

    const product = { id: 'prod_001', name: 'Test', priceInCents: 50, sku: 'TST' };
    const customer = { id: 'cust_001', name: 'Test', email: '[email protected]' };

    const result = await createProductPaymentLink(product, customer);
    expect(result.success).toBe(false);
    expect(result.error.code).toBe('ERR_AMOUNT_BELOW_NMI_MINIMUM');
  });
});

Tests de integración ​

Usa el modo de prueba (isTest: true) para ejecutar tests de integración contra la API real:

javascript
import { describe, it, expect } from 'vitest';

describe('Zelta Pay Integration', () => {
  it('debe crear y consultar un link de pago de prueba', async () => {
    // Crear link de prueba
    const createResponse = await fetch('https://api-pay.zelta.dev/v1/payment-links', {
      method: 'POST',
      headers: {
        'X-API-Key': process.env.ZELTA_API_KEY,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        concept: 'Test de integración',
        amount: 1000,
        customerName: 'Test User',
        isTest: true,
        metadata: { testId: 'integration-001' }
      })
    });

    const createResult = await createResponse.json();
    expect(createResult.success).toBe(true);

    const { id: paymentLinkId, paymentLinkUrl } = createResult.data.paymentLink;
    const hashUrl = new URL(paymentLinkUrl).pathname.split('/').pop();

    // Consultar el link creado (por su hashUrl, no por su id)
    const getResponse = await fetch(
      `https://api-pay.zelta.dev/v1/payment-links/${hashUrl}`,
      {
        headers: {
          'X-API-Key': process.env.ZELTA_API_KEY,
          'Content-Type': 'application/json'
        }
      }
    );

    const getResult = await getResponse.json();
    expect(getResult.success).toBe(true);
    expect(getResult.data.paymentLink.id).toBe(paymentLinkId);
    expect(getResult.data.paymentLink.isTest).toBe(true);
  });
});

Usa modo de prueba para tests

Siempre usa isTest: true en tus tests de integración. Los links de prueba se cobran con las credenciales de sandbox de tus métodos de pago, así que no mueven dinero real. Necesitas un proveedor activo en sandbox para crearlos.

Siguientes pasos ​

  • -- Referencia completa para crear links de pago
  • -- Recibe notificaciones en tiempo real
  • -- API keys y rate limiting
  • -- Documentación completa de endpoints

Documentación oficial de Zelta