Links de Colección (API)
Los links de colección son páginas alojadas por Zelta Pay que capturan la tarjeta del cliente, la guardan con el procesador de tarjetas activo de la cuenta (NMI o Emetec) y crean un cliente vaulteado. Son el punto de entrada del flujo de integración: una vez capturada la tarjeta, tu software mueve el dinero por API.
Existen dos variantes:
- Link de colección (
POST /v1/collections/links): solo captura y vaultea la tarjeta y crea el cliente. No cobra nada. - Link de pago de cliente (
POST /v1/collections/payment-links): cobra unamounty vaultea la tarjeta en el mismo checkout; requiere unproductCode.
Ambas variantes son solo tarjeta: Yappy y PayPal no pueden almacenar una tarjeta para cobros posteriores. Necesitas un procesador de tarjetas activo en el entorno del link; si no, la solicitud se rechaza con ERR_NO_ACTIVE_PAYMENT_PROVIDER. Todos los endpoints requieren tu API key de proyecto en el header X-API-Key.
Los links de ambas variantes vencen 3 días después de crearse (esta vigencia no se puede cambiar) y se pueden usar una sola vez.
Crear un link de colección
Crea una página alojada que únicamente recolecta la tarjeta: se guarda con el procesador activo de tu cuenta (NMI o Emetec) y se crea un cliente. Sin producto, sin monto, sin cobro.
POST /v1/collections/linksCuerpo de la solicitud:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
customer.name | string | Sí | Nombre del cliente (1-120 caracteres). Se usa si el cliente no escribe el nombre del titular en la página |
customer.email | string | No | Correo del cliente. Si lo envías, la página no se lo pide |
isTest | boolean | No | Entorno sandbox (por defecto false) |
redirectUrl | string | No | URL http o https a la que redirigir al cliente tras una captura exitosa (máx. 2048 caracteres) |
metadata | object | No | Mismas reglas que en los . Se devuelve en el webhook customer.created |
El cuerpo no acepta campos adicionales: un campo desconocido devuelve un error de validación 400.
curl -X POST https://api-pay.zelta.dev/v1/collections/links \
-H "X-API-Key: tu-api-key-de-proyecto" \
-H "Content-Type: application/json" \
-d '{
"customer": { "name": "John Doe", "email": "[email protected]" },
"redirectUrl": "https://tu-app.com/billing/payment-method-saved",
"metadata": { "userId": "usr_123" }
}'Respuesta (201):
{
"success": true,
"data": {
"id": "7312345678901234567",
"paymentLinkUrl": "https://pay.zelta.dev/Xk3v9Q_aB7mN2pL8rT4wYz1c",
"hashUrl": "Xk3v9Q_aB7mN2pL8rT4wYz1c",
"customerName": "John Doe",
"customerEmail": "[email protected]",
"status": "pending",
"isTest": false,
"expiresAt": "2026-06-07T00:00:00.000Z",
"createdAt": "2026-06-04T00:00:00.000Z",
"redirectUrl": "https://tu-app.com/billing/payment-method-saved"
},
"message": "Customer collection link created",
"timestamp": "2026-06-04T00:00:00.000Z"
}customerEmail y redirectUrl se omiten cuando no los enviaste.
Envía al cliente a paymentLinkUrl. Cuando guarde su tarjeta recibirás el webhook en el webhook del proyecto: guarda customer.id y los datos de la tarjeta, y luego mueve las transacciones por API. El cliente se crea con el nombre del titular que escribe en la página (o customer.name si no lo escribe).
Crear un link de pago de cliente
Igual que un link de colección, pero la página alojada cobra amount en el mismo checkout en que almacena la tarjeta y crea el cliente. Úsalo para un primer pago que además inscribe al cliente para cobros futuros (p. ej. una compra inicial + una tarjeta guardada para una suscripción).
POST /v1/collections/payment-linksCuerpo de la solicitud:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
customer.name | string | Sí | Nombre del cliente (1-120 caracteres) |
customer.email | string | No | Correo del cliente. Si lo envías, la página no se lo pide |
concept | string | Sí | Qué se está pagando (se muestra en el checkout) |
amount | integer | Sí | Monto en centavos (100–1,000,000, es decir $1–$10,000). No tiene que coincidir con el precio del producto |
productCode | string | Sí | Código de un producto one_time activo del proyecto (máx. 60 caracteres). El pago liquidado se registra como un cargo de este producto contra el nuevo cliente |
isTest | boolean | No | Entorno sandbox (por defecto false) |
redirectUrl | string | No | URL http o https a la que redirigir al cliente tras un pago exitoso (máx. 2048 caracteres) |
metadata | object | No | Mismas reglas que en los . Se devuelve en los webhooks payment.success, payment.failed, customer.created y charge.succeeded |
Un amount de 1 a 99 centavos se rechaza con 400 y el código ERR_AMOUNT_BELOW_NMI_MINIMUM, y además se envía el webhook al webhook del proyecto.
curl -X POST https://api-pay.zelta.dev/v1/collections/payment-links \
-H "X-API-Key: tu-api-key-de-proyecto" \
-H "Content-Type: application/json" \
-d '{
"customer": { "name": "John Doe", "email": "[email protected]" },
"concept": "Plan Pro — primer mes",
"amount": 2999,
"productCode": "prod_ACTIVACION",
"redirectUrl": "https://tu-app.com/billing/welcome",
"metadata": { "userId": "usr_123" }
}'Respuesta (201):
{
"success": true,
"data": {
"id": "7312345678901234567",
"paymentLinkUrl": "https://pay.zelta.dev/Xk3v9Q_aB7mN2pL8rT4wYz1c",
"hashUrl": "Xk3v9Q_aB7mN2pL8rT4wYz1c",
"customerName": "John Doe",
"customerEmail": "[email protected]",
"concept": "Plan Pro — primer mes",
"amount": 2999,
"status": "pending",
"isTest": false,
"expiresAt": "2026-06-07T00:00:00.000Z",
"createdAt": "2026-06-04T00:00:00.000Z",
"redirectUrl": "https://tu-app.com/billing/welcome"
},
"message": "Customer payment link created",
"timestamp": "2026-06-04T00:00:00.000Z"
}Envía al cliente a paymentLinkUrl. En un checkout exitoso recibirás, en este orden, (por el pago), (con customer.id y los datos de la tarjeta almacenada) y . Guarda el cliente y luego mueve más cargos o suscripciones por API.
El pago liquidado se registra como un cargo de productCode contra el nuevo cliente (cantidad 1, en USD), aparece en GET /v1/charges y en la vista de cargos del proyecto, y se reconcilia por el mismo camino que tus llamadas posteriores a POST /v1/charges.
Si el pago es rechazado, no se crea ningún cliente: recibes payment.failed y el link sigue disponible para que el cliente lo intente de nuevo hasta que venza.
Errores
| Código | HTTP | Cuándo |
|---|---|---|
ERR_NO_ACTIVE_PAYMENT_PROVIDER | 400 | No hay un procesador de tarjetas (NMI o Emetec) activo en el entorno del link |
ERR_AMOUNT_BELOW_NMI_MINIMUM | 400 | amount de 1 a 99 centavos (solo link de pago de cliente) |
ERR_PRODUCT_NOT_FOUND | 404 | El productCode no existe en el proyecto |
ERR_PRODUCT_NOT_ONE_TIME | 400 | El producto no es one_time |
ERR_PRODUCT_NOT_ACTIVE | 400 | El producto no está activo |
ERR_API_KEY_NOT_PROJECT_SCOPED | 403 | Usaste una API key de la cuenta en lugar de la del proyecto |
Los campos inválidos, faltantes o desconocidos devuelven 400 con el formato de .
Abrir el link en una ventana flotante
Un link alojado es una URL normal, así que una página no puede forzarse a abrirse como popup: la ventana la abre tu código. Para mantener al cliente en tu sitio, abre paymentLinkUrl con window.open en lugar de navegar fuera. La página alojada detecta que fue abierta de esta forma (window.opener) y, al tener éxito, publica un mensaje a tu página y se cierra sola a los pocos segundos.
function openZeltaCheckout(paymentLinkUrl, onDone) {
const w = 480;
const h = 720;
const left = window.screenX + (window.outerWidth - w) / 2;
const top = window.screenY + (window.outerHeight - h) / 2;
const popup = window.open(
paymentLinkUrl,
"zeltapay-checkout",
`width=${w},height=${h},left=${left},top=${top},resizable,scrollbars`,
);
if (!popup) {
// El navegador bloqueó la ventana: navega al link en la misma pestaña
window.location.href = paymentLinkUrl;
return;
}
function handleMessage(event) {
// Solo confía en mensajes de la página alojada de Zelta Pay.
if (event.origin !== "https://pay.zelta.dev") return;
const { type } = event.data || {};
if (type === "zeltapay:collected" || type === "zeltapay:paid") {
window.removeEventListener("message", handleMessage);
popup.close();
onDone(event.data);
}
}
window.addEventListener("message", handleMessage);
}El mensaje publicado no contiene datos personales:
{
"type": "zeltapay:collected",
"status": "collected",
"orderId": null
}type es zeltapay:collected (con status: "collected") para un link de colección y zeltapay:paid (con status: "completed" y el orderId del pago) para un link de pago de cliente. Si el navegador bloquea la ventana, tu código debe manejarlo (por ejemplo, abriendo el link en la misma pestaña, como en el ejemplo). Cuando el link se abre como una pestaña normal, la página redirige a redirectUrl a los pocos segundos del éxito, si lo enviaste.
El webhook es la fuente de verdad
El mensaje de postMessage es solo una señal de interfaz para cerrar la ventana y refrescar. El webhook sigue siendo la fuente de verdad: confirma el resultado desde los webhooks customer.created / payment.success, no desde el mensaje de la ventana.
Siguientes pasos
- — consulta el cliente creado tras la captura
- — cobra al cliente vaulteado
- — suscribe al cliente a un plan recurrente
- —
customer.createdypayment.success