Advanced pricing - overrides, keyed prices, multi-currency
This page covers the patterns layered on top of the core pricing models: how the resolver picks which price to use, how to bill the same product across many dimensions via keyed prices, and how multi-currency conversion runs.
Resolution order
When the engine needs the price for a (product, plan, customer, time) tuple, it scores every candidate and picks the most specific. Highest precedence first:
- Phase-item override - when the subscription is on a phased plan (used by promotions on phased subscriptions) and the active phase carries a per-item price for this product, that wins outright.
- Customer-scoped price - a
pricesrow withcustomer_idset to this customer. - Plan-scoped price - a row with
plan_idset to this plan. - Country-coded price - a row with
country_codematching the customer's billing country. - Custom-dimension specificity - among rows whose
custom_dimensionsare a subset of the caller's dimensions, the one with the most dimensions matched wins. - Most recent version - among ties, the candidate with the latest
effective_fromon its version timeline (or the version active atat_timefor historical replay).
This single ORDER BY runs in one indexed query - there's no separate "look up override, fall back to base" round-trip. A Price row's kind field (BASE vs CUSTOMER_OVERRIDE) is metadata for tooling; the resolver doesn't gate on it.
The full resolution including FX conversion is auditable on the invoice line. The line records the
price_id,price_version_id,unit_amount, and any conversion adjustment, so a regenerated invoice always reproduces the same total.
Customer price overrides
Override the catalog price for a specific customer - useful for negotiated enterprise rates, loyalty discounts, or per-tenant pricing.
There is no separate /customer-price-overrides endpoint. You create an override by POST /v1/prices with customer_id set; the resolver picks it up automatically because of the cascade above. (Pre-prod migration 100 unified the two - the historical customer_price_overrides table is gone.)
Code
Overrides:
- can scope to a specific
plan_idtoo - combine fields to express "Acme on the Pro plan" - can be time-bounded via
effective_from/effective_to- useful for promotional windows or contract terms that auto-expire - can carry their own tier structure (different from the catalog price's tiers)
- versioned independently - updating the override tiers creates a new
price_versionso historical invoices replay against the version that was active at issue time
Geo and dimension scoping
Two ways to vary a price by context other than customer:
| Field | Use it for | Example |
|---|---|---|
country_code | Single ISO 3166-1 alpha-2 code matched against the customer's billing country | A reduced rate for DE customers |
custom_dimensions | Arbitrary key/value scope (region, environment, tier) - JSONB containment, caller's dimensions ⊇ price's dimensions | A {"region": "EU"} price matches calls with region=EU in their dimension_vars |
Both are subordinate to customer / plan scoping in the cascade above, so a customer override always beats a country price.
Keyed prices
A product becomes keyed the moment you set price_key_label on it. From that point on:
- Every
Priceunder the product must carry a non-nullprice_key. - Every usage event (and reserve) must reference the corresponding
price_key. - Each keyed price can carry an optional
display_name- the customer-facing label used on invoices and PDFs in place of the bare key. - The
(product_id, price_key)pair is the natural key - two prices with the same key collide.
Code
Unmatched-key policy
When an event arrives with a price_key that doesn't match any price for the product, the product's unmatched_price_key_policy decides what happens. Enforcement is in the shared enforceKeyedPricesPolicy gate that runs on both usage-event ingestion and reserve handlers - both paths behave identically.
| Policy | Behaviour |
|---|---|
reject (default) | The request returns 400 VALIDATION and a usage_event.dropped event with reason: "unmatched_price_key" is emitted. Safest - surfaces upstream catalog drift immediately. |
use_default | The event is routed to the product's default_price_key. The stored event records price_key_remapped = true for audit. Required when this policy is selected: default_price_key must be a real price_key on the product. |
drop | The event is silently absorbed (HTTP 202) and a usage_event.dropped event is emitted with the same reason. Use when ingestion can't be gated on catalog completeness - e.g. you ship new SKUs faster than the billing catalog is updated. |
A missing price_key (the field absent entirely on a keyed-product event) follows the same policy under the reason missing_price_key.
Invoice-line behaviour
How keyed lines roll up onto the invoice is controlled per plan:
expanded/per_key- one invoice line per(product, price_key). The line'sprice_keyandprice_key_display_nameare populated; per-key audit detail is keyed.per_product- one collapsed line per product. The line'sprice_keyis null; per-key detail lives in theatomsarray (returned with?depth=full).
See Invoices for line groupings and the audit-atom shape.
Multi-currency
Two parts: catalog and conversion.
Catalog
Create one Price per (product, plan, currency) combination - same product, side-by-side prices in different currencies:
Code
If a Price row matches the customer's preferred currency exactly, it's used as-is - no conversion runs.
Conversion
When no per-currency price matches, the resolver picks the best price by the cascade above and the post-processor converts it. Target currency selection runs in this order:
req.Currency- caller explicitly demanded a currency (e.g. an invoice template fixed to one).- The customer's
preferred_currency. - Falls back to the price's native currency (no conversion).
The FX rate's effective time is also resolved by precedence:
req.ConversionAt- caller explicitly anchored FX to a moment (e.g.period_startFX policy).req.AtTime- historical price lookup; FX is matched to the same instant for replay determinism.time.Now()- live path; defaultinvoice_issuebehaviour.
The org's conversion-fee percentage (configured under Organization settings) is applied during conversion. The full FX policy model - three candidate rates pinned per invoice and the four policies that select between them - is in Exchange Rates.
Endpoints
- Prices - single endpoint for base, customer-overrides, plan-scoped, geo, and keyed prices
- Products -
price_key_label,default_price_key,unmatched_price_key_policylive here
Related
- Pricing models - VOLUME / STAIRCASE / PACKAGE + per-tier rate expressions
- Products - keyed-product setup, type ↔ model compatibility
- Exchange rates - FX policies and the three pinned candidates per invoice
- Currencies & dates -
Moneyenvelope, per-currency scale - Subscriptions - phased subscriptions where phase-item overrides apply