Pagination

List queries are cursor-paginated. You ask for a page size with first and walk forward with after, using the cursor the previous page handed back.

Without a search term, this stays correct as records are added or removed between requests. Sorting by a value that can change -- a status, a total, a name, a date that gets edited, an updated-at time, revenue -- is different: a record whose value changes mid-walk can appear twice or be missed. For a complete export, walk in an order that never changes: the default order for orders, quotes and purchase orders, ORDER_NUMBER or QUOTE_NUMBER, sortBy: CREATED_AT for companies, and sortBy: CREATED_AT for products.

Searching (passing a search term) pages by position in the ranked results and stops at 10,000 results. Records added or removed mid-walk shift those positions, so use search to find records, not to export them.

The shape

Every paginated list returns the same structure:

  • edges — the items, each an object with a node (the record) and a cursor.
  • pageInfo — hasNextPage and endCursor for fetching the next page.
  • approxTotalCount — an approximate total, handy for showing "about N results". A few lists (the product catalog, for one) return an exact totalCount instead; the reference page for each operation shows which.
query ListPurchaseOrders($first: Int, $after: String) {
  purchaseOrders {
    list(first: $first, after: $after) {
      edges {
        node {
          id
          poNumber
          status
          totals {
            total {
              amount
              currency
            }
          }
          createdAt
        }
        cursor
      }
      pageInfo {
        hasNextPage
        endCursor
      }
      approxTotalCount
    }
  }
}

Fetching the next page

  1. Request the first page with first (and no after).
  2. Read pageInfo.hasNextPage. If it's false, you're done.
  3. Otherwise, request the next page with the same first and after set to pageInfo.endCursor.
  4. Repeat until hasNextPage is false.
// Page 1
{ "first": 50 }
 
// Page 2 — pass the previous page's endCursor
{ "first": 50, "after": "eyJpZCI6Li4ufQ==" }

Pick a sensible page size

Page sizes in the low hundreds are a good balance. Very large pages are slower and heavier to transfer; tiny pages mean more round trips.

Exporting everything? Pick an order that never changes (see above) and don't pass a search term. Other orders are fine for browsing, but a record whose sort value changes while you page can be repeated or skipped.

Treat cursors as opaque — don't parse or construct them. Only ever pass back a cursor the API returned. approxTotalCount is an estimate for display, not an exact count to loop on; rely on hasNextPage to know when to stop.