API conventions
Read these once and every endpoint becomes predictable.
- Why satoshi amounts arrive as JSON strings and heights as numbers, and what a proxy breaks by swapping them.
- How to walk a list with
cursorandlimit, and which filters have to be resent with every cursor. - Why a long page walk is not a snapshot, and which field to record before and after one.
- The five fields on
/statusthat identify the indexer you are talking to. - Which response changes can land without warning, and which ones have to go through the upgrade boundary.
Base path
https://your-indexer.example.org/patina/...
The app backend proxies a read only subset under /patina/... with the organisation's fail
closed conventions, plus two build helpers that return unsigned transaction plans. It holds no keys and
takes no custody.
The program has a rule against version labels in names, folders, and copy, and the indexer base path
follows it like everything else. An earlier draft of the baseline put a numbered segment in this path,
which broke the rule; the baseline was corrected before launch, and every implementation serves
/patina.
Types
The table below is the whole serialisation contract. One rule explains most of it: satoshi amounts travel as strings so that a consumer holding them in a 32 bit integer cannot truncate one silently, and everything else travels as the type you would expect. Read the last column when you are tempted to normalise a field in your own layer.
| Kind | JSON type | Example | Why |
|---|---|---|---|
| Satoshi amounts | string | "100000" | Survives parsers that lose precision on large integers |
| Block heights | number | 893000 | Always small enough to be safe |
| Depths and counts | number | 13000 | Same |
| Ids and hashes | string, lowercase hex | "d3b8d301..." | One fixed spelling |
| Txids | string, display order | "1d29e5ba..." | Matches block explorers. Wire order appears only inside derivations. |
| Outpoints in requests | string | "txid:vout" | One field instead of two |
| Absent values | null | "carrier": null | Fields are present with null, not omitted |
There are no timestamps anywhere in the protocol surface. Heights are the clock. If you need calendar time, convert from heights yourself and label it as an estimate.
Requests
- Everything is
GETexceptPOST /patina/safety/outpoints, which is a POST only because the request body can be long. It stores nothing. - Request bodies are
application/json. - No authentication is defined by the protocol. An operator may put their own in front.
- Unknown query parameters are ignored rather than rejected, so clients can add hints safely.
Pagination
List endpoints take cursor and limit, and return items plus
next_cursor.
GET /patina/artifacts?limit=100
GET /patina/artifacts?limit=100&cursor=<next_cursor from the previous page>
| Rule | Detail |
|---|---|
| Cursors are opaque | Treat the string as a token. Do not parse it, do not build one, do not assume it encodes an offset. |
| End of list | next_cursor is null. An empty items array with a null cursor means there is nothing more. |
| Limit | Client asks, server decides. Always read how many items you actually received rather than assuming you got limit. |
| Ordering | Stable within a page sequence, and documented per endpoint. Do not rely on ordering that is not stated. |
| Filters must be repeated | Send the same founding, status, or address filters with the cursor. |
New blocks arrive while you page. An artifact can move between page one and page four, so a long walk
can miss items or repeat them. If you need a consistent view, record indexed_height before
you start, and check it is unchanged when you finish. If it moved and you need exactness, walk again.
Sync state
Every consumer should read this before trusting anything else:
{
"tip_height": 0,
"indexed_height": 0,
"synced": false
}
tip_heightis what the Bitcoin node reports.indexed_heightis what this indexer has processed.syncedis the operator's view of whether the gap is acceptable.
A large gap means the answers are correct for an older chain, not wrong. Show the height you read at, and never present data from an unsynced indexer as current.
Sync state tells you how fresh an answer is, not whether it is true. For that, check the indexer against a node: the public site walks one such check end to end on Verify it yourself.
Identity of the indexer
status also carries network, protocol_id,
spec_sha256, parser_version, and indexer_version. Log all five
alongside any data you keep. When two systems disagree, the first question is whether they are running the
same spec on the same network.
Compatibility promise
- New fields may appear on existing responses. Ignore fields you do not know.
- New endpoints may appear.
- Existing fields are not removed, renamed, or repurposed.
- Sat values stay strings. Do not switch them to numbers in your own proxy.
Anything beyond that is a protocol change and goes through the upgrade boundary.