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.
Product types
The type field decides how the product gets billed:
| Type | What it represents | Quantity source |
|---|---|---|
FIXED_CHARGE | A flat charge that's the same every cycle (a base fee, an add-on price tag) | Always 1 |
SEAT | Per-seat licensing | The subscription's quantity field |
USAGE | Metered consumption (API calls, GB, minutes) | Aggregated usage events for the period |
The type is set on create and is not mutable after publish.
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.
Not every model is allowed on every product type:
| Product type | Allowed pricing models |
|---|---|
FIXED_CHARGE | VOLUME |
SEAT | VOLUME, STAIRCASE |
USAGE | VOLUME, STAIRCASE, PACKAGE |
Trying to use an unsupported pair returns 400 VALIDATION at create time.
Keyed products
Setting price_key_label on a product 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).
Code
| Field | Behaviour |
|---|---|
price_key_label | UI label for the dimension ("tld", "region", "sku"). Setting any non-null value turns the product keyed; clearing it back to null is rejected once prices exist. |
unmatched_price_key_policy | What to do when an event arrives with a price_key that doesn't match any price. reject (default) returns 400 + usage_event.dropped webhook. use_default routes to default_price_key and flags the event with price_key_remapped: true. drop silently discards + emits the same dropped webhook. |
default_price_key | Required when policy = use_default; must be a real price_key on this product. Forbidden on non-keyed products. |
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.
The legacy single
cost_amount/cost_currencyfields on the product itself are deprecated - use thecosts[]collection. They'll be removed once the migration off them is complete.
Costs have their own sub-resource for CRUD: POST/PUT/DELETE /v1/products/{productId}/costs[/...].
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. |
active | The default working state. Plans and prices can attach. |
deprecated | Soft-warning state - existing subscribers keep working, but new plans / prices targeting this product are discouraged in tooling. |
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. |
Status transitions are made by PUT /v1/products/{id} with the new status. 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 via POST /v1/products/{id}/schedule - returns a scheduled change. 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
USAGE-typed products - Taxes - how
tax_categoryresolves to a rate