INTEGRATE · LIVE
Read API
Read-only, unauthenticated, aggregates first. No endpoint on this surface can write, and none returns personal data. Versioned; breaking changes get a new major.
Version
v1Rate limit
60/min/ipFreshness
≤3600sCORS
*| Endpoint | Returns | Status |
|---|---|---|
| /api/v1/public/ledger/circulation | Circulating total per asset, with its honesty state | LIVE |
| /api/v1/public/ledger/invariant | The last invariant run — pass or fail, with the counters that name why | LIVE |
| /api/v1/public/ledger/assets | The asset registry and each asset's decimals | LIVE |
| /api/v1/public/ledger/entries | Anonymised entries, newest first. ?limit= (max 200), ?type=, ?before= keyset cursor. Each page carries total and next_cursor, back to the first entry ever written | LIVE |
| /api/v1/public/ledger/snapshot.jsonl | The ledger as one downloadable JSONL snapshot, its Merkle root and fixture label in x-snapshot-* headers — the label travels with the data | LIVE |
| /api/v1/public/ledger/proof/{entryId} | A self-contained Merkle inclusion proof: the entry as hashed, the sibling path, the root — recomputable without calling us again | LIVE |
Every response carries its own honesty state
{
"asset": "FG",
"circulation": "84203915.00",
"state": "reported",
"invariant": { "last_run": "2026-08-25T04:03:00Z", "result": "pass", "drift_minor": "0" },
"as_of": "2026-08-25T04:03:00Z"
}The state field is not cosmetic. An integrator rendering our figures inherits our honesty state, which is the point: nobody downstream can present a Reported figure as Reconciled without actively discarding a field.
Why /entries earned its cursor
This page used to explain why there was no cursor: a cursor over a live, appending collection is a commitment to a stable ordering, and one that skipped or repeated rows under write load would be worse than none. That reasoning stands — what changed is that the ordering became a guarantee we can make. The cursor keys on two fields that are written once and never updated, so everything behind any cursor is frozen history: rows can be appended in front of your position, never inserted behind it. That is also why the cursor is an unsigned opaque token — there is nothing behind it to tamper with. A garbage ?before= is refused with a 400, not silently treated as the first page.
Entries are anonymised at the query. The projection is an allow-list, so the customer id, the source id and the free-text description are never read — not read and then dropped. A column added to the ledger tomorrow does not appear on this endpoint by default.
What v1 promises
Every endpoint in the table is stable: within v1, changes are additive only. A field, once published, keeps its name, its type and its meaning; new fields may appear beside it and your parser should ignore what it does not know. Removing or redefining a field is a breaking change, and a breaking change ships as /api/v2/ with v1 still answering — an integration built against this page does not break by us deciding it should. The same rule binds the x-snapshot-* headers, because provenance a consumer cannot rely on is not provenance.
The freshness budget is part of the contract, not an aspiration: a figure older than 3600 seconds degrades to unavailable in the response itself, carrying its last known value as exactly that. The surface also probes itself through this same public door — /status/ shows the current answer, and the deeper acceptance probe runs in CI on every deploy, where its record is public.