Skip to content

Suscripciones (API)

Las suscripciones cobran de forma recurrente a un cliente vaulteado por uno o más productos recurrentes ("items"), cada uno con una cantidad. Cada ciclo la factura es Σ(product.price × quantity) sobre los items, con los precios del catálogo vigentes al momento de facturar — editar el precio de un producto en el dashboard cambia la próxima factura, no la ya emitida.

Todos los endpoints requieren tu API key de proyecto en el header X-API-Key.

Suscripciones de un proyecto con desglose de items y total por ciclo

Crear una suscripción

Suscribe a un cliente vaulteado a uno o más productos recurrentes (items), cada uno con una cantidad.

Modelo de facturación — cobro en mora, no al crear

Crear una suscripción no cobra al cliente. El primer período comienza de inmediato y el primer cargo recurrente se ejecuta en currentPeriodEnd (un ciclo completo después), y luego cada ciclo. Esto difiere de procesadores como Stripe, que cobran por adelantado al crear la suscripción.

El flujo esperado es que el cliente pague por — y vaultee su tarjeta durante — el primer período fuera de banda, por ejemplo con un que cobra amount y almacena la tarjeta en el mismo checkout. La suscripción luego mueve las renovaciones a partir del período siguiente. Una suscripción creada sobre una tarjeta que nunca fue cobrada significa que su primera renovación es la primera vez que se ejercita la tarjeta — no hay validación de tarjeta al momento de crear.

Así es como un software con precios dinámicos por inquilino compone su total mensual desde un catálogo fijo del dashboard en lugar de crear productos desechables. Ejemplo: un plan POS de $35 base + $20 por sucursal extra + $5 por usuario extra, para un inquilino con 2 sucursales y 8 usuarios:

items: [
  { "productCode": "pos-base",     "quantity": 1 },  // $35.00
  { "productCode": "pos-sucursal", "quantity": 1 },  // $20.00
  { "productCode": "pos-usuario",  "quantity": 2 }   // $10.00
]                                      → factura $65.00 / mes
http
POST /v1/subscriptions

Cuerpo de la solicitud:

CampoTipoRequeridoDescripción
customerIdstringCliente con un método de pago almacenado
itemsarraySí*1–20 items: { productCode, quantity? }. quantity por defecto es 1 (máx 1000)
productCodestringSí*Forma legada de un solo producto — equivale a items: [{ productCode, quantity: 1 }]
isTestbooleanNoEntorno sandbox (por defecto false)

* Envía o items o productCode, no ambos.

Reglas sobre el conjunto de items:

  • Todo producto debe ser recurring, estar activo y pertenecer al proyecto.
  • Todos los items deben compartir el mismo billingPeriod / billingInterval (un ciclo por suscripción) y la misma currency (una factura).
  • No se permiten productCodes duplicados — usa quantity en su lugar.
bash
curl -X POST https://api-pay.zelta.dev/v1/subscriptions \
  -H "X-API-Key: tu-api-key-de-proyecto" \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "1234567890",
    "items": [
      { "productCode": "pos-base" },
      { "productCode": "pos-sucursal" },
      { "productCode": "pos-usuario", "quantity": 2 }
    ]
  }'

Respuesta (201):

json
{
  "success": true,
  "data": {
    "subscription": {
      "id": "9876543210",
      "customerId": "1234567890",
      "status": "active",
      "currentPeriodStart": "2026-06-04T00:00:00.000Z",
      "currentPeriodEnd": "2026-07-04T00:00:00.000Z",
      "nextBillingAt": "2026-07-04T00:00:00.000Z",
      "items": [
        { "productId": "111", "quantity": 1, "effectiveFrom": "2026-06-04T00:00:00.000Z" },
        { "productId": "222", "quantity": 1, "effectiveFrom": "2026-06-04T00:00:00.000Z" },
        { "productId": "333", "quantity": 2, "effectiveFrom": "2026-06-04T00:00:00.000Z" }
      ],
      "cycleTotal": 6500
    }
  },
  "message": "Subscription created"
}

Se emite un webhook subscription.created con los items (productCode, name, price, quantity) y el cycleTotal. Las facturas recurrentes luego emiten invoice.paid / invoice.payment_failed con el mismo desglose de items.

GET /v1/subscriptions/:id siempre devuelve los items configurados (cada uno con su product anidado) más cycleTotal; en GET /v1/subscriptions agrega expand=items.

