Pagination
QuickBooks Desktop pages large queries with iterators that exist only inside one QuickBooks session. This API exposes them as cursors and is explicit about their limits: every page says when its cursor expires, a page you already fetched can be fetched again after a network error, and an expired cursor fails with a clear error instead of skipping records.
Two kinds of lists
Section titled “Two kinds of lists”Each list operation uses one of two modes, shown in the API reference and in the spec as x-daapi-pagination:
- Cursor lists (
cursor): invoices, customers, bills, transactions and most other large lists. Uselimit(1 to 150, default 150) andcursor. - Complete lists (
none): small lists QuickBooks returns in one piece, such as accounts, classes, terms, payment methods and payroll wage items. The response contains every matching record. Where QuickBooks supports it,limitcaps the count.
Cursor pages
Section titled “Cursor pages”{ "objectType": "list", "url": "/v1/quickbooks-desktop/customers", "data": [ { "id": "80000001-1730311000", "objectType": "qbd_customer", "name": "Acme Supply" } ], "nextCursor": "c1.MDFqOXg0bTZ2NGM4.2.Yk3kQ9", "hasMore": true, "remainingCount": 412, "cursorExpiresAt": "2026-10-05T16:04:11.120Z"}| Field | Meaning |
|---|---|
nextCursor |
Send it as cursor to get the next page. null on the last page. |
hasMore |
Whether more pages remain. |
remainingCount |
How many records QuickBooks has left after this page. null on the last page. |
cursorExpiresAt |
Our estimate of when nextCursor stops working if you do not use it. null on the last page. |
To continue, send cursor, and limit if you want a different page size. Filters were fixed by the first request and the cursor remembers them. You can leave them out, or resend exactly the same filters (Conductor’s SDKs do); a changed, added or dropped filter returns 400 CURSOR_PARAMS_MISMATCH, because it would describe a different list.
Each page returns a new nextCursor; always continue with the one from the latest page. Sending a cursor again repeats its page, so a retry after a network error never skips records. A finished page is repeated at most twice; after that the same cursor returns 400 CURSOR_INVALID, so a loop that keeps sending one cursor stops with an error instead of reading the same page forever.
Looking records up by ids, fullNames or refNumbers returns every match in a single page, with hasMore: false and nextCursor: null. QuickBooks does not page these lookups, so limit next to them is ignored. Other filters cannot be combined with them (400 INVALID_PARAMETER naming both parameters).
curl "https://api.desktopaccountingapi.com/v1/quickbooks-desktop/customers?cursor=c1.MDFqOXg0bTZ2NGM4.2.Yk3kQ9" \ -H "Authorization: Bearer $DAAPI_SECRET_KEY" \ -H "Daapi-End-User-Id: eu_01j9..."Why cursors expire
Section titled “Why cursors expire”A cursor points to an iterator inside a live QuickBooks session. The session stays open while you keep reading, but only for a short idle window: currently about 10 seconds after each page. While the cursor is open, QuickBooks treats the company file as in use by your app, so read the remaining pages promptly or stop reading. The iterator also ends when:
- the QuickBooks session closes (sessions are capped at 2 minutes of continuous work),
- QuickBooks restarts,
- you start a fourth paged query on the same connection while three are still open (the least recently used one is dropped).
Continuing an expired cursor returns 410 CURSOR_EXPIRED:
{ "error": { "type": "INVALID_REQUEST_ERROR", "code": "CURSOR_EXPIRED", "httpStatusCode": 410, "retryable": false, "outcome": "not_applicable", "details": { "reason": "idle_timeout", "pagesServed": 3, "recordsServed": 450 } }}details.reason is idle_timeout, session_ended, quickbooks_restarted or evicted. We never restart the query for you, because records may have changed in between, and a silent restart can skip or repeat records.
Safe page fetching
Section titled “Safe page fetching”Retrying the latest page is safe. Each page returns a new cursor. If your network drops while fetching page N, send the same cursor again: you get page N again from a short-lived cache, and the iterator does not advance twice. An older cursor, a cursor from another end user, or a malformed one returns 400 CURSOR_INVALID.
Fetch pages back to back. Process a page after you have requested the next one, or collect the records and process them afterwards. The SDKs request the next page only when your loop needs it, so a loop that stops early never runs an extra QuickBooks query; when your code holds a page for more than 2 seconds, they request the next one in the background to keep the cursor alive. listAll() requests each next page as soon as a page arrives.
Auto-pagination in the SDKs
Section titled “Auto-pagination in the SDKs”import { DesktopAccountingApi } from "@desktopaccountingapi/quickbooks-desktop";
const qb = new DesktopAccountingApi().forEndUser("eu_01j9...");
// Item by item, across pages; slow loops get the next page in the backgroundfor await (const customer of qb.qbd.customers.list({ status: "all" })) { console.log(customer.id, customer.fullName);}
// Page by pagefor await (const page of qb.qbd.invoices.list({ limit: 100 }).pages()) { console.log(page.data.length, page.remainingCount);}
// Everything into memory, as fast as possibleconst all = await qb.qbd.vendors.list().listAll();console.log(all.length);from desktopaccountingapi import DesktopAccountingApi
qb = DesktopAccountingApi().for_end_user("eu_01j9...")
# Item by item, across pages; slow loops get the next page in the backgroundfor customer in qb.qbd.customers.list(status="all"): print(customer.id, customer.full_name)
# Page by pagefor page in qb.qbd.invoices.list(limit=100).iter_pages(): print(len(page.data), page.remaining_count)
# Everything into memory, as fast as possiblevendors = qb.qbd.vendors.list().list_all()print(len(vendors))await foreach (var customer in qb.Qbd.Customers.ListAsync(new CustomerListParams { Status = ActiveStatus.All })){ Console.WriteLine($"{customer.Id} {customer.FullName}");}for (Customer customer : qb.qbd().customers().list(new CustomerListParams().status(ActiveStatus.ALL))) { System.out.println(customer.id() + " " + customer.fullName());}If a cursor expires mid-iteration, the SDKs raise CursorExpiredError (CursorExpiredException in C# and Java). It carries the reason, how many pages were served and items yielded, and the id and updatedAt of the last record you received. The SDKs never restart silently.
Recovering from CURSOR_EXPIRED
Section titled “Recovering from CURSOR_EXPIRED”Restart from a watermark instead of from the beginning. Sort-independent watermarks work best with updatedAfter:
import { DesktopAccountingApi, CursorExpiredError } from "@desktopaccountingapi/quickbooks-desktop";
const qb = new DesktopAccountingApi().forEndUser("eu_01j9...");const seen = new Set<string>();let since = "2026-01-01";
for (let attempt = 0; attempt < 5; attempt++) { try { for await (const invoice of qb.qbd.invoices.list({ updatedAfter: since })) { if (seen.has(invoice.id)) continue; // records at the watermark can repeat seen.add(invoice.id); // ...store the invoice... if (invoice.updatedAt > since) since = invoice.updatedAt; } break; // finished } catch (err) { if (!(err instanceof CursorExpiredError)) throw err; // continue from the newest record already stored }}from datetime import datetimefrom typing import Optionalfrom desktopaccountingapi import CursorExpiredError, DesktopAccountingApi
qb = DesktopAccountingApi().for_end_user("eu_01j9...")seen: set[str] = set()since = "2026-01-01"newest: Optional[datetime] = None
for attempt in range(5): try: for invoice in qb.qbd.invoices.list(updated_after=since): if invoice.id in seen: continue # records at the watermark can repeat seen.add(invoice.id) # ...store the invoice... if newest is None or invoice.updated_at > newest: newest = invoice.updated_at break # finished except CursorExpiredError: if newest is not None: since = newest.isoformat() # continue from the newest record already storedQuickBooks iterators return records in their own order, not by updatedAt, so this pattern re-reads some records after a restart. Deduplicate on id, as above, or make your writes idempotent by id. For full syncs of very large files, split the work into updatedAfter / updatedBefore windows (for example one month at a time) so each paged query finishes well inside one session. See Incremental sync.
Large responses
Section titled “Large responses”Complete lists and reports return everything at once. If QuickBooks’ answer is too large to process, the API returns 422 QBD_RESPONSE_TOO_LARGE. Narrow the filters, such as a date range or status, or use the cursor version of the list when one exists.
Platform lists
Section titled “Platform lists”GET /v1/end-users (and the other platform lists) use the same cursor, limit, nextCursor and hasMore names, with limit from 1 to 100 (default 50). These cursors come from our database and do not expire; cursorExpiresAt is always null.