Reference / REST API

REST API

Every /v1 surface answers with the same envelope, cursor, and filter grammar, so one client shape reads Index, Streams, and subgraphs. Served at http://127.0.0.1:3800 (SL_API_URL overrides); tokens: Authentication.

Per-endpoint parameters and response codes: API reference. Optional /extended JSON on :3999 is a different shape: Extended view.

{
  "events": [{ "block_height": 8054704, "event_index": 12 }],
  "next_cursor": "8054704:12",
  "tip": { "block_height": 8054704, "finalized_height": 8054262 },
  "reorgs": []
}
{
  "rows": [{ "_id": 48138, "block_height": 8054704 }],
  "next_cursor": "48137",
  "tip": { "block_height": 8054704, "subgraph_height": 8054704, "blocks_behind": 0 }
}

Cursor format is <block_height>:<event_index>. tip.finalized_height is the burn-confirmation finality boundary; rows at or below it never reorg, which is what a finalizedOnly consumer gates on.

SurfacePage sizeOrdering
Subgraph tables_limit (default 50, max 1000; on /v1 a non-integer, 0, negative, or over-1000 value is rejected with 400 rather than clamped)_id by default, or _sort=<column>&_order=asc|desc for one column (/v1); _order on its own is asc/desc only; any other value (a column name, desc,asc, …) returns 400; bare limit, order, or sort returns 400
Indexlimit (default 200, max 1000)Fixed by the keyset cursor, no order param
Streamslimit (default 100, max 1000)Fixed by the keyset cursor, no order param

Pass the previous response's next_cursor back as cursor (index and /v1/subgraphs). Index also accepts from_cursor (exclusive-start alias, mutually exclusive with cursor) and from_height/to_height.

curl http://127.0.0.1:3800/v1/subgraphs/sbtc-flows/transfers \
  -G \
  -d "sender=SP3PE7Q9...X44J" \
  -d "_order=desc"

Subgraph tables filter on any column, validated against the table's schema. Index and streams take a fixed set per endpoint (types, not_types, contract_id, sender, recipient, asset_identifier on /v1/streams/events).

Unknown params are rejected

Any unrecognized query param returns 400; the error suggests the closest valid param.

RouteReturns
GET /v1/subgraphsYour instance's subgraphs
GET /v1/subgraphs/:nameMetadata: tables, columns, indexing tip
GET /v1/subgraphs/:name/:tableRows; /count, /aggregate, /:id, /stream (SSE) alongside

Reads are open on loopback with wildcard CORS; published past loopback they require the INSTANCE_TOKEN bearer. Same rule on /v1/index and /v1/streams — there is no per-subgraph visibility flag, so who can reach the instance is what your publish spec and reverse proxy decide.

Keyset pagination: ?cursor=<next_cursor> resumes. By default the keyset is _id; add ?_sort=<column>&_order=asc|desc to sort by one column instead (jsonb columns and multi-column _sort=a,b are rejected with 400; the cursor pairs the sort column with _id as a tiebreaker, which only works for one column). _order on its own, with no _sort, is still asc/desc direction of the _id scan. The cursor's shape follows the sort: don't hand-construct one, and a cursor issued under one _sort/_order returns 400 if replayed under a different one. _limit, _fields, _search, and column filters work as everywhere else. _offset is rejected with 400: deep OFFSET scans hurt on big tables.

_search needs at least one column marked search: true in the subgraph's schema; on a table with none, it's rejected with 400 rather than silently matching everything.

curl -N "http://127.0.0.1:3800/v1/streams/events/stream?types=ft_transfer"

Two endpoints push rows as Server-Sent Events (Content-Type: text/event-stream) over one long-lived connection, each with a ping keepalive every 20s.

  • GET /v1/streams/events/stream: raw chain event firehose. Same filters as /v1/streams/events, plus a start position (cursor/from_cursor/from_height); with none it live-tails from the reorg-clamped tip.
  • GET /v1/subgraphs/:name/:table/stream: new subgraph-table rows. Takes ?since=<block_height> (replay from a height, then tail) plus the table's column filters.

Ergonomic clients: Streams, SDK.

GET /v1/subgraphs/:name/:table/aggregate runs scalar aggregates over the same filtered set as the list and count endpoints.

curl http://127.0.0.1:3800/v1/subgraphs/sbtc-flows/transfers/aggregate \
  -G \
  -d "status=active" \
  -d "_count=true" \
  -d "_sum=amount" \
  -d "_min=amount" \
  -d "_countDistinct=sender"
{
  "count": 42,
  "countDistinct": { "sender": 7 },
  "sum": { "amount": "12300000000" },
  "min": { "amount": "1000000" }
}
ParamEffect
_count=trueCOUNT(*)
_countDistinct=col1,col2Distinct count per column
_sum=col, _min=col, _max=colComma-separated; numeric columns only (uint/int, plus the system _block_height)

count and countDistinct.col are JSON numbers; sum/min/max are lossless strings. Over an empty set sum is "0", min/max are null. No aggregate param → { "count": <n> }.

Status / codeCause
400 NON_NUMERIC_COLUMNA non-numeric _sum/_min/_max column
400 INVALID_COLUMNAn unknown column
400 TOO_MANY_AGGREGATESPast 32 aggregate columns per request

Typed as subgraph.<table>.aggregate(spec) in the SDK.

curl "http://127.0.0.1:3800/v1/index/contracts/SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry/print-schema"

GET /v1/index/contracts/:contract_id/print-schema returns per-topic print payload schemas inferred from indexed history; response shape in Index.

  • Cached Cache-Control: public, max-age=300 with a weak ETag; send If-None-Match for 304.
  • Takes no query params; any param is rejected with 400 VALIDATION_ERROR.
curl "http://127.0.0.1:3800/v1/index/transactions/0x<tx_id>/proof"

GET /v1/index/transactions/:tx_id/proof returns a trustless inclusion proof. Response body, trust levels, error codes, and the @secondlayer/sdk verifier: Verification.