Response structure
Every paginated response includes these fields:
In incremental sync mode,
GET /transactions adds one more field to this envelope: a watermark timestamp to pass back as updated_after on your next sync. It appears only when the request carried updated_after, so treat it as optional when parsing. Incremental mode also changes what the cursor looks like: the classic transactions cursor happens to resemble a transaction id, while the incremental cursor is an unmistakably opaque token. Treat both as opaque and pass them back unchanged.
POST /transactions/{id}/match_invoices uses this envelope but returns the entire matched group at once, with has_more: false and next_cursor: null. No pagination request is needed.
Parameters
integer
default:"100"
Number of results per page. Minimum 1, maximum 500.
string
Pass the
next_cursor value from the previous response unchanged to fetch the next page.Paginated endpoints
-
GET /sheets -
GET /sheets/{id}/cells -
GET /accounts -
GET /cards -
GET /transactions -
GET /transactions/splits -
GET /spreads -
GET /journal_entries -
GET /parties/merchants -
GET /rules -
GET /tax_credits -
GET /taxes -
GET /aggregations/totals -
GET /invoices -
GET /forms/1099
GET /accounts and GET /cards cursors are opaque tokens, never acct_ or card_ ids, and they can contain characters that need URL-encoding when you assemble the query string by hand. Pass one back exactly as issued; substituting an id returns 400 invalid_request.
A /cards cursor is additionally bound to the exact account_id filter the page was fetched with, so keep the same filter on every page of one sequence. Changing the filter mid-pagination, or replaying a cursor issued by /accounts, returns the same generic 400 invalid_request. To switch filters, start a new sequence without a cursor.
GET /transactions/splits and GET /spreads rows have no ID, so their cursors are opaque tokens. Pass one back unchanged with the same filters. A malformed cursor, a cursor from the other list, or one with an out-of-range date or ID returns 400 invalid_request with param: "starting_after".
GET /rules cursors currently look like rule_ ids, the same quirk the classic transactions cursor has. Treat them as opaque all the same and pass them back unchanged. An unknown, deleted, malformed, or inaccessible cursor returns 404 resource_missing with param: "starting_after" (not the 400 that /accounts and /cards use), so discard the stored cursor and restart from the first page. Rules come back newest first, and pagination is live keyset pagination rather than a snapshot: a rule created ahead of your cursor appears on a fresh traversal, not on the next page of the current one.
GET /aggregations/totals adds request details to the envelope, such as group_by, order, the dates, and point_in_time, alongside the usual fields. Its cursor is bound to the exact request it came from: send starting_after with the same category_id, dates, merchant, group_by, and order, or the request is rejected with 400 invalid_request. Changing only a day within the same month is a different request too. Merchant rows default to 100 per page. Month rows come back all at once, with has_more: false, unless you pass limit. See Paging grouped totals.
GET /taxes sorts by creation time descending, then public ID descending. Pass next_cursor back unchanged as starting_after, with the same type, jurisdiction, year, and status filters. A cursor absent from that filtered company collection returns 404 resource_missing, with param: "starting_after"; restart from the first page rather than retrying that cursor. The response’s summaries array repeats on every page and depends only on year and type. See Tax filings.
GET /invoices returns invoices newest invoice date first, each in full with its line items, adjustments, and payment. A starting_after cursor that isn’t found returns 404 resource_missing; restart from the first page.
GET /tax_credits returns all available years newest first when year is omitted. Keep type, year, and claim_status unchanged while paging, and pass next_cursor back as starting_after. An unknown, deleted, inaccessible, or filtered-out cursor returns 404 resource_missing with param: "starting_after"; restart from the first page. Pagination is live, so a credit whose claim status changes may stop matching your filters. Empty matches return data: [], has_more: false, and next_cursor: null. See Tax credit discovery.
Keep amount_cents and all other filters unchanged while paging GET /transactions. The exact signed amount filter also works under incremental sync; see Finding transactions by amount.
GET /forms/1099 orders records by reporting year descending, then stable record creation ID descending. Its opaque cursor binds the company and normalized record filters; preserve those filters on later pages, though the page size may change. A malformed, foreign, or differently filtered cursor returns 400 invalid_request. Start a new traversal to change filters. The response’s summaries array covers complete annual reports on every page and depends only on year, independently of vendor/status filters and pagination. See 1099 Filings.
Sheets pagination
GET /sheets orders documents by creation time descending, then internal ID descending. Keep its optional name query unchanged and pass next_cursor back unchanged. The list is live, not a frozen snapshot; a cursor absent under the current company/query returns 404 resource_missing.
GET /sheets/{id}/cells searches authored input/formula text in worksheet, row and column order. Its response adds the document revision. Cursors bind the document, query, match mode, worksheet filter and revision. A cursor from a different search returns 400 invalid_request; a changed document returns 409 revision_conflict with current_revision. Inspect the new document and restart without starting_after. Both endpoints use the default 100 / maximum 500 page size. See Sheets.
Unpaginated list endpoints
These return a list envelope but always deliver the full set in a single response. Their sets are small and bounded, so there is nothing to page through: They do not acceptlimit or starting_after, and their envelopes carry only object, url, and data. There is no has_more and no next_cursor field at all, so a generic pagination helper must treat an absent has_more as “this is the only page” rather than reading it as false off an object that never had it.
Endpoints with no list envelope
GET /team is not a list endpoint, even though it returns collections. It returns a single team document whose members and invitations arrays sit directly on the response:
object: "list", no url, and no data, so a generic helper that reaches for response.data will read undefined rather than an empty page. Branch on object before treating any response as a list. Both arrays are always present and are [] when empty, never null, and neither accepts limit or starting_after.
Lists embedded in an object
GET /jobs/{id} returns a single job, not a list, but the job’s errors field is a complete list envelope with its own has_more and next_cursor. Page through it with the usual limit (1 through 500, default 100) and starting_after on the job’s URL. Each page returns the whole job again along with the next slice of errors.
The embedded envelope’s url is the job’s own path (for example /api/v1/jobs/job_example123), so a generic helper works if you point it at response.errors rather than at the response itself. Job error cursors are opaque signed tokens bound to one job: pass them back unchanged (URL-encoded in the query string), and expect 400 invalid_request for a cursor that is malformed, tampered with, or issued for another job. See Reading job errors.
Example
Fetch the first page of transactions:has_more is true, use next_cursor to get the next page: