Subscriptions
Read and manage recurring subscriptions — list and retrieve subscriptions, inspect offer-change history, cancel, pause, resume, switch offers, and update the billing payment instrument.
A subscription is a long-lived billing relationship between one of your customers and an offer. Tokeflow tracks its lifecycle, drives renewals on the offer's billing cycle, retries failed charges (dunning), and records every state change as an immutable transition.
Tokeflow orchestrates each renewal charge across the payment providers you connect; it does not process or settle funds itself. Settlement is performed by the connected provider. Tokeflow normalizes the outcome and advances the subscription accordingly.
Subscriptions are created by the billing engine, not by a direct API call. The first successful recurring charge against an offer mints the subscription record. From then on, you read and manage it through the endpoints on this page.
How subscriptions fit together
Every subscription stays inside one product family. Offer changes are allowed only between offers in the same family; moving across families is modeled as a cancel plus a new subscription.
The subscription object
A subscription is a denormalized snapshot of the customer's plan plus the live billing state the engine needs to drive it.
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier, prefixed sub_. |
merchant_id | string | Merchant that owns the subscription (mrc_). |
customer_id | string | Subscribed customer (cust_). |
customer_name | string | null | Resolved customer name. |
customer_email | string | null | Resolved customer email. |
current_offer_id | string | Offer currently billed (ofr_). |
offer_name | string | Resolved offer name. |
product_id | string | Product of the current offer (prd_). |
product_name | string | Resolved product name. |
product_family_id | string | Product family the subscription is locked to (pfa_). |
billing_cycle | enum | One of daily, biweekly, monthly, quarterly, half_yearly, yearly, custom, none. Copied from the offer at creation. |
currency | string | ISO 4217 currency, e.g. BRL. |
current_amount | integer | Amount charged each cycle, in minor units (cents). |
current_period_start | string | ISO 8601 start of the current billing period. |
current_period_end | string | ISO 8601 end of the current billing period. |
next_billing_at | string | null | When the next renewal fires. null when billing_cycle is none (on-demand). |
billing_anchor_day | integer | null | Day-of-month (1–31) used to resolve renewal dates for monthly+ cycles. |
trial_start | string | null | Trial start, set after the trial setup charge is confirmed. |
trial_end | string | null | Trial end. Equals the first real charge date. |
dunning_started_at | string | null | When the subscription first entered dunning. |
dunning_attempt_count | integer | Number of dunning retry attempts executed. Resets to 0 after a successful charge. |
dunning_next_retry_at | string | null | When the next dunning retry is scheduled. |
cycles_completed | integer | Count of successfully billed cycles. |
cycle_limit | integer | null | Maximum cycles. null means bill until cancelled. |
status | enum | Lifecycle state — see Lifecycle. |
cancel_at_period_end | boolean | When true, the subscription cancels at current_period_end instead of immediately. |
cancelled_at | string | null | When cancellation took effect. |
cancellation_reason | string | null | Reason persisted at cancellation. |
payment_instrument_id | string | Payment instrument billed on renewal (pi_). |
preferred_connector_name | string | Resolved name of the connection that renewals route to. |
preferred_installments | integer | Installment count carried from the initial charge. 1 is a single charge. |
created_at | string | ISO 8601 creation timestamp. |
updated_at | string | ISO 8601 last-update timestamp. |
current_amount and currency are denormalized onto the subscription so renewals are deterministic. They are refreshed when you change the offer.
Lifecycle
A subscription moves through six states. The billing engine drives most transitions; you drive the rest through the endpoints below.
| Status | Meaning |
|---|---|
trialing | In a free trial. No recurring amount has been charged yet. |
active | Billing normally on schedule. |
dunning | A renewal charge failed; the engine is retrying. See Dunning. |
paused | Billing is suspended. No renewals fire until resumed. |
cancelled | Terminated. No further billing. Terminal. |
expired | Reached its cycle limit and ended. Terminal. |
Dunning
When a renewal charge fails, the subscription enters dunning instead of cancelling outright. The engine schedules retries at dunning_next_retry_at, incrementing dunning_attempt_count on each attempt.
- A retry that succeeds returns the subscription to
activeand resets the dunning counters. - If retries are exhausted, the subscription is cancelled.
- While in dunning, the customer can fix the problem by switching to a working card — see Change the payment instrument.
If you use the Account Updater capability, an issuer-pushed card update on a dunning instrument can fast-track the next retry automatically — no extra call needed.
Endpoints
All subscription routes are nested under a merchant:
/api/v1/merchants/:merchant_id/subscriptionsSubscription endpoints require a Merchant-scoped key. The :merchant_id in the path must match the key's merchant. Organization keys do not address these merchant-nested routes directly — issue a merchant key for the target merchant during onboarding in the Tokeflow Dashboard.
Mutations are guarded by the subscriptions:write scope; reads require subscriptions:read.
GET/api/v1/merchants/:merchant_id/subscriptions
List subscriptions for a merchant, newest first. Supports pagination and filtering.
Auth: Merchant key (subscriptions:read)
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number, 1-indexed. Default 1. |
limit | integer | No | Items per page. Default 20, max 100. |
customer_id | string | No | Filter by customer (cust_). |
status | enum | No | Filter by status (trialing, active, dunning, paused, cancelled, expired). |
current_offer_id | string | No | Filter by current offer (ofr_). |
product_family_id | string | No | Filter by product family (pfa_). |
curl -G "https://api.tokeflow.com/api/v1/merchants/mrc_8a1c2d/subscriptions" \
-H "Authorization: Bearer sk_live_mer_…" \
-d page=1 \
-d limit=20 \
-d status=activeResponse 200 OK
{
"success": true,
"data": [
{
"id": "sub_3f9a7c2e1b",
"merchant_id": "mrc_8a1c2d",
"customer_id": "cust_5b2e9f",
"customer_name": "Ana Souza",
"customer_email": "ana@example.com",
"current_offer_id": "ofr_premium_monthly",
"offer_name": "Premium — Monthly",
"product_id": "prd_7d4a1c",
"product_name": "Premium Plan",
"product_family_id": "pfa_streaming",
"billing_cycle": "monthly",
"currency": "BRL",
"current_amount": 4990,
"current_period_start": "2026-06-25T00:00:00.000Z",
"current_period_end": "2026-07-25T00:00:00.000Z",
"next_billing_at": "2026-07-25T00:00:00.000Z",
"billing_anchor_day": 25,
"trial_start": null,
"trial_end": null,
"dunning_started_at": null,
"dunning_attempt_count": 0,
"dunning_next_retry_at": null,
"cycles_completed": 3,
"cycle_limit": null,
"status": "active",
"cancel_at_period_end": false,
"cancelled_at": null,
"cancellation_reason": null,
"payment_instrument_id": "pi_9c3f2a",
"preferred_connector_name": "acquirer_a",
"preferred_installments": 1,
"created_at": "2026-03-25T12:00:00.000Z",
"updated_at": "2026-06-25T00:00:01.000Z"
}
],
"meta": {
"pagination": {
"page": 1,
"limit": 20,
"total": 42,
"total_pages": 3,
"has_next": true,
"has_prev": false
}
},
"request_id": "req_8f3c2a1b",
"timestamp": "2026-06-25T12:30:00.000Z"
}GET/api/v1/merchants/:merchant_id/subscriptions/:subscription_id
Retrieve a single subscription by id.
Auth: Merchant key (subscriptions:read)
curl "https://api.tokeflow.com/api/v1/merchants/mrc_8a1c2d/subscriptions/sub_3f9a7c2e1b" \
-H "Authorization: Bearer sk_live_mer_…"Response 200 OK
{
"success": true,
"data": {
"id": "sub_3f9a7c2e1b",
"merchant_id": "mrc_8a1c2d",
"customer_id": "cust_5b2e9f",
"current_offer_id": "ofr_premium_monthly",
"product_family_id": "pfa_streaming",
"billing_cycle": "monthly",
"currency": "BRL",
"current_amount": 4990,
"current_period_start": "2026-06-25T00:00:00.000Z",
"current_period_end": "2026-07-25T00:00:00.000Z",
"next_billing_at": "2026-07-25T00:00:00.000Z",
"billing_anchor_day": 25,
"trial_start": null,
"trial_end": null,
"dunning_started_at": null,
"dunning_attempt_count": 0,
"dunning_next_retry_at": null,
"cycles_completed": 3,
"cycle_limit": null,
"status": "active",
"cancel_at_period_end": false,
"cancelled_at": null,
"cancellation_reason": null,
"payment_instrument_id": "pi_9c3f2a",
"preferred_installments": 1,
"created_at": "2026-03-25T12:00:00.000Z",
"updated_at": "2026-06-25T00:00:01.000Z"
},
"request_id": "req_8f3c2a1b",
"timestamp": "2026-06-25T12:30:00.000Z"
}A missing or out-of-scope id returns 404 (not_found_error).
GET/api/v1/merchants/:merchant_id/subscriptions/:subscription_id/transitions
List the subscription's transition history — an append-only audit log of every state and offer change, newest first. Each transition records what changed, who triggered it, and the order that caused it (when applicable). This is the system of record for offer-change history.
Auth: Merchant key (subscriptions:read)
Transition object
| Field | Type | Description |
|---|---|---|
id | string | Transition id (sbt_). |
subscription_id | string | Owning subscription (sub_). |
transition_type | enum | creation, upgrade, downgrade, reactivation, cancellation, trial_start, trial_conversion, payment_method_change, dunning_entry, dunning_retry, dunning_cancelled, expiration, cycle_limit_renewed, pause, resume. |
from_offer_id | string | null | Offer before the change (ofr_). null on creation. |
to_offer_id | string | null | Offer after the change (ofr_). null on cancellation. |
from_status | enum | null | Status before the change. null on the first (creation) event. |
to_status | enum | Status after the change. |
triggered_by | enum | customer, system, or admin. |
order_id | string | null | Order that triggered the transition (ord_), if any. |
reason | string | null | Human-readable reason, when supplied. |
metadata | object | null | Additional context, e.g. the resolved change_charge_behavior for an offer change. |
created_at | string | ISO 8601 timestamp. |
curl "https://api.tokeflow.com/api/v1/merchants/mrc_8a1c2d/subscriptions/sub_3f9a7c2e1b/transitions" \
-H "Authorization: Bearer sk_live_mer_…"Response 200 OK
{
"success": true,
"data": [
{
"id": "sbt_2b7e1a",
"subscription_id": "sub_3f9a7c2e1b",
"transition_type": "upgrade",
"from_offer_id": "ofr_basic_monthly",
"to_offer_id": "ofr_premium_monthly",
"from_status": "active",
"to_status": "active",
"triggered_by": "customer",
"order_id": null,
"reason": null,
"metadata": { "change_charge_behavior": "next_renew" },
"created_at": "2026-06-01T09:14:00.000Z"
},
{
"id": "sbt_1a9c4d",
"subscription_id": "sub_3f9a7c2e1b",
"transition_type": "creation",
"from_offer_id": null,
"to_offer_id": "ofr_basic_monthly",
"from_status": null,
"to_status": "active",
"triggered_by": "system",
"order_id": "ord_5e2f8b",
"reason": null,
"metadata": null,
"created_at": "2026-03-25T12:00:00.000Z"
}
],
"request_id": "req_8f3c2a1b",
"timestamp": "2026-06-25T12:30:00.000Z"
}POST/api/v1/merchants/:merchant_id/subscriptions/:subscription_id/cancel
Cancel a subscription, either immediately or at the end of the current period.
Auth: Merchant key (subscriptions:write)
Body
| Field | Type | Required | Description |
|---|---|---|---|
at_period_end | boolean | Yes | true defers cancellation to current_period_end (the customer keeps access until then). false cancels immediately. |
reason | string | No | Human-readable reason, persisted on the subscription. Max 500 chars. |
When at_period_end is true, the subscription is flagged (cancel_at_period_end: true) and the actual status flip happens on the next renewal tick — no transition is recorded yet, because nothing has changed state. When false, the status flips to cancelled right away and a cancellation transition is recorded.
curl -X POST "https://api.tokeflow.com/api/v1/merchants/mrc_8a1c2d/subscriptions/sub_3f9a7c2e1b/cancel" \
-H "Authorization: Bearer sk_live_mer_…" \
-H "Content-Type: application/json" \
-d '{ "at_period_end": true, "reason": "customer no longer needs the service" }'Response 200 OK — the updated subscription object. With at_period_end: true:
{
"success": true,
"data": {
"id": "sub_3f9a7c2e1b",
"status": "active",
"cancel_at_period_end": true,
"cancellation_reason": "customer no longer needs the service",
"current_period_end": "2026-07-25T00:00:00.000Z",
"next_billing_at": "2026-07-25T00:00:00.000Z"
},
"request_id": "req_8f3c2a1b",
"timestamp": "2026-06-25T12:30:00.000Z"
}Cancelling a subscription that is already cancelled or expired returns 400 (validation_error) — terminal states cannot be re-cancelled.
POST/api/v1/merchants/:merchant_id/subscriptions/:subscription_id/change-offer
Move the subscription to a different offer (an upgrade or downgrade) within the same product family. Tokeflow classifies the change as upgrade or downgrade from the offers' tier order and records the corresponding transition.
Auth: Merchant key (subscriptions:write)
Body
| Field | Type | Required | Description |
|---|---|---|---|
to_offer_id | string | Yes | Target offer (ofr_). Must belong to the same product family as the current offer and have a price in the subscription's currency. |
How the charge behavior is resolved
Each offer pair can declare a change_charge_behavior that governs how money is handled when the plan changes:
| Behavior | Meaning |
|---|---|
next_renew | The new amount takes effect on the next renewal. No immediate charge, no proration. |
prorated | The customer is charged (or credited) the prorated difference for the remainder of the period. |
override | A custom amount defined for the transition is charged. |
The effective behavior for a given (from_offer → to_offer) pair is resolved in order:
- If an active offer transition exists for the pair with an explicit behavior, that value wins.
- Otherwise, fall back to the product family default.
In the current engine version, only next_renew is processed: the new current_amount applies from the next cycle, with no immediate charge or proration. prorated and override are accepted for configuration and recorded on the transition's metadata, but the V1 billing engine applies them as next_renew. You can resolve the effective behavior ahead of time per offer pair on the Offers resource.
curl -X POST "https://api.tokeflow.com/api/v1/merchants/mrc_8a1c2d/subscriptions/sub_3f9a7c2e1b/change-offer" \
-H "Authorization: Bearer sk_live_mer_…" \
-H "Content-Type: application/json" \
-d '{ "to_offer_id": "ofr_premium_monthly" }'Response 200 OK — the subscription with the new current_offer_id and refreshed current_amount. billing_cycle, next_billing_at, and routing fields are unchanged.
{
"success": true,
"data": {
"id": "sub_3f9a7c2e1b",
"current_offer_id": "ofr_premium_monthly",
"current_amount": 4990,
"billing_cycle": "monthly",
"next_billing_at": "2026-07-25T00:00:00.000Z",
"status": "active"
},
"request_id": "req_8f3c2a1b",
"timestamp": "2026-06-25T12:30:00.000Z"
}Common errors:
400(validation_error) — the target offer is the current one, the subscription iscancelled/expired, no active offer-transition exists for the pair, the target offer lacks a price in the subscription's currency, or the offers are in different families (handle a cross-family move as a cancel plus a new subscription).
POST/api/v1/merchants/:merchant_id/subscriptions/:subscription_id/change-payment-instrument
Switch the subscription to a different payment instrument for future renewals. This is the recovery path out of dunning.
Auth: Merchant key (subscriptions:write)
Body
| Field | Type | Required | Description |
|---|---|---|---|
payment_instrument_id | string | Yes | The instrument to bill going forward (pi_). |
The instrument must already have a confirmed customer-initiated charge against it, so Tokeflow can route future renewals through the right connection. If it does not, the call returns 422 (business_rule_error).
A payment-instrument change is only valid while the subscription is in dunning. On success, the subscription returns to active, the dunning counters reset, and the billing period re-anchors forward one cycle (no back-billing for the dunning gap). Calling this on a non-dunning subscription returns 400 (validation_error).
curl -X POST "https://api.tokeflow.com/api/v1/merchants/mrc_8a1c2d/subscriptions/sub_3f9a7c2e1b/change-payment-instrument" \
-H "Authorization: Bearer sk_live_mer_…" \
-H "Content-Type: application/json" \
-d '{ "payment_instrument_id": "pi_new1234" }'Response 200 OK
{
"success": true,
"data": {
"id": "sub_3f9a7c2e1b",
"status": "active",
"payment_instrument_id": "pi_new1234",
"dunning_attempt_count": 0,
"dunning_started_at": null,
"dunning_next_retry_at": null,
"current_period_end": "2026-07-25T00:00:00.000Z",
"next_billing_at": "2026-07-25T00:00:00.000Z"
},
"request_id": "req_8f3c2a1b",
"timestamp": "2026-06-25T12:30:00.000Z"
}POST/api/v1/merchants/:merchant_id/subscriptions/:subscription_id/pause
Suspend billing. While paused, no renewals fire. A pause transition is recorded.
Auth: Merchant key (subscriptions:write)
Body
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | No | Human-readable reason, persisted on the transition. Max 500 chars. |
A subscription can only be paused from active or trialing. Pausing from any other state returns 409 (conflict_error).
curl -X POST "https://api.tokeflow.com/api/v1/merchants/mrc_8a1c2d/subscriptions/sub_3f9a7c2e1b/pause" \
-H "Authorization: Bearer sk_live_mer_…" \
-H "Content-Type: application/json" \
-d '{ "reason": "customer travelling for 6 weeks" }'Response 200 OK
{
"success": true,
"data": {
"id": "sub_3f9a7c2e1b",
"status": "paused"
},
"request_id": "req_8f3c2a1b",
"timestamp": "2026-06-25T12:30:00.000Z"
}POST/api/v1/merchants/:merchant_id/subscriptions/:subscription_id/resume
Resume a paused subscription. It returns to active, or to trialing if the trial is still running. If the next billing date drifted into the past while paused, the period is advanced to start from now — pausing stops the clock rather than skipping a cycle, so cycles_completed is not bumped. A resume transition is recorded. No request body.
Auth: Merchant key (subscriptions:write)
Only a paused subscription can be resumed. Resuming from any other state returns 409 (conflict_error).
curl -X POST "https://api.tokeflow.com/api/v1/merchants/mrc_8a1c2d/subscriptions/sub_3f9a7c2e1b/resume" \
-H "Authorization: Bearer sk_live_mer_…"Response 200 OK
{
"success": true,
"data": {
"id": "sub_3f9a7c2e1b",
"status": "active",
"current_period_start": "2026-06-25T00:00:00.000Z",
"current_period_end": "2026-07-25T00:00:00.000Z",
"next_billing_at": "2026-07-25T00:00:00.000Z"
},
"request_id": "req_8f3c2a1b",
"timestamp": "2026-06-25T12:30:00.000Z"
}Errors
Subscription endpoints use the standard error envelope. The most common cases:
| HTTP | Type | When |
|---|---|---|
400 | validation_error | Cancel/change on a terminal subscription, cross-family offer change, change-payment-instrument outside dunning, no active offer transition. |
403 | authorization_error | Key lacks subscriptions:read / subscriptions:write. |
404 | not_found_error | Subscription not found or not owned by the merchant. |
409 | conflict_error | Pause from a non-pausable state, or resume from a non-paused state. |
422 | business_rule_error | The target payment instrument has no confirmed prior charge to route from. |
Related
- Offers — define billing cycles, prices, trials, and offer transitions.
- Orders — the order minted on each renewal.
- Transactions — the underlying charge and payment instruments.
- API reference overview — envelopes, pagination, IDs, and money conventions.