Fixed-size bulk endpoints don’t paginate. Endpoints whose result size
is bounded by the request itself never cursor, but their envelopes differ:
bulk create (
POST /v1/watches/bulk)
and bulk lookup (POST /v1/alerts/lookup)
return an object: "list" envelope with data only (no has_more, no
pagination), while batch get
(POST /v1/trademarks/batch)
keeps the full list envelope with the fields pinned: has_more is always
false and pagination.cursor is always null.GET /v1/offices/votes is
bounded the same way — the votable vocabulary caps the list — so it accepts
limit and cursor for generic list-client compatibility but ignores
them: every vote comes back in one page with has_more: false and
pagination.cursor: null.Basic Usage
Your first request specifies alimit (items per page). The response includes a cursor and has_more flag:
cursor from the previous response:
has_more is false, you have reached the end of the result set. The cursor field will be null.
Parameters
Response Shape
All list endpoints return the same structure. Default response (withoutinclude_total):
?include_total=true:
has_moreis always at the top level (not insidepagination).pagination.cursorisnullwhenhas_moreisfalse.pagination.total_countis only present wheninclude_total=trueis passed as a query parameter. It is omitted by default.pagination.total_count_approximateis emitted alongsidetotal_countwhenever the total is present. It isfalsewhen the count is exact andtruewhen the count was capped (for example, the search index caps total hits at 10,000 for deep searches). Treat any exact count as a hard number and any approximate count as “at least this many”.
Cursor Stability
Cursors encode a position in the result set, not an offset. This means: Records inserted after your cursor was created will not appear in your current pagination session. You will not see duplicates or gaps. Records deleted after your cursor was created are silently skipped. Your page may contain fewer items than the requestedlimit in rare cases.
Concurrent updates do not affect cursor validity. Even if the underlying data changes, your cursor continues from where it left off.
Cursors are scoped to a specific query, sort order, and filter combination. Changing any of these parameters invalidates the cursor: start a fresh pagination session instead.
Cursor Invalidation
Cursors have no fixed lifetime. Nothing in a cursor records when it was issued and nothing ages one out, so a cursor you checkpointed a week ago may still work. What can happen at any time is invalidation, and how it surfaces depends on the endpoint. Signed cursors. Most list endpoints (trademark search, the entity lists, events, citations, assignments, webhooks and the rest of the org-scoped surfaces) mint signed cursors. A malformed, tampered-with, or wrong-endpoint cursor is rejected, and so is any cursor minted under a cursor format that has since changed. Most of these endpoints also reject an explicitly blankcursor= rather than quietly restarting at page one, so send the parameter only when you have a value for it. All of these surface as cursor_expired:
cursor_expired as something that can occur on any page rather than a deadline you can plan around: handle it wherever you paginate and restart from the first page. For long-running syncs, checkpoint your own progress alongside the cursor (the last updated_at or date you processed, for example) so a restart can skip what you already have.
Cached reference catalogs behave differently. GET /v1/jurisdictions, GET /v1/classifications, GET /v1/offices and GET /v1/event-types page through a fully cached catalog in memory, and their cursor is a plain catalog key (a jurisdiction code, a class number) rather than a signed token. These endpoints never return cursor_expired. A cursor value that is not in the catalog, including one carried over from another endpoint, is treated as past-the-end and returns 200 with an empty data array, and a blank cursor= starts again at the first page. So do not read “no error” as “more data” there: follow has_more and pagination.cursor exactly as returned, because a foreign cursor truncates the walk silently and a blank one silently restarts it.
A cursor replayed against a request that changed one of the parameters the endpoint binds into its cursor is a different error: cursor_invalid, also a 400. What is bound varies by endpoint. Trademark search binds sort, limit, the filter set and the query text; the citations endpoints bind only the endpoint and the sort direction, so changing a filter or limit mid-walk is accepted there. See the endpoint’s reference page for its exact contract, and keep every parameter identical across pages if you want one rule that holds everywhere. Branch on both slugs if you retry pagination automatically. In each case the fix is the same, restart from the first page under the new parameters.
Sorting
You can sort results using thesort parameter. The cursor encodes the sort position, so you must use the same sort value for every page in a session.
Supported sort fields vary by endpoint:
Sort direction is specified with a
- prefix for descending (no prefix = ascending):
When a text query (
q) is provided, the default sort is by relevance. When only filters are used (no q), results are returned in index order (fast, unordered). Providing an explicit sort overrides the default in both cases.Total Count
By default, thetotal_count is not included in the response. To include it on most list endpoints, pass include_total=true (the fixed-size endpoints in the note above don’t paginate, so they don’t take it):
pagination.total_count: the count itself (integer, only present wheninclude_total=true).pagination.total_count_approximate:falsewhen the count is exact;truewhen the underlying engine capped the count (the search index caps at 10,000 for deep search queries).
SDK Support
The TypeScript SDK wraps pagination with async iterators,toArray(), and manual page control. See the TypeScript SDK reference for usage patterns.