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.
| Surface | Page size | Ordering |
|---|---|---|
| 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 |
| Index | limit (default 200, max 1000) | Fixed by the keyset cursor, no order param |
| Streams | limit (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.
| Route | Returns |
|---|---|
GET /v1/subgraphs | Your instance's subgraphs |
GET /v1/subgraphs/:name | Metadata: tables, columns, indexing tip |
GET /v1/subgraphs/:name/:table | Rows; /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" }
}| Param | Effect |
|---|---|
_count=true | COUNT(*) |
_countDistinct=col1,col2 | Distinct count per column |
_sum=col, _min=col, _max=col | Comma-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 / code | Cause |
|---|---|
400 NON_NUMERIC_COLUMN | A non-numeric _sum/_min/_max column |
400 INVALID_COLUMN | An unknown column |
400 TOO_MANY_AGGREGATES | Past 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=300with a weakETag; sendIf-None-Matchfor304. - 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.