Core Concepts
Pagination
Every list endpoint in the public API returns cursor-paginated results. Cursors are opaque, stable across writes, and dirt cheap to walk — never guess page numbers.
The envelope
Every paginated endpoint returns the same shape:
json{ "data": [ /* array of items — type depends on the endpoint */ ], "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wNC0wMlQxMDoxMjozNCswMDowMCIsImlkIjoiOGYyZTNjMWEifQ", "has_more": true }
| Field | Type | Meaning |
|---|---|---|
| data | array | Items in this page. Length is at most the request limit. |
| next_cursor | string | null | Pass this back as ?cursor=... to fetch the next page. null means you have reached the end. |
| has_more | boolean | True when a next page exists. Equivalent to (next_cursor !== null). |
Request parameters
| Param | Default | Max | Notes |
|---|---|---|---|
| limit | 25 | 100 | Clamped server-side. Values above 100 are silently capped; negative or zero values fall back to the default. |
| cursor | (none) | 1024 bytes | Opaque base64 payload. Do NOT decode or construct cursors manually — they are a server-owned contract. |
bashcurl -sS "https://api.alloovium.com/api/v2/vault/projects?limit=50&cursor=eyJjcm..." \ -H "Authorization: Bearer $ALLOOVIUM_API_KEY"
Why cursors and not page numbers
Page numbers work for static datasets but fall apart under concurrent writes. If you are walking page 1 while a new row is inserted at the top, page 2 will contain a duplicate of the last row on page 1. Offset pagination also gets slow as the offset grows — the database has to scan and discard N rows on every page.
Alloovium uses keyset pagination, encoded as an opaque cursor:
- Every list endpoint orders by
order— a tuple that uniquely identifies a row. - The cursor stores the last row's
atandid. - The next query uses a tuple comparison
where. - Index range scan — O(log n + k) regardless of how deep you are.
- Stable under concurrent inserts — new rows appearing above the cursor simply show up on earlier pages next time you restart the walk.
Cursor = opaque
Walking every page
pythonimport httpx def list_all_projects(api_key: str): url = "https://api.alloovium.com/api/v2/vault/projects" params = {"limit": 100} headers = {"Authorization": f"Bearer {api_key}"} while True: resp = httpx.get(url, params=params, headers=headers) resp.raise_for_status() page = resp.json() for project in page["data"]: yield project if not page["has_more"]: return params["cursor"] = page["next_cursor"]
Check has_more, not an empty data array
limit items and still have flag: true. Always branch on flag (or equivalently on whether cursor is null) — never on check.Invalid cursors
A malformed, truncated, tampered, or too-large cursor returns:
json{ "type": "https://api.alloovium.com/errors/invalid_cursor", "title": "Invalid cursor", "status": 400, "detail": "The cursor is malformed or has been truncated.", "code": "invalid_cursor" }
The most common cause is URL-encoding. Cursors are URL-safe base64 without padding, so they do not need to be URL-encoded, but they also do not mind it. Double-encoding, or decoding + re-encoding through a non-URL-safe alphabet, will break them.
Stability and concurrency
Cursors are stable under concurrent inserts. Concretely, if you cursor-walk projects while someone uploads a new one:
- The new project has a newer
atthan anything you have seen. - Your cursor points at the oldest row you have already returned.
- The next page query asks for rows strictly older than your cursor — the new project is above the cursor and is not included.
- You will see the new project if and only if you start a fresh walk.
Deletes are also safe: if the row you cursor on gets deleted, the tuple comparison still works because it is based on stored values, not on a reference to the row itself.
Which endpoints are paginated
| Capability | Method | Path |
|---|---|---|
| vault.list_projects | GET | /api/v2/vault/projects |
| vault.list_documents | GET | /api/v2/vault/documents |
| chat.list_conversations | GET | /api/v2/conversations |
| workflows.list | GET | /api/v2/workflows |
Non-paginated endpoints (single-resource lookups like get, actions like ask, search that returns a fixed k) return results directly without the page envelope.
See also
- Capabilities — full list of list endpoints and their item schemas.
- Errors —
codeand other 4xx codes.