Skip to main content
Signa uses cursor-based pagination for all list and search endpoints. Cursors provide stable, consistent iteration over datasets that may change between requests: no skipped records, no duplicates, no performance degradation at depth.
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 a limit (items per page). The response includes a cursor and has_more flag:
Response:
To fetch the next page, pass the cursor from the previous response:
When 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 (without include_total):
With ?include_total=true:
Key details:
  • has_more is always at the top level (not inside pagination).
  • pagination.cursor is null when has_more is false.
  • pagination.total_count is only present when include_total=true is passed as a query parameter. It is omitted by default.
  • pagination.total_count_approximate is emitted alongside total_count whenever the total is present. It is false when the count is exact and true when 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 requested limit 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 blank cursor= 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:
Rotating the signing key does not invalidate cursors by itself. The API verifies an incoming cursor against an ordered list of accepted keys, so a cursor signed with a previous key keeps verifying for as long as that key is still configured as a fallback. Cursors stop verifying once the key that signed them is dropped from that list. Treat 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.
Cursors are opaque strings. Do not parse, decode, or construct them. Their internal format may change without notice.

Sorting

You can sort results using the sort 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, the total_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):
The shape is the same for every list endpoint:
  • pagination.total_count: the count itself (integer, only present when include_total=true).
  • pagination.total_count_approximate: false when the count is exact; true when the underlying engine capped the count (the search index caps at 10,000 for deep search queries).
Computing the total count requires a separate database or search-index query and can be expensive on large datasets. Only request it when you need to display a progress indicator or total count in your UI. Avoid including it on every page request.

SDK Support

The TypeScript SDK wraps pagination with async iterators, toArray(), and manual page control. See the TypeScript SDK reference for usage patterns.