Products
A product is the catalog unit you sell. Every price, plan, charge, and usage event ultimately points at one. Products live independently of plans - the same product can be sold across multiple plans at different prices.
Quantity source
The quantity_source field decides where the quantity on each invoice line comes from. It replaces the old product-type taxonomy:
| Value | What it represents | Quantity source |
|---|---|---|
DECLARED (default) | The quantity lives on the subscription item - flat charges, seats, licenses, devices | The subscription item's quantity |
METERED | Metered consumption (API calls, GB, minutes) | Aggregated usage events for the period, via a BILLABLE_QUANTITY meter binding |
quantity_source is set on create (defaulting to DECLARED) and is immutable afterwards - there is no update path for it.
Two related fields shape how a DECLARED product behaves:
| Field | Behaviour |
|---|---|
quantity_adjustable | When true, the buyer picks the quantity (seats, licenses, devices); checkout/portal default their quantity steppers from it. Defaults to false (a flat charge offers no quantity). Rejected on METERED products - the meter owns the quantity. Live-editable; no version bump. |
unit_label | The singular noun one unit is called ("seat", "GB", "domain"). Defaults to "unit"; max 64 characters. Live-editable presentation. |
Pricing model
The pricing_model lives on the product, not on individual prices. It controls how tier walks evaluate against the quantity above. Three models - VOLUME, STAIRCASE, PACKAGE - covered in detail in Pricing models.
Which models are allowed depends only on quantity_source:
| Quantity source | Allowed pricing models |
|---|---|
DECLARED | VOLUME, STAIRCASE |
METERED | VOLUME, STAIRCASE, PACKAGE |
VOLUME and STAIRCASE are universal. PACKAGE consumes prepaid metered units and therefore requires a METERED product - pairing PACKAGE with a DECLARED product returns 400 VALIDATION at create time.
Keyed products
Linking a product to a key set (key_set_id) turns it keyed: the same logical product can carry many sibling prices, each disambiguated by a price_key. Useful when you'd otherwise create hundreds of near-duplicate products (TLDs on a registrar, instance types on a cloud). A non-null key_set_id is what makes a product keyed.
The key set is an org-global catalog of allowed keys. A keyed product's price_key domain is the set's active entries, minus any keys listed in key_set_excluded_keys (exclusions are subtract-only - a product can never add keys the set doesn't define).
Code
| Field | Behaviour |
|---|---|
key_set_id | The org-global key set this product draws its price_key values from. Non-null = keyed. Optional; immutable once the product has any non-archived price or subscription item (to re-key, archive the product and create a new one). |
key_set_excluded_keys | An array of keys to subtract from the linked set's domain. Each must already exist in the set (subtract-only); supplying a key the set doesn't define returns 400 VALIDATION. Requires a linked key_set_id - exclusions on an unkeyed product are rejected. |
The keyed-vs-not split also affects events: keyed products must carry a price_key on every event, single-price products must not. See Advanced pricing → Keyed prices for the price-side mechanics.
Costs
Each product can carry zero or more named cost variants under costs[]. Each one is a ProductCost row with its own id, key (e.g. "primary", "shipping"), and amount (Money envelope), versioned independently of the product itself.
Costs feed two things:
- Cost-plus tier expressions in pricing-model formulas -
cost * 1.15 + 200resolves to the active cost variant at evaluation time. - Margin reporting in the analytics dashboard.
Costs live only in the
costs[]collection. There is no singlecost_amount/cost_currencypair on the product itself - each cost is a named, independently versionedProductCostrow.
Costs have their own sub-resource for CRUD. Listing and creation are nested under the product (GET/POST /v1/products/{productId}/costs); fetch, update, archive, and copy address the cost directly by id (GET/PUT/DELETE /v1/costs/{costId}, POST /v1/costs/{costId}/copy).
Dependencies
requires_product_ids is an array of product IDs that must already be on the subscription before this one can be added as an add-on. The check runs in the subscription-add-on service - the API rejects an add-on attempt with 400 VALIDATION listing the missing dependencies. (It's not enforced at plan publish; the constraint is "on the subscription at the moment the add-on is attached", not "in the plan definition".)
Tax category
tax_category decides which tax rule applies at invoice time. Combined with the customer's billing country it picks the rate and any required reverse-charge / exemption marker on the line.
| Value | Use for |
|---|---|
DEFAULT | Standard VAT rate for the jurisdiction |
REDUCED | Reduced rate (e.g. 7% on books in DE) |
ZERO | Zero-rated supplies |
EXEMPT | VAT-exempt |
Lifecycle
Code
| Status | Meaning |
|---|---|
draft | Created but not yet visible to plans / prices that require an active product. Edit freely. May move to scheduled, active, or archived. |
scheduled | A future-effective version is pending (see Scheduling changes). May move back to draft, forward to active, or be archived. |
active | The default working state. Plans and prices can attach. Its only onward transition is archived. |
archived | Hard stop - no new prices, no new plans. Existing subscriptions referencing the product continue to bill. Archive is guarded - the API rejects an archive attempt while there are still references that would orphan. Terminal. |
Status transitions are made by PUT /v1/products/{id} with the new status; the allowed moves are enforced by the product-status FSM. Mutating fields like pricing_model or tax_category increments the product's version, which plans pin to.
Scheduling changes
Mutations can be scheduled instead of applied immediately by sending PUT /v1/products/{id} with a future effective_at timestamp - the change publishes a new product version that becomes active at that moment rather than immediately. Products have no save_as_draft publish mode (a draft product is already the pre-activation state, so the API rejects save_as_draft with 409); use effective_at to schedule, or edit the draft and activate. The pending release surfaces as a scheduled change, and you manage it on the version timeline: POST /v1/products/{id}/versions/{vid}/cancel, /reconfirm, or /unschedule. The product's full version timeline (including any pending scheduled releases) lives at GET /v1/products/{id}/timeline.
Endpoints
- All Products endpoints
- ProductCosts - the named-cost-variants sub-resource
Related
- Pricing models - the three tier-walk algorithms and per-tier formula expressions
- Advanced pricing - keyed prices, customer overrides, multi-currency
- Plans - bundle products into customer-purchasable offers
- Usage metering - feed quantity into
METEREDproducts - Taxes - how
tax_categoryresolves to a rate