Usage meters: definitions, threshold breaches, and consuming products.
List meters
Returns a paginated list of meters. Supports the unified field.op=value filter grammar.
Required scope: meter.read (Read meters).
query Parameters
created_atFilter on created_at (date-time). Operators: eq, gt, gte, lt, lte — dot grammar, e.g. created_at.gt=value; a bare created_at=value means eq.
functionFilter on function (enum). Operators: eq, in — dot grammar, e.g. function.in=value; a bare function=value means eq. A bare comma-separated value is in-sugar: function=a,b means function.in=a,b. Legal values: SUM, COUNT, MAX, MIN, AVG, UNIQUE_COUNT, LAST, P95.
keyFilter on key (string). Operators: eq, in, contains — dot grammar, e.g. key.in=value; a bare key=value means eq. A bare comma-separated value is in-sugar: key=a,b means key.in=a,b.
late_event_policyFilter on late_event_policy (enum). Operators: eq, in — dot grammar, e.g. late_event_policy.in=value; a bare late_event_policy=value means eq. A bare comma-separated value is in-sugar: late_event_policy=a,b means late_event_policy.in=a,b. Legal values: CLAMP_TO_PERIOD.
metric_keyFilter on metric_key (string). Operators: eq, in, contains — dot grammar, e.g. metric_key.in=value; a bare metric_key=value means eq. A bare comma-separated value is in-sugar: metric_key=a,b means metric_key.in=a,b.
nameFilter on name (string). Operators: eq, contains — dot grammar, e.g. name.contains=value; a bare name=value means eq.
updated_atFilter on updated_at (date-time). Operators: eq, gt, gte, lt, lte — dot grammar, e.g. updated_at.gt=value; a bare updated_at=value means eq.
limitPage size. Values above the server-side cap are clamped (standard default 50, cap 200; a few document-heavy lists use larger windows). Invalid values fall back to the default.
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.
searchCase-insensitive substring match over key, name, and metric_key. Filterable fields (key, name, metric_key, function, late_event_policy, created_at, updated_at) accept apifilter operator suffixes
sortOrdering: created_at, name, metric_key, function or updated_at, each with an optional :asc/:desc suffix (default created_at:desc). An unknown value is rejected. Cursors are bound to the ordering that issued them, so changing sort mid-walk needs a fresh first page.
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 meters › Responses
OK
Create a meter
Creates an org-scoped meter usable by any product.
Required scope: meter.write (Manage meters).
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.
Create a meter › Request Body
functionAggregation applied over the window: SUM, COUNT, MAX, MIN, AVG, UNIQUE_COUNT, LAST, or P95.
keyOrg-unique slug identifying the meter; lowercase letter then [a-z0-9_-], 1-64 chars.
metric_keyEvent ingestion key this meter aggregates over.
nameHuman-readable label for the meter.
When set, defines this as a derived meter computed from other meters via a formula. An empty object is the same as omitting it.
dedup_key_pathJSON-path to the event field used for deduplication; default uses the event's idempotency_key.
dimensionsEvent metadata fields exposed for group-by.
event_schemaJSON-Schema document validating ingested events; null or omitted disables validation. Must compile as a JSON Schema — a document that does not is rejected here rather than failing every later ingest.
Predicate tree restricting which events the meter includes.
late_event_policyWhat ingest does with an event dated before its subscription's current billing period: omitted refuses it (422 USAGE_PERIOD_CLOSED); CLAMP_TO_PERIOD stores a positive one at the start of the current period, where the next invoice bills it.
How the billable quantity is snapped before pricing (increment + mode). Omitted means exact: priced at the metered precision, up to 8 decimal places.
Alert conditions evaluated against the meter's output.
unique_by_fieldEvent field whose distinct values are counted; required for UNIQUE_COUNT.
unit_currency_fieldEvent field carrying the currency when the metric measures money.
unit_labelDisplay label for one metric unit (e.g. "calls").
value_fieldEvent field aggregated; required for SUM/MAX/MIN/AVG/P95/LAST, forbidden for COUNT/UNIQUE_COUNT.
Reset cadence for the aggregation; null aggregates over the whole billing period.
Create a meter › Responses
Created
List threshold-breach events across every meter in the org
The org-wide breach inbox: threshold-breach events across every meter, newest first, keyset-paginated.
Required scope: meter.read (Read meters).
query Parameters
limitPage size. Values above the server-side cap are clamped (standard default 50, cap 200; a few document-heavy lists use larger windows). Invalid values fall back to the default.
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.
List threshold-breach events across every meter in the org › Responses
OK
Test one event against a filter tree and event schema
Answers two authoring questions about one event, using the production implementations of both. Would this filter count it: the tree is compiled by the same compiler that builds the invoice's WHERE clause and run against a one-row synthetic usage_events, so every operator, cast and NULL rule is Postgres' own; per-leaf verdicts say which leaf failed and whether it failed because the value differed, because the path is absent, or because the leaf is not evaluable at all (a gt against a non-numeric value, a malformed jsonpath, an invalid regex). Would THIS schema accept it: the supplied JSON Schema is compiled by the same jsonschema compiler ingest uses and validated against the event's metadata. That is narrower than what ingest does, deliberately - ingest validates an event against EVERY meter in the org sharing its metric_key, and this endpoint is asked from the create sheet where the meter does not exist yet, so it takes the schema as a document and answers about that one document. A green answer here means this schema accepts the event; it is not a promise that no OTHER meter's schema rejects it. The tree and the schema travel in the BODY for the same reason: there is no meter id to reference while authoring.
Required scope: meter.read (Read meters).
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.
Test one event against a filter tree and event schema › Request Body
A usage-event-shaped object, read one of two ways. Send a metadata object (optionally with dimension_vars) and it is treated as an ingest BODY: metadata is bound to the usage_events metadata column the filter compiler reads, dimension_vars to its own column, and every other top-level field is envelope no filter can reach. Send a flat object and it IS the metadata — the properties, without the envelope around them. Two or more envelope-only field names (customer_id, product_id, subscription_id, metric_key, price_key, idempotency_key, hold_id, wallet_currency, price/cost fields) also mark it a body; a single one does not, so a bag holding quantity or timestamp stays properties. The response reports which reading was taken in event_interpreted_as.
event_schemaJSON Schema to validate the event against, as a document rather than a meter reference, for the same reason. Omit to skip schema checking.
The predicate to test. Sent in the body rather than referenced by meter id because the question is asked while AUTHORING — in the create sheet where no meter exists yet, and on unsaved draft trees in the filter workbench. An empty tree matches everything.
Test one event against a filter tree and event schema › Responses
OK
Test one event against a filter tree and event schema
Deprecated: use POST /meters/preview instead. Removed on 2026-11-30.
Answers two authoring questions about one event, using the production implementations of both. Would this filter count it: the tree is compiled by the same compiler that builds the invoice's WHERE clause and run against a one-row synthetic usage_events, so every operator, cast and NULL rule is Postgres' own; per-leaf verdicts say which leaf failed and whether it failed because the value differed, because the path is absent, or because the leaf is not evaluable at all (a gt against a non-numeric value, a malformed jsonpath, an invalid regex). Would THIS schema accept it: the supplied JSON Schema is compiled by the same jsonschema compiler ingest uses and validated against the event's metadata. That is narrower than what ingest does, deliberately - ingest validates an event against EVERY meter in the org sharing its metric_key, and this endpoint is asked from the create sheet where the meter does not exist yet, so it takes the schema as a document and answers about that one document. A green answer here means this schema accepts the event; it is not a promise that no OTHER meter's schema rejects it. The tree and the schema travel in the BODY for the same reason: there is no meter id to reference while authoring.
Required scope: meter.read (Read meters).
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.
Test one event against a filter tree and event schema › Request Body
A usage-event-shaped object, read one of two ways. Send a metadata object (optionally with dimension_vars) and it is treated as an ingest BODY: metadata is bound to the usage_events metadata column the filter compiler reads, dimension_vars to its own column, and every other top-level field is envelope no filter can reach. Send a flat object and it IS the metadata — the properties, without the envelope around them. Two or more envelope-only field names (customer_id, product_id, subscription_id, metric_key, price_key, idempotency_key, hold_id, wallet_currency, price/cost fields) also mark it a body; a single one does not, so a bag holding quantity or timestamp stays properties. The response reports which reading was taken in event_interpreted_as.
event_schemaJSON Schema to validate the event against, as a document rather than a meter reference, for the same reason. Omit to skip schema checking.
The predicate to test. Sent in the body rather than referenced by meter id because the question is asked while AUTHORING — in the create sheet where no meter exists yet, and on unsaved draft trees in the filter workbench. An empty tree matches everything.
Test one event against a filter tree and event schema › Responses
OK
List metric keys arriving for no meter
Usage events whose metric_key matches no meter are accepted (201) and aggregated by nothing - a silent revenue leak, because neither a meter's key nor its metric_key can be renamed after creation. This returns the keys that happened to, biggest first, with how many events each swallowed and over what window. Filled hourly by the unmetered_metric_key_scan sweep and bounded at 100 rows per workspace (metric_key is client-supplied and unbounded, so the board keeps the highest-volume findings, not every distinct key). The fix for an entry is to create a meter carrying that exact metric_key: the evaluator reads raw usage_events over the billing period, so events already stored start billing.
Required scope: meter.read (Read meters).
query Parameters
include_resolvedAlso return keys a meter now exists for. Those rows are the re-bill worksheet: they say how much usage arrived before the meter did.
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 metric keys arriving for no meter › Responses
OK
Get a meter
Returns a single org-scoped meter with its aggregation configuration (function, value field, filter tree, window, composed sources).
Required scope: meter.read (Read meters).
path Parameters
idMeter UUID
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.
Get a meter › Responses
OK
Delete a meter
Fails with 409 if any product binding still references this meter.
Required scope: meter.write (Manage meters).
path Parameters
idMeter UUID
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.
Delete a meter › Responses
No Content
Update a meter
Merge-patch update. Meters are shared, de-versioned entities: billing-relevant fields (function, value_field, filter_tree, window, unique_by_field, composed_of) go live immediately and trigger a republish of every ACTIVE product consuming the meter (see /meters/{id}/consumers).
Required scope: meter.write (Manage meters).
path Parameters
idMeter UUID
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.
Update a meter › Request Body
New derived-meter formula; {} clears it (the meter becomes a plain one); omitted leaves it unchanged. null is indistinguishable from omission on the wire and also leaves it unchanged.
dedup_key_pathNew dedup JSON-path; null clears it (dedup falls back to the event's idempotency_key); omitted leaves it unchanged.
dimensionsNew group-by dimension fields; omitted leaves them unchanged.
event_schemaNew event JSON-Schema; null clears it (ingest validation off); omitted leaves it unchanged. Must compile as a JSON Schema.
New event predicate tree; omitted leaves it unchanged.
functionNew aggregation function; omitted leaves it unchanged.
late_event_policyNew late-event policy; null clears it back to unset; omitted leaves it unchanged.
Requests also accept the new spelling: each value in lower case (clamp_to_period for CLAMP_TO_PERIOD). The listed spelling is deprecated in requests and accepted until 2026-11-30.
nameNew human-readable label; omitted leaves it unchanged.
quantity_roundingNew billable-quantity rounding (increment + mode); null clears it back to exact; omitted leaves it unchanged. A snapshot field: saving it republishes every consuming product, like a function or filter edit.
New alert thresholds; [] clears them; omitted leaves them unchanged.
unique_by_fieldNew distinct-count event field; null clears it; omitted leaves it unchanged.
unit_currency_fieldNew unit-currency event field; null clears it; omitted leaves it unchanged.
unit_labelNew unit display label; null clears it; omitted leaves it unchanged.
value_fieldNew aggregated event field; null clears it (a COUNT or UNIQUE_COUNT has none); omitted leaves it unchanged.
New aggregation window; omitted leaves it unchanged.
Update a meter › Responses
OK
List bindings for a meter
Returns every product-meter binding that references the meter, across products. The meter is sent on each binding without include=meter until 2026-11-30; after that only with include=meter.
Required scope: meter.read (Read meters).
path Parameters
idMeter UUID
query Parameters
includeExtra data to add to the response: meter. Send a comma-separated list or repeat the parameter. An unknown name is refused with 400. meter is sent without being asked until 2026-11-30; after that date it is sent only when named here.
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 bindings for a meter › Responses
OK
Count a meter's breaches per day
Daily count of threshold-breach events for one meter over a recent window, one row per day that had any.
Required scope: meter.read (Read meters).
path Parameters
idMeter UUID
query Parameters
daysHow many days back from today to count (1-90, default 30). A value outside that range, or not a whole number, is refused with 400.
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.
Count a meter's breaches per day › Responses
OK
List threshold-breach events for a meter
Reads from the shared domain_event_outbox; newest first; capped at the standard pagination limit.
Required scope: meter.read (Read meters).
path Parameters
idMeter UUID
query Parameters
limitPage size. Values above the server-side cap are clamped (standard default 50, cap 200; a few document-heavy lists use larger windows). Invalid values fall back to the default.
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.
List threshold-breach events for a meter › Responses
OK
Daily breach counts for a meter
Deprecated: use GET /meters/{id}/breach-counts instead. Removed on 2026-11-30.
Daily count of threshold-breach events for one meter over a recent window, one row per day that had any.
Required scope: meter.read (Read meters).
path Parameters
idMeter UUID
query Parameters
daysHow many days back from today to count (1-90, default 30). A value outside that range, or not a whole number, is refused with 400.
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.
Daily breach counts for a meter › Responses
OK
List products consuming a meter
The consumer census: every ACTIVE product that bills through this meter (via a live binding, or a composed-source snapshot at the product's head version).
Required scope: meter.read (Read meters).
path Parameters
idMeter UUID
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 products consuming a meter › Responses
OK
Evaluate a meter for one subscription
Deprecated: use POST /meters/{id}/preview instead. Removed on 2026-11-30.
Runs the meter for one subscription through the evaluator and the product-version snapshot the invoice uses, and returns the buckets it produced (one per reset window, price_key and wallet_currency), the reset windows the period tiled into and the aggregation definition that produced them. It needs a subscription because usage events are keyed by product and subscription: the same meter under two products measures two different numbers. billed is what the invoice will carry (the product-version snapshot). head is what the meter as it is now would carry; no invoice reads it until the consuming products are republished. Nothing is stored and no threshold notification is sent.
Required scope: meter.read (Read meters).
path Parameters
idMeter UUID
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.
Evaluate a meter for one subscription › Request Body
subscription_idSubscription to evaluate against. Required: usage_events are keyed by (product, subscription), so "what does this meter compute" is not a well-formed question without one.
fromInclusive start. Defaults to the subscription's current period start; required when the subscription has no current period (a trial before its first paid phase, a deal with no paid phase).
include_headAlso evaluate the LIVE meter row alongside the billed snapshot, so an unpublished edit is visible as a difference. Defaults to true; set false to skip the second aggregation.
product_idWhich of the subscription's metered products to scope to. Optional when exactly one of them bills through this meter; required when more than one does.
toExclusive end. Defaults to the subscription's current period end; required when the subscription has no current period.
Evaluate a meter for one subscription › Responses
OK
Preview a meter for one subscription
Runs the meter for one subscription through the evaluator and the product-version snapshot the invoice uses, and returns the buckets it produced (one per reset window, price_key and wallet_currency), the reset windows the period tiled into and the aggregation definition that produced them. It needs a subscription because usage events are keyed by product and subscription: the same meter under two products measures two different numbers. billed is what the invoice will carry (the product-version snapshot). head is what the meter as it is now would carry; no invoice reads it until the consuming products are republished. Nothing is stored and no threshold notification is sent.
Required scope: meter.read (Read meters).
path Parameters
idMeter UUID
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.
Preview a meter for one subscription › Request Body
subscription_idSubscription to evaluate against. Required: usage_events are keyed by (product, subscription), so "what does this meter compute" is not a well-formed question without one.
fromInclusive start. Defaults to the subscription's current period start; required when the subscription has no current period (a trial before its first paid phase, a deal with no paid phase).
include_headAlso evaluate the LIVE meter row alongside the billed snapshot, so an unpublished edit is visible as a difference. Defaults to true; set false to skip the second aggregation.
product_idWhich of the subscription's metered products to scope to. Optional when exactly one of them bills through this meter; required when more than one does.
toExclusive end. Defaults to the subscription's current period end; required when the subscription has no current period.
Preview a meter for one subscription › Responses
OK