Offer Transitions
Define how moving a subscription from one offer to another is billed, with per-pair overrides that take precedence over the product-family default.
An offer transition describes how a subscription is billed when it moves from one offer to another — an upgrade, a downgrade, or a plan switch within the same product family. It pins a specific from_offer_id → to_offer_id pair to a billing rule (change_charge_behavior) and a toggle (is_active).
Product families already carry a default change-charge behavior for every move inside the family. An offer transition is the fine-grained override: when you need one specific pair to bill differently from the family default, you create a transition for that pair.
Tokeflow orchestrates payments; it does not process or settle funds. Offer transitions are pure catalog modeling — they describe how a plan change should be charged, never who moves the money.
Offer transitions are part of the catalog, which is merchant-scoped. With a Merchant key, the merchant is inferred. With an Organization key, target a merchant with the merchant_id query parameter (on list/get) or body field (on create). See Authentication.
The problem they solve
Most plan changes within a family bill the same way — set that rule once on the family and you're done. But real catalogs have exceptions:
- An upgrade from a monthly to an annual offer should switch immediately (
override), while every other move waits for the next renewal. - A specific downgrade should never prorate, even though the family default prorates.
- You want to temporarily disable billing for one transition pair without touching the rest of the family.
Offer transitions let you encode these exceptions per pair, leaving the family default to handle everything else.
Precedence: explicit transition beats family default
When a subscription moves between two offers, Tokeflow resolves the effective billing rule in this order:
In words:
- If there is an active transition for the exact
from_offer_id→to_offer_idpair and it sets a non-nullchange_charge_behavior, that value wins. - Otherwise — no transition, an inactive transition, or a transition with a null
change_charge_behavior— Tokeflow falls back to the product family's defaultchange_charge_behavior.
To resolve the effective behavior for any pair without replaying this logic yourself, call the offers effective-behavior endpoint. It returns the single value that would apply, after precedence.
The current V1 transition engine processes next_renew. The prorated and override values are accepted and stored so you can model your intended behavior today, but only next_renew is executed by the engine in this release.
The change-charge behavior
change_charge_behavior is optional on a transition. Leave it null to inherit the family default for that pair while still using the transition row for its is_active toggle.
| Value | Meaning |
|---|---|
next_renew | Apply the change at the next renewal — the new price takes effect on the following billing cycle. |
prorated | Prorate the difference for the current cycle. |
override | Replace the current charge with the new offer's terms immediately. |
The offer transition object
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier, prefixed oft_. |
from_offer_id | string | The offer the subscription is moving away from. Prefixed ofr_. |
to_offer_id | string | The offer the subscription is moving to. Prefixed ofr_. Must belong to the same product family as from_offer_id. |
change_charge_behavior | enum | null | Per-pair override: next_renew, prorated, or override. null means fall back to the family default. |
is_active | boolean | Whether the transition is eligible for processing. Inactive transitions are ignored and the family default applies. Defaults to true. |
created_at | string | ISO 8601 UTC creation timestamp. |
updated_at | string | ISO 8601 UTC last-update timestamp. |
{
"id": "oft_a1b2c3d4e5",
"from_offer_id": "ofr_3hk9d2",
"to_offer_id": "ofr_7pq4m1",
"change_charge_behavior": "override",
"is_active": true,
"created_at": "2026-05-19T12:00:00.000Z",
"updated_at": "2026-05-19T12:00:00.000Z"
}A transition pair is unique. Each from_offer_id → to_offer_id combination can have at most one transition. Both offers must belong to the same product family, and from_offer_id must differ from to_offer_id.
Endpoints
POST/api/v1/offer-transitions
Auth: Organization key (with merchant_id) or Merchant key. Requires the offers:write scope.
Creates a transition for a specific offer pair.
| Field | Type | Required | Description |
|---|---|---|---|
from_offer_id | string | Yes | The offer the subscription is moving away from. |
to_offer_id | string | Yes | The offer the subscription is moving to. Must share the same product family as from_offer_id and differ from it. |
change_charge_behavior | enum | No | next_renew, prorated, or override. Omit or null to inherit the family default. |
is_active | boolean | No | Whether the transition is eligible for processing. Defaults to true. |
merchant_id | string | Conditional | Required with an Organization key; omit with a Merchant key. |
curl -X POST https://api.tokeflow.com/api/v1/offer-transitions \
-H "Authorization: Bearer sk_live_mer_REPLACE_WITH_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3a7f9c2e-2c1d-4a8b-9f10-1e2d3c4b5a6f" \
-d '{
"from_offer_id": "ofr_3hk9d2",
"to_offer_id": "ofr_7pq4m1",
"change_charge_behavior": "override",
"is_active": true
}'{
"success": true,
"data": {
"id": "oft_a1b2c3d4e5",
"from_offer_id": "ofr_3hk9d2",
"to_offer_id": "ofr_7pq4m1",
"change_charge_behavior": "override",
"is_active": true,
"created_at": "2026-05-19T12:00:00.000Z",
"updated_at": "2026-05-19T12:00:00.000Z"
},
"request_id": "req_8f3c1a2b4d5e",
"timestamp": "2026-05-19T12:00:00.000Z"
}Send an Idempotency-Key header on create so a retried request never produces a duplicate transition. Replaying the same key returns the original result. See Idempotency & rate limits.
Common errors:
| Status | Type | When |
|---|---|---|
| 400 | validation_error | from_offer_id equals to_offer_id, an offer does not exist, or the two offers are in different product families. |
| 409 | conflict_error | A transition already exists for this from_offer_id → to_offer_id pair. |
{
"error": {
"type": "conflict_error",
"code": "OFFER_TRANSITION_ALREADY_EXISTS",
"message": "A transition for this offer pair already exists.",
"details": {
"from_offer_id": "ofr_3hk9d2",
"to_offer_id": "ofr_7pq4m1"
},
"request_id": "req_2b9e7c1f0a3d",
"timestamp": "2026-05-19T12:00:00.000Z"
}
}GET/api/v1/offer-transitions
Auth: Organization key (with merchant_id) or Merchant key. Requires the offers:read scope.
Lists transitions for the merchant. Supports pagination and filtering.
| Query param | Type | Description |
|---|---|---|
page | integer | Page number, 1-indexed. Default 1, min 1. |
limit | integer | Items per page. Default 20, max 100. |
from_offer_id | string | Filter to transitions originating from this offer. |
to_offer_id | string | Filter to transitions targeting this offer. |
is_active | boolean | Filter to active (true) or inactive (false) transitions. |
change_charge_behavior | enum | Filter by behavior: next_renew, prorated, or override. |
merchant_id | string | Required with an Organization key; omit with a Merchant key. |
curl "https://api.tokeflow.com/api/v1/offer-transitions?page=1&limit=20&from_offer_id=ofr_3hk9d2&is_active=true" \
-H "Authorization: Bearer sk_live_mer_REPLACE_WITH_YOUR_KEY"{
"success": true,
"data": [
{
"id": "oft_a1b2c3d4e5",
"from_offer_id": "ofr_3hk9d2",
"to_offer_id": "ofr_7pq4m1",
"change_charge_behavior": "override",
"is_active": true,
"created_at": "2026-05-19T12:00:00.000Z",
"updated_at": "2026-05-19T12:00:00.000Z"
},
{
"id": "oft_f6g7h8i9j0",
"from_offer_id": "ofr_3hk9d2",
"to_offer_id": "ofr_2zx5n8",
"change_charge_behavior": null,
"is_active": true,
"created_at": "2026-05-19T12:05:00.000Z",
"updated_at": "2026-05-19T12:05:00.000Z"
}
],
"meta": {
"pagination": {
"page": 1,
"limit": 20,
"total": 2,
"total_pages": 1,
"has_next": false,
"has_prev": false
}
},
"request_id": "req_5c1d8e2f9a0b",
"timestamp": "2026-05-19T12:30:00.000Z"
}See Pagination & filtering for shared conventions.
GET/api/v1/offer-transitions/:id
Auth: Organization key (with merchant_id) or Merchant key. Requires the offers:read scope.
Retrieves a single transition by ID.
curl "https://api.tokeflow.com/api/v1/offer-transitions/oft_a1b2c3d4e5" \
-H "Authorization: Bearer sk_live_mer_REPLACE_WITH_YOUR_KEY"{
"success": true,
"data": {
"id": "oft_a1b2c3d4e5",
"from_offer_id": "ofr_3hk9d2",
"to_offer_id": "ofr_7pq4m1",
"change_charge_behavior": "override",
"is_active": true,
"created_at": "2026-05-19T12:00:00.000Z",
"updated_at": "2026-05-19T12:00:00.000Z"
},
"request_id": "req_9a0b1c2d3e4f",
"timestamp": "2026-05-19T12:30:00.000Z"
}A transition that does not exist or belongs to another merchant returns 404 not_found_error.
PATCH/api/v1/offer-transitions/:id
Auth: Organization key (with merchant_id) or Merchant key. Requires the offers:write scope.
Updates a transition's billing rule or active state. Send only the fields you want to change.
| Field | Type | Required | Description |
|---|---|---|---|
change_charge_behavior | enum | null | No | New behavior, or null to fall back to the family default. |
is_active | boolean | No | Enable or disable the transition. |
The offer pair (from_offer_id, to_offer_id) is immutable. Changing the pair is a structural change, not an update — delete the transition and create a new one to retarget.
curl -X PATCH https://api.tokeflow.com/api/v1/offer-transitions/oft_a1b2c3d4e5 \
-H "Authorization: Bearer sk_live_mer_REPLACE_WITH_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"change_charge_behavior": "next_renew",
"is_active": false
}'{
"success": true,
"data": {
"id": "oft_a1b2c3d4e5",
"from_offer_id": "ofr_3hk9d2",
"to_offer_id": "ofr_7pq4m1",
"change_charge_behavior": "next_renew",
"is_active": false,
"created_at": "2026-05-19T12:00:00.000Z",
"updated_at": "2026-05-19T13:15:00.000Z"
},
"request_id": "req_0b1c2d3e4f5a",
"timestamp": "2026-05-19T13:15:00.000Z"
}DELETE/api/v1/offer-transitions/:id
Auth: Organization key (with merchant_id) or Merchant key. Requires the offers:write scope.
Removes a transition. After deletion, moves between that offer pair fall back to the product-family default. Returns 204 No Content.
curl -X DELETE https://api.tokeflow.com/api/v1/offer-transitions/oft_a1b2c3d4e5 \
-H "Authorization: Bearer sk_live_mer_REPLACE_WITH_YOUR_KEY"Disabling a transition with PATCH … { "is_active": false } is reversible and preserves the row. Use DELETE only when you want the pair gone for good.
Related
- Product families — set the default
change_charge_behaviorthat transitions override. - Offers — and the effective-behavior endpoint, which resolves the value that applies for any pair after precedence.
- Subscriptions — the change-offer endpoint consumes the resolved behavior when a subscriber switches plans.