Skip to content

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 un amount y vaultea la tarjeta en el mismo checkout; requiere un productCode.

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.

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.

http
POST /v1/collections/links

Cuerpo de la solicitud:

CampoTipoRequeridoDescripción
customer.namestringSíNombre del cliente (1-120 caracteres). Se usa si el cliente no escribe el nombre del titular en la página
customer.emailstringNoCorreo del cliente. Si lo envías, la página no se lo pide
isTestbooleanNoEntorno sandbox (por defecto false)
redirectUrlstringNoURL http o https a la que redirigir al cliente tras una captura exitosa (máx. 2048 caracteres)
metadataobjectNoMismas 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.

bash
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):

json
{
  "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).

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).

http
POST /v1/collections/payment-links

Cuerpo de la solicitud:

CampoTipoRequeridoDescripción
customer.namestringSíNombre del cliente (1-120 caracteres)
customer.emailstringNoCorreo del cliente. Si lo envías, la página no se lo pide
conceptstringSíQué se está pagando (se muestra en el checkout)
amountintegerSíMonto en centavos (100–1,000,000, es decir $1–$10,000). No tiene que coincidir con el precio del producto
productCodestringSí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
isTestbooleanNoEntorno sandbox (por defecto false)
redirectUrlstringNoURL http o https a la que redirigir al cliente tras un pago exitoso (máx. 2048 caracteres)
metadataobjectNoMismas 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.

bash
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):

json
{
  "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ódigoHTTPCuándo
ERR_NO_ACTIVE_PAYMENT_PROVIDER400No hay un procesador de tarjetas (NMI o Emetec) activo en el entorno del link
ERR_AMOUNT_BELOW_NMI_MINIMUM400amount de 1 a 99 centavos (solo link de pago de cliente)
ERR_PRODUCT_NOT_FOUND404El productCode no existe en el proyecto
ERR_PRODUCT_NOT_ONE_TIME400El producto no es one_time
ERR_PRODUCT_NOT_ACTIVE400El producto no está activo
ERR_API_KEY_NOT_PROJECT_SCOPED403Usaste 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 .

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.

js
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:

json
{
  "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.created y payment.success

Documentación oficial de Zelta