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.

pro-overage.luau
01 return plan("pro", "Pro", {
02 price = 5000,
03 metrics = list(
04 metric("api_requests", "API Requests", "request", { included = 1000 }),
05 ),
06 pricing = function(ctx)
07 local over = math.max(usage(ctx, "api_requests") - included(ctx, "api_requests"), 0)
08 return lines(
09 line("Base fee", prorate(ctx.monthly_price, ctx.window_ratio)),
10 over > 0 and line("API overage", over * 2, { qty = over, unit = 2, kind = "usage" }) or nil
11 )
12 end,
13 })

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.

growth-volume.luau
01 return plan("growth", "Growth", {
02 price = 5000,
03 metrics = list(
04 metric("api_requests", "API Requests", "request", { included = 1000 }),
05 ),
06 pricing = function(ctx)
07 local over = math.max(usage(ctx, "api_requests") - included(ctx, "api_requests"), 0)
08 -- first 1000 overage units at 3, remainder at 1
09 local first = math.min(over, 1000)
10 local rest = math.max(over - first, 0)
11
12 return lines(
13 line("Base fee", prorate(ctx.monthly_price, ctx.window_ratio)),
14 first > 0 and line("API requests (standard)", first * 3, { qty = first, unit = 3 }) or nil,
15 rest > 0 and line("API requests (volume)", rest, { qty = rest, unit = 1 }) or nil
16 )
17 end,
18 })

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.

llm-api.luau
01 return plan("llm_api", "LLM API", {
02 price = 2000,
03 metrics = list(
04 metric("tokens_input", "Input tokens", "token"),
05 metric("tokens_output", "Output tokens", "token"),
06 ),
07 pricing = function(ctx)
08 local input = usage(ctx, "tokens_input")
09 local output = usage(ctx, "tokens_output")
10 return lines(
11 line("Platform fee", prorate(ctx.monthly_price, ctx.window_ratio)),
12 input > 0 and line("Input tokens", input * 2, { qty = input, unit = 2 }) or nil,
13 output > 0 and line("Output tokens", output * 6, { qty = output, unit = 6 }) or nil
14 )
15 end,
16 })

Fields

ctx is a Luau table. Timestamps are RFC3339 strings; money-like values are integers in abstract billing units.

FieldTypeDescription
plan_codestringPlan code for this billing segment
monthly_priceintegerFull-period base price from the plan (not prorated — use prorate with window_ratio)
period_startRFC3339Start of the subscription billing period (inclusive)
period_endRFC3339End of the subscription billing period (exclusive)
window_startRFC3339max(period_start, invoice.period_start) — start of the overlap (inclusive)
window_endRFC3339min(period_end, invoice.period_end) — end of the overlap (exclusive)
window_rationumber(window_end − window_start) / (period_end − period_start) — fraction of the billing period inside the invoice window (0..1)
usagetablemetric_code → integer — sum of usage in [window_start, window_end)
includedtablemetric_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:

json
{
  "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_code strings (e.g. api_requests), not internal metric_id
  • Only metrics bound to the plan appear in usage and included
  • 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:

FunctionReturnsDescription
usage(ctx, metric_code)integerctx.usage[metric_code] or 0
included(ctx, metric_code)integerctx.included[metric_code] or 0
prorate(amount, ratio)integerround(amount × ratio) — typically prorate(ctx.monthly_price, ctx.window_ratio)
lines(...) / line(desc, amount, opts)arrayBuild 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.