Luau pricing context
When a plan defines pricing = function(ctx) ... end, the engine calls it during POST /invoices/generate. This page documents ctx fields, helpers, and
examples. Sandbox limits: Pricing.
When pricing(ctx) runs
The engine walks each subscription billing period that overlaps the invoice window. For plans with a
Luau pricing hook it calls pricing(ctx) once per such period.
Returned lines fully replace built-in base fee and usage math for that period. Adjustments and refunds
are appended separately, outside Luau.
Use prorate(ctx.monthly_price, ctx.window_ratio) for the base fee — it handles mid-month
upgrades, pauses, and partial invoice windows automatically.
Examples
Base fee + included overage
Classic SaaS: monthly platform fee plus metered API calls above the included allowance. With 2500
requests and 1000 included, the overage line is 1500 × 2 = 3000 on top of the prorated
base fee.
Volume discount on overage
After included units are consumed, the first chunk of overage bills at a higher rate and the rest at a volume price. At 2500 requests with 1000 included: 1000 overage at 3 + 500 at 1 → usage lines total 3500, plus base fee.
Multiple meters at different rates
An LLM API meters input and output tokens separately. Each metric gets its own invoice line with quantity and unit price — no need to merge them in your app.
Fields
ctx is a Luau table. Timestamps are RFC3339 strings; money-like values are integers in
abstract billing units.
| Field | Type | Description |
|---|---|---|
plan_code | string | Plan code for this billing segment |
monthly_price | integer | Full-period base price from the plan (not prorated — use prorate with window_ratio) |
period_start | RFC3339 | Start of the subscription billing period (inclusive) |
period_end | RFC3339 | End of the subscription billing period (exclusive) |
window_start | RFC3339 | max(period_start, invoice.period_start) — start of the overlap (inclusive) |
window_end | RFC3339 | min(period_end, invoice.period_end) — end of the overlap (exclusive) |
window_ratio | number | (window_end − window_start) / (period_end − period_start) — fraction of the billing period inside the invoice window (0..1) |
usage | table | metric_code → integer — sum of usage in [window_start, window_end) |
included | table | metric_code → integer — plan included_units scaled by window_ratio (rounded) |
Example ctx snapshot
Full-month invoice, monthly_price = 5000, metric api_requests with included_units = 1000, 2500 units recorded:
{
"plan_code": "pro",
"monthly_price": 5000,
"period_start": "2026-07-01T00:00:00Z",
"period_end": "2026-08-01T00:00:00Z",
"window_start": "2026-07-01T00:00:00Z",
"window_end": "2026-08-01T00:00:00Z",
"window_ratio": 1.0,
"usage": { "api_requests": 2500 },
"included": { "api_requests": 1000 }
}Metric tables
- Keys are
metric_codestrings (e.g.api_requests), not internalmetric_id - Only metrics bound to the plan appear in
usageandincluded - If the same metric is on several subscriptions for one user, only the first subscription (deterministic order) includes it — same rule as built-in invoice math
- Missing keys read as
0
Not in ctx
user_id,subscription_id- Tier tables, feature flags
- Adjustments and refunds (separate invoice lines)
Helpers
Available inside pricing(ctx) during invoice generation:
| Function | Returns | Description |
|---|---|---|
usage(ctx, metric_code) | integer | ctx.usage[metric_code] or 0 |
included(ctx, metric_code) | integer | ctx.included[metric_code] or 0 |
prorate(amount, ratio) | integer | round(amount × ratio) — typically prorate(ctx.monthly_price, ctx.window_ratio) |
lines(...) / line(desc, amount, opts) | array | Build invoice lines; nil entries are skipped |
Return value
pricing(ctx) must return an array of line tables from lines(...) / line(...).
Each line requires description and amount (integer). Optional: quantity, unit_amount, type. Max 100 lines; 250ms wall timeout;
runtime errors fail invoice generation.