HTTP API
Complete HTTP reference with parameters. Declarative plan fields: Plan extensions. Luau hooks: Pricing.
Base URL
HTTP: https://api.hume.run · StatsD UDP: api.hume.run:8125
Common headers
| Header | When | Description |
|---|---|---|
Authorization | All except /healthz, /tenants | Basic base64(tenant_id:api_key) |
Content-Type | POST/PUT bodies | application/json |
Idempotency-Key | Domain writes | Required on catalog, subscription, usage, adjustment, invoice writes |
X-Bootstrap-Token | POST /tenants | Required when TENANT_CREATE_TOKEN env is set |
JSON bodies reject unknown fields. Errors return {"error":"..."}.
Entity identifiers
Entity ids use {prefix}_{ulid} (for example usr_01ARZ3NDEKTSV4RRFFQ69G5FAV). Prefixes identify the type (usr, met, pln, sub, evt, ten, whk, …); the suffix is a monotonic ULID.
Amounts
All money-like fields (monthly_price, amount, unit_amount, total, adjustments, refunds) are integers in abstract billing units.
There is no currency field in the API. Display labels (USD, EUR,
credits, tokens) and FX are downstream concerns.
Pagination
GET /events and GET /usage accept:
limit— max items (0 = empty page)offset— skip from the newest end
Response shape: {"total": N, "events"|"usage": [...]}
Authentication setup
Endpoints
Quick reference
| Method | Path | Section |
|---|---|---|
| GET | /healthz | System |
| POST | /tenants | System |
| GET | /events | Events |
| GET | /state | Events |
| GET/POST | /users | Catalog |
| GET/POST | /metrics | Catalog |
| GET/POST | /features | Catalog |
| GET/POST | /plans | Catalog |
| GET/PUT | /plans/{id}/alerts | Catalog |
| POST | /pricing/packages | Catalog |
| POST | /pricing/bootstrap | Catalog |
| GET/POST | /plans/{id}/features | Catalog |
| GET/POST | /subscriptions | Subscriptions |
| GET | /subscriptions/{id} | Subscriptions |
| POST | /subscriptions/{id}/… | Subscriptions |
| GET/POST | /adjustments | Adjustments |
| GET | /refunds | Adjustments |
| GET/POST | /usage | Usage |
| POST | /entitlements/check | Entitlements |
| GET | /consumption-flags | Entitlements |
| POST | /events | Event ingestion |
| POST | /events/batch | Event ingestion |
| GET/POST | /webhooks | Webhooks |
| DELETE | /webhooks/{id} | Webhooks |
| GET | /invoices | Invoices |
| POST | /invoices/generate | Invoices |
System
GET /healthz
| Auth | No |
| Success | 200 — { status, time } |
POST /tenants
| Auth | No |
| Success | 201 — tenant credentials |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
name | string | yes | Tenant display name |
Response
{ tenant: { id, name }, api_key }
- Rate limit: 10 requests/minute when token not configured
Events and state
GET /events
| Auth | Required (Basic) |
| Success | 200 |
Query parameters
| Field | Type | Req | Description |
|---|---|---|---|
limit | int | no | Page size |
offset | int | no | Skip from newest |
Response
{ total, events[] } — audit journal, newest first (by sequence)
- Does not accept writes — use REST routes or POST /events for ingestion
GET /state
| Auth | Required (Basic) |
| Success | 200 |
Response
Current tenant state (users, metrics, plans, subscriptions, usage aggregates, …)
Catalog
GET /users
| Auth | Required (Basic) |
| Success | 200 |
Query parameters
| Field | Type | Req | Description |
|---|---|---|---|
external_id | string | no | Lookup by CRM / auth system ID (unique per tenant when set) |
Response
Array of user objects, or single user when external_id matches
POST /users
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 201 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
name | string | yes | Display name |
email | string | yes | Email address |
external_id | string | no | Your CRM / auth system ID |
timezone | string | no | IANA timezone (e.g. Europe/Berlin); drives billing period boundaries |
GET /users/{id}
| Auth | Required (Basic) |
| Success | 200 |
Response
User object including blocked / deleted flags
POST /users/{id}/timezone
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
timezone | string | yes | IANA timezone (Europe/Berlin, America/New_York, …) |
- Does not rewrite active subscription billing periods
POST /users/{id}/block
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
- Suspends entitlements and subscription reads (403); usage ingest continues
- Invoice admin APIs still work — see /docs/billing
POST /users/{id}/unblock
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
DELETE /users/{id}
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
- Permanent tombstone; user hidden from GET /users list
GET /metrics
| Auth | Required (Basic) |
| Success | 200 |
Response
Array of metric definitions
POST /metrics
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 201 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
code | string | yes | Stable metric key (e.g. api_requests) |
name | string | yes | Display name |
unit | string | yes | Unit label (request, gb, …) |
DELETE /metrics/{id}
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
- Returns 400 if any plan still references the metric
- monthly_tracked_users cannot be deleted
- Event: metric.deleted
GET /features
| Auth | Required (Basic) |
| Success | 200 |
Response
Array of feature catalog entries
POST /features
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 201 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
code | string | yes | Feature key |
name | string | yes | Display name |
description | string | no | Optional description |
DELETE /features/{id}
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
- Returns 400 if any plan has the feature in binding history
- Event: feature.deleted
GET /plans
| Auth | Required (Basic) |
| Success | 200 |
Response
Array of plans with metric bindings
POST /plans
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 201 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
code | string | yes | Plan key |
name | string | yes | Display name |
monthly_price | int | yes | Base fee in abstract units |
metrics[] | object | yes | Bind existing metrics by metric_id |
features[] | object | no | Optional feature bindings |
pricing_lua | string | no | Luau plan manifest (field name is historical) |
metric_groups[] | object | no | Shared included/tiers across metrics — see Plan extensions |
usage_pool | object | no | Single shared included bucket for listed metrics |
classify | object | no | JSON routes and reject rules at ingest |
limits[] | object | no | Hard caps per metric or group (action: reject) |
alerts[] | object | no | Threshold notifications (action: webhook) — see Plan alerts |
minimum_monthly_charge | int | no | Top-up when subtotal is below minimum |
commitment_units | object | no | metric_code + min usage commitment |
included_rollover | object | no | max_periods, cap_units |
seat_pricing | object | no | included_seats, overage_per_seat |
billing_interval | string | no | month (default) or year |
annual_discount_percent | int | no | 0–99 when billing_interval is year |
line_labels | object | no | base, overage invoice descriptions |
- metrics[].metric_id required
- tiers[] and overage_unit_price are mutually exclusive per metric
- stairstep cannot combine with tiers or overage_unit_price
- Full extension reference: /docs/pricing/plan-extensions
POST /pricing/packages
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 201 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
code | string | yes | Plan key |
name | string | yes | Display name |
monthly_price | int | yes | Base fee |
metrics[] | object | yes | Creates metrics + binds to plan |
features[] | object | no | Creates features + binds to plan |
pricing_lua | string | no | Luau manifest — skips built-in JSON billing when set |
metric_groups[] | object | no | Shared included/tiers across metrics — see Plan extensions |
usage_pool | object | no | Single shared included bucket for listed metrics |
classify | object | no | JSON routes and reject rules at ingest |
limits[] | object | no | Hard caps per metric or group (action: reject) |
alerts[] | object | no | Threshold notifications (action: webhook) — see Plan alerts |
minimum_monthly_charge | int | no | Top-up when subtotal is below minimum |
commitment_units | object | no | metric_code + min usage commitment |
included_rollover | object | no | max_periods, cap_units |
seat_pricing | object | no | included_seats, overage_per_seat |
billing_interval | string | no | month (default) or year |
annual_discount_percent | int | no | 0–99 when billing_interval is year |
line_labels | object | no | base, overage invoice descriptions |
Response
{ plan, metrics[], features[], pricing? }
- metrics[].metric_id — Existing metric ID (low-level plans)
- metrics[].metric_code — Metric code — creates metric in packages
- metrics[].metric_name — Display name when creating via package
- metrics[].metric_unit — Unit label when creating via package
- metrics[].included_units — Free units per billing period
- metrics[].overage_unit_price — Per-unit price after included (if no tiers)
- metrics[].overage_description — Custom overage line label
- metrics[].tiers[] — Volume tiers: from, to (null = open), price_per_unit
- metrics[].pricing_model — stairstep or empty
- metrics[].block_size — Stairstep: units per block
- metrics[].block_price — Stairstep: price per block
- metrics[].aggregation — Invoice window: sum (default), max, last
- Without pricing_lua, built-in billing uses extension fields — see /docs/pricing/plan-extensions
POST /pricing/bootstrap
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 201 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
plans[] | object | yes | Array of plan definitions — same fields as /pricing/packages |
- Compact syntax supported: price (alias for monthly_price), metrics as map code→{included, overage}, features as string[]
- Shared metrics created once and reused across plans
- See /docs/pricing — bootstrap example
DELETE /plans/{id}
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
- Returns 400 if any subscription references the plan (current plan_id or pending_change)
- After delete the plan code can be reused
- Event: plan.deleted
Plan edit (unused plans only)
When no subscription references a plan, you can remove metrics/features and manage ingest limits. Removing a metric also clears groups, pool, limits, classify, commitment, and seat bindings for that code.
DELETE /plans/{id}/metrics/{metric_id}
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
- Event: plan.metric_removed
DELETE /plans/{id}/features/{feature_id}
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
- Clears binding history entry; event: plan.feature_removed
GET /plans/{id}/limits
| Auth | Required (Basic) |
| Success | 200 |
Response
Current limits[] on the plan
POST /plans/{id}/limits
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
scope | string | yes | metric | metric_group |
metric_code | string | no | When scope is metric |
group | string | no | When scope is metric_group |
max_per_period | int | yes | Hard cap in current subscription period |
action | string | yes | reject (only supported action) |
- Appends a limit; event: plan.limits_updated
PUT /plans/{id}/limits
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
limits[] | object | yes | Replace entire limits array |
PUT /plans/{id}/limits/{index}
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
scope | string | yes | metric | metric_group |
max_per_period | int | yes | Updated cap |
action | string | yes | reject |
DELETE /plans/{id}/limits/{index}
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
Plan alerts
Alerts emit a durable usage.threshold_crossed event once per billing period when usage
crosses percent of the baseline. Baseline is baseline_units if set, else a
matching hard limits[].max_per_period, else included_units. Engine action is always webhook — email and other side effects belong to receivers.
Allowed on plans that are already in use.
GET /plans/{id}/alerts
| Auth | Required (Basic) |
| Success | 200 |
Response
Current alerts[] on the plan
PUT /plans/{id}/alerts
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
scope | string | yes | metric | metric_group |
metric_code | string | no | When scope is metric |
group | string | no | When scope is metric_group |
percent | int | yes | 1–100; fire when period usage crosses this % of baseline |
baseline_units | int | no | Optional explicit baseline; otherwise limit or included_units |
action | string | no | webhook (default) |
- Body is a JSON array replacing the entire alerts list; event: plan.alerts_updated
GET /plans/{id}/features
| Auth | Required (Basic) |
| Success | 200 |
Query parameters
| Field | Type | Req | Description |
|---|---|---|---|
at | RFC3339 | no | Point-in-time feature bindings |
POST /plans/{id}/features
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
feature_code | string | no | Feature code (or feature_id) |
feature_id | string | no | Feature ID alternative |
enabled | bool | yes | Grant or revoke |
effective_from | RFC3339 | no | Delayed activation |
Subscriptions
GET /subscriptions
| Auth | Required (Basic) |
| Success | 200 |
Response
All subscriptions with current lifecycle state
GET /subscriptions/{id}
| Auth | Required (Basic) |
| Success | 200 / 404 |
Response
Single subscription snapshot at current time
POST /subscriptions
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 201 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
user_id | string | yes | Billable user |
plan_id | string | yes | Plan to subscribe to |
trial_days | int | no | Trial length (mutually exclusive with trial_ends_at) |
trial_ends_at | RFC3339 | no | Explicit trial end |
POST /subscriptions/{id}/change-plan
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
plan_id | string | yes | Target plan |
policy | string | yes | prorate_immediately | refund_immediately | effective_next_cycle |
Response
{ subscription, adjustment?, refund? }
- prorate_immediately → separate adjustment.posted (kind: prorate)
- refund_immediately → separate refund.issued for unused time credit
- effective_next_cycle → subscription.plan_change_scheduled
POST /subscriptions/{id}/pause
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
- No request body
- Paused time excluded from base fee
POST /subscriptions/{id}/resume
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
POST /subscriptions/{id}/cancel-at-period-end
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
Query parameters
| Field | Type | Req | Description |
|---|---|---|---|
enabled | bool | no | true (default) to cancel at period end; false to clear |
Adjustments and refunds
Manual billing corrections are first-class events, not embedded fields on other records. Amounts are
signed integers in billing units (negative = credit, positive = charge). There is no currency code. kind: prorate (plan change), credit, debit (manual;
inferred from sign if omitted). They appear as invoice lines when created_at falls inside
the invoice window.
POST /adjustments
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 201 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
subscription_id | string | yes | Subscription to adjust |
amount | int | yes | Signed amount in abstract units (negative = credit) |
kind | string | no | prorate | credit | debit (inferred from sign if omitted) |
description | string | yes | Line description on invoice |
Response
Adjustment record — emits adjustment.posted
- Response fields: id, subscription_id, user_id, amount, description, source_event_id?, created_at
- Plan changes with refund_immediately append refund.issued automatically (amount always positive; invoice line is negative)
GET /adjustments
| Auth | Required (Basic) |
| Success | 200 |
Query parameters
| Field | Type | Req | Description |
|---|---|---|---|
user_id | string | no | Filter by user (optional) |
Response
Array of adjustment records
GET /refunds
| Auth | Required (Basic) |
| Success | 200 |
Query parameters
| Field | Type | Req | Description |
|---|---|---|---|
user_id | string | no | Filter by user (optional) |
Response
Array of refund records — fields: id, subscription_id, user_id, amount, reason, source_event_id?, created_at
Usage
GET /usage
| Auth | Required (Basic) |
| Success | 200 |
Query parameters
| Field | Type | Req | Description |
|---|---|---|---|
user_id | string | no | Filter by user |
metric_id | string | no | Filter by metric |
from | RFC3339 | no | Range start (inclusive) |
to | RFC3339 | no | Range end (exclusive) |
limit | int | no | Pagination |
offset | int | no | Pagination (newest first) |
Response
{ total, usage[] }
POST /usage
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 201 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
user_id | string | yes | User consuming the metric |
metric_id | string | yes | Metric ID |
quantity | int | yes | Units consumed |
created_at | RFC3339 | no | Event time; max +24h in future |
- Shorthand for usage.recorded event
- Plan may apply JSON classify, limits, then Luau classify before commit — metric may be rerouted or rejected with 400
Entitlements
POST /entitlements/check
| Auth | Required (Basic) |
| Success | 200 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
user_id | string | yes | User to check |
feature_code | string | no | Feature key (or feature_id) |
feature_id | string | no | Feature ID alternative |
at | RFC3339 | no | Defaults to now |
Response
{ allowed, reason, subscription_status, plan_id, checked_at, … }
GET /consumption-flags
| Auth | Required (Basic) |
| Success | 200 |
Query parameters
| Field | Type | Req | Description |
|---|---|---|---|
user_id | string | yes | User to inspect |
at | RFC3339 | no | Historical point-in-time |
Response
Flags: usage.near_limit, usage.over_limit, usage.current_tier
Event ingestion (optional)
For normal use, prefer the dedicated REST routes (POST /users, POST /usage,
…). POST /events is a generic envelope if an external system prefers one JSON shape.
For usage.recorded, POST /usage is equivalent and simpler. POST /events/batch accepts many events in one request, with a per-item result
(207 if some rows failed).
POST /events
| Auth | Required (Basic) |
| Success | 202 Accepted |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
event_id | string | yes | Idempotency key for the log |
type | string | yes | Event type (see table below) |
source | string | yes | Originating system |
occurred_at | RFC3339 | yes | When it happened; max +24h in future |
data | object | yes | Type-specific payload |
- Catalog events (user.created, metric.created, …) create entities — e.g. user.created requires data.user_name and data.user_email; data.user_id is stored as external_id
POST /events/batch
| Auth | Required (Basic) |
| Success | 202 / 207 Multi-Status |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
events[] | EventInput | yes | Array of events, applied sequentially |
Response
{ results: [{ index, record? } | { index, error }], accepted, failed } — HTTP 207 if any row failed
Event types
Writable types for POST /events and POST /events/batch:
| type | REST equivalent | Notes |
|---|---|---|
user.created | POST /users | requires data.user_name, data.user_email |
user.blocked | POST /users/{id}/block | suspends read access; usage ingest continues |
user.unblocked | POST /users/{id}/unblock | restores read access |
user.deleted | DELETE /users/{id} | permanent tombstone |
user.timezone_updated | POST /users/{id}/timezone | IANA timezone for local billing |
metric.created | POST /metrics | — |
metric.deleted | DELETE /metrics/{id} | only when no plan references the metric |
feature.created | POST /features | — |
feature.deleted | DELETE /features/{id} | only when no plan binding history references the feature |
plan.created | POST /plans | — |
plan.deleted | DELETE /plans/{id} | only when no subscription references the plan |
plan.metric_removed | DELETE /plans/{id}/metrics/{metric_id} | only when plan has no subscriptions |
plan.feature_removed | DELETE /plans/{id}/features/{feature_id} | clears binding history; plan must have no subscriptions |
plan.limits_updated | PUT/POST /plans/{id}/limits, PUT/DELETE …/limits/{index} | replace, add, update, or remove ingest limits on unused plans |
plan.alerts_updated | PUT /plans/{id}/alerts | replace threshold alerts (allowed on plans in use) |
plan.feature_enabled | POST /plans/{id}/features | enabled: true |
plan.feature_disabled | POST /plans/{id}/features | enabled: false |
usage.recorded | POST /usage | — |
usage.threshold_crossed | (side effect of usage + plan alerts) | once per period per alert when crossing percent of baseline |
subscription.created | POST /subscriptions | — |
subscription.plan_changed | POST …/change-plan | may emit adjustment.posted or refund.issued |
subscription.plan_change_scheduled | change-plan effective_next_cycle | — |
subscription.paused | POST …/pause | — |
subscription.resumed | POST …/resume | — |
subscription.cancel_at_period_end_set | cancel-at-period-end enabled=true | — |
subscription.cancel_at_period_end_cleared | cancel-at-period-end enabled=false | — |
adjustment.posted | POST /adjustments | manual or plan-change proration |
refund.issued | (side effect of refund plan change) | credit for unused subscription time |
Lifecycle events
Emitted automatically on subscription state transitions (not writable via public REST):
| type | When |
|---|---|
subscription.activated | Trial ends or subscription becomes billable |
subscription.canceled | Subscription is canceled (immediate or at period end) |
subscription.period_rolled | Billing period advances |
Webhooks
Register an outbound URL per tenant. On each committed event, the service POSTs the EventRecord JSON asynchronously. Failed deliveries retry 3 times with a 2s pause.
GET /webhooks
| Auth | Required (Basic) |
| Success | 200 |
Response
{ webhooks: WebhookEndpoint[] } — id, url, events, created_at (secret is never returned)
POST /webhooks
| Auth | Required (Basic) |
| Success | 201 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
url | string | yes | http or https endpoint URL |
events | string[] | yes | Event types to forward, or ["*"] for all |
secret | string | no | HMAC signing secret; server generates one if omitted |
Response
WebhookEndpoint + secret — secret is returned only in this response
- Same event types as GET /events (user.created, usage.recorded, subscription.canceled, …)
DELETE /webhooks/{id}
| Auth | Required (Basic) |
| Success | 204 / 404 |
Delivery payload
Each delivery is a POST with Content-Type: application/json. The body is the
committed EventRecord — the same shape as entries in GET /events.
{
"sequence": 12,
"event_id": "evt_01J8ZK5BQY8XQ9R2M4V7W3N6T5",
"type": "subscription.created",
"source": "http",
"occurred_at": "2026-07-01T12:00:00Z",
"accepted_at": "2026-07-01T12:00:00Z",
"data": { "...": "..." },
"result": { "subscription": { "...": "..." } }
}Signature verification
POST /webhooks returns a secret once. Store it on the receiver and verify every
delivery before processing. Each delivery includes:
| Header | Meaning |
|---|---|
Billing-Webhook-Signature | t=<unix>,v1=<hex> |
Compute v1 = HMAC-SHA256(secret, "<unix>.<raw body>") and compare with a
constant-time function. Reject requests older than ~5 minutes (replay protection).
On retry the body and signature timestamp are identical — deduplicate by event_id and return 2xx without re-processing duplicates.
Subscribable event types
events lists event types to forward, or ["*"] for all. The same types as in the
event types apply, including lifecycle events:
| type | When |
|---|---|
user.created | User created |
user.blocked | User suspended (non-payment) |
user.unblocked | User restored |
user.deleted | User permanently removed |
metric.created | Metric created |
feature.created | Feature created |
plan.created | Plan created |
plan.feature_enabled / plan.feature_disabled | Plan feature binding changed |
usage.recorded | Usage recorded (HTTP, StatsD, or synthetic) |
usage.threshold_crossed | Plan alert threshold crossed (once per period) |
plan.alerts_updated | Plan alerts replaced |
subscription.created | Subscription created |
subscription.plan_changed | Plan changed immediately |
subscription.plan_change_scheduled | Plan change queued for next cycle |
subscription.paused / subscription.resumed | Pause / resume |
subscription.cancel_at_period_end_set / subscription.cancel_at_period_end_cleared | Cancel-at-period-end toggled |
subscription.activated | Trial ended or subscription became billable |
subscription.canceled | Subscription canceled |
subscription.period_rolled | Billing period advanced |
adjustment.posted | Adjustment recorded |
refund.issued | Refund recorded |
invoice.generated | Draft invoice created |
invoice.finalized | Invoice amounts locked |
invoice.paid | Payment recorded |
invoice.voided | Invoice voided |
Invoices
GET /invoices
| Auth | Required (Basic) |
| Success | 200 |
Response
Array of invoices (draft, finalized, paid, void)
GET /invoices/{id}
| Auth | Required (Basic) |
| Success | 200 |
Response
Single invoice
POST /invoices/generate
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 201 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
user_id | string | yes | Billable user |
period_start | RFC3339 | yes | Invoice window start |
period_end | RFC3339 | yes | Invoice window end (exclusive) |
Response
Draft invoice JSON with line items in abstract units
- Status is draft — finalize before collection
- Invoices for any window, including past months — period rollover does not break historical calculations
- adjustment.posted and refund.issued in window → type: adjustment / refund lines (refunds are negative amounts)
- Base fee proportional to active time — trials and pauses excluded; plan segments billed separately
- Usage billed within periods; included_units and tier boundaries scaled for partial windows
- Same metric across multiple subscriptions for one user billed once
- Plans with Luau pricing replace built-in lines per billing period
- Plans without Luau pricing use JSON billing (groups, pools, tiers, stairstep, seats, minimum, commitment) — see Plan extensions
- Classify (JSON or Luau) runs only at usage ingest, not during invoicing
POST /invoices/{id}/finalize
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
Response
Invoice with status finalized
- Works while user is blocked — tenant admin only
POST /invoices/{id}/mark-paid
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
Request body
| Field | Type | Req | Description |
|---|---|---|---|
payment_ref | string | yes | PSP reference (e.g. stripe_pi_…) |
Response
Invoice with status paid
- Call after PSP confirms payment; works while user is blocked
POST /invoices/{id}/void
| Auth | Required (Basic) |
| Idempotency-Key | Required header |
| Success | 200 |
Response
Invoice with status void
- Draft or finalized only — not paid
StatsD ingestion
UDP packet format:
billing.<tenant_id>.<api_key>.user.<user_id>.<metric_code>:<value>|c|g gauge type also accepted — value is still treated as usage quantity. Optional idempotency tag: |#event_id:evt-123.