API pagination skipping records at page boundaries

Observed behavior:

  • Fetching /v1/work_orders with limit=100
  • Using cursor from next_cursor in response
  • Page 1 returns records A–J (IDs: 1001–1100)
  • Page 2 returns records B–K (IDs: 1101–1200), but record A from page 1 never appears on page 2
  • After ~10 pages, total count is 40 short of expected

Reproduction:

GET /v1/work_orders?limit=100&cursor=eyJpZCI6MTEwMH0=
# response
{
  "data": [...],
  "next_cursor": "eyJpZCI6MTIwMH0=",
  "has_more": true
}

Timestamp ordering in DB appears stable. No concurrent deletes during fetch. Same behavior observed with customers endpoint.

Documentation states cursor is "opaque" — need to know if implementation is timestamp-based vs ID-based. Gap suggests records with identical sort values are being dropped at boundary.

Parents
  • Yeah, I hit this last quarter. The cursor is technically a base64-encoded JSON struct with id and created_at, but the sorting only uses created_at ASC, id ASC. Problem is the query uses WHERE created_at >= cursor.created_at — that >= is the issue.

    If two records have the same created_at (common with bulk imports), the second page's >= includes the last record from page 1 again, but the API dedupes it... somehow incorrectly? I never traced exactly where the drop happens.

    Workaround that worked for me: add an explicit sort_by=id and sort_dir=asc. This forces ID-only cursor generation and the boundary logic becomes WHERE id > cursor.id — no more duplicates at boundaries.

    Worth noting: sort_by isn't documented for the work orders endpoint, but it's there. YMMV on other endpoints.

Reply
  • Yeah, I hit this last quarter. The cursor is technically a base64-encoded JSON struct with id and created_at, but the sorting only uses created_at ASC, id ASC. Problem is the query uses WHERE created_at >= cursor.created_at — that >= is the issue.

    If two records have the same created_at (common with bulk imports), the second page's >= includes the last record from page 1 again, but the API dedupes it... somehow incorrectly? I never traced exactly where the drop happens.

    Workaround that worked for me: add an explicit sort_by=id and sort_dir=asc. This forces ID-only cursor generation and the boundary logic becomes WHERE id > cursor.id — no more duplicates at boundaries.

    Worth noting: sort_by isn't documented for the work orders endpoint, but it's there. YMMV on other endpoints.

Children
No Data