Usage Reports and Message Detail Records (MDR)
The reports API turns your message history into usage per phone number — message counts, segments, delivery outcomes, and cost — without you having to page through every message yourself. There are two endpoints:
GET /reports/usage— usage aggregated per phone number over a date range. One row per number.GET /reports/messages— the underlying detail records (the classic MDR), one row per message.
Both are computed on demand from the same message history that powers GET /messaging/history, so they work retroactively over data you already have. For the dashboard equivalents, see Messaging Reports and Debugging Tools; for plan limits, see Understanding Quotas and Usage.
Before you start
Both endpoints require an API key with the messages.read permission, and your account must have the sms capability (granted by default on every plan). Create and scope keys under Developer → API Keys — see Managing API Keys.
Aggregated usage per number
GET /reports/usage returns one row per owned number, plus account-wide totals.
curl "https://api.otterblocx.com/reports/usage?start=2026-05-01&end=2026-05-31" \
-H "x-access-key-id: $BLOCX_ACCESS_KEY_ID" \
-H "x-secret-access-key: $BLOCX_SECRET_ACCESS_KEY"
{
"range": { "start": "2026-05-01T00:00:00.000Z", "end": "2026-05-31T23:59:59.999Z" },
"rows": [
{
"phoneNumber": "+13105551234",
"provider": "TELNYX",
"capabilities": ["SMS", "MMS"],
"status": "ACTIVE",
"messages": 1915,
"outbound": 1820,
"inbound": 95,
"segments": 2140,
"delivered": 1790,
"failed": 30,
"costMicros": "4280000",
"carrierCostMicros": "640000",
"totalCostMicros": "4920000"
}
],
"totals": {
"phoneNumbers": 1,
"messages": 1915,
"outbound": 1820,
"inbound": 95,
"segments": 2140,
"delivered": 1790,
"failed": 30,
"costMicros": "4280000",
"carrierCostMicros": "640000",
"totalCostMicros": "4920000"
},
"truncated": false
}
| Field | Meaning |
|---|---|
phoneNumber |
The owned number this row aggregates (sender for outbound, recipient for inbound). |
provider, capabilities, status |
Pulled from your number inventory. null/empty if the number has since been released. |
messages, outbound, inbound |
Message counts in the range. |
segments |
Total billed segments (a long SMS is several segments). |
delivered, failed |
Outbound delivery outcomes. |
costMicros |
Per-segment message charges (microdollars). |
carrierCostMicros |
Carrier pass-through surcharges (microdollars). |
totalCostMicros |
costMicros + carrierCostMicros. |
Microdollars. All amounts are integers-as-strings in microdollars, where 1 USD = 1,000,000. Divide by 1,000,000 for dollars:
"4920000"→$4.92. We use strings so large totals never lose precision.
Per-message detail records
GET /reports/messages returns one row per message — the line-item MDR you'd reconcile against an invoice. JSON responses are cursor-paginated: follow nextToken until it comes back null.
curl "https://api.otterblocx.com/reports/messages?from=+13105551234&start=2026-05-01&end=2026-05-31&limit=100" \
-H "x-access-key-id: $BLOCX_ACCESS_KEY_ID" \
-H "x-secret-access-key: $BLOCX_SECRET_ACCESS_KEY"
{
"messages": [
{
"messageId": "msg_01J9Z8X2Q0",
"direction": "OUTBOUND",
"type": "SMS",
"status": "DELIVERED",
"from": "+13105551234",
"to": "+14155550101",
"segments": 2,
"priceCode": "MESSAGE_SMS_OUTBOUND",
"costMicros": "4760",
"carrierCostMicros": "700",
"totalCostMicros": "5460",
"createdAt": "2026-05-15T12:01:03.118Z",
"sentAt": "2026-05-15T12:01:03.642Z",
"deliveredAt": "2026-05-15T12:01:09.004Z",
"failedAt": null
}
],
"nextToken": "eyJwayI6InRlbmFudCMxIn0"
}
To pull a full range, loop on the cursor:
let nextToken: string | undefined
do {
const url = new URL('https://api.otterblocx.com/reports/messages')
url.search = new URLSearchParams({
from: '+13105551234',
start: '2026-05-01',
end: '2026-05-31',
limit: '200',
...(nextToken ? { nextToken } : {}),
}).toString()
const res = await fetch(url, {
headers: {
'x-access-key-id': process.env.BLOCX_ACCESS_KEY_ID!,
'x-secret-access-key': process.env.BLOCX_SECRET_ACCESS_KEY!,
},
}).then((r) => r.json())
for (const m of res.messages) console.log(m.messageId, m.totalCostMicros)
nextToken = res.nextToken ?? undefined
} while (nextToken)
Filtering
| Parameter | Applies to | Notes |
|---|---|---|
start, end |
both | YYYY-MM-DD or ISO 8601. Defaults to the trailing 30 days; capped at 92 days. The applied range is returned in range. |
phoneNumber |
both | Restrict to a single owned number (E.164). |
direction |
both | OUTBOUND or INBOUND. |
from, to |
/reports/messages |
Filter by sender / recipient. Most efficient way to drill into one number. |
status |
/reports/messages |
QUEUED, SENT, DELIVERED, FAILED, RECEIVED. |
tag |
/reports/messages |
Any custom label you set at send time — handy for per-team cost allocation. |
Requesting a window wider than 92 days isn't an error; the start is clamped forward and the real range is reflected in the response range.
CSV export
Add format=csv to either endpoint to download a spreadsheet-ready file instead of JSON:
curl "https://api.otterblocx.com/reports/usage?start=2026-05-01&end=2026-05-31&format=csv" \
-H "x-access-key-id: $BLOCX_ACCESS_KEY_ID" \
-H "x-secret-access-key: $BLOCX_SECRET_ACCESS_KEY" \
-o usage-by-number.csv
CSV exports cover the full range (they don't paginate). If an export hits the internal scan safety cap, the response sets X-Report-Truncated: true — narrow the date range and export in slices when you see it.
Using the SDK
The official SDK exposes these as the Reports service (regenerate/update @otterlabs/blocx to pick up new endpoints):
import { createBlocxClient, Reports } from '@otterlabs/blocx'
const { client } = createBlocxClient({
accessKeyId: process.env.BLOCX_ACCESS_KEY_ID!,
secretAccessKey: process.env.BLOCX_SECRET_ACCESS_KEY!,
})
const { data } = await Reports.getUsageReport({
client,
query: { start: '2026-05-01', end: '2026-05-31' },
})
for (const row of data.rows) {
console.log(row.phoneNumber, Number(row.totalCostMicros) / 1_000_000)
}
Detail records are Reports.getMessageDetailRecords({ client, query: { ... } }).
Good to know
- Computed on demand. Reports read your message history at request time — no rollup lag, and they cover data sent before you started using this API.
- Cost is split.
costMicrosis the per-segment charge that lands at send time;carrierCostMicrosis the carrier pass-through surcharge that arrives on the delivery receipt.totalCostMicrossums them. - Carrier cost is forward-looking. Carrier surcharges are recorded per message from the point this feature shipped onward. Messages finalized before then report
0carrier cost; their per-segment charge is unaffected, and account-level billing is unchanged. - Inbound is free. Inbound messages count toward volume but carry no per-segment or carrier charge.