API reference
The authoritative specification is docs/openapi.yaml in the repository. This page is the map.
Admin surface (bearer-authenticated)
| Method & path | Purpose |
|---|---|
POST /v1/contracts |
Register a contract (or reconcile an existing registration); classification and backfill start automatically |
POST /v1/admin/gaps/plan |
Plan which parts of the open gaps the archive leg replays; the rest stay open and declared (sparse healing) |
GET /v1/contracts/:id |
Detail: classification, discovered events, backfill progress, coverage |
DELETE /v1/contracts/:id |
Stop indexing; data is kept, re-registration resumes |
A contract whose instance is archived (TTL expired) registers too, as long
as kinds is explicit: it classifies as unknown and the response says so
in a warnings array, while its history is still reconstructed — an
archived contract’s past exists in the History Archives even when its
instance does not. Without explicit kinds the 404 stands, because the
kinds default is derived from the classification. (Since 1.6.0.)
Read surface
| Method & path | Purpose |
|---|---|
GET /v1/contracts |
List every registration with its classification and kinds |
GET /v1/contracts/:id/events |
Events with getEvents-v2-style filters and cursors |
GET /v1/contracts/:id/state |
Current storage snapshot, paginated by key + durability |
GET /v1/contracts/:id/state/history |
Change history of storage entries, with provenance |
GET /v1/contracts/:id/transfers |
Decoded token movements in chain order |
GET /v1/contracts/:id/trustlines |
Current trustline holders of the SAC asset |
GET /v1/contracts/:id/trustlines/history |
Trustline changes with before/after balances |
GET /v1/contracts/:id/movements |
Token transfers this contract took part in, whoever emitted them |
Events
The getEvents-v2-compatible stream: positional topic0–topic3 filters
(exact base64 ScVals), a ledger range, limit and the opaque cursor.
Since 1.11.0, include=rawXdr adds the stored ContractEvent to every
event on the page — the original bytes, for consumers that re-emit events
into their own pipelines rather than re-encode our decode. The parameter
is presentation only: it is accepted beside a cursor and never encoded in
one, and without it the response does not change by a byte. An unknown
include value is a 400.
Token transfers
SEP-41 movements (transfer, mint, burn, clawback) decoded into structured
rows: from/to addresses, the exact i128 amount, the SEP-0011 asset and
the CAP-67 muxed destination id. Filter by account (either side of the
movement, exclusive with from/to), from, to, type, and a ledger
range. SAC registrations derive transfers by default; custom SEP-41 tokens
opt in through kinds.
Trustlines
For a registered SAC, Sierpe attributes the classic trustline changes of
the asset it wraps: live holders at /trustlines, and chain-order changes
with before/after balances at /trustlines/history. Opt in through
kinds. Native XLM has no trustlines, so the kind observes issued assets
only.
Movements
Transfers answer “what did this token emit”. Movements answer the other
question — “what came into and went out of my contract” — and they are
different questions: a payment to your contract is emitted by the asset’s
own SAC, not by your contract. Register the movements kind and every
token transfer naming your contract as sender or recipient lands here,
without registering the asset’s SAC at all.
Query parameters: role (recipient | sender; omit for both),
token (the emitting contract id — the asset’s real identity, never its
SEP-0011 string), type (transfer | mint | burn | clawback),
startLedger, endLedger, limit (1–1000, default 100) and cursor.
“Deposits” in the everyday sense are role=recipient — that includes
mints to the contract, since a mint is value arriving too.
Each row: id (shared by the two rows of a self-transfer — key on id + role), role, transferType, tokenContractId,
counterparty (absent on mints and burns), amount (exact i128 in raw
token units, as a string), rawXdr (since 1.6.0: the base64
ContractEvent the movement was decoded from — the emitting token is
usually not registered, so this is the only place the original bytes
exist; absent on rows ingested before the 1.6.0 migration), txHash
(since 1.12.0: the hex hash of the emitting transaction), ledger,
ledgerClosedAt. The page carries the usual cursor, scanStatus,
coverage and a note.
Movements indexed before 1.12.0 have no stored hash; the authenticated
POST /v1/admin/movements/tx-hashes fills them without any replay —
local joins over rows the database already trusts first, the remainder
resolved from the public History Archives in application order. It is
idempotent and paced by the caller: repeat the POST until done: true.
Because ingestion downloads whole ledgers, the descending backfill derives movement history from before the contract was registered — the thing dynamic-source indexers cannot do.
One honest caveat, stated by the API itself in a note field: movements
are not a balance. Value can also arrive without any SEP-41 transfer
event, and amounts are raw base units of different tokens — never sum
across tokenContractId.
Operational surface
| Path | Purpose |
|---|---|
/health |
Liveness |
/ready |
Readiness — 503 while catching up |
/status |
Cursor position, tip distance, per-contract summary |
/metrics |
Prometheus metrics (documented) |
The honesty contract
Every paginated response carries:
coverage— the ledger ranges this instance can actually answer for, derived from backfill progress and the live cursor. Since 1.5.0 coverage is declared per (contract, kind): a walk only vouches for the kinds it actually derived, and a kind added later reopens the walk instead of silently claiming history it never looked at.scanStatus—COMPLETE,HAS_MORE,WAITING_FOR_LEDGERSorOLDEST_REACHED.cursor— opaque, encodes the full query. Cursors are bound to their endpoint and cannot drift across filters. Since 1.8.0 every cursor —/eventsincluded — carries its kind and a cursor minted by a different endpoint is rejected with 400; cursors minted before the stamp remain valid.
Event ids follow the getEvents format: {toid}-{event_index},
zero-padded, stable across replays.
Access model
Reads are open; admin mutations require the ADMIN_TOKEN bearer. That
suits the default deployment shape — private networking, where nothing
outside your platform’s internal network reaches the instance.
If you do expose a public domain, set HTTP_BASIC_AUTH=user:password and
every request needs those credentials — the UI, the API and
/metrics — leaving only /health and /ready open for orchestrator
probes. Browsers prompt natively; clients send the standard header
(curl -u user:password …). Admin mutations still need the bearer on top.