Product Families
Group related products and plans under a single catalog entity, and set a default change-charge behavior for offer transitions within the group.
A product family groups related products and plans into one catalog entity — for example, the tiers of a single subscription (Light, Pro, Premium) or a set of one-time add-ons sold together. Families give your catalog structure: instead of a flat list of products, you model the relationships between them.
A family also carries a default change-charge behavior — the rule applied when a subscription moves between offers belonging to products in the same family (an upgrade, downgrade, or plan switch). Set it once on the family, and every transition within that family inherits it unless overridden.
Tokeflow orchestrates payments; it does not process or settle funds. Product families are pure catalog modeling — they describe what you sell and how plan changes are charged, never who moves the money.
Product families 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.
Where families fit
A family is the top of the catalog hierarchy. Products belong to a family; offers and prices hang off products; subscriptions ride on offers.
When a subscriber transitions from an offer on one product to an offer on another product in the same family, the family's change_charge_behavior decides how the change is billed.
The change-charge behavior
change_charge_behavior sets the default billing treatment for offer transitions within the family.
| 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. |
If you omit change_charge_behavior on create, the family defaults to next_renew.
The current V1 transition engine processes next_renew. The prorated and override values are accepted and stored on the family so you can model your intended behavior today, but only next_renew is executed by the engine in this release.
The product family object
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier, prefixed pfa_. |
merchant_id | string | Owning merchant, prefixed mrc_. |
name | string | Human-readable name, e.g. Streaming Plans. |
slug | string | Lowercase identifier, unique per merchant. |
description | string | null | Optional free-text description. |
custom_plan_id | string | null | Optional external plan reference, for display only. Unique per merchant when set. |
change_charge_behavior | enum | Default transition behavior: next_renew, prorated, or override. |
created_at | string | ISO 8601 UTC creation timestamp. |
updated_at | string | ISO 8601 UTC last-update timestamp. |
deleted_at | string | null | ISO 8601 UTC soft-delete timestamp, or null when active. Present only on soft-deleted records. |
{
"id": "pfa_a1b2c3d4e5",
"merchant_id": "mrc_a1b2c3d4e5",
"name": "Streaming Plans",
"slug": "streaming-plans",
"description": "Family of streaming subscription plans",
"custom_plan_id": "plan_external_42",
"change_charge_behavior": "next_renew",
"created_at": "2026-05-19T12:00:00.000Z",
"updated_at": "2026-05-19T12:00:00.000Z"
}Create a product family
POST/api/v1/product-families
Auth: Organization key (with merchant_id) or Merchant key. Requires scope products:write.
Creates a new family in the target merchant's catalog.
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable name. |
slug | string | Yes | Lowercase identifier, unique per merchant (among active families). |
description | string | No | Free-text description. |
custom_plan_id | string | No | External plan reference (display only), unique per merchant when set. |
change_charge_behavior | enum | No | next_renew (default), prorated, or override. |
merchant_id | string | Conditional | Required only with an Organization key, to target a merchant. Inferred from a Merchant key. |
curl -X POST https://api.tokeflow.com/api/v1/product-families \
-H "Authorization: Bearer sk_live_mer_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f3c1d20-9b4a-4e2c-9f6a-2c1d0e7b5a44" \
-d '{
"name": "Streaming Plans",
"slug": "streaming-plans",
"description": "Family of streaming subscription plans",
"custom_plan_id": "plan_external_42",
"change_charge_behavior": "next_renew"
}'Response 201 Created
{
"success": true,
"data": {
"id": "pfa_a1b2c3d4e5",
"merchant_id": "mrc_a1b2c3d4e5",
"name": "Streaming Plans",
"slug": "streaming-plans",
"description": "Family of streaming subscription plans",
"custom_plan_id": "plan_external_42",
"change_charge_behavior": "next_renew",
"created_at": "2026-05-19T12:00:00.000Z",
"updated_at": "2026-05-19T12:00:00.000Z"
},
"request_id": "req_8f3c1d20",
"timestamp": "2026-05-19T12:00:00.000Z"
}Send an Idempotency-Key header on create. Replaying the same key returns the original result (HTTP 200 on replay vs 201 on first create) instead of creating a duplicate. See Idempotency and rate limits.
slug must be unique per merchant among active families. A duplicate returns 409 (conflict_error). The same applies to custom_plan_id when set.
List product families
GET/api/v1/product-families
Auth: Organization key (with merchant_id) or Merchant key. Requires scope products:read.
Returns a paginated list of families for the merchant, newest first.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Partial-match (case-insensitive) filter on family name. |
slug | string | No | Filter by exact slug. |
change_charge_behavior | enum | No | Filter by next_renew, prorated, or override. |
page | integer | No | Page number, default 1, min 1. |
limit | integer | No | Items per page, default 20, max 100. |
merchant_id | string | Conditional | Required only with an Organization key. |
curl -G https://api.tokeflow.com/api/v1/product-families \
-H "Authorization: Bearer sk_live_mer_…" \
--data-urlencode "page=1" \
--data-urlencode "limit=20" \
--data-urlencode "name=Streaming" \
--data-urlencode "change_charge_behavior=next_renew"Response 200 OK
{
"success": true,
"data": [
{
"id": "pfa_a1b2c3d4e5",
"merchant_id": "mrc_a1b2c3d4e5",
"name": "Streaming Plans",
"slug": "streaming-plans",
"description": "Family of streaming subscription plans",
"custom_plan_id": "plan_external_42",
"change_charge_behavior": "next_renew",
"created_at": "2026-05-19T12:00:00.000Z",
"updated_at": "2026-05-19T12:00:00.000Z"
}
],
"meta": {
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"total_pages": 1,
"has_next": false,
"has_prev": false
}
},
"request_id": "req_8f3c1d20",
"timestamp": "2026-05-19T12:00:00.000Z"
}Get a product family
GET/api/v1/product-families/:id
Auth: Organization key (with merchant_id) or Merchant key. Requires scope products:read.
Retrieves a single family by ID.
curl https://api.tokeflow.com/api/v1/product-families/pfa_a1b2c3d4e5 \
-H "Authorization: Bearer sk_live_mer_…"Response 200 OK
{
"success": true,
"data": {
"id": "pfa_a1b2c3d4e5",
"merchant_id": "mrc_a1b2c3d4e5",
"name": "Streaming Plans",
"slug": "streaming-plans",
"description": "Family of streaming subscription plans",
"custom_plan_id": "plan_external_42",
"change_charge_behavior": "next_renew",
"created_at": "2026-05-19T12:00:00.000Z",
"updated_at": "2026-05-19T12:00:00.000Z"
},
"request_id": "req_8f3c1d20",
"timestamp": "2026-05-19T12:00:00.000Z"
}An unknown ID returns 404 (not_found_error).
Update a product family
PATCH/api/v1/product-families/:id
Auth: Organization key (with merchant_id) or Merchant key. Requires scope products:write.
Partially updates a family. Send only the fields you want to change. All create fields are updatable: name, slug, description, custom_plan_id, and change_charge_behavior.
curl -X PATCH https://api.tokeflow.com/api/v1/product-families/pfa_a1b2c3d4e5 \
-H "Authorization: Bearer sk_live_mer_…" \
-H "Content-Type: application/json" \
-d '{
"description": "All streaming subscription tiers",
"change_charge_behavior": "prorated"
}'Response 200 OK
{
"success": true,
"data": {
"id": "pfa_a1b2c3d4e5",
"merchant_id": "mrc_a1b2c3d4e5",
"name": "Streaming Plans",
"slug": "streaming-plans",
"description": "All streaming subscription tiers",
"custom_plan_id": "plan_external_42",
"change_charge_behavior": "prorated",
"created_at": "2026-05-19T12:00:00.000Z",
"updated_at": "2026-05-20T09:15:00.000Z"
},
"request_id": "req_8f3c1d20",
"timestamp": "2026-05-20T09:15:00.000Z"
}Changing slug or custom_plan_id to a value already taken by another active family in the same merchant returns 409 (conflict_error).
Soft-delete a product family
DELETE/api/v1/product-families/:id
Auth: Organization key (with merchant_id) or Merchant key. Requires scope products:write.
Soft-deletes the family. The record is retained with a deleted_at timestamp and excluded from list and get results, preserving auditability. The slug and custom_plan_id slots are freed for reuse by new active families.
curl -X DELETE https://api.tokeflow.com/api/v1/product-families/pfa_a1b2c3d4e5 \
-H "Authorization: Bearer sk_live_mer_…"Response 204 No Content
The response body is empty. An unknown ID returns 404 (not_found_error).
This is a soft delete — the family can be brought back with the restore endpoint below.
Restore a product family
POST/api/v1/product-families/:id/restore
Auth: Organization key (with merchant_id) or Merchant key. Requires scope products:write.
Restores a soft-deleted family, clearing its deleted_at. Restore reclaims the original slug and custom_plan_id only if no active family has taken them in the interim.
curl -X POST https://api.tokeflow.com/api/v1/product-families/pfa_a1b2c3d4e5/restore \
-H "Authorization: Bearer sk_live_mer_…"Response 201 Created
{
"success": true,
"data": {
"id": "pfa_a1b2c3d4e5",
"merchant_id": "mrc_a1b2c3d4e5",
"name": "Streaming Plans",
"slug": "streaming-plans",
"description": "All streaming subscription tiers",
"custom_plan_id": "plan_external_42",
"change_charge_behavior": "prorated",
"created_at": "2026-05-19T12:00:00.000Z",
"updated_at": "2026-05-21T11:00:00.000Z"
},
"request_id": "req_8f3c1d20",
"timestamp": "2026-05-21T11:00:00.000Z"
}Error cases
| Condition | HTTP | Type |
|---|---|---|
| Family is already active (not soft-deleted) | 400 | validation_error |
An active family now holds the same slug | 409 | conflict_error |
An active family now holds the same custom_plan_id | 409 | conflict_error |
| Family not found | 404 | not_found_error |
If restore fails with a slug or custom_plan_id conflict, rename or archive the live family that holds the slot, then retry the restore.
Errors
All errors use the standard envelope. See Errors for the full catalog.
{
"error": {
"type": "conflict_error",
"code": "PRODUCT_FAMILY_SLUG_TAKEN",
"message": "Product family with slug 'streaming-plans' already exists",
"details": {},
"request_id": "req_8f3c1d20",
"timestamp": "2026-05-19T12:00:00.000Z"
}
}Related
- Products — the tiers that belong to a family.
- Core concepts — how catalog, offers, and subscriptions fit together.
- API reference overview — envelopes, pagination, money, and IDs.