The watermark
Each row has anupdated_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 — includeZor an offset) - Billing records:
updatedSince(same format)
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.
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.Cursor pagination
Both endpoints return apagination object. Follow next_cursor until has_more is false:
cursor query param. Do not construct or parse it.
The sync loop
- Start from your stored watermark (or omit it for the first run).
- Request a page with
updated_since=<watermark>(andcursorif continuing). - Upsert each row into your store by its
id. For messages, let the lateststatuswin — a later receipt can revisedeliveredtofailed. - Follow
next_cursoruntilhas_moreisfalse. - Persist the largest
updated_atyou saw as your new watermark.
Python
Run reconciliation as a safety net in addition to webhooks, 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).Idempotent upserts
Because a status can be revised and the feed is at-least-once safe, your upsert must be idempotent: key on the rowid, and for messages take the row with the newer updated_at. Never append — always upsert.
Related
List messages
The message change feed and its filters.
Billing records
The billing change feed for charges and refunds.