# Suscripciones --- - [Suscripciones](#suscripciones) - [Conceptos previos](#conceptos-previos) - [Idempotencia (Idempotency-Key)](#idempotencia) - [Objetos expandibles (expand)](#expand) - [Estados de una suscripción](#estados) - [Dos formatos de respuesta](#formato-respuestas) - [Fechas y hora del cobro](#fechas) - [Metadata](#metadata) - [Listar suscripciones](#getall) - [Obtener una suscripción](#get) - [Suscripciones de un plan](#getbyplan) - [Suscripciones de un suscriptor](#getbysubscriber) - [Crear una suscripción](#create) - [Actualizar una suscripción](#update) - [Cancelar suscripción](#cancel) - [Pausar cobros](#pause) - [Reanudar cobros](#resume) - [Actualizar método de pago (tarjeta)](#updatepaymentmethod) - [Renovar suscripción](#renewsubscription) - [Cambiar de plan (con prorrateo)](#changeplan) - [Previsualizar cambio de plan](#changeplanpreview) - [Aplicar cupón](#applycoupon) - [Listar facturas](#invoices) - [Próxima factura (vista previa)](#upcominginvoice) - [Descargar factura en PDF](#invoicepdf) - [Historial de transacciones](#transactions) - [Próxima transacción (próximo cobro)](#nexttransactions) ## Conceptos previos [#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 alias `product`, `price` y `customer`, que apuntan a los mismos recursos. Ejemplo: `GET /api/v1/subscriptions/customer` equivale a `GET /api/v1/subscriptions/subscriber`. - **Descuentos:** los descuentos ahora se gestionan con [Cupones](/coupons) reutilizables (porcentaje o monto fijo). Los campos `discount_*` del plan se mantienen por compatibilidad pero se recomienda usar cupones. ## Idempotencia (Idempotency-Key) [#idempotencia] 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. ```bash 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 }' ``` :::caution Si reutilizas la misma `Idempotency-Key` con parámetros distintos recibirás un error Código de respuesta: 409. ::: ## Objetos expandibles (expand) [#expand] Al consultar una suscripción puedes incrustar relaciones con `expand[]` y así evitar llamadas adicionales. Valores soportados: `plan`, `subscriber`, `next_transaction`. ```bash 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 [#estados] 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](#pause)). | | `cancelada` | Cancelada y **sin servicio**. Ver la nota de abajo. | | `finalizada` | Terminada; no se realizarán más cobros. | :::note **Cancelar al final del período no cambia `status` todavía.** Como en Stripe, mientras la suscripción siga dando servicio `status` se queda en `activa` (o `en_gracia`) con `canceled: true`, `cancel_at_period_end: true` y `cancel_at` = el instante en que termina. Pasa a `cancelada` cuando llega `cancel_at`, y ahí recibes el webhook [`ended`](/subscription-webhook#ended). Ver [Cancelar suscripción](#cancel). ::: :::note **`en_gracia` no garantiza que se vaya a reintentar.** Los reintentos dependen de la causal del rechazo: ante una tarjeta retenida, robada, restringida o vencida el cobro se detiene de inmediato y la suscripción pasa a `finalizada` aunque quede ventana de gracia. El detalle está en [Rechazos que no se reintentan](/subscription-webhook#rechazos-duros). ::: ## Dos formatos de respuesta [#formato-respuestas] 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. :::note La fuente de verdad siempre es un `GET` a la suscripción. Si después de una acción necesitas el objeto completo y consistente, vuelve a consultarla. ::: ## Fechas y hora del cobro [#fechas] 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. :::caution **La renovación no se cobra en el instante exacto de `ends_at`.** La cobra un proceso diario que corre a las **19:00 hora de Colombia** del día de `next_renew_at` (que es el día de `ends_at`). Ese instante es `charge_scheduled_at` / `next_charge_at`. Lo que confirma el cobro es el webhook [`renew`](/subscription-webhook), no la hora. ::: ## Metadata [#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](/subscriptor#metadata), [planes](/plan#metadata) y [cobros generados](/generate-transaction#metadata). 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-webhook) (`subscription.metadata`, y también dentro de `subscription.plan` y `subscription.subscriber`). Para buscar por ella, filtra con coincidencia exacta: ```bash 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 [#getall] **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/subscription ``` `GET /api/v1/subscriptions/subscription` ### 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](#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 | ```bash 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' ``` :::tip **Para reconciliar después de un fallo tuyo.** Si tu sistema se cayó justo después de crear una suscripción y no sabes si quedó registrada, búscala por `filter[subscriber_id]` o `filter[email]` y compara `created_at`. No hace falta que tengas el id que nunca alcanzaste a guardar. ::: :::note **¿Referencia externa?** Guárdala en [`metadata`](#metadata) al [crear la suscripción](#create) (por ejemplo `{"user_id": "42"}`) y búscala con `filter[metadata][user_id]=42`. `description` sigue sirviendo y es filtrable con `filter[description]`. ::: ## Obtener una suscripción [#get] **Descripción:** Devuelve el detalle de una suscripción a partir de su `id`. Acepta [objetos expandibles](#expand) para incluir el plan, el suscriptor o el próximo cobro. `GET /api/v1/subscriptions/subscription/your-subscription-id` :::tip Respuesta satisfactoria ::: Código de respuesta: 200 ```json { "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 | 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](#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](#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 | | :::caution `canceled` e `is_active` **no son opuestos**. Una suscripción cancelada al final del período tiene `canceled: true`, `cancel_at_period_end: true`, `is_active: true` y `status: "activa"` hasta `cancel_at`. Para decidir si el cliente tiene acceso, mira `is_active`. ::: :::note Las fechas históricas van en hora de Colombia con formato `Y-m-d H:i:s`; las nuevas (`ends_at_iso`, `next_charge_at`, `cancel_at`) en ISO 8601 con offset. Ver [Fechas](#fechas). Los campos `plan`, `subscriber`, `next_transaction` y `next_charge_at` solo aparecen si los pides con [`expand[]`](#expand). ::: ## Suscripciones de un plan [#getbyplan] **Descripción:** Lista las suscripciones asociadas a un plan específico. Útil para saber cuántos clientes están suscritos a un plan. `GET /api/v1/subscriptions/subscription/plan/your-plan-id` ## Suscripciones de un suscriptor [#getbysubscriber] **Descripción:** Lista las suscripciones de un suscriptor (cliente) específico. `GET /api/v1/subscriptions/subscription/subscriber/your-subscriber-id` ## Crear una suscripción [#create] **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](/tokenized)) 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](/tokenized)) 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](/commercio) | `['required', 'exists:offices,id']` | | description | Descripción libre de esta suscripción | `['nullable', 'string', 'max:255']` | | metadata | Tus pares clave-valor. Ver [Metadata](#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](/subscription-webhook) | `['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:']` | | 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*** `POST /api/v1/subscriptions/subscription` Cuerpo de ejemplo: ```json { "plan_id": "9a4698e5-d936-4298-84dc-4a770bfacc19", "subscriber_id": "9a469901-d010-4afe-94fe-da6790a4f72b", "card": "your-token-card-here", "office": 3 } ``` ```bash 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:** `POST /api/v1/subscriptions/subscription` Cuerpo de ejemplo: ```json { "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 } ``` ```bash 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 [#update] `PUT /api/v1/subscriptions/subscription/{subscription-id}` **Descripción:** Actualiza los datos propios del integrador: `description` y [`metadata`](#metadata). No cambia el plan, la tarjeta ni las fechas; para eso están [cambiar de plan](#changeplan) y [actualizar el método de pago](#updatepaymentmethod). | 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']` | `PUT /api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b` Cuerpo de ejemplo: ```json { "description": "Cliente premium", "metadata": { "user_id": "42", "channel": "web" } } ``` ```bash 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": "" } }' ``` :::tip Respuesta satisfactoria ::: Código de respuesta: 200 Devuelve el objeto de la suscripción, igual que [obtener una suscripción](#get): ```json { "id": "9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b", "description": "Cliente premium", "metadata": { "user_id": "42" }, "status": "activa", "...": "..." } ``` :::danger Metadata inválida ::: Código de respuesta: 422 ```json { "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 [#cancel] **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`. `PUT /api/v1/subscriptions/subscription/cancel/your-subscription-id` ### 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`. ```bash 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 }' ``` :::note **No llega un `renew` después de cancelar.** En cuanto queda registrada la cancelación, el motor de cobro deja de considerar la suscripción: no hay más cobros ni más reintentos pendientes. Recibes `canceled` al cancelar y, cuando deja de dar servicio (al llegar `cancel_at`), un último webhook [`ended`](/subscription-webhook#ended) con `status: "cancelada"`. Con `at_period_end: false`, `ended` llega en la siguiente pasada del proceso horario. ::: :::caution `at_period_end: false` **no devuelve dinero**. Corta el servicio, no reembolsa el ciclo ya cobrado. Para eso está la [devolución](/refund-transaction). ::: :::tip Respuesta satisfactoria ::: Código de respuesta: 200 ```json { "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`. :::danger La suscripción ya estaba cancelada ::: Código de respuesta: 400 ```json { "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 [#pause] **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](#resume). | 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](#resume) | `['sometimes', 'nullable', 'date', 'after:now']` | `POST /api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/pause` Cuerpo de ejemplo: ```json { "resumes_at": "2026-09-01 00:00:00" } ``` ```bash 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" }' ``` :::tip Respuesta satisfactoria ::: Código de respuesta: 200 ```json { "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" } } ``` :::danger No se puede pausar ::: Código de respuesta: 400 ```json { "paused": false, "message": "No se puede pausar una suscripción cancelada." } ``` ## Reanudar cobros [#resume] **Descripción:** Reanuda los cobros de una suscripción pausada. `POST /api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/resume` ```bash curl -X POST\ "/api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/resume"\ -H 'Authorization: Bearer ACCESS_TOKEN' \ -H "Content-type: application/json" ``` :::tip Respuesta satisfactoria ::: Código de respuesta: 200 ```json { "resumed": true, "subscription": { "id": "9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b", "status": "activa", "paused": false } } ``` ## Actualizar método de pago (tarjeta) [#updatepaymentmethod] **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']` | `PUT /api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/update-payment-method` Cuerpo de ejemplo: ```json { "number": "5249314023340339", "datetime": "2028-05", "cvv": 899 } ``` ```bash 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 }' ``` :::tip Respuesta satisfactoria ::: Código de respuesta: 200 ```json { "updated": true } ``` :::danger Suscripción cancelada ::: Código de respuesta: 400 ```json { "message": "No se puede actualizar el método de pago de una suscripción cancelada" } ``` ## Renovar suscripción [#renewsubscription] `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ó. :::danger **No cobra nada.** Este endpoint no genera un cobro inmediato ni reactiva la suscripción por sí solo: solo manda el correo. El cobro ocurre cuando el suscriptor acepta la invitación. La invitación **vence a los 7 días** y solo puede haber una pendiente por suscripción. Si lo que quieres es cobrar ya mismo, crea una [suscripción nueva](#create) o usa un [cobro puntual](/generate-transaction). ::: No recibe cuerpo. El suscriptor debe tener una tarjeta guardada, porque la invitación la reutiliza. `POST /api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/renew` ```bash curl -X POST \ '/api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/renew' \ -H 'Authorization: Bearer ACCESS_TOKEN' \ -H 'Accept: application/json' ``` :::tip Invitación enviada ::: Código de respuesta: 200 ```json { "renewed": true, "message": "Se ha enviado una invitación de renovación al suscriptor por correo electrónico." } ``` :::danger Ya hay una invitación pendiente ::: Código de respuesta: 400 ```json { "renewed": false, "message": "Ya existe una invitación de renovación pendiente para esta suscripción." } ``` :::danger El suscriptor no tiene tarjeta guardada ::: Código de respuesta: 400 ```json { "renewed": false, "message": "El suscriptor no tiene una tarjeta guardada. No se puede enviar la invitación." } ``` ## Cambiar de plan (con prorrateo) [#changeplan] **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 decide `proration_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` [#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 con `proration: true` en la [próxima factura](#upcominginvoice), y el cobro pendiente expone `proration_amount` en [next-transaction](#nexttransactions) y en el [historial](#transactions). - **`always_invoice`:** el cobro aparece en el [historial](#transactions) y la respuesta trae `proration.transaction`, el mismo objeto de transacción del [checkout](/checkout-transaction#ejemplo-para-tarjetas). **Si ese cobro se rechaza, el plan no cambia** y la respuesta es un `402` (ver abajo). - **`none`:** el plan cambia ya y la próxima renovación cobra el precio del plan nuevo. La respuesta trae `net: 0`, `line_items: []` y el cálculo teórico en `proration.preview`, solo como referencia. :::caution Antes `create_prorations` cobraba el neto de inmediato. **Ya no:** lo suma a la próxima renovación, como Stripe. Si necesitas cobrar el ajuste hoy, usa `always_invoice`. ::: :::note Usa primero [ChangePlanPreview](#changeplanpreview) para mostrarle al cliente el prorrateo antes de confirmar. ::: | 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](#proration-behavior) | `['sometimes', 'in:create_prorations,none,always_invoice']` | `PUT /api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/change-plan` Cuerpo de ejemplo: ```json { "plan_id": "9a4698e5-d936-4298-84dc-4a770bfacc19", "mode": "direct", "proration_behavior": "create_prorations" } ``` ```bash 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" }' ``` :::tip Cambio aplicado con `create_prorations` (el neto va a la próxima renovación) ::: Código de respuesta: 200 ```json { "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" } } ``` :::tip Cambio aplicado con `always_invoice` (el neto se cobró ahora) ::: Código de respuesta: 200 ```json { "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": { "...": "..." } } ``` :::tip Cambio aplicado con `none` (sin prorrateo) ::: Código de respuesta: 200 ```json { "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": { "...": "..." } } ``` :::danger `always_invoice`: el cobro inmediato fue rechazado y el plan no cambió ::: Código de respuesta: 402 ```json { "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](/error-codes#red). :::danger La suscripción está cancelada o inactiva ::: Código de respuesta: 400 ```json { "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). ### 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 | | | | - | - | | 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** | :::caution Como no hay evento de expiración, si dependes de la invitación **no dejes el cambio en «pendiente» para siempre**: guarda la fecha de envío y date por vencido a los 7 días. Puedes confirmar el estado real con un `GET` a la suscripción y comparar el `plan`. ::: 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 [#changeplanpreview] **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']` | `GET /api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/change-plan-preview` Cuerpo de ejemplo: ```json { "plan_id": "9a4698e5-d936-4298-84dc-4a770bfacc19" } ``` ```bash 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" ``` :::tip Respuesta satisfactoria ::: Código de respuesta: 200 ```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 [#applycoupon] **Descripción:** Aplica un [cupón](/coupons) (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']` | `POST /api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/apply-coupon` Cuerpo de ejemplo: ```json { "code": "WELCOME10" } ``` ```bash 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" }' ``` :::tip Respuesta satisfactoria ::: Código de respuesta: 200 ```json { "applied": true, "coupon_id": "9b0f2a13-2c44-4a1a-9f2e-1d3b4c5d6e7f", "discount_ends_at": "2026-12-30 00:00:00" } ``` :::danger Cupón inválido o no disponible ::: Código de respuesta: 400 ```json { "applied": false, "message": "Cupón inválido o no disponible." } ``` ## Listar facturas [#invoices] **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. `GET /api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/invoices` :::tip Respuesta satisfactoria ::: Código de respuesta: 200 ```json { "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) [#upcominginvoice] **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](#changeplan) 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']` | `GET /api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/upcoming-invoice` :::tip Respuesta satisfactoria ::: Código de respuesta: 200 ```json { "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](#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`:** ```bash 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' ``` ```json { "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`: ```json { "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 [#invoicepdf] **Descripción:** Descarga el PDF de una factura específica de la suscripción. La respuesta es el archivo PDF (`application/pdf`). `GET /api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/invoices/your-invoice-id/pdf` ```bash curl -X GET\ "/api/v1/subscriptions/subscription/9da5903d-3d1a-4f60-b487-b6b6cd7d9d1b/invoices/your-invoice-id/pdf"\ -H 'Authorization: Bearer ACCESS_TOKEN' \ --output factura.pdf ``` ## Historial de transacciones [#transactions] **Descripción:** Devuelve el historial paginado de cobros (transacciones) realizados por la suscripción, con el detalle de cada pago y su estado. `GET /api/v1/subscriptions/subscription/{subscription-id}/transaction-history` :::tip Respuesta satisfactoria ::: Código de respuesta: 200 ```json { "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) [#nexttransactions] **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`. `GET /api/v1/subscriptions/subscription/{subscription-id}/next-transaction` :::tip Respuesta satisfactoria ::: Código de respuesta: 200 ```json { "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`](#proration-behavior). Negativa si fue un downgrade. `null` o `0` si no hay | | `amount` | Total a cobrar, **ya con el prorrateo sumado** | :::note El cobro no ocurre en el instante de `ends_at` sino cuando corre el proceso diario (`charge_scheduled_at`). El webhook [`renew`](/subscription-webhook) es el que confirma que se cobró. Ver [Fechas](#fechas). :::