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=30sms-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.
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-rulessms_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.
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
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
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
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,callfrom 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.
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.csvAccept: 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.
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}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.
{} 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/rollupamount_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/aiwindow=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=…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/evaluatecURL
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-costdays, country_splits=1).
12. Post the rollup forward as a credit note
POST /api/v1/usage/meters/credit-notePer-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/aicost_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-rulesitems 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/eventsPOST /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/cdrfrom 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.csvfrom / 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-costsms 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-rulesgt / 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/evaluatebreached 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-noteamount_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/rollupbilling.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}"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}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 onspend — 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:
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
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:
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
pricing_source names the rate-ladder rung that priced each line.
Errors worth branching on
A malformed rule body returns422 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=50from/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.
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 withchannels=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—segmentsis the billed unit count (2 on a multipart message);pricecovers all segments on the row.whatsapp— one row per message;segmentsisnullandpriceis the per-message price.voice—duration_secondsis the billed length (carrier-rounded minutes);started_at/answered_at/ended_attime the call.email— per-message rows on the row-level feed; see the usage-records page for its column legend.
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=50GET /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
endpoint column. The two agree on (id, price, currency) for the same window.
R4. Reconcile to the invoice
Sumprice 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.
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_atand the usage-recordstsare UTC. If your finance calendar cuts days atEurope/Amsterdammidnight, 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/channelsbefore 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.