Conventions
Rules that apply across the whole API. Reading this page once will save you most of the surprises.
Pagination
Most list endpoints use offset pagination:
| Parameter | Default | Meaning |
|---|---|---|
page | 1 | 1-based page number |
page_size | 10 | Records per page (20 for /notifications/in-app) |
{
"total_records": 42,
"current_page": 1,
"total_pages": 5,
"content": [ ... ]
}
Empty results return 204, not an empty page
When a list endpoint has no results it returns 204 No Content with no response body — not a
200 with an empty content array. Clients that assume a JSON body on every 2xx will break.
resp = requests.get(url, headers=headers)
if resp.status_code == 204:
items = []
else:
items = resp.json()["content"]
Not every list endpoint does this. These always return 200 with an empty content array:
GET /notifications/in-app, GET /attachments, GET /me/proof-packages, and
GET /assets/{asset_id}/prices/{price_type}. Handling both shapes, as above, is the safe approach.
Proof packages use cursor pagination
GET /me/proof-packages is the one exception. It takes limit (default 50, min 1, max 200) and an
opaque cursor, and returns a next_cursor. Treat the cursor as opaque — do not parse or construct
it — and keep requesting until next_cursor is null.
Errors
Failures return a two-field body:
{ "error": "ItemNotFoundError", "message": "Oops! Vault not found" }
| Status | error | Meaning |
|---|---|---|
| 400 | ValidationError | Malformed or rejected input |
| 402 | InsufficientCreditsError | Not enough credits — see below |
| 403 | ForbiddenError | Bad credentials, or not permitted |
| 404 | ItemNotFoundError | Not found, or not visible to you |
| 409 | ConflictError | Conflicts with existing state |
| 422 | MissingWalletCredentialError | A wallet credential is required but absent |
| 500 | InternalError | Server-side failure |
The Oops! prefix appears on 404 and 500 messages only; 400, 403, 409 and 422 carry the bare
message.
Two shapes do not follow this envelope:
- Request validation failures return the framework's
{"detail": [...]}shape. - Authentication failures — a missing or invalid
x-api-key/x-api-secret— also return{"detail": "..."}, not the envelope. TheForbiddenErrorcode above covers authorization ("you are known but not permitted"), not authentication.
A 500 body interpolates the underlying exception text into message. It is not a stable contract
and may expose internal detail — log it, but never parse or branch on it.
402 carries extra fields
Creating a vault, stream or data attachment runs a credit check first. If it fails:
{
"error": "InsufficientCreditsError",
"message": "Insufficient credits",
"required": 1.0,
"balance": 0.0,
"has_subscription": true
}
Ledgers
Where a ledger value is accepted, the canonical values are:
XRPL · POLYGON_ZKEVM · POLYGON_POS · ETHEREUM
The enum names do not map cleanly onto the underlying networks in every environment — for example
POLYGON_ZKEVM resolves to Polygon Amoy in development. Treat these as opaque identifiers and use
the value the API returned to you rather than constructing your own. Some aliases (amoy,
polygon) are accepted on input, but the canonical value above is what you get back.
Response headers
Every response carries Cache-Control: no-cache. Fields that are null are serialised explicitly
rather than omitted, so an optional field you have not set comes back as "field": null.