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

NeedUse JSONUse Luau
Shared included across metricsmetric_groups, usage_poolpricing(ctx)
Route usage by time, calendar, quantity, period usage, sourceclassify.routesclassify(ctx)
Hard cap at ingestlimits, classify.rejectclassify(ctx)
Graduated / stairstep pricingtiers, pricing_model: stairsteppricing(ctx)
Minimum / commitment / rolloverplan-level JSON fieldspricing(ctx)
Custom invoice line itemspricing(ctx)
Cross-metric formulasLuau

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):

  1. JSON classifyclassify.routes may reroute the metric (first match wins)
  2. JSON rejectclassify.reject may return 400
  3. JSON limitslimits[] with action: reject may return 400
  4. Luau classify — runs last if the plan defines a classify function

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):

FieldDescription
included_unitsFree units per billing period before overage
overage_unit_priceFlat price per unit above included (mutually exclusive with tiers)
overage_descriptionCustom invoice line label for flat overage
tiers[]Graduated ranges: from, to (omit or null for open-ended), price_per_unit
pricing_modelstairstep or empty (default graduated/flat)
block_size / block_priceStairstep only — e.g. 2500 units with block 1000 → 3 blocks billed
aggregationInvoice 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:

json
{
  "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 in classify.reject and limits)
  • Each metric_codes entry must appear in metrics[]
  • 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.

json
{
  "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"}
  ]
}
FieldDescription
routes[].when / routes[].ifAll predicate fields below combine with AND. Omitted fields are ignored.
metricMatch incoming metric code
hour_utc, hour_localHalf-open hour range [start, end) (0–23); local uses the user's billing timezone
weekday, weekday_localSingle weekday 0=Sunday … 6=Saturday (UTC or local)
weekdays, weekdays_localMatch any listed weekday
month, month_localCalendar month 1–12
months, months_localMatch any listed month
day_of_month, day_of_month_localDay of month 1–31
date_on_or_after, date_beforeRFC3339 timestamp bounds (UTC)
date_local_on_or_after, date_local_beforeYYYY-MM-DD bounds in user timezone
quantity_gte, quantity_lte, quantity_gt, quantity_ltIncoming event quantity
period_usage_gte, period_usage_lte, period_usage_gt, period_usage_ltSum in current period before this event (metric or group scope)
sourcehttp, statsd, or system
routes[].toTarget metric code on the plan
reject[].whenSame predicate fields as routes, plus group and period_usage_plus_qty_gt
reject[].reasonError message returned to the client

Equivalent Luau examples: Luau classify ctx.

Ingest limits

json
{
  "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:

json
{
  "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:

json
{
  "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

FieldDescription
minimum_monthly_chargeInteger. 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_intervalmonth (default) or year
annual_discount_percent0–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:

  1. Base fee (prorated; annual interval if configured)
  2. Metric groups — shared included, then group tiers/overage or per-member rates
  3. Usage pool — shared included, then per-member rates
  4. Individual metrics not in a group or pool
  5. Seat pricing
  6. 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_groups and usage_pool
  • a metric or group uses both tiers and overage_unit_price
  • stairstep is combined with tiers or overage_unit_price
  • classify.routes[].to references an unknown metric
  • billing_interval is not month or year

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.

curl
01 curl -s -X POST https://api.hume.run/pricing/packages \
02 -H 'Content-Type: application/json' \
03 -H "$AUTH_HEADER" \
04 -H 'Idempotency-Key: pricing-tokens' \
05 -d '{
06 "code":"pro",
07 "name":"Pro",
08 "monthly_price":10000,
09 "metrics":[
10 {"metric_code":"tokens","metric_name":"Tokens","metric_unit":"token","included_units":0},
11 {"metric_code":"tokens_peak","metric_name":"Peak","metric_unit":"token","included_units":0,"overage_unit_price":3},
12 {"metric_code":"tokens_offpeak","metric_name":"Off-peak","metric_unit":"token","included_units":0,"overage_unit_price":1}
13 ],
14 "metric_groups":[{
15 "code":"tokens",
16 "metric_codes":["tokens_peak","tokens_offpeak"],
17 "included_units":1000000
18 }],
19 "classify":{
20 "routes":[
21 {"when":{"metric":"tokens"},"if":{"hour_utc":[9,18]},"to":"tokens_peak"},
22 {"when":{"metric":"tokens"},"to":"tokens_offpeak"}
23 ],
24 "reject":[
25 {"when":{"group":"tokens","period_usage_plus_qty_gt":1000000},"reason":"monthly cap exceeded"}
26 ]
27 },
28 "limits":[{
29 "scope":"metric_group",
30 "group":"tokens",
31 "max_per_period":1000000,
32 "action":"reject"
33 }]
34 }'

Growth plan with pool, stairstep, seats, and rollover

json
{
  "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"}
}