Los estados de una suscripción son active, past_due y cancelled.

Errores comunes:

CódigoSignificado
ERR_PRODUCT_NOT_FOUNDUn productCode no existe en el proyecto
ERR_PRODUCT_NOT_RECURRINGUn item es un producto one_time — usa cargos en su lugar
ERR_PRODUCT_NOT_ACTIVEEl producto de un item está inactivo o archivado
ERR_MIXED_BILLING_PERIODSItems con distinto billingPeriod/billingInterval
ERR_MIXED_CURRENCIESItems con distintas monedas
ERR_CUSTOMER_NO_VAULTEl cliente no tiene método de pago almacenado

Actualizar los items de una suscripción

Reemplaza todo el conjunto de items de la suscripción (mismas reglas que al crear, y el nuevo conjunto debe conservar el billingPeriod/billingInterval actual — para cambiar la cadencia, cancela y crea una nueva suscripción).

http
PATCH /v1/subscriptions/:id/items
bash
curl -X PATCH https://api-pay.zelta.dev/v1/subscriptions/9876543210/items \
  -H "X-API-Key: tu-api-key-de-proyecto" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "productCode": "pos-base" },
      { "productCode": "pos-sucursal", "quantity": 2 },
      { "productCode": "pos-usuario", "quantity": 2 }
    ]
  }'

Efectivo el próximo ciclo

El cambio surte efecto en el próximo ciclo de facturación (currentPeriodEnd): el período en curso sigue facturando el conjunto con el que empezó, y la respuesta/webhook llevan effectiveAt con esa frontera. Para el dinero que el cliente debe ahora mismo (p. ej. un addon activado a mitad de período), combina el PATCH con un cargo de addon único usando — juntos facturan el resto del período actual exactamente una vez y cada ciclo siguiente con la nueva composición.

Se permite mientras la suscripción esté active o past_due. Se emite un webhook subscription.items_updated con los nuevos items, cycleTotal y effectiveAt.

deferredAmountCents — solo para addons menores a $1 en NMI

El campo opcional deferredAmountCents existe para un caso específico: tienes un addon que cuesta menos de $1.00 (menos de 100¢) y la suscripción se factura por NMI, cuyo gateway rechaza cualquier cargo por debajo de su piso de $1.00. Como ese addon no puede cobrarse por sí solo, esto acumula su monto en la próxima factura de la suscripción como un recargo de un ciclo:

bash
curl -X PATCH https://api-pay.zelta.dev/v1/subscriptions/9876543210/items \
  -H "X-API-Key: tu-api-key-de-proyecto" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [ { "productCode": "pos-base" } ],
    "deferredAmountCents": 83
  }'

Acepta 199 (centavos enteros), se acumula entre llamadas y se limpia cuando la próxima factura lo cobra. No lo uses en ningún otro caso — para cualquier monto ≥ $1.00, o en cualquier otro gateway, cobra el addon directamente con .

Cambiar el cliente de la suscripción

Reapunta la suscripción a otro cliente vaulteado del mismo proyecto. Como cada cliente tiene exactamente una tarjeta almacenada, así es como el software cambia el método de pago activo de un plan recurrente sin cancelar + recrear (lo que movería el ancla de facturación) y sin cobrar nada.

http
PATCH /v1/subscriptions/:id/customer

Cuerpo de la solicitud:

CampoTipoRequeridoDescripción
customerIdstringCliente de reemplazo; debe pertenecer al proyecto y tener un método de pago almacenado
bash
curl -X PATCH https://api-pay.zelta.dev/v1/subscriptions/9876543210/customer \
  -H "X-API-Key: tu-api-key-de-proyecto" \
  -H "Content-Type: application/json" \
  -d '{ "customerId": "1234567890" }'

Efectivo de inmediato, sin mover el ancla

Todo cargo futuro — la próxima renovación y cualquier reintento de dunning ya programado — factura al vault del nuevo cliente (el cliente se resuelve al momento del cargo). El período en curso, currentPeriodEnd y nextBillingAt no se mueven, así que nada se factura al momento del cambio.

Se permite mientras la suscripción esté active o past_due (este último es el caso de rescate de dunning: cambia a una tarjeta que funcione y deja que los reintentos recuperen el ciclo). Se emite un webhook subscription.customer_updated con el id del cliente nuevo y del anterior. Falla con ERR_CUSTOMER_NO_VAULT si el cliente de reemplazo no tiene método de pago almacenado.

