Send usage events, read them back, and aggregate them over time. Each event records a quantity of one metric for one customer and subscription; the meters of the subscription's products turn the events into billed quantities on the next invoice.
Use the aggregate to build usage dashboards, and GET /usage-events/billing to find the invoice that billed one event.
List usage events
Returns paginated raw usage events for the org, newest first - the read side of POST /usage-events. All filter params AND together; multi-value metric_key is repeatable or comma-separated. customer_id, product_id, subscription_id, price_key, idempotency_key and wallet_currency take the unified field.op=value grammar (eq, in). One event is found by its idempotency_key, which is unique in a workspace.
Required scope: usage.read (Read usage).
query Parameters
customer_idFilter on customer_id (uuid). Operators: eq, in — dot grammar, e.g. customer_id.in=value; a bare customer_id=value means eq. A bare comma-separated value is in-sugar: customer_id=a,b means customer_id.in=a,b.
idempotency_keyFilter on idempotency_key (string). Operators: eq, in — dot grammar, e.g. idempotency_key.in=value; a bare idempotency_key=value means eq. A bare comma-separated value is in-sugar: idempotency_key=a,b means idempotency_key.in=a,b.
price_keyFilter on price_key (string). Operators: eq, in — dot grammar, e.g. price_key.in=value; a bare price_key=value means eq. A bare comma-separated value is in-sugar: price_key=a,b means price_key.in=a,b.
product_idFilter on product_id (uuid). Operators: eq, in — dot grammar, e.g. product_id.in=value; a bare product_id=value means eq. A bare comma-separated value is in-sugar: product_id=a,b means product_id.in=a,b.
subscription_idFilter on subscription_id (uuid). Operators: eq, in — dot grammar, e.g. subscription_id.in=value; a bare subscription_id=value means eq. A bare comma-separated value is in-sugar: subscription_id=a,b means subscription_id.in=a,b.
wallet_currencyFilter on wallet_currency (string). Operators: eq, in — dot grammar, e.g. wallet_currency.in=value; a bare wallet_currency=value means eq. A bare comma-separated value is in-sugar: wallet_currency=a,b means wallet_currency.in=a,b.
occurred_afterDeprecated: use timestamp.gte instead. Removed on 2026-11-30.
occurred_beforeDeprecated: use timestamp.lt instead. Removed on 2026-11-30.
limitPage size (default 25, max 100)
cursorOpaque continuation token from the previous response's pagination.cursor. Omit for the first page. Cursors are stateless and do not expire, but are only valid for the list and filters that produced them.
metric_keyFilter by metric key. Repeatable or comma-separated
timestamp.gteKeep events whose timestamp is at or after this instant (RFC 3339).
timestamp.ltKeep events whose timestamp is before this instant (RFC 3339).
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List usage events › Responses
OK
Ingest a usage event (fire-and-forget billing)
Accepts a single usage event or a batch ({"events": [...]}). Records the event for billing on the next invoice; does not check wallet balance, credit limits, or reservation availability. Use the reserve/capture flow (POST /reservations) when up-front fund availability matters. Returns 202 {status: queued} when a single event settles a reservation (hold_id); a batch refuses events that name a hold. A negative quantity is a correction: it takes back usage recorded at the same timestamp with the same fields, on meters that sum quantity; anything else is refused with 422 USAGE_CORRECTION_REFUSED. An event dated before its subscription's current billing period belongs to a period already invoiced: it is refused with 422 USAGE_PERIOD_CLOSED (correct that period with a credit note), unless every meter on its metric_key has late_event_policy CLAMP_TO_PERIOD, which stores a positive one at the start of the current period, where the next invoice bills it. Read ingested events back at GET /usage-events.
Required scope: usage.write (Ingest usage).
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Ingest a usage event (fire-and-forget billing) › Request Body
Decision Table
| Variant | Matching Criteria |
|---|---|
| type = object · requires: customer_id, product_id, subscription_id +3 more | |
| type = object · requires: events |
customer_idCustomer that consumed the metric.
idempotency_keyCaller key that dedupes replays of the same event; required.
metric_keyMetric this event records usage for; must match the product's meter binding.
product_idMetered product the event is billed under.
quantity^-?\d+(\.\d+)?$ · requiredNumber of metric units to record. A negative quantity is a correction: it takes back usage recorded at the same timestamp with the same fields, never more than is recorded there, only in the subscription's current billing period, only on SUM meters over quantity, and never with hold_id or external_cost_amount.
subscription_idSubscription the usage is charged against.
Named dimension values used to select the price tier.
Caller-supplied wholesale cost (COGS) fed to cost-plus formulas, {value, currency}; one currency per (subscription, metric) group.
hold_idReserve hold this event captures against; null for direct (non-reserve) ingest. Single-event body only: a batch refuses any event that names a hold. A retry with the same idempotency_key and hold_id answers with the outcome_id its first attempt queued.
Arbitrary caller-supplied key/value annotations carried on the event.
price_keyPrice key selecting a tier for keyed products; required when the product exposes keyed prices.
Named numeric variables bound into the price formula at finalize time.
timestampWhen the usage occurred; omitted or zero stamps the server's ingest time. Set explicitly for backfill, inside the subscription's current billing period: an earlier one is refused (422 USAGE_PERIOD_CLOSED), or moved to the period's start where the meter's late_event_policy is CLAMP_TO_PERIOD.
wallet_currencyISO 4217 currency pinning the event to a specific wallet; the wallet must exist for (customer_id, wallet_currency). Null uses the customer's preferred-currency cascade.
external_cost_amount^-?\d+(\.\d+)?$Caller-supplied wholesale cost (COGS) fed to cost-plus formulas, major units; requires external_cost_currency. Sent with external_cost, both must state the same cost. Deprecated: use external_cost instead. Removed on 2026-11-30.
external_cost_currencyISO 4217 currency of external_cost_amount; one currency per (subscription, metric) group. Deprecated: use external_cost instead. Removed on 2026-11-30.
Ingest a usage event (fire-and-forget billing) › Responses
Created
Outcome-dependent: on 201 the created UsageEvent (single-event body) or the BatchIngestResult (batch body); on 202 the UsageEventQueuedResponse when the event settles a reservation and is processed asynchronously (the hold's state is observable at GET /wallets/{id}/holds).
Time-bucketed usage aggregate
Counts or sums the usage events in [timestamp.gte, timestamp.lt) per time bucket, one series per group. The window holds at most 200 buckets at the requested size.
Required scope: usage.read (Read usage).
query Parameters
timestamp.gteStart of the window, included (RFC 3339).
sinceDeprecated: use timestamp.gte instead. Removed on 2026-11-30.
untilDeprecated: use timestamp.lt instead. Removed on 2026-11-30.
bucketBucket size: hour | day (default) | week
timestamp.ltEnd of the window, not included (RFC 3339). Defaults to now.
metricMetric: count (default) | sum_quantity
group_byGrouping: none (default) | metric_key | product | price_key | dim:
customer_idFilter by customer UUID
product_idFilter by product UUID
subscription_idFilter by subscription UUID
metric_keyFilter by metric_key. Repeatable or comma-separated
price_keyFilter by price key. Repeatable or comma-separated
topTruncate to top-N series, fold the rest into 'Other' (default 8; 0 disables)
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Time-bucketed usage aggregate › Responses
OK
Where a usage event was billed
Finds one usage event by its idempotency key and answers the invoice atoms that billed it. Nothing stores that link: an atom billed the event when it is a deal atom of the event's product, of its key or of an unkeyed line, on a live invoice of its subscription, whose period holds the event, and whose meter — under the atom's own product version and period, filter tree and late-event policy included — counts it. An atom whose meter cannot be asked is listed as undetermined, never guessed. No atom: no invoice bills the event yet. Usage sent against a reservation is not a usage event and is billed by its capture's invoice. include=document adds each atom's invoice (id, number, status).
Required scope: usage.read (Read usage).
query Parameters
idempotency_keyThe event's idempotency key: unique in a workspace.
includeExtra data to add to the response: document. Send a comma-separated list or repeat the parameter. An unknown name is refused with 400.
limitPage size (default 50, max 200).
cursorOpaque continuation token from the previous response's pagination.cursor. Omit for the first page. Cursors are stateless and do not expire, but are only valid for the list and filters that produced them.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Where a usage event was billed › Responses
OK