findings:read.
Use order=recent, and assert it
Why the order matters
severity is the backlog view a person reads. recent is the one a machine
enumerates with. Pretending they are equivalent is how a client trusts a
traversal it should not — and for a gate deciding whether a release ships, a
missed row is a clean result reported by something that never saw the finding
that mattered.
page.stable reports which of the two you got, on every response. Assert it;
that is what it is for. An unknown order is refused rather than defaulted,
because falling back would hand a caller who asked for the exhaustive order the
browsable one, and it would traverse happily while missing rows.
Offsets are not offered at all. LIMIT 50 OFFSET 50 re-runs the ordering and
counts forward, so rows inserted before the cursor position between two
requests shift everything and the client silently skips some. Findings are
written constantly.
What a cursor is bound to
Opaque, signed, and bound to the tenant, the order and the filter set. Any of those changing invalidates it — a position in one sequence is meaningless in another. In particular:Do not changeCursors expire after 24 hours. That is not a security control: a traversal resumed a day later silently misses everything created since, and failing loudly beats paginating through a set that no longer exists. If you need to resume a long job, start it again rather than storing a cursor overnight.status,severity,kindorasset_idmid-traversal. Resuming across a filter change would return neither the old set nor the new one, with nothing in the response to say so, which is why the cursor is rejected instead.
Limits and cost
limitis clamped to 200.page.limitreports the value actually used, so read it back rather than assuming your request was honoured.- A cursor over 4096 characters is rejected before any work.
has_morecomes from rows already read — each page is fetched withn+1and trimmed — so it costs nothing extra.totalis a separate indexed aggregate. It is returned today, and it is the half of a listing that will get expensive first; do not call the endpoint in a loop purely to read it.
Gotchas
- A cursor from another tenant is refused even though it is genuinely signed. The signature prevents forgery; forgery is not the threat. A cursor reaching the wrong tenant through a shared log, a copied URL or a support ticket is.
- Filters are comma-separated:
status=open,triaged,severity=critical,high. 400on a listing usually means a stale, re-scoped or expired cursor. Restart the traversal.- The asset listing is in creation order, not “most recently scanned
first”.
last_scanned_atchanges on every scan, so a row would move mid-traversal.
See also
- Pagination — the full parameter reference
and
pageobject. - Triage in bulk — acting on what you enumerated.
- Errors and limits — every bound in one place.

