> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Worked reconciliation chains

> Filter the paginated usage feed, page through its cursor, export the same filters as a billing-period CSV, and reconcile the rows against the aggregate records feed.

## Worked reconciliation chains

One row per billed event: a message (`kind: "message"`) or a call detail record (`kind: "call"`). The row-level feed below is the parent of the per-category aggregate — the chains here cover **read → page → export → reconcile**. Copy a request as written and compare the response envelope.

### 1. Read the JSON feed with filters

<Note>
  `GET /api/v1/usage-records?since=…&until=…&kinds=…&channels=…`
</Note>

Every filter narrows the same row set on both arms (messages and calls). `kinds` picks `message` and/or `call`; `channels` takes the inbox vocabulary (`sms`, `mms`, `whatsapp`, `email`, `voice` — `voice` selects the calls arm); `statuses` accepts the open union of call and message statuses; `endpoint` matches the remote party; `direction` narrows to `inbound` or `outbound`; `since`/`until` is an ISO-8601 window on the billing event time.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://api.orbit.devotel.io/api/v1/usage-records\
  ?since=2026-08-01T00:00:00Z&until=2026-08-31T23:59:59Z\
  &kinds=message&channels=sms&direction=outbound&statuses=delivered&limit=25" \
    -H "X-API-Key: dv_live_sk_your_key_here"
  ```

  ```typescript Node.js theme={null}
  const url = new URL('https://api.orbit.devotel.io/api/v1/usage-records')
  url.searchParams.set('since', '2026-08-01T00:00:00Z')
  url.searchParams.set('until', '2026-08-31T23:59:59Z')
  url.searchParams.set('kinds', 'message')
  url.searchParams.set('channels', 'sms')
  url.searchParams.set('direction', 'outbound')
  url.searchParams.set('statuses', 'delivered')
  url.searchParams.set('limit', '25')

  const res = await fetch(url, { headers: { 'X-API-Key': process.env.ORBIT_API_KEY! } })
  console.log(await res.json())
  ```

  ```python Python theme={null}
  import os, requests

  params = {
      "since": "2026-08-01T00:00:00Z",
      "until": "2026-08-31T23:59:59Z",
      "kinds": "message",
      "channels": "sms",
      "direction": "outbound",
      "statuses": "delivered",
      "limit": 25,
  }
  headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
  r = requests.get("https://api.orbit.devotel.io/api/v1/usage-records", params=params, headers=headers)
  print(r.json())
  ```
</CodeGroup>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "data": [
        {
          "id": "msg_2f8a1c9b",
          "kind": "message",
          "channel": "sms",
          "direction": "outbound",
          "endpoint": "+14155550123",
          "status": "delivered",
          "units": 1,
          "price": 0.0083,
          "currency": "USD",
          "ts": "2026-08-14T21:07:12.441Z"
        },
        {
          "id": "cl_7d0e2b4a",
          "kind": "call",
          "channel": "voice",
          "direction": "outbound",
          "endpoint": "+14455550098",
          "status": "completed",
          "units": 1,
          "price": 0.012,
          "currency": "USD",
          "ts": "2026-08-14T20:31:07.102Z"
        }
      ],
      "pagination": {
        "cursor": "ur_hw2v4tHx9Q3mGkNw",
        "has_more": true
      }
    },
    "meta": {
      "request_id": "req_usr_feed1",
      "timestamp": "2026-09-09T12:00:00.000Z"
    }
  }
  ```
</ResponseExample>

### 2. Page through the cursor

`has_more` in `data.pagination` answers whether another page exists; pass `data.pagination.cursor` back as the `cursor` query parameter. A replayed or tampered cursor degrades to the first page again rather than failing, so polling restarts always make progress.

```bash cURL theme={null}
curl -X GET "https://api.orbit.devotel.io/api/v1/usage-records\
?cursor=ur_hw2v4tHx9Q3mGkNw&limit=25" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

### 3. Export the same window as a CSV

<Note>
  `GET /api/v1/usage-records/export.csv?since=…&until=…&filename=…`
</Note>

The export repeats the feed with the same filter set, bounded to 20,000 rows (pass a smaller `limit` to bound it further). It differs from the JSON feed in four ways you build on:

* **Content type** — the body is `text/csv; charset=utf-8`, not JSON (`res.json()` would throw; read the body as text).
* **Content-Disposition** — `attachment; filename="<stem>-YYYY-MM-DD.csv"`; the optional `filename` query parameter overrides the stem (the date stamp is appended either way).
* **Truncation signal** — an `X-Export-Truncated: true` header tells you the window exceeded the cap, so split it (e.g. day-by-day) instead of reconciling a partial snapshot.
* **Higher gate** — export requires an owner, admin, or developer role with the `voice:read` scope (the JSON feed is any authenticated session), and it is rate-limited to 5 requests per minute.

Both the JSON list and this export are GET reads: they never create a resource, so an idempotency key is unnecessary on them. (POST endpoints that create resources accept an `Idempotency-Key`; on this page every retained retry of either GET returns the same artefact with no double cost.)

```bash cURL theme={null}
curl -X GET "https://api.orbit.devotel.io/api/v1/usage-records/export.csv\
?since=2026-08-01T00:00:00Z&until=2026-08-31T23:59:59Z\
&kinds=message&channels=sms&direction=outbound&filename=usage-records-2026-08" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -o usage-records-2026-08.csv
```

The CSV columns are `id, kind, channel, direction, endpoint, status, units, price, currency, ts`:

```csv theme={null}
id,kind,channel,direction,endpoint,status,units,price,currency,ts
msg_2f8a1c9b,message,sms,outbound,+14155550123,delivered,1,0.0083,USD,2026-08-14T21:07:12.441Z
cl_7d0e2b4a,call,voice,outbound,+14455550098,completed,1,0.012,USD,2026-08-14T20:31:07.102Z
```

### 4. Reconcile against the per-category aggregate

Sum the export's `units` column per `(channel, direction, currency)` and compare against `GET /api/v1/messages/usage/records`, the aggregate sibling on the [usage page](/api-reference/usage) that rolls the same underlying rows up per category — the numbers match 1:1 for the same time window, so a discrepancy means the filters drifted, not the ledger. For per-record billing reconciliation with line-item timing see the [CDR export feed](/api-reference/cdr-export); this endpoint is the per-record companion feed that takes its window from the billing event time and returns directly comparable sums.
