Skip to main content

Worked usage samples

The chain below covers read the aggregate → page its window → stand up an alert rule → branch on the errors. The aggregate (GET /api/v1/messages/usage/records) is the Twilio-parity surface that rolls every billable row up per channel and direction with cost attribution — the exact numbers the usage-series and usage-records dashboard pages render. The cursor-windowed row-level feed behind the same pages (since/until/channel/cursor) lives on the usage records overlay; this page adds the aggregate and its alerting sibling.

1. Read the per-channel usage aggregate

GET /api/v1/messages/usage/records?days=30
Returns one record per billing category (sms-inbound, sms-outbound, mms-inbound, mms-outbound, voice-inbound, voice-outbound) over a trailing window. days (1–365, default 30) sizes the window; SMS counts by segment, MMS and voice by message. Currency stays in the group key, so a mid-window currency flip returns one record per currency bucket rather than a mixed sum. count and price are the cost-attribution pair — price is the debited amount in currency; sms-inbound always reads price: 0, because received SMS is free on Orbit and is never debited.
Add country_splits=1 (also accepts true / yes) to extend each record with a countries array — a per-ISO-3166 alpha-2 breakdown of country, count, and price for that category, derived from the recipient’s E.164. Pass a smaller or larger days to re-slice the window; there is no cursor here because the aggregate returns its full category set in one body. The row-level counterpart with since/until/channel/cursor pagination is documented on the usage records page.

2. Stand up an alert rule on the same signal

POST /api/v1/usage/alert-rules
The self-serve alerting sibling on this page watches the two signals that move a usage integration: delivery (sms_delivery_rate) and volume/spend (outbound_message_volume, spend). Create at most 50 rules per organization; a threshold rule needs a comparator (gt / gte / lt / lte) and a finite threshold on the metric’s native unit — percent, count, or USD.
Each fired breach lands in GET /api/v1/usage/alert-rules/events and the in-app notification dropdown. POST /api/v1/usage/alert-rules/evaluate runs the evaluator for your organization now instead of waiting for the next sweep.

3. Error branches

