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 paginated list of products. Supports the unified field.op=value filter grammar (see /docs/pagination) plus custom-field filters via the custom_fields.
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.
invoiceable_standaloneFilter on invoiceable_standalone (boolean). Operators: eq — dot grammar, e.g. invoiceable_standalone.eq=value; a bare invoiceable_standalone=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.
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 (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 entity itself stays draft, published 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 new product in draft status.
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.
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.
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.
Locale-keyed overrides for name, description and public_description.
Create a product › Responses
Created
Get a product
Returns a single product enriched with its active-price count; translatable fields are localized to the caller's locale.
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.
Get a product › Responses
OK
Delete a product
Soft-deletes a product by UUID.
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.
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.
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 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 or clears the product's key set; changing it once the product has keyed prices or items is rejected.
Free-form operator key/value metadata.
nameNew internal product name; null leaves it unchanged.
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.
statusNew catalog lifecycle status (draft, scheduled, active, archived); null leaves it unchanged.
supply_natureNew supply nature (GOODS, SERVICES, DIGITAL_SERVICES); null leaves it unchanged, empty string clears the declaration.
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 new product whose attributes are copied from the source. Prices, costs and meter bindings on the source are NOT copied; only the product definition.
path Parameters
idSource product 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.
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 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).
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.
List plans that use this product › Responses
OK
Get the product's open working draft
The open working-draft metadata (draft version id, number, created_at, changed-child counts vs the head). 404 when the product has no open draft.
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.
Get the product's open working draft › Responses
OK
Open the product's 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 product's scheduled versions. Re-basing an open draft keeps its edits 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.
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.
Open the product's 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. Rebasing an existing draft keeps its edits and re-materialises them over the new base.
Open the product's working draft on a chosen base › Responses
OK
Discard the product's open working draft
Deletes the open working-draft version row; its revision rows cascade. 404 when no draft is open.
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.
Discard the product's open working draft › Responses
No Content
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.
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.
Preview the impact of publishing a product › Responses
OK
List product versions (with lifecycle filter)
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 (ADR-0007). Supports ?include=usage_count and ?archived_reason=.
path Parameters
idEntity UUID
query Parameters
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
includeComma-separated includes. Currently supports usage_count.
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.
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 (with lifecycle filter) › Responses
OK
Get a product version snapshot
Returns the full snapshot row from product_versions, including the Phase 2.D child version pin maps.
path Parameters
idEntity UUID
versionVersion number
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; an empty string clears it.
path Parameters
idEntity UUID
versionThe version number
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; empty string clears it. Absent leaves it unchanged.
release_noteNew note; empty string clears it. Absent leaves it unchanged.
Name or annotate a product version › Responses
OK
created_ateffective_fromeffective_toidoriginWho made this version: manual (an operator published it), scheduled_release (a scheduled publish fired), cascade (republished automatically because something it depends on published).
published_atstatusversionarchived_reasonFor origin=cascade: the version whose publish caused this one, resolved to its entity and number.
created_byrank1-based position in the entity's queue of scheduled versions — the order they will fire. Present only on scheduled rows.
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 the row a pending catalog.publish_version Change is bound to.
usage_countForecast 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.
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.
path Parameters
idEntity UUID
versionVersion number (positive integer)
query Parameters
typeReferrer type to list (required)
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.
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.
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