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_LEDGERS or OLDEST_REACHED.
  • cursor — opaque, encodes the full query. Cursors are bound to their endpoint and cannot drift across filters. Since 1.8.0 every cursor — /events included — 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.