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

v1

Rate limit

60/min/ip

Freshness

≤3600s

CORS

*
EndpointReturnsStatus
/api/v1/public/ledger/circulationCirculating total per asset, with its honesty stateLIVE
/api/v1/public/ledger/invariantThe last invariant run — pass or fail, with the counters that name whyLIVE
/api/v1/public/ledger/assetsThe asset registry and each asset's decimalsLIVE
/api/v1/public/ledger/entriesAnonymised entries, newest first. ?limit= (max 200), ?type=, ?before= keyset cursor. Each page carries total and next_cursor, back to the first entry ever writtenLIVE
/api/v1/public/ledger/snapshot.jsonlThe ledger as one downloadable JSONL snapshot, its Merkle root and fixture label in x-snapshot-* headers — the label travels with the dataLIVE
/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 againLIVE

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.