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

# Reconciliation

> Keep a local mirror of messages and billing in sync using the updated_since / updatedSince watermark and cursor pagination.

To keep your own database in sync with Trackly, don't re-scan everything on a schedule — drive an **incremental change feed** off a watermark. Both the [messages](/api-reference/v2/messages/list-messages) and [billing records](/api-reference/v2/usage/billing-records) endpoints support this pattern: ask for everything that changed since the last time you synced, page through it, and remember the newest change you saw.

## The watermark

Each row has an `updated_at` timestamp that advances whenever the row changes (a delivery receipt revises a message's status; a charge is refunded). Pass the timestamp of the newest change you've already processed:

* Messages: `updated_since` (ISO-8601, **timezone-aware** — include `Z` or an offset)
* Billing records: `updatedSince` (same format)

When you pass a watermark, the endpoint switches from newest-first browsing to an **ascending change feed**: rows are filtered to `updated_at > watermark`, sorted oldest-change-first, and keyset-paginated on `(updated_at, id)`. Because pagination is keyset-based, a row that changes *while* you're paging is never skipped or duplicated.

<Note>
  Rows that predate the `updated_at` field are not in the change feed. Do one **initial full sync** (list without a watermark) to capture the existing state, then switch to incremental.
</Note>

## Cursor pagination

Both endpoints return a `pagination` object. Follow `next_cursor` until `has_more` is `false`:

```json theme={null}
{
  "messages": [ ... ],
  "pagination": { "limit": 50, "has_more": true, "next_cursor": "MjAyNi0wNy..." }
}
```

The cursor is opaque — pass it back verbatim as the `cursor` query param. Do not construct or parse it.

## The sync loop

1. Start from your stored watermark (or omit it for the first run).
2. Request a page with `updated_since=<watermark>` (and `cursor` if continuing).
3. Upsert each row into your store by its `id`. For messages, **let the latest `status` win** — a later receipt can revise `delivered` to `failed`.
4. Follow `next_cursor` until `has_more` is `false`.
5. Persist the **largest `updated_at`** you saw as your new watermark.

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

BASE = "https://api.tracklysms.com/api/v2/messages"
HEADERS = {"X-Api-Key": "trk_your_api_key"}


def sync(watermark: str | None):
    params = {"limit": 200}
    if watermark:
        params["updated_since"] = watermark
    newest = watermark
    while True:
        page = requests.get(BASE, headers=HEADERS, params=params).json()
        for row in page["messages"]:
            upsert(row)  # keyed on row["id"]; latest status wins
            newest = max(newest or row["updated_at"], row["updated_at"])
        if not page["pagination"]["has_more"]:
            break
        params = {"limit": 200, "cursor": page["pagination"]["next_cursor"]}
    return newest  # store as the next run's watermark
```

<Note>
  Run reconciliation as a safety net **in addition to** [webhooks](/api-reference/v2/webhooks/signing), not instead of them. Webhooks give you real-time updates; the watermark feed guarantees you catch anything a webhook missed (a downed endpoint, a `dead` delivery).
</Note>

## Idempotent upserts

Because a status can be revised and the feed is at-least-once safe, your upsert must be idempotent: key on the row `id`, and for messages take the row with the newer `updated_at`. Never append — always upsert.

## Related

<CardGroup cols={2}>
  <Card title="List messages" icon="list" href="/api-reference/v2/messages/list-messages">
    The message change feed and its filters.
  </Card>

  <Card title="Billing records" icon="receipt" href="/api-reference/v2/usage/billing-records">
    The billing change feed for charges and refunds.
  </Card>
</CardGroup>
