Pricing models - Volume, Staircase & Package
The pricing_model lives on the product, not on individual prices - every price under a product walks its tiers using the model the product declares. Three models are supported: VOLUME, STAIRCASE, and PACKAGE. Each tier on a price can additionally carry a rate expression that overrides the static rate at billing time.
For per-customer overrides, geo-pricing, keyed prices, and multi-currency, see Advanced pricing.
How tiers are defined
A price has an ordered list of tiers:
| Field | Notes |
|---|---|
tier_order | Position in the walk; tiers must be in ascending up_to order. |
up_to | Upper bound of the tier (inclusive). null on the last tier means open-ended. |
unit_amount | Static per-unit rate. Reinterpreted by PACKAGE - see below. |
flat_amount | Static fee. Reinterpreted by VOLUME and PACKAGE - see below. |
rate_expression | Optional formula string that overrides unit_amount at billing time. |
unit_amount and flat_amount must be non-negative. The currency on the parent Price determines the decimal scale every amount is rounded to.
VOLUME
Find the tier where prev_boundary < quantity ≤ up_to. The matched tier's rate is then applied to all units, plus the tier's flat fee:
Code
| Tier | Range | unit_amount | flat_amount |
|---|---|---|---|
| 1 | 0 – 1,000 | 0.10 | 0 |
| 2 | 1,001 – 10,000 | 0.08 | 0 |
| 3 | 10,001+ | 0.05 | 0 |
5,000 units → tier 2 → 5,000 × 0.08 + 0 = 400.00.
When to use: higher consumption unlocks a lower rate retroactively across the whole bill (per-seat pricing, GB storage, API call metering).
STAIRCASE (graduated)
Walk the tiers in order. Each tier consumes min(remaining, tier_size) units at its unit_amount. The total is the sum of every tier's contribution.
Code
| Tier | Range | unit_amount |
|---|---|---|
| 1 | 0 – 1,000 | 0.10 |
| 2 | 1,001 – 10,000 | 0.08 |
| 3 | 10,001+ | 0.05 |
5,000 units → 1,000 × 0.10 + 4,000 × 0.08 = 420.00.
When to use: progressive billing where the early units are more expensive (utility-style, bandwidth, graduated commission).
PACKAGE
PACKAGE repurposes the tier fields:
flat_amount→ package size (units per package).unit_amount→ package price.
Find the matching tier (same boundary check as VOLUME), then round up to whole packages:
Code
| Tier | Range | Package size (flat_amount) | Package price (unit_amount) |
|---|---|---|---|
| 1 | 0 – 100 | 10 | 5.00 |
| 2 | 101 – 1,000 | 50 | 20.00 |
| 3 | 1,001+ | 100 | 35.00 |
75 units → tier 1 → ceil(75 / 10) × 5.00 = 8 × 5.00 = 40.00.
When to use: SMS bundles, email credit packs, storage block pricing - any commitment unit you can only sell whole.
Allowed pricing models depend on product type:
FIXED_CHARGEonly allows VOLUME;SEATallows VOLUME or STAIRCASE;USAGEallows all three. See Products.
Per-tier rate expressions
Any tier can set rate_expression to a formula string. When evaluated at billing time it overrides the static rate (unit_amount); the flat_amount is still applied as written. If the expression fails to parse or evaluate, the engine logs a warning and falls back to the static unit_amount - billing never errors out on a bad expression, it just degrades to the literal.
Code
Expressions are evaluated with safety limits - 200 AST nodes max, 50 nesting levels max - so a malformed catalog can't burn arbitrary CPU at billing time.
Variables available
| Variable | Source |
|---|---|
tier_quantity | The number of units that fell into this tier (built-in). |
| Custom numeric variables | The resolver injects per-line context: aggregated usage, the line's cost from the linked ProductCost, custom field values from the subscription / customer. |
| Custom string variables | Used by if(…) discriminators and the lookup family. |
Functions
| Group | Functions |
|---|---|
| Conditionals | if(condition, then, else) |
| Numeric | min, max, abs, round, ceil, floor |
| Lookups | lookup, bracket (resolved against rate-table data the resolver pre-loads) |
| Operators | + - * /, comparison < <= > >= == !=, parentheses |
The expression language is intentionally narrow - it's not a general scripting environment. There are no loops, no string concatenation, no I/O. If the formula you need isn't expressible, it usually means the calculation belongs upstream (rate-table lookup, custom field on the subscription, derived value posted as a usage event).
Reusable price formulas
PriceFormula is a stored, named expression with declared variables. Useful when the same rule (markup curves, commission ladders) is used across many tiers and you want one place to edit it.
Code
| Field | Notes |
|---|---|
expression | Same language and limits as per-tier rate_expression. |
variables[].type | number, string, or boolean. |
variables[].default | Numeric default applied when the caller omits the variable. |
is_system | Read-only flag - true for the auto-seeded templates Kontorion ships per organization (markup, volume discount, commission ladders). |
version | Bumps on every update; lets the dry-run endpoint pin a specific revision. |
Stored formulas live at /v1/price-formulas. The full set of system templates seeded into a new org is documented at the PriceFormulas API tag.
Dry-run with POST /v1/prices/compute
Evaluate either an inline expression or a stored formula with sample inputs - useful for previewing a tier change before publishing.
Code
Pass exactly one of expression or formula_id. With debug: true the response carries a debug_trace showing the evaluation step-by-step.
Endpoints
- Prices - tier definitions live on the price;
prices/computeis the dry-run handler - PriceFormulas - CRUD on stored formulas
- Products - the
pricing_modelfield lives here
Related
- Advanced pricing - keyed prices, customer overrides, multi-currency
- Products - the
pricing_modelchoice + per-type compatibility table - Currencies & dates - how
valuestrings are scaled per currency - Plans - bundle priced products into a customer-purchasable offer