Pagination
Every list endpoint in the Fibric API paginates the same way: opaque cursors, a bounded limit, and stable newest-first ordering. There are no page numbers and no offsets. This page specifies the contract once, so each endpoint page only has to name its filters.
Shared conventions, including authentication and the error envelope, are defined in the API overview. Error codes are catalogued in Errors.
The list envelope
Every list response is wrapped in the same four-field envelope, regardless of what it lists.
Always list.
The page of results, each a full or summarized API object. An empty page is [], never null.
true when at least one more page exists beyond this one. The loop condition: keep fetching while has_more is true.
Opaque cursor for the next page, prefixed cur_. null exactly when has_more is false. Pass it back verbatim as the cursor query parameter.
{
"object": "list",
"data": [
{ "id": "rc_5b21", "object": "receipt", "outcome": "applied", "at": "2026-07-02T15:04:41Z" },
{ "id": "rc_5b1f", "object": "receipt", "outcome": "blocked", "at": "2026-07-02T14:58:07Z" }
],
"has_more": true,
"next_cursor": "cur_eyJpZCI6InJjXzViMWYifQ"
}
Request parameters
Two query parameters drive pagination on every list endpoint. Endpoint-specific filters are documented on each endpoint page; filters combine with AND and are compatible with pagination.
Page size, 1–100. Defaults to 20. A value outside the bounds fails with 400 invalid_parameter; it is never silently clamped.
The next_cursor from a previous response, verbatim. Omit it for the first page. A cursor is bound to the query that produced it: reusing it with different filters, a different limit is permitted, fails with 400 invalid_cursor.
curl "https://api.fibric.io/v1/receipts?verdict=BLOCK&limit=50" \
-H "Authorization: Bearer $FIBRIC_KEY"
# then, using next_cursor from the response:
curl "https://api.fibric.io/v1/receipts?verdict=BLOCK&limit=50&cursor=cur_eyJpZCI6InJjXzViMWYifQ" \
-H "Authorization: Bearer $FIBRIC_KEY"
Stable ordering
Every list returns newest first, ordered by the object's creation-time field and tie-broken by id, so the order is total and deterministic. The anchor field per endpoint group:
| Endpoint | Ordered by |
|---|---|
GET /events | received_at, then id |
GET /operators | created_at, then id |
GET /connectors | created_at, then id |
GET /plans | proposed_at, then id |
GET /actions | disposed_at, then id |
GET /receipts | at, then id |
GET /guardrails | created_at, then id |
GET /webhook_endpoints | created_at, then id |
GET /keys | created_at, then id |
There is no sort parameter in v0.9. To read oldest first, walk to the end and reverse client-side, or bound the range with a since filter where the endpoint offers one.
Consistency under writes
A cursor marks a fixed position in the ordering, not a snapshot of the data. The guarantees while you paginate:
- No skips, no duplicates from paging itself. The cursor encodes the last-seen position; the next page starts strictly after it. An object is never returned twice by consecutive pages and never falls into the gap between them.
- New objects land before your cursor. Because ordering is newest first, anything created after your first page began sits on pages you have already passed. Re-run the query from the top to pick up new arrivals.
- Mutable fields read current. Pages are not snapshots: an operator that was
activeon page one may readpausedif you fetch it again later. Immutable objects, events and receipts, cannot change under you at all.
Pagination is for reading history. For keeping up with new activity, prefer a webhook endpoint, which pushes plan proposals and action dispositions as they happen, or poll the first page with a since filter anchored at your high-water mark.
Cursor lifetime
Cursors are opaque and expire. Treat them as short-lived continuation tokens, not durable bookmarks:
- A cursor is valid for at least one hour after it is issued. Do not persist cursors across sessions.
- An expired, malformed, or foreign-query cursor fails with
400 invalid_cursor. The recovery is always the same: restart from the first page. - Never construct or modify a cursor. The encoding is an implementation detail and changes without notice within the preview.
Walking all pages
CURSOR=""
while :; do
PAGE=$(curl -s "https://api.fibric.io/v1/receipts?entity=order:SO-10884&limit=100${CURSOR:+&cursor=$CURSOR}" \
-H "Authorization: Bearer $FIBRIC_KEY")
echo "$PAGE" | jq -r '.data[].id'
[ "$(echo "$PAGE" | jq -r '.has_more')" = "true" ] || break
CURSOR=$(echo "$PAGE" | jq -r '.next_cursor')
done
async function* pages(path: string, params: Record<string, string>) {
let cursor: string | null = null;
do {
const qs = new URLSearchParams({ ...params, limit: "100", ...(cursor ? { cursor } : {}) });
const res = await fetch(`https://api.fibric.io/v1${path}?${qs}`, {
headers: { Authorization: `Bearer ${process.env.FIBRIC_KEY}` },
});
const page = await res.json();
yield* page.data;
cursor = page.has_more ? page.next_cursor : null;
} while (cursor);
}
for await (const receipt of pages("/receipts", { verdict: "BLOCK" })) {
console.log(receipt.id, receipt.outcome);
}
Draining a long history counts against the read rate limit for the route class; use limit=100 and back off on 429 per Rate limits & quotas. For a complete extract of the audit ledger, prefer the asynchronous receipts export over paging.
Errors
| Status | Code | When |
|---|---|---|
400 | invalid_parameter | limit is outside 1–100 or not an integer. |
400 | invalid_cursor | The cursor is malformed, expired, or was issued for a different query. Restart from the first page. |
429 | rate_limited | The read budget for the route class is exhausted. Honor Retry-After; the cursor remains valid within its lifetime. |
Both 400 codes are client errors; do not retry unchanged. See retryability in Errors.