The cursor
The ledger gives every change a number, seq. A cursor is just the last seq you applied. GET /v1/events?cursor=N answers the events after N for your cut, in order; next is the last seq of the page, and you pass it back. Company changes (/v1/companies/events) use the same numbers, so one cursor covers both streams.
Why it never skips
The ledger has one writer, and events become visible in seq order. A page that is not full ends at the highest seq visible to the statement that read it, so nothing can appear later with a lower number than your cursor. You will see every event once, in order, however long you were away.
Three shapes, one idea
| Route | Cursor shape | Where you get it |
|---|---|---|
/v1/events, /v1/updates, /v1/companies/events, /v1/events/count |
a plain seq, like 9648658 |
next or top of a previous answer; ledger_seq of your first load; a snapshot's seq |
GET /v1/jobs |
<seq>:<job_guid>, like 9648658:21b3928d-... |
next of the previous page; the first page has none |
GET /v1/companies |
a company_guid |
next of the previous page |
The full feed's cursor carries the ledger's seq at the moment the first page was read, so the load and the events that follow line up: read events from that seq (the last page's ledger_seq) and you miss nothing that happened while you paged.
A rare cut
A call scans at most 200,000 sequence numbers. A rare country may answer a short page, even an empty one, with more: true: that is not the end, call again with next. top tells you how far the ledger is; /v1/events/count tells you how many events wait, by kind.
since instead of a cursor
Lost the cursor? GET /v1/events?since=2026-11-04T00:00:00Z starts at the first event at or after that time. It is a fallback, not the loop: times are not unique, numbers are.
Keep it with your data
Write the cursor in the same transaction as the rows it produced. The account page shows the last cursor each key sent (the cursor parameter of your newest /v1/events call) as a hint when you have lost yours.
curl -s 'https://api.jobmonkey.ai/v1/events/count?cursor=9600000&country=Canada' \
-H 'Authorization: Bearer cb_live_XXXXXXXX'
cursor = db.one("select cursor from sync_state") # yours
ev = call(f"/v1/events?country=Canada&cursor={cursor}&limit=10000")
with db.transaction():
apply(ev["events"])
db.run("update sync_state set cursor = %s", ev["next"])