Merchant offers
Create, list, update, archive, restore, and soft-delete offers — the pricing plans attached to a product — from the Tokeflow Dashboard merchant surface.
An offer is a purchasable plan attached to a product: it defines the billing cycle, trial and setup behavior, cycle limits, and one or more currency-specific prices. A product can carry several offers — a monthly plan, a yearly plan, a promotional variant — and each offer carries its own prices in minor units per ISO 4217 currency. Offers are what customers actually subscribe to or buy at checkout.
These endpoints power the offer management screens of the Tokeflow Dashboard for a single merchant. Offers reference a product and are referenced in turn by subscriptions, which is why deletion is guarded: an offer with any subscription history is preserved for billing records.
These endpoints power the Tokeflow Dashboard UI. They are authenticated with a dashboard session (JWT bearer) obtained by signing in — not with sk_/pk_ API keys. Every request must also send the x-merchant-id header identifying the merchant context (mrc_…). See Dashboard authentication.
Offer lifecycle
Archiving hides an offer from new checkouts while keeping it fully readable; it is reversible with unarchive. Soft-deletion removes the offer (and cascades to its prices) but is refused with 409 while any subscription — in any status — still references it, and can be reversed with restore.
The offer object
Derived from OfferResponseDto, returned by every endpoint on this page that returns a body.
| Field | Type | Description |
|---|---|---|
id | string | Offer ID, prefixed ofr_. |
product_id | string | Parent product, prefixed prd_. |
name | string | Display name of the offer. |
slug | string | Lowercase identifier, unique per product. |
description | string | null | Free-text description. |
billing_cycle | string | One of daily, biweekly, monthly, quarterly, half_yearly, yearly, custom, none. |
custom_billing_days | number | null | Period in days. Required when billing_cycle = custom. |
cycle_limit | number | null | Fixed number of charges. null = charge until cancelled. |
free_trial | boolean | Whether the offer starts with a free trial. |
trial_days | number | null | Trial duration in days. Required when free_trial = true. |
setup_charge | boolean | Whether the first cycle is billed at a distinct first-charge amount. |
renew_after_cycle_limit | boolean | Whether the subscription renews after cycle_limit is reached. |
renewal_offer_id | string | null | Offer used for post-cycle_limit renewal. null = renew with the same offer. |
is_default | boolean | Whether this is the default offer for its product. |
status | string | active or archived. |
created_at | string | Creation timestamp (ISO 8601 UTC). |
updated_at | string | Last update timestamp (ISO 8601 UTC). |
prices | array | Offer prices. Populated when prices are eager-loaded (create, get by ID). |
billing_cycle: "none" describes a one-time offer with no recurrence. For recurring cycles that do not fit the presets, use custom together with custom_billing_days.
Endpoints
GET/api/v1/merchant/offers
Auth: Dashboard session (JWT bearer). Merchant membership required.
Returns a paginated list of the merchant's offers, filterable by product, status, billing cycle, and name.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
page | number | No | Page number, 1-indexed (default 1, min 1). |
limit | number | No | Items per page (default 20, min 1, max 100). |
product_id | string | No | Filter offers to a single product. |
status | string | No | Filter by status: active or archived. |
billing_cycle | string | No | Filter by billing cycle: daily, biweekly, monthly, quarterly, half_yearly, yearly, custom, or none. |
is_default | boolean | No | Filter to default offers only. |
name | string | No | Partial-match filter on offer name. |
Example request
curl -G https://api.tokeflow.com/api/v1/merchant/offers \
-H "Authorization: Bearer <session-jwt>" \
-H "x-merchant-id: mrc_9f8e7d6c5b" \
--data-urlencode "product_id=prd_a1b2c3d4e5" \
--data-urlencode "status=active" \
--data-urlencode "page=1" \
--data-urlencode "limit=20"Example response — 200 OK
{
"data": [
{
"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": false,
"renew_after_cycle_limit": false,
"renewal_offer_id": null,
"is_default": true,
"status": "active",
"created_at": "2026-05-19T12:00:00Z",
"updated_at": "2026-05-19T12:00:00Z"
}
],
"meta": {
"page": 1,
"limit": 20,
"total": 42,
"total_pages": 3,
"has_next": true,
"has_prev": false
}
}POST/api/v1/merchant/offers
Auth: Dashboard session (JWT bearer). Merchant membership required.
Creates an offer with its initial prices. At least one price is required; each price is an amount in minor units for one ISO 4217 currency.
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
product_id | string | Yes | Parent product, prefixed prd_. |
name | string | Yes | Display name (max 255 characters). |
slug | string | Yes | Lowercase identifier, unique per product (max 100 characters). |
description | string | No | Free-text description (max 2000 characters). |
billing_cycle | string | Yes | One of daily, biweekly, monthly, quarterly, half_yearly, yearly, custom, none. |
custom_billing_days | number | Conditional | Period in days (min 1). Required when billing_cycle = custom. |
cycle_limit | number | No | Fixed number of charges (min 1). Omit or null = charge until cancelled. |
free_trial | boolean | No | Whether the offer starts with a free trial. Default false. |
trial_days | number | Conditional | Trial duration in days (min 1). Required when free_trial = true. |
setup_charge | boolean | No | Bill the first cycle at each price's first_charge_amount. Default false. |
renew_after_cycle_limit | boolean | No | Renew the subscription after cycle_limit charges. Default false. |
renewal_offer_id | string | No | Offer for post-cycle_limit renewal. Omit = renew with the same offer. |
is_default | boolean | No | Make this the default offer for its product. Default false. |
status | string | Yes | active or archived. |
prices | array | Yes | At least one price (see below). |
Price fields (prices[])
| Field | Type | Required | Description |
|---|---|---|---|
currency | string | Yes | ISO 4217 currency code. |
amount | number | Yes | Amount in minor units (min 0). E.g. R$99.00 = 9900. |
first_charge_amount | number | No | First-cycle amount in minor units. Used when setup_charge = true. 0 = card validation only. |
is_default | boolean | No | Mark this currency as the offer's default. |
Example request
curl -X POST https://api.tokeflow.com/api/v1/merchant/offers \
-H "Authorization: Bearer <session-jwt>" \
-H "x-merchant-id: mrc_9f8e7d6c5b" \
-H "Content-Type: application/json" \
-d '{
"product_id": "prd_a1b2c3d4e5",
"name": "Mensal",
"slug": "mensal",
"description": "Plano mensal padrão",
"billing_cycle": "monthly",
"cycle_limit": 12,
"free_trial": false,
"setup_charge": false,
"is_default": true,
"status": "active",
"prices": [
{ "currency": "BRL", "amount": 9900, "is_default": true },
{ "currency": "USD", "amount": 1900 }
]
}'Example response — 201 Created
{
"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": false,
"renew_after_cycle_limit": false,
"renewal_offer_id": null,
"is_default": true,
"status": "active",
"created_at": "2026-05-19T12:00:00Z",
"updated_at": "2026-05-19T12:00:00Z",
"prices": [
{
"currency": "BRL",
"amount": 9900,
"is_default": true
},
{
"currency": "USD",
"amount": 1900,
"is_default": false
}
]
}A free trial does not need a setup charge: free_trial: true with trial_days charges 0 up front on its own, and the card is still enrolled by a card validation. To take a joining fee alongside the trial, add setup_charge: true with a first_charge_amount above 0 on the price; first_charge_amount: 0 is the explicit way to spell "validate the card, charge nothing".
GET/api/v1/merchant/offers/:id
Auth: Dashboard session (JWT bearer). Merchant membership required.
Retrieves a single offer by ID, including its prices. Returns the offer object.
Example request
curl https://api.tokeflow.com/api/v1/merchant/offers/ofr_a1b2c3d4e5 \
-H "Authorization: Bearer <session-jwt>" \
-H "x-merchant-id: mrc_9f8e7d6c5b"Example response — 200 OK
{
"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": false,
"renew_after_cycle_limit": false,
"renewal_offer_id": null,
"is_default": true,
"status": "active",
"created_at": "2026-05-19T12:00:00Z",
"updated_at": "2026-05-19T12:00:00Z",
"prices": [
{
"currency": "BRL",
"amount": 9900,
"is_default": true
}
]
}PATCH/api/v1/merchant/offers/:id
Auth: Dashboard session (JWT bearer). Merchant membership required.
Updates an offer. Send only the fields you want to change; the updated offer object is returned.
Example request
curl -X PATCH https://api.tokeflow.com/api/v1/merchant/offers/ofr_a1b2c3d4e5 \
-H "Authorization: Bearer <session-jwt>" \
-H "x-merchant-id: mrc_9f8e7d6c5b" \
-H "Content-Type: application/json" \
-d '{ "name": "Mensal Plus", "description": "Plano mensal com upgrade" }'Example response — 200 OK
{
"id": "ofr_a1b2c3d4e5",
"product_id": "prd_a1b2c3d4e5",
"name": "Mensal Plus",
"slug": "mensal",
"description": "Plano mensal com upgrade",
"billing_cycle": "monthly",
"custom_billing_days": null,
"cycle_limit": 12,
"free_trial": false,
"trial_days": null,
"setup_charge": false,
"renew_after_cycle_limit": false,
"renewal_offer_id": null,
"is_default": true,
"status": "active",
"created_at": "2026-05-19T12:00:00Z",
"updated_at": "2026-05-19T14:30:00Z"
}DELETE/api/v1/merchant/offers/:id
Auth: Dashboard session (JWT bearer). Merchant membership required.
Soft-deletes an offer, cascading to its prices. The request is refused with 409 while any subscription — in any status — references the offer; offers referenced by subscriptions are kept for billing history. A soft-deleted offer can be brought back with restore.
Response codes
| Code | When |
|---|---|
204 No Content | Offer soft-deleted. |
404 Not Found | Offer not found. |
409 Conflict | Offer has subscriptions and cannot be deleted. |
Example request
curl -X DELETE https://api.tokeflow.com/api/v1/merchant/offers/ofr_a1b2c3d4e5 \
-H "Authorization: Bearer <session-jwt>" \
-H "x-merchant-id: mrc_9f8e7d6c5b"Example response — 204 No Content (empty body)
When an offer cannot be deleted because subscriptions reference it, archive it instead — archived offers stay out of new checkouts while their billing history remains intact.
POST/api/v1/merchant/offers/:id/archive
Auth: Dashboard session (JWT bearer). Merchant membership required.
Archives an offer, setting status to archived. Returns the updated offer object.
Example request
curl -X POST https://api.tokeflow.com/api/v1/merchant/offers/ofr_a1b2c3d4e5/archive \
-H "Authorization: Bearer <session-jwt>" \
-H "x-merchant-id: mrc_9f8e7d6c5b"Example response — 201 Created
{
"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": false,
"renew_after_cycle_limit": false,
"renewal_offer_id": null,
"is_default": false,
"status": "archived",
"created_at": "2026-05-19T12:00:00Z",
"updated_at": "2026-05-19T15:00:00Z"
}POST/api/v1/merchant/offers/:id/unarchive
Auth: Dashboard session (JWT bearer). Merchant membership required.
Unarchives an offer, setting status back to active. Returns the updated offer object.
Example request
curl -X POST https://api.tokeflow.com/api/v1/merchant/offers/ofr_a1b2c3d4e5/unarchive \
-H "Authorization: Bearer <session-jwt>" \
-H "x-merchant-id: mrc_9f8e7d6c5b"Example response — 201 Created
{
"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": false,
"renew_after_cycle_limit": false,
"renewal_offer_id": null,
"is_default": false,
"status": "active",
"created_at": "2026-05-19T12:00:00Z",
"updated_at": "2026-05-19T15:10:00Z"
}POST/api/v1/merchant/offers/:id/restore
Auth: Dashboard session (JWT bearer). Merchant membership required.
Restores a soft-deleted offer and its prices. Returns the restored offer object.
Example request
curl -X POST https://api.tokeflow.com/api/v1/merchant/offers/ofr_a1b2c3d4e5/restore \
-H "Authorization: Bearer <session-jwt>" \
-H "x-merchant-id: mrc_9f8e7d6c5b"Example response — 201 Created
{
"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": false,
"renew_after_cycle_limit": false,
"renewal_offer_id": null,
"is_default": false,
"status": "active",
"created_at": "2026-05-19T12:00:00Z",
"updated_at": "2026-05-19T16:00:00Z"
}GET/api/v1/merchant/offers/:id/default-price
Auth: Dashboard session (JWT bearer). Merchant membership required.
Returns the default price of an offer for a given currency, or null if the offer has no default price in that currency.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
currency | string | Yes | ISO 4217 currency code to resolve the default price for. |
Example request
curl -G https://api.tokeflow.com/api/v1/merchant/offers/ofr_a1b2c3d4e5/default-price \
-H "Authorization: Bearer <session-jwt>" \
-H "x-merchant-id: mrc_9f8e7d6c5b" \
--data-urlencode "currency=BRL"Example response — 200 OK
{
"currency": "BRL",
"amount": 9900,
"is_default": true
}GET/api/v1/merchant/offers/:fromId/transitions/:toId/effective-behavior
Auth: Dashboard session (JWT bearer). Merchant membership required.
Resolves the effective change_charge_behavior for an offer transition pair — how a subscription moving from offer :fromId to offer :toId will be charged. Use it to preview the charge behavior the Dashboard will apply before confirming a plan change.
Example request
curl https://api.tokeflow.com/api/v1/merchant/offers/ofr_a1b2c3d4e5/transitions/ofr_f6g7h8i9j0/effective-behavior \
-H "Authorization: Bearer <session-jwt>" \
-H "x-merchant-id: mrc_9f8e7d6c5b"Example response — 200 OK
Errors
Common error responses for this resource:
| HTTP | Typical cause |
|---|---|
400 | Invalid body — e.g. missing prices, billing_cycle: "custom" without custom_billing_days, or free_trial: true without trial_days. |
401 | Missing or expired dashboard session. |
403 | The session's user is not a member of the merchant in x-merchant-id. |
404 | Offer not found or not owned by the merchant. |
409 | Delete refused — the offer is referenced by at least one subscription. |
Related
- Products — the catalog entries offers belong to.
- Subscriptions — recurring billing created from offers.
- Checkout sessions — where customers pick an offer and pay.
- Dashboard authentication — how dashboard sessions are issued.