List endpoints in the Titan Public API support two pagination modes:
- Cursor (keyset) pagination — opaque cursor pointing at the next page. Stable across concurrent updates.
- Offset pagination — classic
limit+offset. Simple but can skip or duplicate rows when the underlying set changes between requests.
Cursor is the recommended mode for anything scanning more than a few pages or resuming a paused sync. Offset is fine for small, one-off queries.
Every paginated endpoint accepts:
| Parameter | Type | Default | Meaning |
|---|---|---|---|
limit | integer | server default | Maximum rows returned per page. |
offset | integer | 0 | Zero-based offset. Ignored when cursor is set. |
cursor | string | (unset) | Opaque cursor. "" = request the first cursor-based page. Omit entirely to use offset mode. |
cursorunset → offset mode.limit+offsetdrive paging.cursorpresent (including empty string"") → cursor mode.offsetis ignored;limitstill caps page size.
{
"observations": [ ],
"next_cursor": "eyJvZmZzZXQiOjEwMCwic25hcHNob3QiOiIyMDI2LTA3LTAxIn0="
}- The primary result field contains the current page.
next_cursor— pass ascursoron the next call. When absent or empty, you have reached the end.
The cursor is opaque — treat it as a bag of bytes. Do not decode, mutate, or persist parts of it.
CURSOR=""
while :; do
RESP=$(curl -sX POST "https://titanapi.securityscorecard.io/public/v1/observations" \
-H "Authorization: Bearer $TITAN_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"limit\": 100, \"cursor\": \"$CURSOR\"}")
echo "$RESP" | jq '.observations[]'
CURSOR=$(echo "$RESP" | jq -r '.next_cursor // ""')
[ -z "$CURSOR" ] && break
doneOFFSET=0
LIMIT=100
while :; do
RESP=$(curl -sX POST "https://titanapi.securityscorecard.io/public/v1/observations" \
-H "Authorization: Bearer $TITAN_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"limit\": $LIMIT, \"offset\": $OFFSET}")
COUNT=$(echo "$RESP" | jq '.observations | length')
[ "$COUNT" -eq 0 ] && break
echo "$RESP" | jq '.observations[]'
OFFSET=$((OFFSET + LIMIT))
done- Prefer cursor for anything non-trivial. Cursor is stable across updates; offset can silently skip or duplicate rows when observations arrive mid-scan.
- Do not decode the cursor. It's opaque — do not persist parts of it, mutate it, or compare cursors for ordering.
- Do not persist cursors across long gaps. Cursors typically expire after 24 hours. Restart from
cursor=""and dedupe by resource id. - Do not parallelise a single stream. Give each worker its own disjoint filter (e.g. by time range) instead of splitting one cursor across workers.
- Stop on empty
next_cursor(cursor mode) or empty results (offset mode). A short page in cursor mode is not necessarily the last page — always checknext_cursor.