Cancelar una suscripción

Cancela una suscripción de inmediato o al final del período actual.

http
PATCH /v1/subscriptions/:id/cancel

Cuerpo de la solicitud:

CampoTipoRequeridoDescripción
cancelAtPeriodEndbooleanNofalse (por defecto) cancela de inmediato; true programa la cancelación para currentPeriodEnd
bash
curl -X PATCH https://api-pay.zelta.dev/v1/subscriptions/9876543210/cancel \
  -H "X-API-Key: tu-api-key-de-proyecto" \
  -H "Content-Type: application/json" \
  -d '{ "cancelAtPeriodEnd": true }'
  • Inmediata (cancelAtPeriodEnd: false / omitido): la suscripción pasa a cancelled de inmediato y nextBillingAt se limpia. Emite subscription.cancelled con reason: "cancelled_by_request".
  • Al final del período (cancelAtPeriodEnd: true): la suscripción sigue active hasta currentPeriodEnd, y entonces se cancela.

Cancelar al final del período nunca produce un cargo final

Cuando llega la frontera del período, una suscripción programada para cancelar no se factura por el período próximo — se cancela limpiamente y emite subscription.cancelled con reason: "cancel_at_period_end". Bajo el modelo de mora, el período que el cliente ya usó fue pagado por el cargo de la frontera anterior (o por el primer pago fuera de banda), así que no se debe ningún cargo adicional.

Reactivar una suscripción

Deshace una cancelación, en cualquiera de sus dos formas:

  • Cancelación pendiente — la suscripción aún está active/past_due pero programada para cancelar al final del período (cancelAtPeriodEnd: true). Reactivar simplemente levanta esa cancelación programada; la facturación continúa intacta.
  • Ya cancelada (status: "cancelled") — permitido solo mientras aún esté dentro del período que el cliente ya pagó (currentPeriodEnd en el futuro). Vuelve a active y la facturación se re-arma en currentPeriodEnd, de modo que el barrido la retoma desde la próxima frontera y el período en curso (ya pagado) nunca se cobra dos veces.
http
PATCH /v1/subscriptions/:id/reactivate
bash
curl -X PATCH https://api-pay.zelta.dev/v1/subscriptions/9876543210/reactivate \
  -H "X-API-Key: tu-api-key-de-proyecto"

Emite subscription.resumed. Falla con ERR_INVALID_SUBSCRIPTION_TRANSITION si la suscripción está activa sin cancelación pendiente, y con ERR_SUBSCRIPTION_PERIOD_ELAPSED si está cancelled y el período pagado ya venció — pasado ese punto el período terminó y debe crearse una nueva suscripción.

Dunning (renovaciones fallidas)

Cuando un cargo de renovación es rechazado, la suscripción entra en past_due y el cargo se reintenta automáticamente en un calendario fijo — el intento original más cuatro reintentos en los días +1, +3, +5 y +7 desde la fecha de vencimiento original (una ventana de gracia de 7 días). Cada reintento emite invoice.payment_failed y avanza subscription.past_due con el siguiente nextBillingAt. Si el reintento del día 7 también falla, la suscripción se cancela y emite subscription.cancelled con reason: "dunning_exhausted".

Para rescatar una suscripción past_due, a uno con una tarjeta que funcione — los reintentos ya programados cobrarán al nuevo vault.

Listar las facturas de una suscripción

Devuelve las facturas (una por período facturado) generadas para una suscripción.

http
GET /v1/subscriptions/:id/invoices

Parámetros de consulta: paginación estándar (lim, off) más un filtro opcional status (open, paid, failed, void).

bash
curl "https://api-pay.zelta.dev/v1/subscriptions/9876543210/invoices?status=paid" \
  -H "X-API-Key: tu-api-key-de-proyecto"

Cada factura lleva su amount, currency, status, periodStart/periodEnd, attempts y un itemsSnapshot congelado al momento de facturar, de modo que la composición cobrada se preserva aunque el catálogo cambie después.

Siguientes pasos

  • — cobra addons y productos únicos, con proración
  • — gestiona los clientes vaulteados a los que facturas
  • subscription.* e invoice.*

Documentación oficial de Zelta