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.

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.

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

FieldTypeDescription
idstringUnique identifier, prefixed pfa_.
merchant_idstringOwning merchant, prefixed mrc_.
namestringHuman-readable name, e.g. Streaming Plans.
slugstringLowercase identifier, unique per merchant.
descriptionstring | nullOptional free-text description.
custom_plan_idstring | nullOptional external plan reference, for display only. Unique per merchant when set.
change_charge_behaviorenumDefault transition behavior: next_renew, prorated, or override.
created_atstringISO 8601 UTC creation timestamp.
updated_atstringISO 8601 UTC last-update timestamp.
deleted_atstring | nullISO 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

FieldTypeRequiredDescription
namestringYesHuman-readable name.
slugstringYesLowercase identifier, unique per merchant (among active families).
descriptionstringNoFree-text description.
custom_plan_idstringNoExternal plan reference (display only), unique per merchant when set.
change_charge_behaviorenumNonext_renew (default), prorated, or override.
merchant_idstringConditionalRequired 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

FieldTypeRequiredDescription
namestringNoPartial-match (case-insensitive) filter on family name.
slugstringNoFilter by exact slug.
change_charge_behaviorenumNoFilter by next_renew, prorated, or override.
pageintegerNoPage number, default 1, min 1.
limitintegerNoItems per page, default 20, max 100.
merchant_idstringConditionalRequired 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

ConditionHTTPType
Family is already active (not soft-deleted)400validation_error
An active family now holds the same slug409conflict_error
An active family now holds the same custom_plan_id409conflict_error
Family not found404not_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"
  }
}

On this page