Plan extensions
POST /pricing/packages and POST /plans accept the same optional plan-level
fields. Without pricing_lua, built-in logic handles invoicing and JSON classify at ingest.
For copy-paste scenarios by use case, start with Recipes. Luau escape hatch: Pricing → Luau.
JSON vs Luau
| Need | Use JSON | Use Luau |
|---|---|---|
| Shared included across metrics | metric_groups, usage_pool | pricing(ctx) |
| Route usage by time, calendar, quantity, period usage, source | classify.routes | classify(ctx) |
| Hard cap at ingest | limits, classify.reject | classify(ctx) |
| Graduated / stairstep pricing | tiers, pricing_model: stairstep | pricing(ctx) |
| Minimum / commitment / rollover | plan-level JSON fields | pricing(ctx) |
| Custom invoice line items | — | pricing(ctx) |
| Cross-metric formulas | — | Luau |
JSON and Luau can coexist: JSON classify runs first at ingest, then Luau classify. Invoicing uses Luau pricing when present; otherwise JSON billing applies.
Usage ingest pipeline
When a subscribed user records usage (POST /usage, StatsD, usage.recorded):
- JSON classify —
classify.routesmay reroute the metric (first match wins) - JSON reject —
classify.rejectmay return400 - JSON limits —
limits[]withaction: rejectmay return400 - Luau classify — runs last if the plan defines a
classifyfunction
The stored usage event contains the final metric after all steps.
Per-metric fields
On each entry in metrics[] (packages create catalog metrics; plans bind by metric_id):
| Field | Description |
|---|---|
included_units | Free units per billing period before overage |
overage_unit_price | Flat price per unit above included (mutually exclusive with tiers) |
overage_description | Custom invoice line label for flat overage |
tiers[] | Graduated ranges: from, to (omit or null for open-ended), price_per_unit |
pricing_model | stairstep or empty (default graduated/flat) |
block_size / block_price | Stairstep only — e.g. 2500 units with block 1000 → 3 blocks billed |
aggregation | Invoice window only: sum (default), max, or last |
Classify and ingest limits always use sum semantics, regardless of aggregation.
Metric groups
Share one included quota (and optional group-level tiers) across several metrics:
{
"code": "tokens",
"metric_codes": ["tokens_peak", "tokens_offpeak"],
"included_units": 1000000,
"overage_unit_price": 2,
"tiers": [{"from": 1000001, "to": null, "price_per_unit": 1}]
}code— group id (referenced inclassify.rejectandlimits)- Each
metric_codesentry must appear inmetrics[] - A metric cannot be in both a group and
usage_pool - Without group-level tiers/overage: included is consumed member-by-member; each member's excess uses that metric's own rates
JSON classify
Route or reject usage at ingest without Luau. Each route matches on the incoming metric plus optional filters — all set fields combine with AND. First matching route wins.
{
"routes": [
{"when": {"metric": "tokens"}, "if": {"hour_local": [9, 18], "weekdays_local": [1, 2, 3, 4, 5]}, "to": "tokens_peak"},
{"when": {"metric": "api_requests", "quantity_gte": 500}, "to": "api_requests_bulk"},
{"when": {"metric": "tokens", "period_usage_gte": 1000000}, "to": "tokens_bulk"},
{"when": {"metric": "tokens"}, "to": "tokens_offpeak"}
],
"reject": [
{"when": {"group": "tokens", "period_usage_plus_qty_gt": 1000000}, "reason": "monthly cap exceeded"}
]
}| Field | Description |
|---|---|
routes[].when / routes[].if | All predicate fields below combine with AND. Omitted fields are ignored. |
metric | Match incoming metric code |
hour_utc, hour_local | Half-open hour range [start, end) (0–23); local uses the user's billing timezone |
weekday, weekday_local | Single weekday 0=Sunday … 6=Saturday (UTC or local) |
weekdays, weekdays_local | Match any listed weekday |
month, month_local | Calendar month 1–12 |
months, months_local | Match any listed month |
day_of_month, day_of_month_local | Day of month 1–31 |
date_on_or_after, date_before | RFC3339 timestamp bounds (UTC) |
date_local_on_or_after, date_local_before | YYYY-MM-DD bounds in user timezone |
quantity_gte, quantity_lte, quantity_gt, quantity_lt | Incoming event quantity |
period_usage_gte, period_usage_lte, period_usage_gt, period_usage_lt | Sum in current period before this event (metric or group scope) |
source | http, statsd, or system |
routes[].to | Target metric code on the plan |
reject[].when | Same predicate fields as routes, plus group and period_usage_plus_qty_gt |
reject[].reason | Error message returned to the client |
Equivalent Luau examples: Luau classify ctx.
Ingest limits
{
"scope": "metric_group",
"group": "tokens",
"max_per_period": 1000000,
"action": "reject"
}scope is metric (requires metric_code) or metric_group (requires group). Only action: reject is supported.
Counts total usage in the current subscription period.
Stairstep pricing
On a metric, set pricing_model: "stairstep" with block_size and block_price:
{
"metric_code": "api_requests",
"metric_name": "API Requests",
"metric_unit": "request",
"included_units": 0,
"pricing_model": "stairstep",
"block_size": 1000,
"block_price": 500
}2500 units → 3 blocks × 500 = 1500. included_units are free before block counting starts.
Cannot combine with tiers or overage_unit_price.
Usage pool
Simpler than groups when metrics are interchangeable — one shared included bucket:
{
"usage_pool": {
"metric_codes": ["api_requests", "exports_rows"],
"included_units": 10000
}
}After the pool is exhausted, each metric's own overage or tiers apply to its billable portion.
SaaS billing blocks
| Field | Description |
|---|---|
minimum_monthly_charge | Integer. If base fee + usage subtotal for a period segment is lower, a top-up line brings it up to the minimum. |
commitment_units | {"metric_code": "...", "min": N} — shortfall line when actual usage in the window is below min (scaled for partial periods), priced at the metric's overage or first tier rate. |
included_rollover | {"max_periods": 1, "cap_units": N} — unused included from the previous period adds to effective included at invoice time. max_periods: 0 disables rollover. |
seat_pricing | {"metric_code": "monthly_tracked_users", "included_seats": 5, "overage_per_seat": 1000} — defaults to monthly_tracked_users when metric_code is omitted. |
billing_interval | month (default) or year |
annual_discount_percent | 0–99 when billing_interval is year. Yearly base = monthly_price × 12 × (100 − discount) / 100, prorated like the monthly fee. |
line_labels | {"base": "...", "overage": "..."} — custom invoice descriptions |
Invoice calculation order
For plans without Luau pricing:
- Base fee (prorated; annual interval if configured)
- Metric groups — shared included, then group tiers/overage or per-member rates
- Usage pool — shared included, then per-member rates
- Individual metrics not in a group or pool
- Seat pricing
- Minimum charge and commitment top-up lines
Luau pricing(ctx) replaces all of the above for that plan segment when present.
Validation
POST /plans and POST /pricing/packages return 400 when:
- a metric appears in both
metric_groupsandusage_pool - a metric or group uses both
tiersandoverage_unit_price stairstepis combined withtiersoroverage_unit_priceclassify.routes[].toreferences an unknown metricbilling_intervalis notmonthoryear
Examples
Peak / off-peak with shared quota
Clients send usage on generic tokens. Ingest routes to peak/off-peak meters; invoice bills
the shared group pool, then per-metric overage rates.
Growth plan with pool, stairstep, seats, and rollover
{
"code": "growth",
"name": "Growth",
"monthly_price": 5000,
"minimum_monthly_charge": 10000,
"billing_interval": "month",
"included_rollover": {"max_periods": 1, "cap_units": 50000},
"usage_pool": {
"metric_codes": ["api_requests", "exports_rows"],
"included_units": 10000
},
"metrics": [
{
"metric_code": "api_requests",
"metric_name": "API Requests",
"metric_unit": "request",
"pricing_model": "stairstep",
"block_size": 1000,
"block_price": 500
},
{
"metric_code": "exports_rows",
"metric_name": "Export rows",
"metric_unit": "row",
"overage_unit_price": 1
}
],
"commitment_units": {"metric_code": "api_requests", "min": 100000},
"seat_pricing": {"included_seats": 10, "overage_per_seat": 800},
"line_labels": {"base": "Growth platform fee", "overage": "API overage"}
}