Pagination

Every collection is cursor paginated. There are no page numbers.

GET /v1/messages?limit=50
{
  "object": "list",
  "data": [ … ],
  "has_more": true,
  "next_cursor": "01j8xk2p7n4qc3m9v0wz5bt6ay"
}

Pass the cursor back to get the next page:

GET /v1/messages?limit=50&starting_after=01j8xk2p7n4qc3m9v0wz5bt6ay

Why not ?page=2

Because these tables are being written to constantly. With offsets, a row inserted between your first and second request shifts everything down by one, and page 2 silently skips a record. You do not get an error; you get a gap you will never notice.

A cursor says "everything after this specific record", so new writes cannot move the boundary. It also stays fast at depth — OFFSET 100000 makes the database count a hundred thousand rows it is about to throw away.

Walking a whole collection

async function* all(path, query = {}) {
  let cursor = null;

  do {
    const page = await get(path, {
      ...query,
      limit: 100,
      ...(cursor ? { starting_after: cursor } : {}),
    });

    yield* page.data;
    cursor = page.next_cursor;
  } while (cursor);
}

Loop on next_cursor, not on data.length === limit. A page can come back short and still have more behind it.

Limits

limit defaults to 25 and caps at 100. Asking for more is a validation error rather than a silent truncation, so you never quietly get less than you asked for.

Ordering

Newest first, by id. Our ids are ULIDs — lexicographically sortable and monotonic within a millisecond — which is what lets a single cursor column order the list and break ties at the same time. Two records written in the same second cannot sort unpredictably.

Delta sync

Conversations additionally accept updated_since and return a server-stampedsynced_at. Send back the value we gave you rather than your own clock — a client whose clock is two minutes fast will silently miss every change in that window.