Links de Pago
Los links de pago son la funcionalidad principal de Zelta Pay. Te permiten crear URLs de cobro que puedes compartir con tus clientes por cualquier canal: WhatsApp, correo electrónico, redes sociales o integrado directamente en tu sitio web.
Crear un link de pago
Para crear un link de pago, realiza una solicitud POST al endpoint /v1/payment-links.
Campos requeridos
| Campo | Tipo | Descripción |
|---|---|---|
concept | string | Descripción del cobro que verá el cliente |
amount | integer | Monto en centavos (min: 100, max: 200000). Ej: 1500 = $15.00 |
customerName | string | Nombre del cliente |
isTest | boolean | true para modo prueba, false para producción |
Campos opcionales
| Campo | Tipo | Descripción |
|---|---|---|
customerEmail | string | Email válido del cliente |
requestCustomerEmail | boolean | Solicitar email al cliente en la página de pago. Default: true. No puede ser true cuando se proporciona customerEmail |
redirectUrl | string | URL válida a donde redirigir después del pago (max 2048 caracteres) |
metadata | object | Datos personalizados (max 20 llaves, max 8192 bytes) |
paymentMethods | array | Métodos de pago habilitados: "yappy", "card". Sin duplicados, al menos 1 cuando se especifica |
Montos en centavos
Todos los montos en Zelta Pay se expresan en centavos. Para cobrar $25.00, envía amount: 2500. El rango permitido es de $1.00 (100 centavos) a $2,000.00 (200000 centavos).
Ejemplo básico
:: tab cURL
curl -X POST "https://api-pay.zelta.dev/v1/payment-links" \
-H "X-API-Key: tu-api-key-aqui" \
-H "Content-Type: application/json" \
-d '{
"concept": "Pago de servicio mensual",
"amount": 5000,
"customerName": "Maria Garcia",
"isTest": false
}'::
:: tab Node.js
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: 'Pago de servicio mensual',
amount: 5000,
customerName: 'Maria Garcia',
isTest: false
})
});
const result = await response.json();
console.log(result.data.paymentLink.paymentLinkUrl);::
:: tab Cloudflare Workers
export default {
async fetch(request, env, ctx) {
const response = await fetch('https://api-pay.zelta.dev/v1/payment-links', {
method: 'POST',
headers: {
'X-API-Key': env.ZELTA_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
concept: 'Pago de servicio mensual',
amount: 5000,
customerName: 'Maria Garcia',
isTest: false
})
});
const result = await response.json();
return new Response(JSON.stringify(result), {
headers: { 'Content-Type': 'application/json' }
});
}
};::
:: tab Hono
import { Hono } from 'hono';
type Bindings = {
ZELTA_API_KEY: string;
};
const app = new Hono<{ Bindings: Bindings }>();
app.post('/create-payment', async (c) => {
const response = await fetch('https://api-pay.zelta.dev/v1/payment-links', {
method: 'POST',
headers: {
'X-API-Key': c.env.ZELTA_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
concept: 'Pago de servicio mensual',
amount: 5000,
customerName: 'Maria Garcia',
isTest: false
})
});
const result = await response.json();
return c.json(result);
});
export default app;::
:: tab Python
import requests
response = requests.post(
'https://api-pay.zelta.dev/v1/payment-links',
headers={
'X-API-Key': 'tu-api-key-aqui',
'Content-Type': 'application/json'
},
json={
'concept': 'Pago de servicio mensual',
'amount': 5000,
'customerName': 'Maria Garcia',
'isTest': False
}
)
result = response.json()
print(result['data']['paymentLink']['paymentLinkUrl'])::
:: tab PHP
<?php
$apiKey = getenv('ZELTA_API_KEY');
$data = [
'concept' => 'Pago de servicio mensual',
'amount' => 5000,
'customerName' => 'Maria Garcia',
'isTest' => false
];
$ch = curl_init('https://api-pay.zelta.dev/v1/payment-links');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-API-Key: ' . $apiKey,
'Content-Type: application/json'
],
CURLOPT_POSTFIELDS => json_encode($data)
]);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
echo $result['data']['paymentLink']['paymentLinkUrl'];::
Ejemplo con metadata
La metadata te permite asociar información personalizada al link de pago. Es ideal para vincular pagos con registros en tu sistema.
:: tab cURL
curl -X POST "https://api-pay.zelta.dev/v1/payment-links" \
-H "X-API-Key: tu-api-key-aqui" \
-H "Content-Type: application/json" \
-d '{
"concept": "Orden #ORD-2026-0042",
"amount": 15000,
"customerName": "Carlos Lopez",
"customerEmail": "[email protected]",
"isTest": false,
"redirectUrl": "https://mitienda.com/orden/confirmacion",
"paymentMethods": ["card", "yappy"],
"metadata": {
"orderId": "ORD-2026-0042",
"productName": "Plan Premium",
"userId": "usr_abc123",
"source": "checkout-web"
}
}'::
:: tab Node.js
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 #ORD-2026-0042',
amount: 15000,
customerName: 'Carlos Lopez',
customerEmail: '[email protected]',
isTest: false,
redirectUrl: 'https://mitienda.com/orden/confirmacion',
paymentMethods: ['card', 'yappy'],
metadata: {
orderId: 'ORD-2026-0042',
productName: 'Plan Premium',
userId: 'usr_abc123',
source: 'checkout-web'
}
})
});
const result = await response.json();
console.log(result);::
:: tab Python
import requests
response = requests.post(
'https://api-pay.zelta.dev/v1/payment-links',
headers={
'X-API-Key': 'tu-api-key-aqui',
'Content-Type': 'application/json'
},
json={
'concept': 'Orden #ORD-2026-0042',
'amount': 15000,
'customerName': 'Carlos Lopez',
'customerEmail': '[email protected]',
'isTest': False,
'redirectUrl': 'https://mitienda.com/orden/confirmacion',
'paymentMethods': ['card', 'yappy'],
'metadata': {
'orderId': 'ORD-2026-0042',
'productName': 'Plan Premium',
'userId': 'usr_abc123',
'source': 'checkout-web'
}
}
)
result = response.json()
print(result)::
:: tab PHP
<?php
$data = [
'concept' => 'Orden #ORD-2026-0042',
'amount' => 15000,
'customerName' => 'Carlos Lopez',
'customerEmail' => '[email protected]',
'isTest' => false,
'redirectUrl' => 'https://mitienda.com/orden/confirmacion',
'paymentMethods' => ['card', 'yappy'],
'metadata' => [
'orderId' => 'ORD-2026-0042',
'productName' => 'Plan Premium',
'userId' => 'usr_abc123',
'source' => 'checkout-web'
]
];
$ch = curl_init('https://api-pay.zelta.dev/v1/payment-links');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-API-Key: ' . getenv('ZELTA_API_KEY'),
'Content-Type: application/json'
],
CURLOPT_POSTFIELDS => json_encode($data)
]);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
print_r($result);::
Respuesta exitosa
Cuando el link se crea correctamente, recibes una respuesta con los detalles completos:
{
"success": true,
"data": {
"paymentLink": {
"id": "pl_1234567890abcdef",
"paymentLinkUrl": "https://pay.zelta.dev/abc123def456",
"customerName": "Carlos Lopez",
"concept": "Orden #ORD-2026-0042",
"amount": 15000,
"status": "pending",
"createdAt": "2026-03-13T10:30:00.000Z",
"expiresAt": "2026-03-16T10:30:00.000Z",
"isTest": false,
"paymentMethods": ["card", "yappy"],
"requestCustomerEmail": false,
"metadata": {
"orderId": "ORD-2026-0042",
"productName": "Plan Premium",
"userId": "usr_abc123",
"source": "checkout-web"
}
}
},
"message": "Payment link created successfully",
"timestamp": "2026-03-13T10:30:00.000Z"
}Campos de la respuesta
| Campo | Descripción |
|---|---|
id | Identificador único del link de pago |
paymentLinkUrl | URL que debes compartir con tu cliente para que realice el pago |
status | Estado actual del link. Siempre "pending" al crearse |
createdAt | Fecha y hora de creación en formato ISO 8601 |
expiresAt | Fecha de expiración (3 días desde la creación vía API) |
paymentMethods | Métodos de pago habilitados para este link |
requestCustomerEmail | Si se solicitará el email al cliente en la página de pago |
Validaciones
Helpers de validación
Antes de enviar la solicitud a la API, puedes validar los datos en tu aplicación para evitar errores:
:: tab Node.js
// Validar monto (100 a 200000 centavos)
function validateAmount(amount) {
if (!Number.isInteger(amount)) {
return { valid: false, error: 'El monto debe ser un número entero' };
}
if (amount < 100 || amount > 200000) {
return { valid: false, error: 'El monto debe estar entre 100 y 200000 centavos ($1.00 - $2,000.00)' };
}
return { valid: true };
}
// Validar metadata (max 20 llaves, max 8192 bytes)
function validateMetadata(metadata) {
if (!metadata || typeof metadata !== 'object') {
return { valid: false, error: 'La metadata debe ser un objeto' };
}
const keys = Object.keys(metadata);
if (keys.length > 20) {
return { valid: false, error: `La metadata tiene ${keys.length} llaves (max: 20)` };
}
const size = new TextEncoder().encode(JSON.stringify(metadata)).length;
if (size > 8192) {
return { valid: false, error: `La metadata ocupa ${size} bytes (max: 8192)` };
}
return { valid: true };
}
// Validar métodos de pago
function validatePaymentMethods(methods) {
const allowed = ['yappy', 'card'];
const unique = new Set(methods);
if (unique.size !== methods.length) {
return { valid: false, error: 'No se permiten métodos de pago duplicados' };
}
for (const method of methods) {
if (!allowed.includes(method)) {
return { valid: false, error: `Método de pago no válido: ${method}` };
}
}
return { valid: true };
}::
:: tab Python
import json
import re
def validate_amount(amount: int) -> dict:
"""Validar monto (100 a 200000 centavos)."""
if not isinstance(amount, int):
return {"valid": False, "error": "El monto debe ser un número entero"}
if amount < 100 or amount > 200000:
return {"valid": False, "error": "El monto debe estar entre 100 y 200000 centavos ($1.00 - $2,000.00)"}
return {"valid": True}
def validate_metadata(metadata: dict) -> dict:
"""Validar metadata (max 20 llaves, max 8192 bytes)."""
if not isinstance(metadata, dict):
return {"valid": False, "error": "La metadata debe ser un diccionario"}
if len(metadata) > 20:
return {"valid": False, "error": f"La metadata tiene {len(metadata)} llaves (max: 20)"}
size = len(json.dumps(metadata).encode("utf-8"))
if size > 8192:
return {"valid": False, "error": f"La metadata ocupa {size} bytes (max: 8192)"}
return {"valid": True}
def validate_payment_methods(methods: list) -> dict:
"""Validar métodos de pago."""
allowed = {"yappy", "card"}
if len(methods) != len(set(methods)):
return {"valid": False, "error": "No se permiten métodos de pago duplicados"}
for method in methods:
if method not in allowed:
return {"valid": False, "error": f"Método de pago no válido: {method}"}
return {"valid": True}::
Códigos de error
Cuando la solicitud falla, la API devuelve un error con un código específico:
| Código | Descripción | Solución |
|---|---|---|
ERR_VALIDATION_FAILED | Los datos enviados no pasan la validación | Revisa los campos requeridos, formatos y rangos permitidos |
ERR_NO_ACTIVE_PAYMENT_PROVIDER | No tienes un proveedor de pagos activo configurado | Configura al menos un proveedor de pagos en tu |
ERR_INACTIVE_PAYMENT_METHOD | El método de pago solicitado no está activo | Verifica que los métodos en paymentMethods estén habilitados en tu cuenta |
Ejemplo de respuesta de error
{
"success": false,
"error": {
"code": "ERR_VALIDATION_FAILED",
"message": "Validation failed",
"details": [
{
"field": "amount",
"message": "Amount must be between 100 and 200000"
}
]
},
"timestamp": "2026-03-13T10:30:00.000Z"
}Casos de uso
E-commerce: link de pago para una orden
async function createOrderPaymentLink(order) {
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 #${order.id} - ${order.items.length} productos`,
amount: order.totalInCents,
customerName: order.customerName,
customerEmail: order.customerEmail,
isTest: false,
redirectUrl: `https://mitienda.com/ordenes/${order.id}/confirmacion`,
paymentMethods: ['card', 'yappy'],
metadata: {
orderId: order.id,
itemCount: String(order.items.length),
source: 'web-checkout'
}
})
});
const result = await response.json();
if (!result.success) {
throw new Error(`Error al crear link: ${result.error.code}`);
}
return result.data.paymentLink;
}Suscripciones: link de pago recurrente
async function createSubscriptionPaymentLink(subscription) {
const monthNames = [
'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio',
'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'
];
const now = new Date();
const monthName = monthNames[now.getMonth()];
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} - ${monthName} ${now.getFullYear()}`,
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();
}Buenas prácticas
Manejo de errores
Siempre envuelve las llamadas a la API en un bloque try/catch y maneja los errores de forma adecuada:
async function createPaymentLinkSafe(data) {
try {
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(data)
});
const result = await response.json();
if (!result.success) {
console.error('Error de API:', result.error.code, result.error.message);
return { success: false, error: result.error };
}
return { success: true, paymentLink: result.data.paymentLink };
} catch (error) {
console.error('Error de red:', error.message);
return { success: false, error: { code: 'NETWORK_ERROR', message: error.message } };
}
}Modo de prueba
Usa siempre isTest: true durante el desarrollo. Los links de prueba no procesan pagos reales pero se comportan de forma idéntica a los de producción.
Modo de prueba en producción
Nunca uses isTest: true en un entorno de producción. Los links de prueba no generan cobros reales y tus clientes no podrán completar el pago.
Estrategia de metadata
Usa la metadata para vincular pagos con tu sistema interno. Algunas recomendaciones:
- Incluye siempre un identificador único de tu sistema (ej.
orderId,subscriptionId) - Agrega el contexto del pago (ej.
source,type) - Usa valores de tipo
stringpara mayor compatibilidad - No almacenes datos sensibles (tarjetas, contraseñas, tokens)
Lógica de reintentos
Si la API devuelve un error de red o un código 5xx, implementa reintentos con backoff exponencial:
async function createPaymentLinkWithRetry(data, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
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(data)
});
// No reintentar errores 4xx (son errores del cliente)
if (response.status >= 400 && response.status < 500) {
const result = await response.json();
return { success: false, error: result.error };
}
const result = await response.json();
if (result.success) {
return { success: true, paymentLink: result.data.paymentLink };
}
} catch (error) {
if (attempt === maxRetries - 1) {
throw error;
}
}
// Backoff exponencial: 1s, 2s, 4s
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
}
}Rate limiting
La API tiene un límite de 60 solicitudes por minuto. Si necesitas crear múltiples links de forma masiva, espacia las solicitudes para no exceder el límite. Consulta la para más detalles.
Siguientes pasos
- — Referencia completa del endpoint
- — Recibe notificaciones cuando se completan los pagos
- — Ejemplos prácticos de integración
- — API keys y rate limiting
- — Documentación completa de endpoints