Offers
Create and manage offers — the purchasable, priced configurations of a product, with billing cycles, trials, setup charges, and per-currency prices.
An offer is a purchasable configuration of a product. The product models what you sell; the offer models how it is purchased and billed: the billing cycle, the trial, the cycle limit, and one or more prices — one per currency.
A single product can carry many offers. A monthly plan and a yearly plan of the same product are two offers. A promo and the standard price are two offers. This separation lets you launch new pricing, run experiments, and localize amounts without touching the product itself.
Tokeflow is a payment orchestration platform, not a payment processor. Offers describe your pricing; the actual authorization, capture, and settlement of any charge happen at the connected payment provider. Catalog objects never move money.
How offers are billed
An offer's billing_cycle defines its cadence:
| Cycle | Meaning |
|---|---|
daily | Renews every day |
biweekly | Renews every two weeks |
monthly | Renews every month |
quarterly | Renews every three months |
half_yearly | Renews every six months |
yearly | Renews every year |
custom | Renews every custom_billing_days days (required when custom is used) |
none | On-demand, no scheduler — a one-time charge with no recurring renewal |
The cycle_limit caps the number of charges. Omit it (or set null) to charge until the subscription is cancelled. When the limit is reached, the subscription expires — unless renew_after_cycle_limit is true, in which case a new subscription is created. If renewal_offer_id is set, that offer is used for the post-limit renewal; otherwise the same offer renews.
First-charge amount
Each price carries a base amount and an optional first_charge_amount. The first_charge_amount is only applied when the offer's setup_charge is true, and it sets the price of the first billing cycle — useful for an intro price, a one-time setup fee, or a discounted first month. A first_charge_amount of 0 means a card-validation charge (verify the instrument without charging).
Free trial
When free_trial is true, trial_days is required and defines the trial length in days. During the trial no recurring charge is made; billing begins after the trial ends.
amount and first_charge_amount are integer minor units (cents). R$99.00 = 9900; US$150.00 = 15000. Always pair an amount with its ISO 4217 currency.
A product may have at most one default offer (is_default = true), and an offer may have at most one default price (is_default = true). Within an offer, each currency may appear at most once.
The offer object
{
"id": "ofr_a1b2c3d4e5",
"product_id": "prd_a1b2c3d4e5",
"name": "Mensal",
"slug": "mensal",
"description": "Plano mensal padrão",
"billing_cycle": "monthly",
"custom_billing_days": null,
"cycle_limit": 12,
"free_trial": false,
"trial_days": null,
"setup_charge": true,
"renew_after_cycle_limit": false,
"renewal_offer_id": null,
"is_default": true,
"status": "active",
"created_at": "2026-01-15T12:30:00.000Z",
"updated_at": "2026-01-15T12:30:00.000Z",
"prices": [
{
"id": "opr_b1c2d3e4f5",
"offer_id": "ofr_a1b2c3d4e5",
"currency": "BRL",
"amount": 9900,
"first_charge_amount": 0,
"is_default": true,
"created_at": "2026-01-15T12:30:00.000Z",
"updated_at": "2026-01-15T12:30:00.000Z"
},
{
"id": "opr_c2d3e4f5g6",
"offer_id": "ofr_a1b2c3d4e5",
"currency": "USD",
"amount": 1900,
"first_charge_amount": null,
"is_default": false,
"created_at": "2026-01-15T12:30:00.000Z",
"updated_at": "2026-01-15T12:30:00.000Z"
}
]
}Attributes
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier, prefixed ofr_. |
product_id | string | The product this offer prices (prd_). |
name | string | Human-readable name, e.g. "Mensal". |
slug | string | Lowercase identifier, unique per product. |
description | string | null | Optional description. |
billing_cycle | enum | daily biweekly monthly quarterly half_yearly yearly custom none. |
custom_billing_days | integer | null | Period in days. Required when billing_cycle = custom. |
cycle_limit | integer | null | Fixed number of charges. null = charge until cancelled. |
free_trial | boolean | Whether the offer includes a free trial. |
trial_days | integer | null | Trial length in days. Required when free_trial = true. |
setup_charge | boolean | When true, the price's first_charge_amount is used for the first cycle. |
renew_after_cycle_limit | boolean | When true and cycle_limit is reached, a new subscription is created instead of expiring. |
renewal_offer_id | string | null | Offer used for post-limit renewal (ofr_). null = renew with the same offer. Must belong to the same product family. |
is_default | boolean | Whether this is the default offer for the product. |
status | enum | active or archived. |
created_at | string | ISO 8601 UTC timestamp. |
updated_at | string | ISO 8601 UTC timestamp. |
prices | array | Nested offer prices (included on get-by-id). |
The offer price object
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier, prefixed opr_. |
offer_id | string | The owning offer (ofr_). |
currency | string | ISO 4217 currency code, e.g. BRL. |
amount | integer | Recurring price in minor units (cents). |
first_charge_amount | integer | null | First-cycle price in minor units. Applied when setup_charge = true. 0 = card validation. |
is_default | boolean | Whether this is the default price for the offer. |
created_at | string | ISO 8601 UTC timestamp. |
updated_at | string | ISO 8601 UTC timestamp. |
Endpoints
All offer endpoints are under https://api.tokeflow.com/api/v1. See Authentication for keys and scopes. Offers are a merchant-scoped resource: with an Organization key you must identify the target merchant via merchant_id; with a Merchant key the merchant is inferred.
POST/api/v1/offers
Create an offer together with at least one price.
Auth: Organization key (with merchant_id) or Merchant key. Scope: offers:write.
| Field | Type | Required | Description |
|---|---|---|---|
product_id | string | Yes | Product to attach the offer to (prd_). |
name | string | Yes | Human-readable name. |
slug | string | Yes | Lowercase identifier, unique per product. |
billing_cycle | enum | Yes | One of the billing cycle values. |
status | enum | Yes | active or archived. |
prices | array | Yes | At least one price object (see below). |
description | string | No | Optional description. |
custom_billing_days | integer | Conditional | Required when billing_cycle = custom. |
cycle_limit | integer | No | Fixed number of charges. Omit = charge until cancelled. |
free_trial | boolean | No | Defaults to false. |
trial_days | integer | Conditional | Required when free_trial = true. |
setup_charge | boolean | No | Defaults to false. |
renew_after_cycle_limit | boolean | No | Defaults to false. |
renewal_offer_id | string | No | Offer for post-limit renewal (ofr_). |
is_default | boolean | No | Defaults to false. |
Each entry in prices:
| Field | Type | Required | Description |
|---|---|---|---|
currency | string | Yes | ISO 4217 code. |
amount | integer | Yes | Recurring price in minor units (≥ 0). |
first_charge_amount | integer | No | First-cycle price in minor units (≥ 0). |
is_default | boolean | No | Marks the default price for the offer. |
curl -X POST https://api.tokeflow.com/api/v1/offers \
-H "Authorization: Bearer sk_live_mer_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f3c1d2a-7b6e-4c0a-9f12-3d4e5f6a7b8c" \
-d '{
"product_id": "prd_a1b2c3d4e5",
"name": "Mensal",
"slug": "mensal",
"billing_cycle": "monthly",
"status": "active",
"setup_charge": true,
"is_default": true,
"prices": [
{ "currency": "BRL", "amount": 9900, "first_charge_amount": 0, "is_default": true },
{ "currency": "USD", "amount": 1900, "is_default": false }
]
}'Response 201 Created:
{
"success": true,
"data": {
"id": "ofr_a1b2c3d4e5",
"product_id": "prd_a1b2c3d4e5",
"name": "Mensal",
"slug": "mensal",
"description": null,
"billing_cycle": "monthly",
"custom_billing_days": null,
"cycle_limit": null,
"free_trial": false,
"trial_days": null,
"setup_charge": true,
"renew_after_cycle_limit": false,
"renewal_offer_id": null,
"is_default": true,
"status": "active",
"created_at": "2026-01-15T12:30:00.000Z",
"updated_at": "2026-01-15T12:30:00.000Z",
"prices": [
{
"id": "opr_b1c2d3e4f5",
"offer_id": "ofr_a1b2c3d4e5",
"currency": "BRL",
"amount": 9900,
"first_charge_amount": 0,
"is_default": true,
"created_at": "2026-01-15T12:30:00.000Z",
"updated_at": "2026-01-15T12:30:00.000Z"
},
{
"id": "opr_c2d3e4f5g6",
"offer_id": "ofr_a1b2c3d4e5",
"currency": "USD",
"amount": 1900,
"first_charge_amount": null,
"is_default": false,
"created_at": "2026-01-15T12:30:00.000Z",
"updated_at": "2026-01-15T12:30:00.000Z"
}
]
},
"request_id": "req_8f3c2a1b",
"timestamp": "2026-01-15T12:30:00.000Z"
}Pass an Idempotency-Key header (or idempotency_key in the body) on create calls. Replaying the same key returns the original result (200 OK) instead of creating a duplicate.
GET/api/v1/offers
List offers with pagination and filters.
Auth: Organization key (with merchant_id) or Merchant key. Scope: offers:read.
| Query param | Type | Description |
|---|---|---|
page | integer | Page number, default 1, min 1. |
limit | integer | Items per page, default 20, max 100. |
product_id | string | Filter to a single product. |
status | enum | active or archived. |
billing_cycle | enum | One of the billing cycle values. |
is_default | boolean | Filter to default offers only. |
name | string | Partial-match filter on offer name. |
merchant_id | string | Required when using an Organization key. |
curl "https://api.tokeflow.com/api/v1/offers?product_id=prd_a1b2c3d4e5&status=active&limit=20" \
-H "Authorization: Bearer sk_live_mer_xxx"Response 200 OK:
{
"success": true,
"data": [
{
"id": "ofr_a1b2c3d4e5",
"product_id": "prd_a1b2c3d4e5",
"name": "Mensal",
"slug": "mensal",
"description": null,
"billing_cycle": "monthly",
"custom_billing_days": null,
"cycle_limit": null,
"free_trial": false,
"trial_days": null,
"setup_charge": true,
"renew_after_cycle_limit": false,
"renewal_offer_id": null,
"is_default": true,
"status": "active",
"created_at": "2026-01-15T12:30:00.000Z",
"updated_at": "2026-01-15T12:30:00.000Z"
}
],
"meta": {
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"total_pages": 1,
"has_next": false,
"has_prev": false
}
},
"request_id": "req_8f3c2a1c",
"timestamp": "2026-01-15T12:30:00.000Z"
}List items return the offer summary. Use Get an offer by ID to retrieve the nested prices.
GET/api/v1/offers/:id
Retrieve a single offer, including its prices.
Auth: Organization key (with merchant_id) or Merchant key. Scope: offers:read.
curl https://api.tokeflow.com/api/v1/offers/ofr_a1b2c3d4e5 \
-H "Authorization: Bearer sk_live_mer_xxx"Response 200 OK returns the offer object (with prices) wrapped in the standard success envelope. A 404 (not_found_error) is returned if the offer does not exist or belongs to another merchant.
PATCH/api/v1/offers/:id
Update an offer's mutable fields. Send only the fields you want to change.
Auth: Organization key (with merchant_id) or Merchant key. Scope: offers:write.
curl -X PATCH https://api.tokeflow.com/api/v1/offers/ofr_a1b2c3d4e5 \
-H "Authorization: Bearer sk_live_mer_xxx" \
-H "Content-Type: application/json" \
-d '{ "name": "Mensal Promo", "cycle_limit": 12 }'Response 200 OK returns the updated offer object.
To manage the prices attached to an offer, use the dedicated Offer Prices endpoints. The offer PATCH updates offer-level fields, not nested prices.
DELETE/api/v1/offers/:id
Soft-delete an offer. The delete cascades to the offer's prices. Soft-deleted offers can be brought back with restore.
Auth: Organization key (with merchant_id) or Merchant key. Scope: offers:write.
curl -X DELETE https://api.tokeflow.com/api/v1/offers/ofr_a1b2c3d4e5 \
-H "Authorization: Bearer sk_live_mer_xxx"Response: 204 No Content (empty body).
GET/api/v1/offers/:id/default-price
Resolve the default price for an offer. Pass currency to get the default price in that currency; the response is null if no matching price exists.
Auth: Organization key (with merchant_id) or Merchant key. Scope: offers:read.
| Query param | Type | Required | Description |
|---|---|---|---|
currency | string | No | ISO 4217 code to resolve the price for. |
curl "https://api.tokeflow.com/api/v1/offers/ofr_a1b2c3d4e5/default-price?currency=BRL" \
-H "Authorization: Bearer sk_live_mer_xxx"Response 200 OK:
{
"success": true,
"data": {
"id": "opr_b1c2d3e4f5",
"offer_id": "ofr_a1b2c3d4e5",
"currency": "BRL",
"amount": 9900,
"first_charge_amount": 0,
"is_default": true,
"created_at": "2026-01-15T12:30:00.000Z",
"updated_at": "2026-01-15T12:30:00.000Z"
},
"request_id": "req_8f3c2a1d",
"timestamp": "2026-01-15T12:30:00.000Z"
}When no default price matches, data is null.
GET/api/v1/offers/:fromId/transitions/:toId/effective-behavior
Resolve the effective change_charge_behavior for moving a subscription from one offer to another — for example, an upgrade or downgrade. Both offers must belong to the same product family.
Resolution order:
- If an active transition exists for the
fromId → toIdpair and sets achange_charge_behavior, that value wins. - Otherwise, the source offer's product-family default applies.
Auth: Organization key (with merchant_id) or Merchant key. Scope: offers:read.
change_charge_behavior | Meaning |
|---|---|
next_renew | Apply the change at the next renewal. |
prorated | Prorate the difference immediately. |
override | Replace the current charge schedule with the new offer's. |
curl "https://api.tokeflow.com/api/v1/offers/ofr_from123/transitions/ofr_to456/effective-behavior" \
-H "Authorization: Bearer sk_live_mer_xxx"Response 200 OK:
{
"success": true,
"data": {
"change_charge_behavior": "next_renew"
},
"request_id": "req_8f3c2a1e",
"timestamp": "2026-01-15T12:30:00.000Z"
}A 400 (validation_error) is returned when the source offer is not in a product family, or when the two offers belong to different families. See Offer Transitions to create and manage transition overrides.
POST/api/v1/offers/:id/restore
Restore a soft-deleted offer and its prices.
Auth: Organization key (with merchant_id) or Merchant key. Scope: offers:write.
curl -X POST https://api.tokeflow.com/api/v1/offers/ofr_a1b2c3d4e5/restore \
-H "Authorization: Bearer sk_live_mer_xxx"Response returns the restored offer object.
POST/api/v1/offers/:id/archive
Archive an offer (sets status to archived). Archived offers stay in the catalog and remain retrievable but should not be offered for new purchases.
Auth: Organization key (with merchant_id) or Merchant key. Scope: offers:write.
curl -X POST https://api.tokeflow.com/api/v1/offers/ofr_a1b2c3d4e5/archive \
-H "Authorization: Bearer sk_live_mer_xxx"Response returns the offer with status: "archived".
POST/api/v1/offers/:id/unarchive
Unarchive an offer (sets status back to active).
Auth: Organization key (with merchant_id) or Merchant key. Scope: offers:write.
curl -X POST https://api.tokeflow.com/api/v1/offers/ofr_a1b2c3d4e5/unarchive \
-H "Authorization: Bearer sk_live_mer_xxx"Response returns the offer with status: "active".
Errors
Offer endpoints use the standard error envelope. Common cases:
| HTTP | type | When |
|---|---|---|
| 400 | validation_error | Missing/invalid fields, or a transition resolved across different families. |
| 401 | authentication_error | Missing or invalid API key. |
| 403 | authorization_error | Key lacks the required scope. |
| 404 | not_found_error | Offer (or referenced product) not found for this merchant. |
| 409 | conflict_error | Duplicate slug per product, a second default offer/price, or a duplicate currency. |
| 429 | rate_limit_error | Rate limit exceeded — back off and retry. |
Related
- Products — what an offer prices.
- Offer Prices — manage per-currency prices on an offer.
- Offer Transitions — override change behavior between offer pairs.
- Product Families — group products and set the default change behavior.