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_idto_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:

  1. If there is an active transition for the exact from_offer_idto_offer_id pair and it sets a non-null change_charge_behavior, that value wins.
  2. Otherwise — no transition, an inactive transition, or a transition with a null change_charge_behavior — Tokeflow falls back to the product family's default change_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.

ValueMeaning
next_renewApply the change at the next renewal — the new price takes effect on the following billing cycle.
proratedProrate the difference for the current cycle.
overrideReplace the current charge with the new offer's terms immediately.

The offer transition object

FieldTypeDescription
idstringUnique identifier, prefixed oft_.
from_offer_idstringThe offer the subscription is moving away from. Prefixed ofr_.
to_offer_idstringThe offer the subscription is moving to. Prefixed ofr_. Must belong to the same product family as from_offer_id.
change_charge_behaviorenum | nullPer-pair override: next_renew, prorated, or override. null means fall back to the family default.
is_activebooleanWhether the transition is eligible for processing. Inactive transitions are ignored and the family default applies. Defaults to true.
created_atstringISO 8601 UTC creation timestamp.
updated_atstringISO 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_idto_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.

FieldTypeRequiredDescription
from_offer_idstringYesThe offer the subscription is moving away from.
to_offer_idstringYesThe offer the subscription is moving to. Must share the same product family as from_offer_id and differ from it.
change_charge_behaviorenumNonext_renew, prorated, or override. Omit or null to inherit the family default.
is_activebooleanNoWhether the transition is eligible for processing. Defaults to true.
merchant_idstringConditionalRequired 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:

StatusTypeWhen
400validation_errorfrom_offer_id equals to_offer_id, an offer does not exist, or the two offers are in different product families.
409conflict_errorA transition already exists for this from_offer_idto_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 paramTypeDescription
pageintegerPage number, 1-indexed. Default 1, min 1.
limitintegerItems per page. Default 20, max 100.
from_offer_idstringFilter to transitions originating from this offer.
to_offer_idstringFilter to transitions targeting this offer.
is_activebooleanFilter to active (true) or inactive (false) transitions.
change_charge_behaviorenumFilter by behavior: next_renew, prorated, or override.
merchant_idstringRequired 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.

FieldTypeRequiredDescription
change_charge_behaviorenum | nullNoNew behavior, or null to fall back to the family default.
is_activebooleanNoEnable 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.

On this page