Suscripciones
- Suscripciones
- Conceptos previos
- Idempotencia (Idempotency-Key)
- Objetos expandibles (expand)
- Estados de una suscripción
- Dos formatos de respuesta
- Fechas y hora del cobro
- Metadata
- Listar suscripciones
- Obtener una suscripción
- Suscripciones de un plan
- Suscripciones de un suscriptor
- Crear una suscripción
- Actualizar una suscripción
- Cancelar suscripción
- Pausar cobros
- Reanudar cobros
- Actualizar método de pago (tarjeta)
- Renovar suscripción
- Cambiar de plan (con prorrateo)
- Previsualizar cambio de plan
- Aplicar cupón
- Listar facturas
- Próxima factura (vista previa)
- Descargar factura en PDF
- Historial de transacciones
- Próxima transacción (próximo cobro)
Conceptos previos
Sección titulada «Conceptos previos»Antes de integrar las suscripciones ten en cuenta:
- Ambiente (test / producción): el token de acceso define el ambiente. Un token
de pruebas (
api-access:test) solo puede leer y crear datos de pruebas; un token de producción (api-access:production) solo opera sobre datos de producción. Las lecturas quedan filtradas automáticamente por el ambiente del token. - Nombres alternativos: además de los nombres históricos (
group,plan,subscriber) puedes usar los aliasproduct,priceycustomer, que apuntan a los mismos recursos. Ejemplo:GET /api/v1/subscriptions/customerequivale aGET /api/v1/subscriptions/subscriber. - Descuentos: los descuentos ahora se gestionan con Cupones
reutilizables (porcentaje o monto fijo). Los campos
discount_*del plan se mantienen por compatibilidad pero se recomienda usar cupones.
Idempotencia (Idempotency-Key)
Sección titulada «Idempotencia (Idempotency-Key)»Todas las escrituras (POST/PUT/DELETE) de subscriptions/* aceptan el header
Idempotency-Key. Si repites una solicitud con la misma key y los mismos
parámetros, se reproduce la respuesta original (con el header
Idempotency-Replayed: true) sin volver a ejecutar el cobro o la creación. Es la
forma segura de reintentar ante errores de red.
curl -X POST "/api/v1/subscriptions/subscription" \ -H 'Authorization: Bearer ACCESS_TOKEN' \ -H 'Content-type: application/json' \ -H 'Idempotency-Key: 9b1f0e34-6b2a-4a7e-9d0a-unico-por-operacion' \ -d '{ "plan_id": "...", "subscriber_id": "...", "card": "...", "office": 1 }'Objetos expandibles (expand)
Sección titulada «Objetos expandibles (expand)»Al consultar una suscripción puedes incrustar relaciones con expand[] y así evitar
llamadas adicionales. Valores soportados: plan, subscriber, next_transaction.
curl -X GET "/api/v1/subscriptions/subscription/your-subscription-id?expand[]=plan&expand[]=subscriber" \ -H 'Authorization: Bearer ACCESS_TOKEN' \ -H 'Content-type: application/json'Estados de una suscripción
Sección titulada «Estados de una suscripción»El campo status (presente en las respuestas de suscripción) refleja el estado
actual de la suscripción:
| status | Significado |
|---|---|
trial |
En período de prueba. |
activa |
Activa y al día. |
en_gracia |
Un cobro falló pero sigue dentro del período de gracia (past due). |
pausada |
Cobros pausados (ver Pause). |
cancelada |
Cancelada y sin servicio. Ver la nota de abajo. |
finalizada |
Terminada; no se realizarán más cobros. |
Dos formatos de respuesta
Sección titulada «Dos formatos de respuesta»Esta página tiene una peculiaridad que conviene conocer antes de escribir el parser:
| Qué llamas | Qué recibes |
|---|---|
Las consultas (GET) |
snake_case: subscription_plan_id, next_transaction, trial_ends_at, is_active… |
| Las acciones (cancelar, pausar, reanudar, cambiar de plan) | camelCase: planId, nextRenewAt, isActive, onGracePeriod… |
No son los mismos campos con otro nombre: son dos representaciones distintas de la suscripción. No reutilices el parser de una para la otra. Cada sección de abajo muestra la que le corresponde.
Fechas y hora del cobro
Sección titulada «Fechas y hora del cobro»Los campos históricos conservan su formato para no romper integraciones, y junto a ellos hay campos nuevos en ISO 8601 con offset explícito, que no obligan a adivinar la zona horaria:
| Campo | Formato | Ejemplo | Dónde |
|---|---|---|---|
starts_at, ends_at, trial_ends_at, canceled_at, paused_at, resumes_at |
Y-m-d H:i:s, hora de Colombia |
2026-10-28 11:28:00 |
GET, acciones y webhooks |
next_transaction |
Y-m-d, día en Colombia |
2026-10-28 |
GET con expand[]=next_transaction |
next_renew_at |
Y-m-d, día en Colombia |
2026-10-28 |
next-transaction, transaction-history y next_charge del webhook |
ends_at_iso |
ISO 8601 con offset | 2026-10-28T11:28:00-05:00 |
GET y webhooks |
next_charge_at |
ISO 8601 con offset | 2026-10-28T19:00:00-05:00 |
GET con expand[]=next_transaction |
charge_scheduled_at |
ISO 8601 con offset | 2026-10-28T19:00:00-05:00 |
next-transaction, transaction-history y next_charge del webhook |
cancel_at |
ISO 8601 con offset | 2026-10-28T11:28:00-05:00 |
GET, cancelación (cancelAt) y webhooks |
next_transaction, next-transaction.next_renew_at y next_charge.next_renew_at son
el mismo día. Antes next-transaction devolvía la medianoche en UTC
(2026-10-28T05:00:00.000000Z); ahora es 2026-10-28, como los demás.
Metadata
Sección titulada «Metadata»metadata es un objeto de pares clave-valor para guardar tus propias referencias (id
de usuario, de pedido, canal…), al estilo de Stripe. Existe en suscripciones,
suscriptores, planes y
cobros generados. Nosotros no lo usamos: te lo
devolvemos.
| Límite | Valor |
|---|---|
| Claves | Hasta 50 |
| Formato de la clave | 1 a 40 caracteres, [A-Za-z0-9_-] |
| Valor | Texto o número, hasta 500 caracteres. Se guarda como texto (42 vuelve como "42") |
| Al actualizar envías | Resultado |
|---|---|
{"metadata": {"plan_tier": "gold"}} |
Se combina con las claves que ya había |
{"metadata": {"plan_tier": ""}} o null como valor |
Se borra esa clave |
{"metadata": null} |
Se borra toda la metadata |
La recibes en los GET y listados y en los
webhooks de suscripción (subscription.metadata, y también
dentro de subscription.plan y subscription.subscriber).
Para buscar por ella, filtra con coincidencia exacta:
curl -X GET \'https://sag.efipay.co/api/v1/subscriptions/subscription?filter[metadata][user_id]=42' \-H 'Authorization: Bearer TU_TOKEN' \-H 'Accept: application/json'Si no cumple los límites, la respuesta es un 422 con error.code: "validation_failed"
y error.param: "metadata".
Listar suscripciones
Sección titulada «Listar suscripciones»Descripción: Devuelve la lista paginada de todas tus suscripciones (filtradas por el ambiente del token: pruebas o producción). Útil para tableros, sincronizaciones y reconciliación.
GET /api/v1/subscriptions/subscriptionFiltros
Sección titulada «Filtros»Todos son opcionales y se combinan entre sí. Sin ninguno, la respuesta es la lista completa del ambiente del token.
| Parámetro | Tipo | Qué hace |
|---|---|---|
filter[subscriber_id] |
UUID exacto | Todas las suscripciones de un suscriptor |
filter[plan_id] |
UUID exacto | Todas las de un plan |
filter[status] |
exacto | trial, activa, en_gracia, pausada, cancelada, finalizada |
filter[email] |
parcial | Por el correo del suscriptor |
filter[subscription_id] |
parcial | Por el id de la suscripción |
filter[description] |
parcial | Por la descripción que guardaste al crearla |
filter[plan_name] |
parcial | Por el nombre del plan |
filter[metadata][clave] |
exacto | Por un par de tu metadata, por ejemplo filter[metadata][user_id]=42 |
filter[active] |
1, 0, all |
Si está dando servicio ahora mismo |
filter[start_date] |
fecha | Creadas desde esa fecha (inclusive) |
filter[finish_date] |
fecha | Creadas hasta esa fecha (inclusive) |
sort |
campo | created_at, starts_at, ends_at, trial_ends_at. Prefija con - para descendente |
curl -X GET \'https://sag.efipay.co/api/v1/subscriptions/subscription?filter[email]=ana@correo.com&filter[status]=activa' \-H 'Authorization: Bearer TU_TOKEN' \-H 'Accept: application/json'Obtener una suscripción
Sección titulada «Obtener una suscripción»Descripción: Devuelve el detalle de una suscripción a partir de su id. Acepta
objetos expandibles para incluir el plan, el suscriptor o el próximo cobro.
{ "id": "9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b", "description": null, "metadata": { "user_id": "42" }, "webhook_url": "https://tu-comercio.com/webhook", "trial_ends_at": "2026-07-15 00:00:00", "discount_ends_at": null, "starts_at": "2026-07-15 00:00:00", "ends_at": "2026-08-15 00:00:00", "grace_ends_at": "2026-08-18 00:00:00", "ends_at_iso": "2026-08-15T00:00:00-05:00", "next_transaction": "2026-08-15", "next_charge_at": "2026-08-15T19:00:00-05:00", "canceled_at": null, "paused_at": null, "resumes_at": null, "environment": "production", "status": "activa", "on_trial": false, "on_discount": false, "on_grace_period": false, "paused": false, "canceled": false, "cancel_at_period_end": false, "cancel_at": null, "is_active": true, "is_ended": false, "subscription_plan_id": "9a4698e5-d936-4298-84dc-4a770bfacc19", "subscriber_id": "9a469901-d010-4afe-94fe-da6790a4f72b", "office_id": "9a3bf0a4-9854-418f-aa05-5a04b8ce4372", "created_at": "2026-07-01 09:12:00", "updated_at": "2026-07-15T00:00:00.000000Z"}Campos y tipos
Sección titulada «Campos y tipos»| Campo | Tipo | Descripción |
|---|---|---|
id |
uuid | Id de la suscripción |
description |
string | null | Texto libre tuyo, hasta 255 caracteres |
metadata |
objeto | null | Tus pares clave-valor. Ver Metadata |
webhook_url |
string | null | Dónde te avisamos los cambios |
status |
string | trial, activa, en_gracia, pausada, cancelada, finalizada. Una cancelación al final del período sigue en activa/en_gracia hasta cancel_at |
environment |
string | production o testing |
starts_at |
fecha | null | Inicio del período vigente |
ends_at |
fecha | null | Fin del período vigente. Hasta aquí hay servicio |
ends_at_iso |
ISO 8601 | null | El mismo ends_at, con offset explícito |
grace_ends_at |
fecha | null | Fin de la ventana de gracia para reintentar el cobro |
trial_ends_at |
fecha | null | Fin de la prueba |
discount_ends_at |
fecha | null | Fin del descuento |
canceled_at |
fecha | null | Cuándo se canceló. Con at_period_end: false es igual a ends_at |
cancel_at_period_end |
bool | Cancelada pero todavía dando servicio hasta cancel_at |
cancel_at |
ISO 8601 | null | Cuándo deja de dar servicio una suscripción cancelada: ends_at, o el fin de la gracia si es posterior. null si no está cancelada |
paused_at |
fecha | null | Cuándo se pausaron los cobros |
resumes_at |
fecha | null | Cuándo se reanudan. null con paused_at = pausa indefinida |
next_transaction |
Y-m-d | null |
Día del próximo cobro programado, en Colombia. Con expand[]=next_transaction |
next_charge_at |
ISO 8601 | null | Instante en que el proceso diario intentará ese cobro (19:00 de Colombia). Con expand[]=next_transaction. Ver Fechas |
on_trial |
bool | En período de prueba |
on_discount |
bool | Con descuento vigente |
on_grace_period |
bool | Dentro de la gracia: el cobro falló pero sigue habiendo servicio |
paused |
bool | Cobros pausados ahora mismo |
canceled |
bool | Cancelada. Puede ser true con is_active en true (ver cancel_at_period_end) |
is_active |
bool | Si hay servicio ahora mismo. Es el que debes mirar para dar o cortar acceso |
is_ended |
bool | El período terminó |
subscription_plan_id |
uuid | Plan |
subscriber_id |
uuid | Suscriptor |
office_id |
— | Sede |
created_at / updated_at |
fecha |
Suscripciones de un plan
Sección titulada «Suscripciones de un plan»Descripción: Lista las suscripciones asociadas a un plan específico. Útil para saber cuántos clientes están suscritos a un plan.
Suscripciones de un suscriptor
Sección titulada «Suscripciones de un suscriptor»Descripción: Lista las suscripciones de un suscriptor (cliente) específico.
Crear una suscripción
Sección titulada «Crear una suscripción»Descripción: Crea una suscripción a partir de un plan y un suscriptor ya
creados, más los datos de la tarjeta con la que se cobrará la recurrencia. La tarjeta se
tokeniza de forma segura; si prefieres, puedes enviar un card (token ya generado en
Tokenizado) y usaremos ese token para los cobros. El
primer cobro se realiza al momento (o al terminar el período de prueba, si el plan lo tiene).
La tarjeta puede llegar de dos maneras excluyentes: como card (un token ya
generado en Tokenizado) o como el objeto
card_information con los datos en claro. Envía una u otra, nunca las dos.
| Nombre del campo | Descripción | Reglas |
|---|---|---|
| plan_id | Id del plan a cobrar. Debe estar activo y pertenecer a la misma sucursal que envías en office |
['required', 'exists:subscription_plans,id'] |
| subscriber_id | Id del suscriptor. Debe pertenecer a la misma sucursal que envías en office |
['required', 'exists:subscribers,id'] |
| office | Sucursal de la suscripción. Debe ser una de tus sucursales | ['required', 'exists:offices,id'] |
| description | Descripción libre de esta suscripción | ['nullable', 'string', 'max:255'] |
| metadata | Tus pares clave-valor. Ver Metadata | ['sometimes', 'nullable', 'metadata'] |
| webhook_url | URL a la que notificaremos cada cobro de esta suscripción. Recibe un POST firmado con la cabecera Signature. Ver webhooks |
['nullable', 'url'] |
| card | Token de una tarjeta guardada. Si lo envías, no envíes card_information |
['required_without:card_information', 'missing_with:card_information', 'string'] |
| card_information.holder | Nombre impreso en la tarjeta | ['sometimes', 'required', 'missing_with:card', 'string', 'max:80'] |
| card_information.number | Número de la tarjeta, sin espacios. Se valida como número de tarjeta real | ['sometimes', 'required', 'missing_with:card', 'numeric'] |
| card_information.datetime | Vencimiento en formato YYYY-MM, con mes entre 01 y 12. No puede estar vencida |
['sometimes', 'required', 'missing_with:card', 'date_format:Y-m', 'after_or_equal:<mes actual>'] |
| card_information.cvv | Código de seguridad. La cantidad de dígitos se valida según la franquicia que resulte de card_information.number |
['sometimes', 'required', 'missing_with:card', 'numeric'] |
Solicitud con token
curl -X POST\"/api/v1/subscriptions/subscription"\-H 'Authorization: Bearer ACCESS_TOKEN' \-H "Content-type: application/json" \-d '{ "plan_id": "9a9331d9-ea9a-4ad4-a9a4-0d23bd91fff1", "subscriber_id": "9a933275-1573-497c-916c-ba7fe4f99326", "card": "your-token-card-here", "office": 1}'Ejemplo de solicitud sin token:
curl -X POST\"/api/v1/subscriptions/subscription"\-H 'Authorization: Bearer ACCESS_TOKEN' \-H "Content-type: application/json" \-d '{ "plan_id": "9a9331d9-ea9a-4ad4-a9a4-0d23bd91fff1", "subscriber_id": "9a933275-1573-497c-916c-ba7fe4f99326", "card_information": { "holder": "Holder Holder", "number": "5249314023340339", "datetime": "2026-02", "cvv": 899 }, "office": 1}'Actualizar una suscripción
Sección titulada «Actualizar una suscripción»PUT /api/v1/subscriptions/subscription/{subscription-id}
Descripción: Actualiza los datos propios del integrador: description y
metadata. No cambia el plan, la tarjeta ni las fechas; para eso están
cambiar de plan y actualizar el método de pago.
| Nombre del campo | Descripción | Reglas |
|---|---|---|
| description | Descripción libre de la suscripción | ['sometimes', 'nullable', 'string', 'max:255'] |
| metadata | Se combina con la existente. Una clave con "" o null se borra; metadata: null borra todo |
['sometimes', 'nullable', 'metadata'] |
curl -X PUT \'/api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b' \-H 'Authorization: Bearer ACCESS_TOKEN' \-H 'Content-type: application/json' \-d '{ "metadata": { "user_id": "42", "channel": "" } }'Devuelve el objeto de la suscripción, igual que obtener una suscripción:
{ "id": "9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b", "description": "Cliente premium", "metadata": { "user_id": "42" }, "status": "activa", "...": "..."}{ "message": "El campo metadata admite como máximo 50 claves.", "errors": { "metadata": ["El campo metadata admite como máximo 50 claves."] }, "error": { "type": "invalid_request_error", "code": "validation_failed", "message": "El campo metadata admite como máximo 50 claves.", "param": "metadata" }}Cancelar suscripción
Sección titulada «Cancelar suscripción»Descripción: Cancela una suscripción activa. Por defecto la cancelación es al
final del período vigente: el cliente conserva el servicio hasta ends_at.
Cuándo surte efecto
Sección titulada «Cuándo surte efecto»| Cuerpo | canceled_at |
ends_at |
is_active |
status |
Qué significa |
|---|---|---|---|---|---|
(vacío) o {"at_period_end": true} |
Ahora | No cambia | true hasta cancel_at |
activa (o en_gracia) hasta cancel_at, luego cancelada |
El cliente ya pagó el ciclo: lo termina |
{"at_period_end": false} |
Ahora | Ahora | false de inmediato |
cancelada |
Corte inmediato, también de la gracia |
Mientras una cancelación al final del período no llega a su fin, la suscripción trae
canceled: true, cancel_at_period_end: true y cancel_at (ISO 8601 con offset, por
ejemplo "2026-10-28T11:28:00-05:00"; es ends_at, o el fin de la gracia si es
posterior). Es igual en el GET, en la respuesta de esta llamada (cancelAtPeriodEnd,
cancelAt) y en el webhook canceled.
curl -X PUT \'https://sag.efipay.co/api/v1/subscriptions/subscription/cancel/your-subscription-id' \-H 'Authorization: Bearer TU_TOKEN' \-H 'Content-type: application/json' \-d '{ "at_period_end": false }'{ "canceled": true, "at_period_end": true, "subscription": { "id": "9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b", "status": "activa", "canceled": true, "cancelAtPeriodEnd": true, "cancelAt": "2026-10-28T11:28:00-05:00", "isActive": true }}Con at_period_end: false llega status: "cancelada", cancelAtPeriodEnd: false e
isActive: false.
{ "canceled": false, "message": "La suscripción ya está cancelada.", "error": { "type": "invalid_request_error", "code": "subscription_already_canceled", "message": "La suscripción ya está cancelada.", "param": null }}Pausar cobros
Sección titulada «Pausar cobros»Descripción: Pausa los cobros de una suscripción. Mientras esté pausada, el motor de
recurrencia no realiza cobros. Puedes programar una reanudación automática con
resumes_at; si no lo envías, queda pausada hasta que llames a Reanudar cobros.
| Nombre del campo | Descripción | Reglas |
|---|---|---|
| resumes_at | Fecha y hora en que la suscripción se reanuda sola. Debe ser futura. Si la omites, la pausa dura hasta que llames a reanudar | ['sometimes', 'nullable', 'date', 'after:now'] |
curl -X POST\"/api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/pause"\-H 'Authorization: Bearer ACCESS_TOKEN' \-H "Content-type: application/json" \-d '{ "resumes_at": "2026-09-01 00:00:00" }'{ "paused": true, "subscription": { "id": "9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b", "status": "pausada", "paused": true, "pausedAt": "2026-07-30 10:00:00", "resumesAt": "2026-09-01 00:00:00" }}{ "paused": false, "message": "No se puede pausar una suscripción cancelada."}Reanudar cobros
Sección titulada «Reanudar cobros»Descripción: Reanuda los cobros de una suscripción pausada.
curl -X POST\"/api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/resume"\-H 'Authorization: Bearer ACCESS_TOKEN' \-H "Content-type: application/json"{ "resumed": true, "subscription": { "id": "9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b", "status": "activa", "paused": false }}Actualizar método de pago (tarjeta)
Sección titulada «Actualizar método de pago (tarjeta)»Descripción: Actualiza la tarjeta con la que se cobran las recurrencias de una suscripción. La tarjeta se tokeniza de forma segura y reemplaza a la anterior.
| Nombre del campo | Descripción | Reglas |
|---|---|---|
| number | Número de la nueva tarjeta, sin espacios. Se valida como número de tarjeta real | ['required', 'numeric'] |
| datetime | Vencimiento en formato YYYY-MM (por ejemplo 2030-05). No puede estar vencida |
['required', 'date'] |
| cvv | Código de seguridad. La cantidad de dígitos se valida según la franquicia que resulte de number |
['required', 'numeric'] |
curl -X PUT\"/api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/update-payment-method"\-H 'Authorization: Bearer ACCESS_TOKEN' \-H "Content-type: application/json" \-d '{ "number": "5249314023340339", "datetime": "2028-05", "cvv": 899}'{ "updated": true }{ "message": "No se puede actualizar el método de pago de una suscripción cancelada" }Renovar suscripción
Sección titulada «Renovar suscripción»POST /api/v1/subscriptions/subscription/{subscription-id}/renew
Descripción: Le envía al suscriptor una invitación de renovación por correo, con un enlace en el que confirma que quiere seguir. Sirve para reactivar una suscripción que ya terminó.
No recibe cuerpo. El suscriptor debe tener una tarjeta guardada, porque la invitación la reutiliza.
curl -X POST \'/api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/renew' \-H 'Authorization: Bearer ACCESS_TOKEN' \-H 'Accept: application/json'{ "renewed": true, "message": "Se ha enviado una invitación de renovación al suscriptor por correo electrónico."}{ "renewed": false, "message": "Ya existe una invitación de renovación pendiente para esta suscripción."}{ "renewed": false, "message": "El suscriptor no tiene una tarjeta guardada. No se puede enviar la invitación."}Cambiar de plan (con prorrateo)
Sección titulada «Cambiar de plan (con prorrateo)»Descripción: Cambia el plan de una suscripción de forma inmediata. No requiere que el suscriptor acepte nada.
El prorrateo calcula:
- Un crédito por el tiempo no usado del plan actual.
- Un cargo por el tiempo restante del período con el nuevo plan.
- El neto (
net) = cargo − crédito. Qué se hace con él lo decideproration_behavior, con la misma semántica que en Stripe.
El cobro pendiente del próximo ciclo se re-tarifica automáticamente al precio del nuevo plan.
Qué hace cada proration_behavior
Sección titulada «Qué hace cada proration_behavior»proration_behavior |
Neto positivo (upgrade) | Neto negativo (downgrade) | applies_on |
|---|---|---|---|
create_prorations (por defecto) |
Se suma a la próxima renovación. Hoy no se cobra nada | Baja la próxima renovación. Si el crédito supera toda la renovación, va al balance del suscriptor | "next_renewal" |
always_invoice |
Se cobra de inmediato con la tarjeta guardada | Va al balance del suscriptor | "now" |
none |
No se cobra ni se acredita nada | No se cobra ni se acredita nada | null |
create_prorations: el ajuste aparece como una línea aparte conproration: trueen la próxima factura, y el cobro pendiente exponeproration_amounten next-transaction y en el historial.always_invoice: el cobro aparece en el historial y la respuesta traeproration.transaction, el mismo objeto de transacción del checkout. Si ese cobro se rechaza, el plan no cambia y la respuesta es un402(ver abajo).none: el plan cambia ya y la próxima renovación cobra el precio del plan nuevo. La respuesta traenet: 0,line_items: []y el cálculo teórico enproration.preview, solo como referencia.
| Nombre del campo | Descripción | Reglas |
|---|---|---|
| plan_id | Id del nuevo plan. Debe estar activo, ser de tu comercio y del mismo ambiente que la suscripción | ['required', 'exists:subscription_plans,id'] |
| mode | direct (por defecto) aplica el cambio de inmediato. invitation le envía al suscriptor una invitación por correo para que lo apruebe. Sin mode, una suscripción inactiva va por invitación; con mode: "direct" explícito sobre una cancelada o inactiva responde 400 |
['sometimes', 'in:direct,invitation'] |
| proration_behavior | Qué hacer con el neto: create_prorations (por defecto) lo suma a la próxima renovación, always_invoice lo cobra de inmediato, none no prorratea. Ver la tabla |
['sometimes', 'in:create_prorations,none,always_invoice'] |
curl -X PUT\"/api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/change-plan"\-H 'Authorization: Bearer ACCESS_TOKEN' \-H "Content-type: application/json" \-d '{ "plan_id": "9a4698e5-d936-4298-84dc-4a770bfacc19"}'{ "changed": true, "proration": { "currency": "COP", "unused_fraction": 0.6, "credit_unused": 6000, "charge_new": 18000, "net": 12000, "line_items": [ { "description": "Tiempo no usado de Plan Básico", "amount": -6000 }, { "description": "Tiempo restante de Plan Premium", "amount": 18000 } ], "behavior": "create_prorations", "applies_on": "next_renewal" }, "subscription": { "id": "9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b", "planId": "9a4698e5-d936-4298-84dc-4a770bfacc19", "planName": "Plan Premium", "status": "activa" }}{ "changed": true, "proration": { "currency": "COP", "net": 12000, "line_items": [ "..." ], "behavior": "always_invoice", "applies_on": "now", "transaction": { "transaction_id": 20490, "status": "Aprobada", "status_key": "approved", "response_code": "00", "error": null, "amount": 12000 } }, "subscription": { "...": "..." }}{ "changed": true, "proration": { "currency": "COP", "unused_fraction": 0.6, "credit_unused": 0, "charge_new": 0, "net": 0, "line_items": [], "behavior": "none", "applies_on": null, "preview": { "credit_unused": 6000, "charge_new": 18000, "net": 12000, "line_items": [ "..." ] } }, "subscription": { "...": "..." }}{ "changed": false, "message": "El cobro del prorrateo no fue aprobado; el plan no cambió.", "error": { "type": "card_error", "code": "card_declined", "decline_code": "51", "message": "El cobro del prorrateo no fue aprobado; el plan no cambió.", "param": null }, "proration": { "net": 12000, "behavior": "always_invoice", "applies_on": "now", "transaction": { "transaction_id": 20491, "status": "Rechazada", "status_key": "rejected", "response_code": "51", "error": { "code": "51", "message": "…", "retryable": false, "action": "contact_issuer" } } }}error.decline_code es el response_code de la transacción rechazada. Ver
códigos de red.
{ "changed": false, "message": "No se puede cambiar el plan de una suscripción cancelada.", "error": { "type": "invalid_request_error", "code": "subscription_canceled", "message": "No se puede cambiar el plan de una suscripción cancelada.", "param": null }}Con mode: "direct" explícito, una suscripción cancelada responde
subscription_canceled y una inactiva subscription_inactive, en lugar de caer sin
aviso en el flujo por invitación. Si no envías mode, una suscripción inactiva sigue
yendo por invitación.
Cambio de plan por invitación (opcional)
Sección titulada «Cambio de plan por invitación (opcional)»Si envías mode: "invitation", en lugar de aplicar el cambio se crea una invitación
change_plan que se envía por correo al suscriptor. El cambio se ejecuta solo cuando
el suscriptor la acepta. Rutas públicas del flujo:
GET /subscription-invitation/{token}: ver detalle de la invitación.POST /subscription-invitation/{token}/accept: aceptar.POST /subscription-invitation/{token}/decline: rechazar.
Ciclo de vida de la invitación
Sección titulada «Ciclo de vida de la invitación»| Vigencia | 7 días desde el envío |
| Simultáneas | Una sola invitación change_plan pendiente por suscripción. Una segunda llamada responde 400 |
| Requisito | El suscriptor debe tener una tarjeta guardada; si no, 400 sin enviar el correo |
| Al aceptar | Se aplica el cambio y llega el webhook plan_changed, con transaction si hubo cobro |
| Al rechazar | La invitación queda en declined. No se emite webhook |
| Al expirar | La invitación deja de ser válida. Hoy no se emite ningún webhook al expirar |
En ambos modos, si la suscripción tiene webhook_url, se notifica el evento
plan_changed. El cuerpo incluye previous_plan_id con el plan que tenía antes.
Previsualizar cambio de plan
Sección titulada «Previsualizar cambio de plan»Descripción: Previsualiza el prorrateo de un cambio de plan sin aplicarlo (la factura que se generaría con el cambio). Ideal para mostrarle al cliente cuánto se le cobrará o acreditará antes de confirmar.
| Nombre del campo | Descripción | Reglas |
|---|---|---|
| plan_id | Id del plan destino, como parámetro de consulta. Debe ser de tu comercio y del mismo ambiente que la suscripción. Aquí no se exige que esté activo | ['required', 'exists:subscription_plans,id'] |
curl -X GET\"/api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/change-plan-preview?plan_id=9a4698e5-d936-4298-84dc-4a770bfacc19"\-H 'Authorization: Bearer ACCESS_TOKEN' \-H "Content-type: application/json"{ "proration": { "currency": "COP", "unused_fraction": 0.6, "credit_unused": 6000, "charge_new": 18000, "net": 12000, "line_items": [ { "description": "Tiempo no usado de Plan Básico", "amount": -6000 }, { "description": "Tiempo restante de Plan Premium", "amount": 18000 } ] }}Aplicar cupón
Sección titulada «Aplicar cupón»Descripción: Aplica un cupón (por su código) a una
suscripción. El descuento se refleja en los cobros recurrentes según la duración del
cupón (once, repeating o forever).
| Nombre del campo | Descripción | Reglas |
|---|---|---|
| code | Código del cupón a aplicar. Debe existir en tu comercio, estar activo y todavía ser redimible | ['required', 'string'] |
curl -X POST\"/api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/apply-coupon"\-H 'Authorization: Bearer ACCESS_TOKEN' \-H "Content-type: application/json" \-d '{ "code": "WELCOME10" }'{ "applied": true, "coupon_id": "9b0f2a13-2c44-4a1a-9f2e-1d3b4c5d6e7f", "discount_ends_at": "2026-12-30 00:00:00"}{ "applied": false, "message": "Cupón inválido o no disponible."}Listar facturas
Sección titulada «Listar facturas»Descripción: Lista las facturas de una suscripción. Cada factura representa un
ciclo de facturación con su estado (open, paid, void, uncollectible),
subtotal, impuesto, total y período.
{ "current_page": 1, "data": [ { "id": "9b0f4d21-8a3e-4c9b-9a1e-2f3a4b5c6d7e", "number": "INV-2026-000012", "status": "paid", "subtotal": 12605.04, "discount_total": 0, "tax_total": 2394.96, "total": 15000, "currency_type": "COP", "period_start": "2026-07-01 00:00:00", "period_end": "2026-08-01 00:00:00", "paid_at": "2026-07-01 19:00:05", "subscription_id": "9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b" } ], "total": 1, "last_page": 1}Próxima factura (vista previa)
Sección titulada «Próxima factura (vista previa)»Descripción: Vista previa (borrador) de la próxima factura de la suscripción, con el descuento vigente ya aplicado y el prorrateo que ya lleva sumado. No genera ningún cobro.
Con plan_id anticipa un cambio de plan con los mismos parámetros que
enviarías a change-plan, como el upcoming invoice de Stripe:
| Parámetro de consulta | Descripción | Reglas |
|---|---|---|
| plan_id | Plan destino a simular. De tu comercio y del mismo ambiente | ['sometimes', 'exists:subscription_plans,id'] |
| proration_behavior | Comportamiento a simular. Por defecto create_prorations |
['sometimes', 'in:create_prorations,none,always_invoice'] |
{ "upcoming_invoice": { "status": "draft", "currency_type": "COP", "subtotal": 12605.04, "tax_total": 2394.96, "total": 15000, "period_start": "2026-08-01 00:00:00", "next_charge_at": "2026-08-01", "line_items": [ { "description": "Suscripción Plan Básico", "amount": 15000, "quantity": 1, "proration": false } ] }}| Campo | Descripción |
|---|---|
total |
Lo que se cobrará en la próxima renovación, incluido el prorrateo ya sumado con create_prorations |
next_charge_at |
Día de ese cobro, Y-m-d en Colombia. Se cobra ese día a las 19:00 (ver Fechas) |
line_items[].proration |
true en la línea «Prorrateo por cambio de plan», que va aparte de la del plan |
immediate_charge |
Solo al simular always_invoice con neto positivo: {total, charge_at, line_items}, lo que se cobraría ahora mismo |
Simulando un cambio con create_prorations:
curl -X GET \'/api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/upcoming-invoice?plan_id=9a4698e5-d936-4298-84dc-4a770bfacc19&proration_behavior=create_prorations' \-H 'Authorization: Bearer ACCESS_TOKEN'{ "upcoming_invoice": { "status": "draft", "currency_type": "COP", "total": 42000, "next_charge_at": "2026-08-01", "line_items": [ { "description": "Suscripción Plan Premium", "amount": 30000, "quantity": 1, "proration": false }, { "description": "Prorrateo por cambio de plan", "amount": 12000, "quantity": 1, "proration": true } ] }}Simulando con always_invoice: el ajuste no va a la renovación sino a
immediate_charge:
{ "upcoming_invoice": { "total": 30000, "next_charge_at": "2026-08-01", "line_items": [ { "description": "Suscripción Plan Premium", "amount": 30000, "quantity": 1, "proration": false } ], "immediate_charge": { "total": 12000, "charge_at": "2026-07-19T10:15:00-05:00", "line_items": [ { "description": "Tiempo no usado de Plan Básico", "amount": -6000 }, { "description": "Tiempo restante de Plan Premium", "amount": 18000 } ] } }}Descargar factura en PDF
Sección titulada «Descargar factura en PDF»Descripción: Descarga el PDF de una factura específica de la suscripción. La
respuesta es el archivo PDF (application/pdf).
curl -X GET\"/api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/invoices/your-invoice-id/pdf"\-H 'Authorization: Bearer ACCESS_TOKEN' \--output factura.pdfHistorial de transacciones
Sección titulada «Historial de transacciones»Descripción: Devuelve el historial paginado de cobros (transacciones) realizados por la suscripción, con el detalle de cada pago y su estado.
{ "data": [ { "id": "01990bf8-e20c-72e4-b408-991e81f70beb", "amount": 15000, "currency_type": "COP", "tax": 0, "next_renew_at": "2025-09-02", "charge_scheduled_at": "2025-09-02T19:00:00-05:00", "proration_amount": null, "charge_at": "2025-09-02 14:48:06", "approved": 1, "subscription_id": "01990bf8-e1f3-7067-9c51-7bb4d1ad05bf", "transaction": { "transaction_id": 548, "amount": 15000, "currency_iso": "COP", "amount_cop": 15000, "tax": 0, "reference_1": "Subscription: 01990bf8-e1f3-7067-9c51-7bb4d1ad05bf", "reference_2": "Email: sdfasd@fsdfsd.ds", "reference_3": "1756842484281", "status": "Aprobada", "payment_method": "credit", "payment_method_source": "Visa", "network": "credibanco", "trazability_id": "548", "authorization_code": "548", "approved_at": "2025-09-02 14:48:04", "expired_at": "2025-09-03 14:48:04", "office_id": 1, "environment": "production", "aggregator": true, "economic_group_id": 1, "created_at": "2025-09-02 14:48:04", "customer_payer_id": "01976094-220e-719d-aa2a-7bca1b757f59", "customer_payer": { "email": "sdfasd@fsdfsd.ds", "name": "Osmi Otalora", "identification_type": "CC", "id_number": "101006464", "dialling_code": "57", "cellphone": "3006776454", "country": "COL", "state": "Bogota", "city": "Bogota", "address_1": "Bogota", "address_2": "Bogota", "zip_code": "0000" }, "subscription_id": "01990bf8-e1f3-7067-9c51-7bb4d1ad05bf" } } ], "links": { "first": "http://localhost:8009/api/v1/subscriptions/subscription/01990bf8-e1f3-7067-9c51-7bb4d1ad05bf/transaction-history?page=1", "last": "http://localhost:8009/api/v1/subscriptions/subscription/01990bf8-e1f3-7067-9c51-7bb4d1ad05bf/transaction-history?page=1", "prev": null, "next": null }, "meta": { "current_page": 1, "from": 1, "last_page": 1, "links": [ { "url": null, "label": "« Anterior", "active": false }, { "url": "http://localhost:8009/api/v1/subscriptions/subscription/01990bf8-e1f3-7067-9c51-7bb4d1ad05bf/transaction-history?page=1", "label": "1", "active": true }, { "url": null, "label": "Siguiente »", "active": false } ], "path": "http://localhost:8009/api/v1/subscriptions/subscription/01990bf8-e1f3-7067-9c51-7bb4d1ad05bf/transaction-history", "per_page": 15, "to": 1, "total": 1 }}Próxima transacción (próximo cobro)
Sección titulada «Próxima transacción (próximo cobro)»Descripción: Devuelve la información del próximo cobro recurrente pendiente: cuánto y
cuándo se cobrará. Si aún no hay un cobro programado, transaction será null.
{ "id": "01990bf8-eadc-7064-9996-06f08a7fbcd5", "amount": 15000, "currency_type": "COP", "tax": 0, "next_renew_at": "2025-10-02", "charge_scheduled_at": "2025-10-02T19:00:00-05:00", "proration_amount": null, "charge_at": null, "approved": null, "subscription_id": "01990bf8-e1f3-7067-9c51-7bb4d1ad05bf", "transaction": null}| Campo | Descripción |
|---|---|
next_renew_at |
Día del cobro, Y-m-d en Colombia. Es el mismo valor que next_transaction de la suscripción. Antes llegaba como medianoche UTC (2025-10-02T05:00:00.000000Z) |
charge_scheduled_at |
Instante en que el proceso diario intentará el cobro: ese día a las 19:00 de Colombia, en ISO 8601 con offset |
proration_amount |
Parte del amount que viene de un cambio de plan con create_prorations. Negativa si fue un downgrade. null o 0 si no hay |
amount |
Total a cobrar, ya con el prorrateo sumado |