A product represents a billable item in your catalog - a feature, service, or resource that customers pay for. Products are the building blocks of plans and appear as line items on invoices.
Products can be recurring (charged every billing period), usage-based (metered and charged based on consumption), or one-time (charged once at purchase).
Key concepts:
- Product type -
recurring,usage,one_time, orseat- determines how charges are calculated - External ID - your internal SKU or product code for integration with other systems
- Cost tracking - named COGS entries via
costs[](per-product or org-scoped) for margin analysis - Dependencies -
requires_product_idsenforces that certain products must be purchased together
List products
Returns a page of products. Supports the field.op=value filter grammar (see /docs/pagination) and custom-field filters in the custom_fields.include=localized adds the translated text under localized. Name include=localized to get the stored text now.
Required scope: product.read (Read products).
query Parameters
created_atFilter on created_at (date-time). Operators: eq, gt, gte, lt, lte — dot grammar, e.g. created_at.gt=value; a bare created_at=value means eq.
external_idFilter on external_id (string). Operators: eq, in — dot grammar, e.g. external_id.in=value; a bare external_id=value means eq. A bare comma-separated value is in-sugar: external_id=a,b means external_id.in=a,b.
idFilter on id (uuid). Operators: eq, in — dot grammar, e.g. id.in=value; a bare id=value means eq. A bare comma-separated value is in-sugar: id=a,b means id.in=a,b.
invoiceable_standaloneFilter on invoiceable_standalone (boolean). Operators: eq — dot grammar, e.g. invoiceable_standalone.eq=value; a bare invoiceable_standalone=value means eq.
is_systemFilter on is_system (boolean). Operators: eq — dot grammar, e.g. is_system.eq=value; a bare is_system=value means eq.
nameFilter on name (string). Operators: eq, in, contains — dot grammar, e.g. name.in=value; a bare name=value means eq. A bare comma-separated value is in-sugar: name=a,b means name.in=a,b.
pricing_modelFilter on pricing_model (enum). Operators: eq, in — dot grammar, e.g. pricing_model.in=value; a bare pricing_model=value means eq. A bare comma-separated value is in-sugar: pricing_model=a,b means pricing_model.in=a,b. Legal values: VOLUME, STAIRCASE, PACKAGE.
quantity_sourceFilter on quantity_source (enum). Operators: eq, in — dot grammar, e.g. quantity_source.in=value; a bare quantity_source=value means eq. A bare comma-separated value is in-sugar: quantity_source=a,b means quantity_source.in=a,b. Legal values: DECLARED, METERED.
tax_categoryFilter on tax_category (enum). Operators: eq, in — dot grammar, e.g. tax_category.in=value; a bare tax_category=value means eq. A bare comma-separated value is in-sugar: tax_category=a,b means tax_category.in=a,b. Legal values: DEFAULT, REDUCED, REDUCED_2, SUPER_REDUCED, PARKING, ZERO, EXEMPT.
updated_atFilter on updated_at (date-time). Operators: eq, gt, gte, lt, lte — dot grammar, e.g. updated_at.gt=value; a bare updated_at=value means eq.
includeExtra data to add to the response: tags, scheduled_versions, costs, prices, localized. Send a comma-separated list or repeat the parameter. An unknown name is refused with 400. tags is sent without being asked until 2026-11-30; after that date it is sent only when named here. scheduled_versions is sent without being asked until 2026-11-30; after that date it is sent only when named here. costs is sent without being asked until 2026-11-30; after that date it is sent only when named here.
limitPage size. Values above the server-side cap are clamped (standard default 50, cap 200; a few document-heavy lists use larger windows). Invalid values fall back to the default.
cursorOpaque continuation token from the previous response's pagination.cursor. Omit for the first page. Cursors are stateless and do not expire, but are only valid for the list and filters that produced them.
searchCase-insensitive substring match over name and external_id; relevance-ranked (exact > prefix > substring) unless an explicit sort is given
statusFilter by status (sugar for status.eq). Filterable fields (id, name, external_id, quantity_source, status, pricing_model, tax_category, invoiceable_standalone, created_at, updated_at) accept apifilter operator suffixes; custom_fields.
tagFilter by tag name (repeatable, containment semantics)
countsComma-separated countable fields (status, quantity_source) to include per-value counts for
has_scheduled_versionFilter by whether the entity has a version scheduled to publish (true or false). A scheduled version is a FROZEN version row a pending catalog.publish_version Change is bound to; the product itself stays draft, active or archived and its working draft stays free for immediate changes.
sortOrdering: created_at, status or quantity_source, each with an optional :asc/:desc suffix (default created_at:desc). quantity_source orders the whole archetype the Type badge reads - (quantity_source, quantity_adjustable, unit_label) - because ordering by quantity_source alone would interleave flat fees and per-seat rows. status and quantity_source order the stored codes, not their translated labels. Name is deliberately not sortable - product names are locale-translated after the query, so SQL would order the English base value. An unknown value is rejected. Cursors are bound to the ordering that issued them, so changing sort mid-walk needs a fresh first page. Sorting suppresses relevance ranking when search is also given.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List products › Responses
OK
Create a product
Creates a product in draft status. It answers the product as GET /products/{id} does.
Required scope: product.write (Write products).
query Parameters
includeExtra data to add to the response: tags, scheduled_versions, costs, prices, localized. Send a comma-separated list or repeat the parameter. An unknown name is refused with 400. tags is sent without being asked until 2026-11-30; after that date it is sent only when named here. scheduled_versions is sent without being asked until 2026-11-30; after that date it is sent only when named here. costs is sent without being asked until 2026-11-30; after that date it is sent only when named here.
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Create a product › Request Body
nameInternal operator-facing product name.
pricing_modelPricing model applied to this product: VOLUME, STAIRCASE or PACKAGE. Required.
quantity_sourceWhere the billed quantity comes from: DECLARED (set on the subscription item) or METERED (from the meter evaluator). Required.
unit_labelSingular noun one billed unit is called (e.g. seat, GB). Required.
Org-defined custom field values for this product.
descriptionInternal operator-facing description; not shown to buyers.
external_idCaller-supplied external identifier for reconciliation with an upstream system.
invoiceable_standaloneWhether the product may appear in the manual-invoice picker; absent defaults to true, explicit false marks it subscription-only.
is_systemMarks a product the platform owns rather than the tenant, which the sale pickers exclude. Defaults to false; it is a display marker and carries no privilege.
key_set_excluded_keysKeys subtracted from the linked key set's active entries (subtract-only).
key_set_idOpts the product into keyed pricing by linking it to an org-global key set.
Free-form operator key/value metadata.
public_descriptionBuyer-facing description shown in checkout and the customer portal.
quantity_adjustableWhen true the buyer picks the quantity (seats/licenses); defaults to false (flat charge) and is rejected on METERED products.
requires_product_idsProducts that must also be present for this product to be sold (dependency set).
supply_natureWhat this product is for place-of-supply purposes. Omit to leave it undeclared, which is treated as SERVICES.
Requests also accept the new spelling: each value in lower case (digital_services for DIGITAL_SERVICES). The listed spelling is deprecated in requests and accepted until 2026-11-30.
tax_categoryProduct tax category: which statutory tier the product is billed at. DEFAULT is the country's standard rate; ZERO and EXEMPT are decided by what the supply is and are not tiers. Defaults to DEFAULT.
Requests also accept the new spelling: each value in lower case (default for DEFAULT). The listed spelling is deprecated in requests and accepted until 2026-11-30.
Locale-keyed overrides for name, description and public_description.
Create a product › Responses
Created
Get a product
Returns one product. Until 2026-11-30 this read sends name, description and public_description in the language of the Accept-Language header when a translation exists; from that date it sends the stored text, and include=localized adds the translated text under localized. Name include=localized to get the stored text now.
Required scope: product.read (Read products).
path Parameters
idProduct UUID
query Parameters
includeExtra data to add to the response: tags, scheduled_versions, costs, prices, localized. Send a comma-separated list or repeat the parameter. An unknown name is refused with 400. tags is sent without being asked until 2026-11-30; after that date it is sent only when named here. scheduled_versions is sent without being asked until 2026-11-30; after that date it is sent only when named here. costs is sent without being asked until 2026-11-30; after that date it is sent only when named here.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Get a product › Responses
OK
Delete a product
Deletes a product: from then on every read of it answers 404, and its versions are archived, so a scheduled release no longer publishes. A product a setting still names is refused with 409. To retire a product and keep it readable, archive it with PATCH /products/{id} {"status": "archived"}.
Required scope: product.write (Write products).
path Parameters
idProduct UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Delete a product › Responses
No Content
Update a product
Merge-patch update: only supplied fields change, and none of them publish. Display fields (name, description, metadata, external_id, …) land live on the product immediately; every billing change is a child — a price, a cost, a meter binding — and is buffered into the product's working draft, which you can read with GET /v1/products/{id}/draft and discard with DELETE. Taking the draft live, now or on a date, is POST /v1/changes with operation catalog.publish_version and subject_kind=product: status="active" here is refused, and there is no effective_at or save_as_draft. status can still archive the product, which is terminal. expected_version is the optimistic-concurrency token: when supplied and stale, the update returns 409.
Required scope: product.write (Write products).
path Parameters
idProduct UUID
query Parameters
includeExtra data to add to the response: tags, scheduled_versions, costs, prices, localized. Send a comma-separated list or repeat the parameter. An unknown name is refused with 400. tags is sent without being asked until 2026-11-30; after that date it is sent only when named here. scheduled_versions is sent without being asked until 2026-11-30; after that date it is sent only when named here. costs is sent without being asked until 2026-11-30; after that date it is sent only when named here.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Update a product › Request Body
Org-defined custom field values for this product.
descriptionNew internal description; null leaves it unchanged.
expected_versionOptimistic-concurrency token: send the version you last read and a stale value answers 409 CONFLICT. Omitted: the update applies unconditionally (last write wins).
external_idNew external identifier; null clears it; omitted leaves it unchanged.
invoiceable_standaloneFlips the manual-invoice picker eligibility; null leaves it unchanged.
key_set_excluded_keysReplacement excluded-keys list; null leaves it unchanged, non-null replaces the list.
key_set_idLinks the product to a key set; null clears the link (and its exclusions); omitted leaves it unchanged. Changing or clearing it once the product has prices or subscription lines is rejected.
Free-form operator key/value metadata.
nameNew internal product name; null leaves it unchanged.
Replaces the product's whole invoice-presentation rung; omit to leave it unchanged. Null fields inside it inherit the organisation's setting.
pricing_modelNew pricing model (VOLUME, STAIRCASE, PACKAGE); null leaves it unchanged.
public_descriptionNew buyer-facing description; null leaves it unchanged.
quantity_adjustableFlips whether the buyer picks the quantity; null leaves it unchanged, and setting true on a METERED product is rejected.
requires_product_idsReplacement product-dependency set for this product.
statusLifecycle move: archived retires the product (terminal); null leaves it unchanged. Going live, now or at a date, is not a status write: it is POST /v1/changes with operation catalog.publish_version.
supply_natureNew supply nature (GOODS, SERVICES, DIGITAL_SERVICES); null clears the declaration back to undeclared; omitted leaves it unchanged.
Requests also accept the new spelling: each value in lower case (digital_services for DIGITAL_SERVICES). The listed spelling is deprecated in requests and accepted until 2026-11-30.
tax_categoryNew tax category (DEFAULT, REDUCED, REDUCED_2, SUPER_REDUCED, PARKING, ZERO, EXEMPT); null leaves it unchanged.
Locale-keyed overrides for name, description and public_description.
unit_labelNew singular unit label; null leaves it unchanged. quantity_source itself is immutable post-create.
Update a product › Responses
OK
Duplicate a product
Creates a draft product with the source's definition. Prices, costs and meter bindings are not copied. It answers the product as GET /products/{id} does.
Required scope: product.write (Write products).
path Parameters
idSource product UUID
query Parameters
includeExtra data to add to the response: tags, scheduled_versions, costs, prices, localized. Send a comma-separated list or repeat the parameter. An unknown name is refused with 400. tags is sent without being asked until 2026-11-30; after that date it is sent only when named here. scheduled_versions is sent without being asked until 2026-11-30; after that date it is sent only when named here. costs is sent without being asked until 2026-11-30; after that date it is sent only when named here.
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Duplicate a product › Request Body
external_idExternal identifier for the new draft copy.
nameName for the new draft copy; defaults to a derived copy name when omitted.
Duplicate a product › Responses
Created
List costs for a product
Lists the costs visible to one product. Values come from the published head by default; ?view=draft serves the product's open working-draft values, ?view=head restricts to published-only rows, and ?product_version_id pins values to one historical product version (mutually exclusive with view).
Required scope: product.read (Read products).
path Parameters
idProduct UUID
query Parameters
limitPage size. Values above the server-side cap are clamped (standard default 50, cap 200; a few document-heavy lists use larger windows). Invalid values fall back to the default.
cursorOpaque continuation token from the previous response's pagination.cursor. Omit for the first page. Cursors are stateless and do not expire, but are only valid for the list and filters that produced them.
viewValue view: draft (working-draft values) or head (published only)
product_version_idPin values to a product version
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List costs for a product › Responses
OK
Create a product-scoped cost
Creates a PRODUCT-scoped cost owned by the product in the URL — the URL determines scope; use POST /costs for shared ORG-scoped costs. The value reaches billing when the product is next published.
Required scope: product.write (Write products).
path Parameters
idProduct UUID
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Create a product-scoped cost › Request Body
keyFormula-reference key for the new cost; 1-64 chars matching [a-zA-Z0-9._-].
Static money value; set exactly one of amount or expression.
descriptionOperator-facing description of what this cost represents.
Formula referencing other costs by key; set exactly one of amount or expression.
Free-form operator key/value metadata.
Locale-keyed overrides for the cost's description.
Create a product-scoped cost › Responses
Created
List plans that use this product
Every plan that includes this product, with the pinned product version, the live head, the membership track mode, and a stale flag (pinned != head).
Required scope: product.read (Read products).
path Parameters
idProduct UUID
query Parameters
includeExtra data to add to the response: plan. Send a comma-separated list or repeat the parameter. An unknown name is refused with 400.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List plans that use this product › Responses
OK
Get the open working draft
The entity's ONE working draft: its row id, the version it publishes as, when it was opened, the version it is authored on top of, and a counted summary of what is staged. data is null when nothing is staged — a state, not an error.
Required scope: product.read (Read products).
path Parameters
idEntity UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Get the open working draft › Responses
OK
Open the working draft on a chosen base
Opens the working draft if none is open, and records which version it is authored on top of: the live head (omit base_version_id) or one of the entity's scheduled versions. Re-basing an open draft keeps its edits — the delta is the author's intent and is never rewritten — and re-materialises them over the new base. A publish of a draft based on a scheduled version cannot be scheduled to fire before that version does.
Required scope: product.write (Write products).
path Parameters
idEntity UUID
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Open the working draft on a chosen base › Request Body
base_version_idThe version the working draft is authored on top of — the live head, or one of the entity's scheduled versions. Omit for the head.
Open the working draft on a chosen base › Responses
OK
Discard the open working draft
Archives the working draft with reason 'discarded'. The live entity is untouched: nothing pointed at the draft and it was never on the billing timeline. 404 when nothing is staged, which is a different answer from a successful discard and is reported as one.
Required scope: product.write (Write products).
path Parameters
idEntity UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Discard the open working draft › Responses
No Content
Replace what the working draft contains
States what the draft should CONTAIN; the server derives the change against the draft's base and validates the result. A child the body omits is removed from the draft. Opens the working draft on the head when none is open.
Required scope: product.write (Write products).
path Parameters
idEntity UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Replace what the working draft contains › Request Body
Every meter binding the version carries.
Every named cost the version carries.
The aggregation config of every meter the version's bindings reach, bound meters and composed sources alike.
Every price the version carries. A price the body omits is REMOVED from the draft.
Rate-table slug → the catalog_version_headers id this draft is pinned to. Omit to let the publish capture the active version.
Replace what the working draft contains › Responses
OK
List a product's version history
Deprecated: use GET /products/{id}/versions?status=published,scheduled&sort=version:desc&include=diff instead. Removed on 2026-11-30.
The product's published and scheduled versions, newest first (default 20, max 100 per page), each with its publisher, release label, when it took or takes effect, when it was recorded, and its readable diff against the version it replaced. The immediate acts (create, copy, archive, label, discard) are not versions and are not listed here; they are audit rows: GET /audit-events?product_id=. Replaced by GET /products/{id}/versions?status=published,scheduled&sort=version:desc&include=diff, which answers the same versions with the diff on each.
Required scope: product.read (Read products).
path Parameters
idEntity UUID
query Parameters
limitPage size (default 20, max 100)
cursorOpaque continuation token from the previous response's pagination.cursor. Omit for the first page. Cursors are stateless and do not expire, but are only valid for the list and filters that produced them.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List a product's version history › Responses
OK
Preview the impact of publishing a product
Read-only dry-run: the active plans that auto-follow this product ('latest' membership) and would be republished, plus the number of subscriptions that would migrate.
Required scope: product.read (Read products).
path Parameters
idProduct UUID
query Parameters
includeExtra data to add to the response: plan. Send a comma-separated list or repeat the parameter. An unknown name is refused with 400.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Preview the impact of publishing a product › Responses
OK
Add tags to a product
Adds the given tags to the product and keeps the tags it already has. Returns all tags on the product after the change. The product itself lists them in tag_ids, and in tags with include=tags.
Required scope: tag.write (Manage tags).
path Parameters
idResource UUID
query Parameters
includeExtra data to add to the response: usage_count. Send a comma-separated list or repeat the parameter. An unknown name is refused with 400.
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Add tags to a product › Request Body
tag_idsThe tags to add. Each must be a tag for this kind of record.
Add tags to a product › Responses
OK
Remove tags from a product
Removes the tags given in the tag_ids query parameter from the product in one call. Its other tags stay; an id it does not carry is ignored.
Required scope: tag.write (Manage tags).
path Parameters
idResource UUID
query Parameters
tag_idsTag UUIDs to remove
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Remove tags from a product › Responses
No Content
List a product's version timeline
The entity's versions in the order they fired and will fire: history, head, scheduled releases, working draft. Published and scheduled rows are returned by default; pass include=all for the draft and the archived rows. Same row shape as /versions.
Required scope: product.read (Read products).
path Parameters
idEntity UUID
query Parameters
includeSet to 'all' to include the working draft and archived rows.
limitPage size (default 100, max 200)
cursorOpaque continuation token from the previous response's pagination.cursor. Omit for the first page. Cursors are stateless and do not expire, but are only valid for the list and filters that produced them.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List a product's version timeline › Responses
OK
List product versions
Returns version rows for the entity, keyset-paginated (default 100, max 200 per page). The status filter accepts draft, scheduled, published, archived; the default excludes draft. include=usage_count adds how many records use each version; sort=version:desc lists the newest first. include=diff adds each version's readable diff against the version it replaced, and when its publish was recorded.
Required scope: product.read (Read products).
path Parameters
idEntity UUID
query Parameters
includeExtra data to add to the response: usage_count, diff. Send a comma-separated list or repeat the parameter. An unknown name is refused with 400.
statusLifecycle status filter (repeatable / comma-separated): draft, scheduled, published, archived. Default excludes draft.
archived_reasonNarrow archived rows by reason (repeatable / comma-separated): canceled, discarded, errored, superseded
sortOrder: version:asc (the default) lists the versions in the order they fired and will fire; version:desc lists the highest number first, versions without a number (the working draft, archived rows) last.
limitPage size (default 100, max 200)
cursorOpaque continuation token from the previous response's pagination.cursor. Omit for the first page. Cursors are stateless and do not expire, but are only valid for the list and filters that produced them.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List product versions › Responses
OK
Get a product version snapshot
The version's own fields plus the common version header. {version} is the number, or the word 'draft' for the open working draft.
Required scope: product.read (Read products).
path Parameters
idEntity UUID
versionThe version number, or the word 'draft' for the open working draft.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Get a product version snapshot › Responses
OK
Name or annotate a product version
Sets the version's release_name and/or release_note. These are display metadata, not content: editing them never creates a new version and is allowed on published versions. An absent field is left unchanged; null clears it; an empty string is refused. It answers the version row without the data envelope; from 2026-11-30 it answers {"data": …} like every other operation.
Required scope: product.write (Write products).
path Parameters
idEntity UUID
versionThe version number, or the word 'draft'.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Name or annotate a product version › Request Body
release_nameNew name; null clears it. Absent leaves it unchanged.
release_noteNew note; null clears it. Absent leaves it unchanged.
Name or annotate a product version › Responses
OK
created_atWhen the row was opened — for a draft when editing started, for a release when it was armed.
effective_fromeffective_toidThe catalog_versions row id. A client selecting a version writes this, never the number.
originWho made this version: manual (an operator published it), scheduled_release (a scheduled publish fired), cascade (republished automatically because something it depends on published).
published_atstatusThe row's own state: draft, scheduled, published or archived.
versionThe version's display number, or 0 for a row that is not on the chain (a working draft, an archived row).
archived_reasonWhy an archived row left the timeline: canceled, discarded or superseded.
For origin=cascade: what caused this version.
What the version does differently from the version it replaced; from is absent for the entity's first version. Sent only with include=diff (plans and products).
Who published the version — the same actor the audit trail names: an operator, an API key, or the system for a cascade. Absent on a version not yet published and on one published before publishers were recorded.
rank1-based position in the entity's queue of scheduled versions — the order they will fire. Present only on scheduled rows.
recorded_atWhen the publish was captured, on the wall clock (its audit row). published_at is the operation's instant — a sandbox's test clock — and effective_from when it governs. Sent with include=diff on published versions that have an audit row.
release_nameOperator-given name of this version, shown beside its number.
release_noteFree-text note about this version.
scheduled_change_idThe pending catalog.publish_version Change that will publish this version — reschedule or cancel it there.
scheduled_forWhen this scheduled version is due to publish. Present only on a scheduled row.
usage_countPresent only with ?include=usage_count.
Compare two product versions
What the version does differently from the one it is compared against, as typed changes: prices per identity (key, currency, country, dimensions) added, removed or changed with their ladders, products and their terms, overrides, billing terms, costs, meters, bindings and the versions it pins, each named. Only values that differ are reported. ?against= takes 'head' (the default), another version number, or 'draft'.
Required scope: product.read (Read products).
path Parameters
idEntity UUID
versionThe version number, or the word 'draft'.
query Parameters
againstWhat to compare against: 'head' (the default) for the live version, or another version number, or 'draft'.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Compare two product versions › Responses
OK
Preview what else publishing this version would publish
The entities that will publish BECAUSE this one does — a plan that tracks this product's latest, say — each with the cells its next head would carry that its current one does not. Derived on every read and never frozen: something that starts following tomorrow appears tomorrow.
Required scope: product.read (Read products).
path Parameters
idEntity UUID
versionThe version number, or the word 'draft' for the open working draft.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Preview what else publishing this version would publish › Responses
OK
Forecast the impact of superseding this product version
Who is using the version in force today, and what a publish over it would move. pinnable reports whether holding a version is possible for this entity at all — it is false for tax rules, which are dated rather than pinned, so a caller must not render a pinned count for them. republishes_consumers is the cross-entity fan-out (publishing a price formula republishes every ACTIVE product whose head tiers reference it), and live_readers counts consumers billing off the live head, who move as soon as the publish lands. Read-only, and uncached: this is read at the moment money changes.
Required scope: product.read (Read products).
path Parameters
idEntity UUID
versionVersion number (positive integer)
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Forecast the impact of superseding this product version › Responses
OK
List product version referrers
Offset-paginated drill-down of the referrers of one type for one version of the entity.
Required scope: product.read (Read products).
path Parameters
idEntity UUID
versionVersion number (positive integer)
query Parameters
typeReferrer type to list (required)
limitPage size (default 50, max 200)
cursorOpaque continuation token from the previous response's pagination.cursor. Omit for the first page. Cursors are stateless and do not expire, but are only valid for the list and filters that produced them.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List product version referrers › Responses
OK
Get product version usage summary
Referrer counts for one version of the entity, grouped by referrer type. Powers the version-switcher hover card.
Required scope: product.read (Read products).
path Parameters
idEntity UUID
versionVersion number (positive integer)
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Get product version usage summary › Responses
OK