Two branches decide what callers do next on this surface. Validation — a malformed rule body. POST /api/v1/usage/alert-rules validates the body before anything persists. A threshold rule missing its comparator or threshold, a metric outside the registry (sms_delivery_rate, outbound_message_volume, spend), or a window_days above 30 returns 422 VALIDATION_ERROR naming the rejected field:
422
Scope — the key lacks the usage or messages scope. Reads on this page are scope-gated: the usage aggregate under messages needs a key minted with messages:read (or messages:write), and the /usage/* reads need usage:read. A dashboard session without an accepted org role, or an API key without the scope, is rejected 403 before any query runs:
403
Scope — the CDR feeds additionally gate on role and billing:read. GET /api/v1/usage/cdr and GET /api/v1/usage/cdr/export.csv are bulk billing data over the whole workspace, so both endpoints require an owner, admin, or developer role on top of the billing:read scope. A key that passes usage:read but lacks billing:read — or a session without an accepted role — is rejected 403 before any query runs:
403
Treat all of these as terminal: fix the rule body before resending a 422, and re-mint the key with the missing scope or assign an accepted role before retrying a 403. The envelope mechanics — error.code / status / details, the retry-vs-terminal decision table — live in the error-handling guide; only the usage-specific triggers above belong here.

4. Page the per-record CDR feed

GET /api/v1/usage/cdr?from&until&types&message,call
When the aggregate says the money moved, the CDR feed names the exact records that moved it — one persisted row per finished message or call, with the unit it was billed on and the price the wallet was actually charged. from and until take ISO-8601 timestamps (default window: the last 24 hours); types, channels, statuses, and direction narrow the projection. Rows page forward on pagination.cursor until has_more flips to false — the cursor is a keyset over (recorded_at, id), so records landing mid-drain never shift the window under you. Both CDR endpoints are gated to an owner, admin, or developer role with the billing:read scope.
The JSON page keys the endpoint columns on from_addr / to_addr, duration_seconds (calls) and segments (SMS); unpriced rows appear with an empty price/currency unless you pass priced_only=true. The month-end close loop around this feed — when to page versus export, how to match rows against invoice line items — is walked end to end in the CDR usage export for billing reconciliation guide.

5. Export the same window as CSV

GET /api/v1/usage/cdr/export.csv
Month-end needs a file, not pages. The export endpoint answers the same query keys as the JSON feed, but streams CSV straight to disk — pass Accept: text/csv (or just save the body), and the row order is a stable, finance-friendly column arrangement. One CSV page carries up to 50,000 rows; when the window holds more, the X-Export-Truncated: true response header flags it and you drain the remainder off the JSON endpoint.
The CSV carries the full column set (terminal timestamps, sip_response_code, error_code/error_message, contact_id, campaign_id, href) beyond the three lines shown; the JSON feed’s field-by-field semantics apply unchanged. The download is audit-logged as usage.cdr_exported.

6. List rules and disable one

GET /api/v1/usage/alert-rules · PATCH /api/v1/usage/alert-rules/{id}
Listing returns every rule in your organization — user-owned config plus the evaluator’s runtime state (last_status, last_value, last_fired_at), so one read answers both “what did we configure” and “what is it doing right now”. Disabling is a PATCH that flips enabled; the rule keeps its history and stops firing until you re-enable it.
A PATCH body must carry at least one field — an empty {} is rejected 422. Deletes are terminal (DELETE /api/v1/usage/alert-rules/{id} returns { "ok": true }); prefer disabling when you might want the rule back.

7. Ingest a rated meter rollup

POST /api/v1/usage/meters/rollup
Resellers rate a sub-tenant’s recorded usage events against a meter’s price plan and get back the per-sub-account rollup — the rows a follow-up credit note or invoice line item is built from. Events carry a quantity and an optional idempotency key; the response serialises each rated row with included/billable quantities and the tier arithmetic, so the client can reproduce the number before posting it to billing.
All money arrives in integer minor units (amount_minor = cents for USD); render it with the org currency, and post the rollup forward as a credit note via POST /api/v1/usage/meters/credit-note when you need the invoice artifact.

8. Read the AI usage rollup

GET /api/v1/usage/ai
AI spend reads differently from messaging: tokens, not segments. This endpoint rolls the billing AI tokens ledger over a window — window=24h|7d|30d, an explicit from/to ISO range, or a calendar period=YYYY-MM — and returns the billed (tenant-priced, cost-plus) total alongside the underlying provider cost so you can see what drives the number. All money is integer cents at the contract; per-agent, per-channel, and per-model breakdowns attribute the same totals down the tree.
tokens and the tokens/cost_breakdown objects on each breakdown row carry input, output, cache-read, and cache-creation classes; cache hits surface as cache_hit_ratio plus estimated savings, the cheapest spend lever to act on. Budget alerts against the same ledger are configured in the dashboard under AI settings.

9. Page the usage-event feed

GET /api/v1/usage/events?cursor=…
The row-level metering feed behind the rollups: one event per billable unit a sub-account produced, before any rating is applied. Each row carries event_type (the meter vocabulary, e.g. sms_segment, voice_minute), quantity in that meter’s unit, the subaccount_id it is attributed to, and occurred_at — the wall-clock time the unit was recorded, which is the keyset the cursor pages on.
has_more: true means more rows remain in the window; replay the cursor until it flips to false. The token is opaque — never build one client-side. Feed these rows into POST /api/v1/usage/meters/rollup (section 7) to rate them per sub-account.

10. Evaluate now and read the firing notification

POST /api/v1/usage/alert-rules/evaluate
Runs the evaluator for your organization immediately instead of waiting for the next sweep, and returns one entry per rule that fired — including the notification the breach produced, so an integration can mirror what the in-app dropdown shows.
cURL
An empty fired array is the healthy answer — every evaluated rule sat inside its threshold. The same rows land in GET /api/v1/usage/alert-rules/events for paging after the fact.

11. Read inbound cost attribution

GET /api/v1/usage/inbound-cost
The inbound counterpart of the aggregate in section 1: one record per channel and country for traffic you received, so carrier-side inbound spend can be attributed to the number that caught it. Received SMS is free on Orbit and never appears in this read. The window keys match the aggregate (days, country_splits=1).

12. Post the rollup forward as a credit note

POST /api/v1/usage/meters/credit-note
Turns a rated rollup (section 7) into the invoice artifact: the credit note echoes the rated quantities and amount it was created from, so the billing record and the rollup reconcile by id.

Per-operation response envelopes

The generator samples each operation’s <ResponseExample> from the route-annotated fences below when the OpenAPI response schema under-declares the body (the generated List … block would otherwise render the canonical { "data": {} } shell). Each section’s fence lands on the operation named in its <Note> callout; the prose belongs to the same envelope.

GET /api/v1/usage/ai

GET /api/v1/usage/ai
The AI spend rollup: cost_this_period_usd is what your tenant is billed (cost-plus markup included); actual_cost_this_period_usd is the underlying provider cost for the same window. All money fields are integer USD cents. Window selection: window=24h|7d|30d, an explicit from+to ISO-8601 pair, or a calendar period=YYYY-MM (default: current month).

GET /api/v1/usage/alert-rules

GET /api/v1/usage/alert-rules
items carries your configured rules plus the evaluator’s runtime state (last_status, last_value, last_fired_at), so one read answers both “what is configured” and “what is it seeing right now”. Money thresholds are whole USD.

GET /api/v1/usage/alert-rules/events

GET /api/v1/usage/alert-rules/events
One entry per breach the evaluator recorded, newest first — the same rows POST /usage/alert-rules/evaluate returns live. An empty items array is the healthy answer: every evaluated rule sat inside its threshold.

GET /api/v1/usage/cdr

GET /api/v1/usage/cdr
from and until take ISO-8601 timestamps (default window: the last 24 hours); types, channels, statuses, and direction narrow the projection. Rows page forward on pagination.cursor until has_more flips to false — the cursor is a keyset, so records landing mid-drain never shift the window under you.

GET /api/v1/usage/cdr/export.csv

GET /api/v1/usage/cdr/export.csv
The CSV twin of the JSON feed above: the same from / until / types / channels / statuses / direction filters, streamed back as a downloadable attachment instead of pages. One pull carries at most 50,000 rows and the endpoint answers at most 5 requests per minute; when the window holds more rows than the cap, the response sets X-Export-Truncated: true and you drain the remainder off the JSON endpoint. The header row lists every column in the stable order shown below — the same row fields as the JSON feed, so a CSV decoder and a JSON walker agree column-for-column — and empty cells are genuinely null fields, not missing values. Every pull is audit-logged as usage.cdr_exported with the row count, the window, and the truncation flag.
200

GET /api/v1/usage/inbound-cost

GET /api/v1/usage/inbound-cost
One line per channel and billing unit for traffic you received — MMS counted by message, voice by carrier-rounded minute — so carrier-side inbound spend is visible next to the traffic that produced it. Received SMS is free on Orbit: it is never metered or charged, so no sms line ever appears. This is reporting only: nothing here debits your wallet. The window keys match the AI rollup.

POST /api/v1/usage/alert-rules

POST /api/v1/usage/alert-rules
A threshold rule needs a comparator (gt / gte / lt / lte) and a threshold in the metric’s native unit — percent, count, or whole USD. The metrics registry is sms_delivery_rate, outbound_message_volume, and spend; window_days caps at 30. Notification delivery is in-app (notify_channels: ["in_app"]); each breach additionally lands in GET /usage/alert-rules/events for paging after the fact. At most 50 rules per organization.

POST /api/v1/usage/alert-rules/evaluate

POST /api/v1/usage/alert-rules/evaluate
Runs the evaluator for your organization immediately instead of waiting for the next sweep. breached counts rules outside their threshold this run; fired counts those that also passed their cooldown and so produced a breach event plus an in-app notification. This is a live run, not a side-effect-free dry-run: to preview whether a rule would fire, read its runtime state on GET /usage/alert-rules (last_value against threshold and comparator) before calling evaluate.

POST /api/v1/usage/meters/credit-note

POST /api/v1/usage/meters/credit-note
The credit note echoes the rated quantities and amount it was created from, plus any adjustment lines, so the billing record and the rollup reconcile line-by-line: negative amount_minor values on lines are credits that offset the billed rows, and total_minor is what lands on the sub-account’s balance. All money fields are integer minor units.

POST /api/v1/usage/meters/rollup

POST /api/v1/usage/meters/rollup
Rates a batch of recorded usage events against the meter’s price plan and returns the per-sub-account rollup — the rows a follow-up credit note or invoice line item is built from. billing.ledger_entries are write descriptors (the debits a reseller would commit per sub-account, base amount plus reseller_margin_pct), not committed writes; billing.invoice previews the rendered invoice artifact. All money is integer minor units, and the tiers array lets a client reproduce every amount_minor before posting anything to billing.

PATCH /api/v1/usage/alert-rules/{id}

PATCH /api/v1/usage/alert-rules/{id}
Returns the full rule after the merge. Use PATCH to pause a rule ("enabled": false, shown below) rather than deleting it — fired events already recorded stay readable on GET /usage/alert-rules/events either way.

DELETE /api/v1/usage/alert-rules/{id}

DELETE /api/v1/usage/alert-rules/{id}
Deletes are terminal — the rule and its runtime state are removed from your organization. Prefer PATCH with "enabled": false when you might want the rule back.

Worked sequence: track usage, set an alert, and export the CDR

The per-operation blocks above show each route in isolation; this sequence chains them the way a finance/ops integration actually does: read the AI spend rollup before the month closes, turn a usage spike into an alert, then reconcile the invoice against the CDR. Every money field on this surface is an integer in minor units (spend_minor: 50000 = $500.00), and every read returns only your own tenant’s data.

Forecast AI spend before the month closes

GET /api/v1/usage/ai?period=2026-09 rolls the billing AI-token ledger over a calendar month (or window=24h|7d|30d, or an explicit from+to range) and returns what you were billed alongside what the underlying providers cost, per agent and per model:
tokens splits the count by class (input, output, cache-read, cache-creation) and breakdown.by_model attributes the same spend per model — cost_usd is what you were billed, actual_cost_usd the underlying provider cost, and the four cost_breakdown classes sum to the billed figure within rounding. Call once per period to compare months; the window keys select which period. For a deeper read on trending this signal, see the stats API guide.

Turn a usage spike into an alert

Create a threshold rule on spend — the metric registry also covers sms_delivery_rate and outbound_message_volume — then watch its runtime state. Notification delivery is in-app today; each breach additionally lands in GET /api/v1/usage/alert-rules/events where your own integration can page it (see usage anomaly alert rules for the full rule model):
GET /api/v1/usage/alert-rules then returns the rule with its evaluator state, and POST /api/v1/usage/alert-rules/evaluate runs the sweep now instead of waiting for the next scheduled pass:
This list read is the dry-run: last_value: 612.75 against comparator: "gt" / threshold: 500 means the rule would fire on the next sweep (last_status: "breached"). Evaluate is the live fire — it records breach events and notifications for rules outside their threshold, and answers how many:
200

Reconcile the invoice against the CDR

When the alert says the money moved, the CDR names the records that moved it. Page the JSON feed for dashboards; pull the CSV when the month closes:
cURL
The JSON page (shape in List cdr above) drains on pagination.cursor; the export answers with headers and rows, one CSV page per 50,000 rows:
200
price on each row is the amount actually debited, so summing price over the window reconciles against the invoice line. The full close loop is walked in the CDR export reconciliation guide.

Meter rollups and credit notes

Resellers rate recorded usage events per sub-account and post the result forward as a credit note. POST /api/v1/usage/meters/rollup takes a meter and an event batch:
and answers the tiered rating plus the per-sub-account billing descriptors (full shape in Rollup meters above). POST /api/v1/usage/meters/credit-note turns the rated rows into the invoice artifact, including the adjustment lines:

Inbound-cost audit

The inbound counterpart attributes carrier-side received spend to the country that caught it — the read behind “which of our numbers is expensive to receive on”:
cURL
MMS is priced per message and voice per carrier-rounded minute; received SMS is free on Orbit and never metered, so it carries no line. pricing_source names the rate-ladder rung that priced each line.

Errors worth branching on

A malformed rule body returns 422 VALIDATION_ERROR naming the rejected field; a key without the usage:read scope returns 403. Both are terminal — fix the body or re-mint the key before retrying. The decision table for retrying versus surfacing lives in the error handling guide; the alerting rule model is covered in usage anomaly alert rules, and trending the same signals in the stats API guide.

Reconcile usage to your invoice — a worked chain

The most common finance/ops integration against this surface is paginate-and-aggregate: page the per-record feed over a billing window, sum the priced rows per day and channel, and match the total to the invoice line. This section walks that chain end to end on one running example (September 2026, outbound traffic), then names the pitfalls that bite reconcilers. Every read on this page returns only your own organization’s rows, so a reconciliation job never sees another tenant’s traffic.

R1. Pick your window and read the first page

GET /api/v1/usage/cdr?from=2026-09-01T00:00:00Z&until=2026-09-02T00:00:00Z&types=message,call&limit=50
from/until are ISO-8601 bounds on recorded_at, the timestamp the platform billed the event; one call per UTC day gives you day grain, and the page below is exactly one day. Build on data.pagination: has_more says whether another page exists in the window, and cursor is the opaque token that fetches it. Each data.data row is one billable event at finalised price — that is the grain you sum.
The full row shape, including the voice and WhatsApp rows this page omits, is shown in section 4 above; a null price means the row is not priced yet, and priced_only=true drops unpriced rows from the projection.

R2. Drill to a channel

Repeat the R1 call with channels=sms (the filter takes the billing vocabulary sms, mms, whatsapp, email, voice; combine with direction=outbound to drop inbound rows a cost reconciliation would double-count). Which fields carry the billed quantity depends on the channel:
  • sms / mms — segments is the billed unit count (2 on a multipart message); price covers all segments on the row.
  • whatsapp — one row per message; segments is null and price is the per-message price.
  • voice — duration_seconds is the billed length (carrier-rounded minutes); started_at / answered_at / ended_at time the call.
  • email — per-message rows on the row-level feed; see the usage-records page for its column legend.
AI spend does not appear on this feed: it bills by token, not by message, and reconciles through the GET /api/v1/usage/ai rollup in section 8 above.

R3. Get unit-level records for the same window

GET /api/v1/usage-records?since=2026-09-01T00:00:00Z&until=2026-09-02T00:00:00Z&kinds=message,call&limit=50
GET /api/v1/usage-records is the row-level companion feed: one row per billable event, folded into the finance-workbook shape (endpoint instead of a from/to pair, units instead of segments). The id ties a row back to this page’s CDR feed — msg_4f8c2a91 is the same message on both sides, priced identically:
200
Use the CDR feed when you need the address pair and call timestamps; use usage-records when the downstream sheet wants one neutral endpoint column. The two agree on (id, price, currency) for the same window.

R4. Reconcile to the invoice

Sum price over every page of the window, per day and channel, then compare with the period’s invoice from GET /api/v1/billing/invoices. The Node walker below drains the cursor and accumulates totals; the cURL+jq equivalent after it does the same for a shell-only reconciler.
On the running example the September 1 page sums to 0.0318 (sms) + 0.0846 (voice) + 0.0052 (whatsapp) = 0.1216 USD; repeat per day and the month’s per-channel sums are what the messaging and voice lines on the September invoice must equal. The invoice reports amount_minor in integer minor units while feed rows carry sub-minor precision, so round the summed total to the nearest minor unit before comparing; a gap wider than one minor unit means the filters drifted, not the ledger. The console version of this close loop is walked in the CDR usage export for billing reconciliation guide.

R5. Pitfalls that bite reconcilers

  • Timezone boundaries. recorded_at and the usage-records ts are UTC. If your finance calendar cuts days at Europe/Amsterdam midnight, convert those bounds to UTC before calling, or rows in the offset window land on the wrong day and the month never ties out. The invoice period is UTC; reconcile in UTC and present locally.
  • Sandbox isolation. Keys prefixed dv_test_sk_ see only sandbox traffic and deduct no credits, so a reconciler built and tested on a test key reconciles sandbox numbers by construction; production rows only ever appear under a live key. There is no cross-key bleed to filter out.
  • Cursor discipline. A cursor is an opaque keyset tied to the exact filter set that issued it. Fix from/until/types/channels before the first page and keep them for the whole drain; changing filters mid-drain and reusing the old cursor silently double-counts or skips rows. Never construct a cursor client-side, and persist the last cursor per window so a crashed job resumes instead of restarting.