Crear Link de Pago
Crea un nuevo link de pago que puedes compartir con tu cliente por cualquier canal. El link expira automáticamente 3 días después de su creación.
Endpoint
POST /v1/payment-linksAutenticación
Requiere el header X-API-Key. Consulta la .
Cuerpo de la solicitud
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
concept | string | Sí | Descripción del cobro que verá el cliente |
amount | integer | Sí | Monto en centavos (min: 100, max: 200000) |
customerName | string | Sí | Nombre del cliente |
isTest | boolean | Sí | true para modo prueba, false para producción |
customerEmail | string | No | Email válido del cliente |
redirectUrl | string | No | URL de redirección después del pago (máx. 2048 caracteres) |
requestCustomerEmail | boolean | No | Solicitar email al cliente en la página de pago. Default: true. No puede ser true cuando se proporciona customerEmail |
metadata | object | No | Datos personalizados (máx. 20 llaves, máx. 8192 bytes) |
paymentMethods | array | No | Métodos de pago habilitados: "yappy", "card". Sin duplicados, al menos 1 cuando se especifica |
Validación de campos
amount
El monto debe ser un entero en centavos dentro del rango 100 a 200000:
| Monto | Equivalente | Válido |
|---|---|---|
100 | $1.00 | Sí |
1500 | $15.00 | Sí |
200000 | $2,000.00 | Sí |
50 | $0.50 | No (mínimo $1.00) |
250000 | $2,500.00 | No (máximo $2,000.00) |
concept
Texto descriptivo que el cliente verá en la página de pago. Campo requerido, no puede estar vacío.
customerName
Nombre del cliente. Campo requerido, no puede estar vacío.
customerEmail
Email válido opcional. Cuando se proporciona, requestCustomerEmail se establece automáticamente en false.
redirectUrl
URL válida a donde redirigir al cliente después de completar el pago. Máximo 2048 caracteres.
requestCustomerEmail
- Default:
true(solicita email al cliente en la página de pago) - No puede ser
truecuando se proporcionacustomerEmail - Si envías
customerEmail, este campo se establece enfalse
metadata
Objeto de datos personalizados con las siguientes restricciones:
- Máximo 20 llaves
- Máximo 8192 bytes de tamaño total
- Los valores deben ser de tipo
string
paymentMethods
Array de métodos de pago habilitados para este link:
- Valores permitidos:
"yappy","card" - No se permiten duplicados
- Debe contener al menos 1 elemento cuando se especifica
- Si no se incluye, se habilitan todos los métodos activos de la cuenta
Ejemplos de solicitud
Solicitud básica
:: 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'];::
Solicitud con metadata y opciones
:: 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 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: '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();
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-order-payment', async (c) => {
const body = await c.req.json();
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: `Orden #${body.orderId}`,
amount: body.amount,
customerName: body.customerName,
customerEmail: body.customerEmail,
isTest: false,
redirectUrl: `https://mitienda.com/ordenes/${body.orderId}/confirmacion`,
paymentMethods: ['card', 'yappy'],
metadata: {
orderId: body.orderId,
source: 'api'
}
})
});
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': '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);::
Formato de respuesta
Una solicitud exitosa devuelve un status 201 Created:
{
"success": true,
"data": {
"paymentLink": {
"id": "pl_1234567890abcdef",
"hashUrl": "abc123def456",
"paymentLinkUrl": "https://pay.zelta.dev/abc123def456",
"customerName": "Carlos Lopez",
"customerEmail": "[email protected]",
"concept": "Orden #ORD-2026-0042",
"amount": 15000,
"status": "pending",
"isTest": false,
"paymentMethods": ["card", "yappy"],
"requestCustomerEmail": false,
"redirectUrl": "https://mitienda.com/orden/confirmacion",
"metadata": {
"orderId": "ORD-2026-0042",
"productName": "Plan Premium",
"userId": "usr_abc123",
"source": "checkout-web"
},
"createdAt": "2026-03-13T10:30:00.000Z",
"expiresAt": "2026-03-16T10:30:00.000Z"
}
},
"message": "Payment link created successfully",
"timestamp": "2026-03-13T10:30:00.000Z"
}Campos de la respuesta
| Campo | Tipo | Descripción |
|---|---|---|
id | string | Identificador único del link de pago |
hashUrl | string | Hash único usado en la URL de pago |
paymentLinkUrl | string | URL completa de la página de pago |
customerName | string | Nombre del cliente |
customerEmail | string | Email del cliente (si fue proporcionado) |
concept | string | Concepto o descripción del cobro |
amount | integer | Monto en centavos |
status | string | Estado del link. Siempre "pending" al crearse |
isTest | boolean | Si es un link de prueba |
paymentMethods | array | Métodos de pago habilitados |
requestCustomerEmail | boolean | Si se solicita email al cliente en la página de pago |
redirectUrl | string | URL de redirección después del pago |
metadata | object | Datos personalizados asociados al link |
createdAt | string | Fecha de creación (ISO 8601) |
expiresAt | string | Fecha de expiración (3 días desde la creación, ISO 8601) |
Códigos de estado
| Código | Descripción |
|---|---|
201 | Link de pago creado exitosamente |
400 | Error de validación en los datos enviados |
401 | API key faltante |
404 | API key no encontrada |
429 | Rate limit excedido |
Respuestas de error
Error de validación
{
"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"
}Sin proveedor de pagos activo
{
"success": false,
"error": {
"code": "ERR_NO_ACTIVE_PAYMENT_PROVIDER",
"message": "No active payment provider found"
},
"timestamp": "2026-03-13T10:30:00.000Z"
}Campos requeridos faltantes
{
"success": false,
"error": {
"code": "ERR_VALIDATION_FAILED",
"message": "Validation failed",
"details": [
{
"field": "concept",
"message": "Concept is required"
},
{
"field": "customerName",
"message": "Customer name is required"
}
]
},
"timestamp": "2026-03-13T10:30:00.000Z"
}Monto inválido
{
"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"
}Error de validación de metadata
{
"success": false,
"error": {
"code": "ERR_METADATA_VALIDATION",
"message": "Metadata exceeds maximum size of 8192 bytes"
},
"timestamp": "2026-03-13T10:30:00.000Z"
}Método de pago inactivo
{
"success": false,
"error": {
"code": "ERR_INACTIVE_PAYMENT_METHOD",
"message": "Payment method 'card' is not active"
},
"timestamp": "2026-03-13T10:30:00.000Z"
}Ejemplos de implementación
Creación básica con manejo de errores
async function createPaymentLink(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);
if (result.error.details) {
result.error.details.forEach(d => console.error(` - ${d.field}: ${d.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 } };
}
}E-commerce: link para una orden
async function createOrderPaymentLink(order) {
const result = await createPaymentLink({
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'
}
});
if (!result.success) {
throw new Error(`Error al crear link: ${result.error.code}`);
}
return result.paymentLink;
}Suscripciones: link de pago periódico
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()];
return await createPaymentLink({
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'
}
});
}Helpers de validación
Valida los datos antes de enviar la solicitud para evitar errores:
:: tab Node.js
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 };
}
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 (máx: 20)` };
}
const size = new TextEncoder().encode(JSON.stringify(metadata)).length;
if (size > 8192) {
return { valid: false, error: `La metadata ocupa ${size} bytes (máx: 8192)` };
}
return { valid: true };
}::
:: tab Python
import json
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 (máx 20 llaves, máx 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 (máx: 20)"}
size = len(json.dumps(metadata).encode("utf-8"))
if size > 8192:
return {"valid": False, "error": f"La metadata ocupa {size} bytes (máx: 8192)"}
return {"valid": True}::
Buenas prácticas
Manejo de errores
Siempre verifica el campo success y maneja los errores de forma apropiada. Muestra mensajes útiles al usuario basándote en el código de error.
Modo de prueba
Usa isTest: true durante el desarrollo. Los links de prueba no procesan pagos reales pero se comportan de forma idéntica.
No uses modo prueba en producción
Nunca uses isTest: true en un entorno de producción. Los links de prueba no generan cobros reales.
Estrategia de metadata
- Incluye siempre un identificador único de tu sistema (ej.
orderId) - 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
Implementa reintentos con backoff exponencial para errores de red y 5xx. No reintentes errores 4xx, ya que son errores del cliente:
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
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, espacia las solicitudes para no exceder el límite. Consulta la para más detalles.
Siguientes pasos
- — Consulta el estado de un link
- — Cancela un link pendiente
- — Obtén todos tus links
- — Recibe notificaciones cuando se completan los pagos
- — Ejemplos prácticos de integración