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
Links de pago para productos
Crea links de pago vinculados a productos de tu catálogo:
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: 'ana@ejemplo.com' }
);Checkout de carrito de compras
Genera un link de pago consolidado para un carrito completo:
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:
async function checkOrderPaymentStatus(paymentLinkId) {
const response = await fetch(
`https://api-pay.zelta.dev/v1/payment-links/${paymentLinkId}`,
{
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;
switch (paymentLink.status) {
case 'pending':
console.log('Pago pendiente - el cliente aún no ha pagado');
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:
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:
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;
}
switch (link.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
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: 'roberto@ejemplo.com',
professionalId: 'prof_045',
date: '2026-03-20',
confirmationUrl: 'https://miconsultorio.com/citas/confirmacion'
});Registro a eventos
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
// 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.paymentLink;
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
async function handleSubscriptionRenewalWebhook(payload) {
const { metadata } = payload.paymentLink;
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: payload.paymentLink.amount,
billingPeriod,
paymentLinkId: payload.paymentLink.id,
paidAt: new Date()
}
});
// 3. Notificar al cliente
await sendEmail({
to: payload.paymentLink.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:
async function getPaymentAnalytics(startDate, endDate) {
const response = await fetch(
`https://api-pay.zelta.dev/v1/payment-links?startDate=${startDate}&endDate=${endDate}`,
{
headers: {
'X-API-Key': process.env.ZELTA_API_KEY,
'Content-Type': 'application/json'
}
}
);
const result = await response.json();
const links = result.data.paymentLinks;
const analytics = {
total: links.length,
completed: links.filter(l => l.status === 'completed').length,
pending: links.filter(l => l.status === 'pending').length,
expired: links.filter(l => l.status === 'expired').length,
totalAmountCollected: links
.filter(l => l.status === 'completed')
.reduce((sum, l) => sum + l.amount, 0),
conversionRate: 0,
averageAmount: 0,
byPaymentMethod: {}
};
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);
}
// Desglose por método de pago
links.filter(l => l.status === 'completed').forEach(link => {
const methods = link.paymentMethods || ['unknown'];
methods.forEach(method => {
if (!analytics.byPaymentMethod[method]) {
analytics.byPaymentMethod[method] = { count: 0, amount: 0 };
}
analytics.byPaymentMethod[method].count++;
analytics.byPaymentMethod[method].amount += link.amount;
});
});
return analytics;
}Historial de pagos por cliente
async function getCustomerPaymentHistory(customerEmail) {
const response = await fetch(
`https://api-pay.zelta.dev/v1/payment-links?customerEmail=${encodeURIComponent(customerEmail)}`,
{
headers: {
'X-API-Key': process.env.ZELTA_API_KEY,
'Content-Type': 'application/json'
}
}
);
const result = await response.json();
return result.data.paymentLinks.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:
class ZeltaPayError extends Error {
constructor(code, message, details = []) {
super(message);
this.code = code;
this.details = details;
}
}
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
}
});
const result = await response.json();
if (!result.success) {
throw new ZeltaPayError(
result.error.code,
result.error.message,
result.error.details || []
);
}
return result.data;
}Rate limiting
Respeta el límite de 60 solicitudes por minuto implementando un control de tasa:
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:
const cache = new Map();
const CACHE_TTL = 30000; // 30 segundos
async function getPaymentLinkCached(paymentLinkId) {
const cacheKey = `pl_${paymentLinkId}`;
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data;
}
const data = await zeltaPayRequest(`/v1/payment-links/${paymentLinkId}`);
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:
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:
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: 'pl_test_123',
paymentLinkUrl: 'https://pay.zelta.dev/test123',
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: 'test@example.com' };
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 errores de validación', async () => {
global.fetch = vi.fn().mockResolvedValue({
json: () => Promise.resolve({
success: false,
error: { code: 'ERR_VALIDATION_FAILED', message: 'Validation failed' }
})
});
const product = { id: 'prod_001', name: 'Test', priceInCents: 50, sku: 'TST' };
const customer = { id: 'cust_001', name: 'Test', email: 'test@example.com' };
const result = await createProductPaymentLink(product, customer);
expect(result.success).toBe(false);
expect(result.error.code).toBe('ERR_VALIDATION_FAILED');
});
});Tests de integración
Usa el modo de prueba (isTest: true) para ejecutar tests de integración contra la API real:
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 paymentLinkId = createResult.data.paymentLink.id;
// Consultar el link creado
const getResponse = await fetch(
`https://api-pay.zelta.dev/v1/payment-links/${paymentLinkId}`,
{
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 comportan idénticamente a los de producción pero no procesan pagos reales.
Siguientes pasos
- -- Referencia completa para crear links de pago
- -- Recibe notificaciones en tiempo real
- -- API keys y rate limiting
- -- Documentación completa de endpoints