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:

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

Related articles