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.

Billing holds

A renewal is only charged when the merchant has an active connector to route it through and the subscription's offer is still available. If either is missing at renewal time, the engine places the renewal on a temporary billing hold rather than charging against a misconfigured setup: no charge is attempted, and the subscription is not cancelled. Renewals resume automatically once an active connector and a valid offer are in place. When a held merchant is brought back online, the billing period is re-anchored so the next renewal is scheduled from that point forward rather than back-dated across the gap.

One live subscription per plan

A customer can hold only one live subscription per plan at a time. A subscription scheduled to cancel at the end of its period (cancel_at_period_end: true) still counts as live. Starting a second subscription for a plan the customer already holds is rejected with 409 (conflict_error); the response points to reactivation as the way to keep the existing subscription rather than create a duplicate.


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.
actorobject | nullFor an admin-triggered transition, a snapshot of the dashboard user who performed the action, taken at the time it happened: { "id", "type", "name", "email" }. null for customer- and system-triggered transitions.
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",
      "actor": null,
      "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",
      "actor": null,
      "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/admin-cancel

OrgMerchant

Auth: Organization key (with merchant_id) or Merchant key.

Privileged variant of cancel, recorded as merchant-initiated. Requires the subscriptions:admin scope and is not part of the public API.

Path parameters

FieldTypeRequiredDescription
merchant_idstringYes
subscription_idstringYes

Request fields

FieldTypeRequiredDescription
at_period_endbooleanYesWhen true defers cancellation to the next renewal; false cancels immediately.
reasonstringNoOptional human-readable reason persisted on the subscription.

Example request

curl -X POST https://api.tokeflow.com/api/v1/merchants/mrc_8a3f12d9/subscriptions/sub_6d80b3a2/admin-cancel \
  -H "Authorization: Bearer sk_live_mer_…" \
  -H "Content-Type: application/json" \
  -d '{
    "at_period_end": true,
    "reason": "customer no longer needs the service"
  }'

Example response200

{
  "id": "sub_123",
  "merchant_id": "mrc_123",
  "customer_id": "cust_123",
  "customer_name": "Acme Store",
  "customer_email": "jane@example.com",
  "current_offer_id": "off_123",
  "offer_name": "Acme Store",
  "product_id": "prd_9b1e6c05",
  "product_name": "Acme Store",
  "product_family_id": "pfam_123",
  "billing_cycle": "monthly",
  "currency": "BRL",
  "current_amount": 4990,
  "current_period_start": "2026-05-25T00:00:00Z",
  "current_period_end": "2026-06-25T00:00:00Z",
  "next_billing_at": "2026-06-25T00:00:00Z",
  "billing_anchor_day": 25,
  "trial_start": "2026-05-25T00:00:00Z",
  "trial_end": "2026-06-08T00:00:00Z",
  "dunning_started_at": "2026-01-15T12:30:00.000Z",
  "dunning_attempt_count": 0,
  "dunning_next_retry_at": "2026-01-15T12:30:00.000Z",
  "cycles_completed": 3,
  "cycle_limit": 12,
  "status": "active",
  "cancel_at_period_end": false
}

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.

The billing engine processes all three behaviors. next_renew applies the new current_amount from the next cycle, with no immediate charge or proration; prorated charges or credits the prorated difference for the remainder of the current period; and override charges the custom amount defined for the transition. 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, a refreshed current_amount, and billing_cycle re-synced to the new offer's cycle. The current period keeps its dates: next_billing_at and routing fields are unchanged, and the new cycle governs renewals from the next billing date onward.

{
  "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"
}

POST/api/v1/merchants/:merchant_id/subscriptions/:subscription_id/reactivate

Clear a scheduled cancellation and keep the subscription on its current plan. Reactivation sets cancel_at_period_end back to false and records a reactivation transition. It does not change the offer, the amount, or the billing dates — it only calls off the pending cancellation.

Use it when a customer who had scheduled a cancellation decides to stay, and as the remedy for the 409 returned when a customer tries to buy a plan they already hold (see One live subscription per plan).

Auth: Merchant key (subscriptions:write)

Reactivation applies to a live subscription — active, trialing, or dunning. A dunning subscription stays in dunning and its retry schedule is untouched; calling off the cancellation is independent of the payment problem. No request body.

Reactivation targets a live subscription that has a scheduled cancellation. A paused subscription cannot be reactivated — resume it with resume instead — and a terminal (cancelled or expired) subscription is rejected. Both return 409 (conflict_error).

curl -X POST "https://api.tokeflow.com/api/v1/merchants/mrc_8a1c2d/subscriptions/sub_3f9a7c2e1b/reactivate" \
  -H "Authorization: Bearer sk_live_mer_…"

Response 200 OK

{
  "success": true,
  "data": {
    "id": "sub_3f9a7c2e1b",
    "status": "active",
    "cancel_at_period_end": false
  },
  "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, resume from a non-paused state, reactivate a paused or terminal subscription, or start a subscription for a plan the customer already holds.
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