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.

FieldTypeDescription
idstringUnique identifier, prefixed sub_.
merchant_idstringMerchant that owns the subscription (mrc_).
customer_idstringSubscribed customer (cust_).
customer_namestring | nullResolved customer name.
customer_emailstring | nullResolved customer email.
current_offer_idstringOffer currently billed (ofr_).
offer_namestringResolved offer name.
product_idstringProduct of the current offer (prd_).
product_namestringResolved product name.
product_family_idstringProduct family the subscription is locked to (pfa_).
billing_cycleenumOne of daily, biweekly, monthly, quarterly, half_yearly, yearly, custom, none. Copied from the offer at creation.
currencystringISO 4217 currency, e.g. BRL.
current_amountintegerAmount charged each cycle, in minor units (cents).
current_period_startstringISO 8601 start of the current billing period.
current_period_endstringISO 8601 end of the current billing period.
next_billing_atstring | nullWhen the next renewal fires. null when billing_cycle is none (on-demand).
billing_anchor_dayinteger | nullDay-of-month (1–31) used to resolve renewal dates for monthly+ cycles.
trial_startstring | nullTrial start, set after the trial setup charge is confirmed.
trial_endstring | nullTrial end. Equals the first real charge date.
dunning_started_atstring | nullWhen the subscription first entered dunning.
dunning_attempt_countintegerNumber of dunning retry attempts executed. Resets to 0 after a successful charge.
dunning_next_retry_atstring | nullWhen the next dunning retry is scheduled.
cycles_completedintegerCount of successfully billed cycles.
cycle_limitinteger | nullMaximum cycles. null means bill until cancelled.
statusenumLifecycle state — see Lifecycle.
cancel_at_period_endbooleanWhen true, the subscription cancels at current_period_end instead of immediately.
cancelled_atstring | nullWhen cancellation took effect.
cancellation_reasonstring | nullReason persisted at cancellation.
payment_instrument_idstringPayment instrument billed on renewal (pi_).
preferred_connector_namestringResolved name of the connection that renewals route to.
preferred_installmentsintegerInstallment count carried from the initial charge. 1 is a single charge.
created_atstringISO 8601 creation timestamp.
updated_atstringISO 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.

StatusMeaning
trialingIn a free trial. No recurring amount has been charged yet.
activeBilling normally on schedule.
dunningA renewal charge failed; the engine is retrying. See Dunning.
pausedBilling is suspended. No renewals fire until resumed.
cancelledTerminated. No further billing. Terminal.
expiredReached 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 active and 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/subscriptions

Subscription 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

FieldTypeRequiredDescription
pageintegerNoPage number, 1-indexed. Default 1.
limitintegerNoItems per page. Default 20, max 100.
customer_idstringNoFilter by customer (cust_).
statusenumNoFilter by status (trialing, active, dunning, paused, cancelled, expired).
current_offer_idstringNoFilter by current offer (ofr_).
product_family_idstringNoFilter 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=active

Response 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

FieldTypeDescription
idstringTransition id (sbt_).
subscription_idstringOwning subscription (sub_).
transition_typeenumcreation, 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_idstring | nullOffer before the change (ofr_). null on creation.
to_offer_idstring | nullOffer after the change (ofr_). null on cancellation.
from_statusenum | nullStatus before the change. null on the first (creation) event.
to_statusenumStatus after the change.
triggered_byenumcustomer, system, or admin.
order_idstring | nullOrder that triggered the transition (ord_), if any.
reasonstring | nullHuman-readable reason, when supplied.
metadataobject | nullAdditional context, e.g. the resolved change_charge_behavior for an offer change.
created_atstringISO 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

FieldTypeRequiredDescription
at_period_endbooleanYestrue defers cancellation to current_period_end (the customer keeps access until then). false cancels immediately.
reasonstringNoHuman-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

FieldTypeRequiredDescription
to_offer_idstringYesTarget 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:

BehaviorMeaning
next_renewThe new amount takes effect on the next renewal. No immediate charge, no proration.
proratedThe customer is charged (or credited) the prorated difference for the remainder of the period.
overrideA custom amount defined for the transition is charged.

The effective behavior for a given (from_offer → to_offer) pair is resolved in order:

  1. If an active offer transition exists for the pair with an explicit behavior, that value wins.
  2. 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 is cancelled/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

FieldTypeRequiredDescription
payment_instrument_idstringYesThe 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

FieldTypeRequiredDescription
reasonstringNoHuman-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:

HTTPTypeWhen
400validation_errorCancel/change on a terminal subscription, cross-family offer change, change-payment-instrument outside dunning, no active offer transition.
403authorization_errorKey lacks subscriptions:read / subscriptions:write.
404not_found_errorSubscription not found or not owned by the merchant.
409conflict_errorPause from a non-pausable state, or resume from a non-paused state.
422business_rule_errorThe target payment instrument has no confirmed prior charge to route from.
  • 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.

On this page