<!-- source: https://www.secondlayer.tools/docs/api-reference -->

# API reference

Every endpoint and object, generated from the API's own OpenAPI description, in six pages by job. Each page shows every endpoint in full, with its parameters, an agent prompt, a request and a response; the sidebar lists them by family.

> **Start here**
>

The [SDK](/docs/sdk) already walks cursors, checkpoints, and rewinds reorgs. Credentials: [Keys and billing](/docs/authentication). Envelope, cursors, and filter grammar: [REST API](/docs/rest-api). Each endpoint's markdown is at `/docs/api-reference/<anchor>.md`, e.g. [`list-pox5-events.md`](/docs/api-reference/list-pox5-events.md).

# general

Discover what this API serves, batch several reads into one request, and fetch this description.

## The API discovery object

The public read surfaces and how to page them.

| Field | Type | Description |
| --- | --- | --- |
| `surfaces` | array of object | Each surface's `name`, base `path`, a one-line `description`, and its `auth` rule. |
| `openapi` | string | Path of this OpenAPI document. |
| `envelope_examples` | object | An illustrative cursor-paginated list envelope. |
| `cursor_format` | string | Index and Streams cursor format. |

```json
{
  "surfaces": [
    {
      "name": "index",
      "path": "/v1/index",
      "description": "Decoded chain events via /v1/index/events?event_type=… (stx/ft/nft transfers, mints, burns, stacking locks, and contract prints), with typed ft-transfers/nft-transfers aliases.",
      "auth": "open on this instance"
    },
    {
      "name": "streams",
      "path": "/v1/streams",
      "description": "Raw, ordered, cursor-paginated firehose with reorg awareness.",
      "auth": "open on this instance"
    },
    {
      "name": "subgraphs",
      "path": "/v1/subgraphs",
      "description": "Custom indexed views. Unique local name. Cursor envelope: { rows, next_cursor, tip }.",
      "auth": "open on this instance"
    },
    {
      "name": "proofs",
      "path": "/v1/proofs",
      "description": "Chain proofs to check Index rows without trusting this API: state witnesses, signed blocks, MARF proofs, burn-block preimages, Bitcoin headers. Proxied unchanged from the node and its proof sidecar. Never metered.",
      "auth": "open on this instance"
    },
    {
      "name": "instance",
      "path": "/v1/instance",
      "description": "Local catalog: instance status, subgraphs, webhooks, and default features. No signup or pricing.",
      "auth": "open on this instance"
    }
  ],
  "openapi": "/v1/openapi.json",
  "envelope_examples": {
    "cursor_paginated": {
      "events": [
        "..."
      ],
      "next_cursor": "7960000:42",
      "tip": {
        "block_height": 7978686,
        "lag_seconds": 12
      },
      "reorgs": [
        {
          "detected_at": "2026-05-16T00:00:00Z",
          "new_canonical_tip": "7960000:42",
          "new_canonical_height": 7960000,
          "new_canonical_event_index": 42
        }
      ]
    }
  },
  "cursor_format": "<block_height>:<event_index> (opaque resume token)"
}
```

## The Batch response object

The batch results.

| Field | Type | Description |
| --- | --- | --- |
| `results` | array of object | One entry per request, in order: the item's `path`, the HTTP `status` it got, and its JSON `body`. A path outside the allowed prefixes gets status 400; an item that throws gets status 500 with code `INTERNAL_ERROR`. |

```json
{
  "results": [
    {
      "path": "/v1/streams/tip",
      "status": 200,
      "body": {
        "block_height": 9048928,
        "block_hash": "0xe79ae38b78b37ad1f5d16c7afd04b4e202efe8d983d981803b785ca64257933f",
        "burn_block_height": 968284,
        "finalized_height": 9048671,
        "lag_seconds": 4,
        "oldest_seekable_height": null,
        "oldest_cursor": null
      }
    },
    {
      "path": "/v1/streams/canonical/9048876",
      "status": 200,
      "body": {
        "block_height": 9048876,
        "block_hash": "0xba0676b375e8d5fc1495a02847cc3cc32b5f7c2113a5e73999d94ee8d75ebde4",
        "burn_block_height": 968284,
        "burn_block_hash": "0x00000000000000000001b1cadea50180db28922325872c9f16ca19edda3f8bae",
        "is_canonical": true
      }
    },
    {
      "path": "/api/subgraphs",
      "status": 400,
      "body": {
        "error": "Path not allowed in batch (public /v1 reads only)",
        "code": "VALIDATION_ERROR"
      }
    }
  ]
}
```

## Surface discovery

`GET /v1`

Lists the public read surfaces on this instance, where this document lives, the list envelope shape, and the cursor format. A starting point for an agent that has only the base URL.

### Returns

One `V1Discovery` object.

### Responses

- `200` The surfaces this instance serves
- `401` This bind requires the instance token, and it was missing or wrong

### Example

```bash
curl https://api.secondlayer.tools/v1 \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "surfaces": [
    {
      "name": "index",
      "path": "/v1/index",
      "description": "Decoded chain events via /v1/index/events?event_type=… (stx/ft/nft transfers, mints, burns, stacking locks, and contract prints), with typed ft-transfers/nft-transfers aliases.",
      "auth": "open on this instance"
    },
    {
      "name": "streams",
      "path": "/v1/streams",
      "description": "Raw, ordered, cursor-paginated firehose with reorg awareness.",
      "auth": "open on this instance"
    },
    {
      "name": "subgraphs",
      "path": "/v1/subgraphs",
      "description": "Custom indexed views. Unique local name. Cursor envelope: { rows, next_cursor, tip }.",
      "auth": "open on this instance"
    },
    {
      "name": "proofs",
      "path": "/v1/proofs",
      "description": "Chain proofs to check Index rows without trusting this API: state witnesses, signed blocks, MARF proofs, burn-block preimages, Bitcoin headers. Proxied unchanged from the node and its proof sidecar. Never metered.",
      "auth": "open on this instance"
    },
    {
      "name": "instance",
      "path": "/v1/instance",
      "description": "Local catalog: instance status, subgraphs, webhooks, and default features. No signup or pricing.",
      "auth": "open on this instance"
    }
  ],
  "openapi": "/v1/openapi.json",
  "envelope_examples": {
    "cursor_paginated": {
      "events": [
        "..."
      ],
      "next_cursor": "7960000:42",
      "tip": {
        "block_height": 7978686,
        "lag_seconds": 12
      },
      "reorgs": [
        {
          "detected_at": "2026-05-16T00:00:00Z",
          "new_canonical_tip": "7960000:42",
          "new_canonical_height": 7960000,
          "new_canonical_event_index": 42
        }
      ]
    }
  },
  "cursor_format": "<block_height>:<event_index> (opaque resume token)"
}
```

## Batch public reads

`POST /v1/batch`

Up to 10 GET reads under `/v1/index/`, `/v1/subgraphs`, `/v1/streams/` and `/v1/contracts` in one round trip. Items run in parallel through the full app, so auth, rate limits and retention apply per item; the request's `Authorization` header is forwarded to every item. Results come back in request order, each with its own HTTP status and JSON body. A refused or failed item does not fail the batch.

### Body

| Field | Type | Description |
| --- | --- | --- |
| `requests` | array of object, required | The reads to run, 1 to 10. |

### Returns

One `BatchResponse` object.

### Responses

- `200` One result per request, in request order
- `400` The body is not `{ requests: [...] }`, `requests` is empty, or it holds more than 10 items (`VALIDATION_ERROR`)
- `401` This bind requires the instance token, and it was missing or wrong

### Example

```bash
curl -X POST https://api.secondlayer.tools/v1/batch \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  --json '{
  "requests": [
    {
      "path": "/v1/streams/tip"
    },
    {
      "path": "/v1/streams/canonical/9048876"
    },
    {
      "path": "/api/subgraphs"
    }
  ]
}'
```

```json
{
  "results": [
    {
      "path": "/v1/streams/tip",
      "status": 200,
      "body": {
        "block_height": 9048928,
        "block_hash": "0xe79ae38b78b37ad1f5d16c7afd04b4e202efe8d983d981803b785ca64257933f",
        "burn_block_height": 968284,
        "finalized_height": 9048671,
        "lag_seconds": 4,
        "oldest_seekable_height": null,
        "oldest_cursor": null
      }
    },
    {
      "path": "/v1/streams/canonical/9048876",
      "status": 200,
      "body": {
        "block_height": 9048876,
        "block_hash": "0xba0676b375e8d5fc1495a02847cc3cc32b5f7c2113a5e73999d94ee8d75ebde4",
        "burn_block_height": 968284,
        "burn_block_hash": "0x00000000000000000001b1cadea50180db28922325872c9f16ca19edda3f8bae",
        "is_canonical": true
      }
    },
    {
      "path": "/api/subgraphs",
      "status": 400,
      "body": {
        "error": "Path not allowed in batch (public /v1 reads only)",
        "code": "VALIDATION_ERROR"
      }
    }
  ]
}
```

## This document

`GET /v1/openapi.json`

The OpenAPI 3.1 description of this instance, as JSON. It describes the instance's own mode: a self-hosted instance includes the `/api` write plane, the hosted archive leaves it out.

### Responses

- `200` The OpenAPI document
- `401` This bind requires the instance token, and it was missing or wrong

### Example

```bash
curl https://api.secondlayer.tools/v1/openapi.json \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "openapi": "3.1.0",
  "info": {
    "title": "Secondlayer Public API",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "http://127.0.0.1:3800",
      "description": "Local instance"
    }
  ],
  "tags": [
    {
      "name": "streams",
      "description": "Raw event firehose, chain tip, and reorg history"
    }
  ],
  "paths": {},
  "components": {}
}
```

---

# index

Decoded chain events (transfers, mints/burns, prints, stacking)

## The Index discovery object

Every Index route with the filters it accepts.

| Field | Type | Description |
| --- | --- | --- |
| `routes` | array of object | One entry per route: `path`, `method`, `description`, and `filters` where it takes any. The `/v1/index/events` entry adds `event_types` and `event_type_filters` (columns and accepted filters per type). |
| `auth` | string | How to authenticate, in prose. |
| `cursor` | object | Cursor format and how to resume with it. |

```json
{
  "routes": [
    {
      "path": "/v1/index/ft-transfers",
      "method": "GET",
      "description": "Alias for /events?event_type=ft_transfer. Fungible token transfers, decoded + filterable.",
      "filters": [
        "limit",
        "cursor",
        "from_cursor",
        "from_height",
        "to_height",
        "order",
        "contract_id",
        "sender",
        "recipient",
        "asset_identifier",
        "fields"
      ]
    },
    {
      "path": "/v1/index/blocks",
      "method": "GET",
      "description": "Canonical blocks, cursor-paginated. Returns blocks[] ({block_height, block_hash, parent_hash, burn_block_height, burn_block_hash, block_time, canonical}), next_cursor, tip.",
      "filters": [
        "limit",
        "cursor",
        "from_cursor",
        "from_height",
        "to_height"
      ]
    }
  ],
  "cursor": {
    "format": "<block_height>:<event_index>",
    "semantics": "opaque resume token; pass back unchanged to continue. Equals last event's cursor (inclusive on output, exclusive on input). Under order=desc it carries a `desc:` prefix and resumes only under order=desc."
  }
}
```

## The Index event object

One decoded event, flat. The universal columns are always there; the rest depend on `event_type`, and a type's own columns are always set for its rows.

| Field | Type | Description |
| --- | --- | --- |
| `cursor` | string | `<block_height>:<event_index>`. Pass as `cursor`. |
| `block_height` | integer | Stacks block the row landed in. |
| `block_time` | string · date-time · nullable | That block's timestamp, ISO 8601 UTC. |
| `tx_id` | string | Transaction id, `0x`-prefixed hex. |
| `tx_index` | integer | The transaction's position in its block. |
| `event_index` | integer | The event's position in the block. With `block_height`, the cursor. |
| `event_type` | enum | The event type. Decides which columns are set. Values: `ft_transfer`, `nft_transfer`, `stx_transfer`, `stx_mint`, `stx_burn`, `stx_lock`, `ft_mint`, `ft_burn`, `nft_mint`, `nft_burn`, `print`. |
| `contract_id` | string · nullable | Contract that emitted the event. `null` for STX transfers, mints, burns and locks. |
| `asset_identifier` | string · nullable | `<contract>::<asset>`, on ft and nft types. |
| `sender` | string · nullable | Sending principal. The locked address on `stx_lock`. |
| `recipient` | string · nullable | Receiving principal. |
| `amount` | string · nullable | Amount as a decimal string (bigint-safe). microSTX on STX types. |
| `value` | string · nullable | NFT id as hex-encoded Clarity, on nft types. |
| `memo` | string · nullable | Transfer memo, on `stx_transfer`. |
| `payload` | object | Decoded print payload on `print`; `{ unlock_height }` on `stx_lock`. |
| `tx_sender` | string · nullable | Submitting transaction's sender. Only with `tx_context=true`. |
| `tx_type` | string · nullable | Submitting transaction's type. Only with `tx_context=true`. |
| `tx_status` | string · nullable | Submitting transaction's status. Only with `tx_context=true`. |
| `tx_contract_id` | string · nullable | Contract the submitting transaction called or deployed. Only with `tx_context=true`. |
| `tx_function_name` | string · nullable | Function the submitting transaction called. Only with `tx_context=true`. |

```json
{
  "cursor": "9048921:2",
  "block_height": 9048921,
  "block_time": "2026-09-23T15:09:05.000Z",
  "tx_id": "0x55249ff4ec221ff6852b7fa37cf3729c7c50bc63f0da760f96e6886e2e8a911e",
  "tx_index": 4,
  "event_index": 2,
  "event_type": "stx_transfer",
  "contract_id": null,
  "sender": "SPX4KR3G5Z1SZT4RVF4ATHV4ZP4KXPWSZ4424W6K",
  "recipient": "SP2Z2CBMGWB9MQZAF5Z8X56KS69XRV3SJF4WKJ7J9",
  "amount": "50",
  "memo": null
}
```

## The FT transfer object

One fungible token transfer.

| Field | Type | Description |
| --- | --- | --- |
| `cursor` | string | `<block_height>:<event_index>`. Pass as `cursor`. |
| `block_height` | integer | Stacks block the row landed in. |
| `block_time` | string · date-time · nullable | That block's timestamp, ISO 8601 UTC. |
| `tx_id` | string | Transaction id, `0x`-prefixed hex. |
| `tx_index` | integer | The transaction's position in its block. |
| `event_index` | integer | The event's position in the block. With `block_height`, the cursor. |
| `event_type` | enum | Always `ft_transfer`. Values: `ft_transfer`. |
| `contract_id` | string | Token contract. |
| `asset_identifier` | string | `<contract>::<asset>`. |
| `sender` | string | Sending principal. |
| `recipient` | string | Receiving principal. |
| `amount` | string | Amount in the token's base units, as a decimal string (bigint-safe). |

```json
{
  "cursor": "9048918:3",
  "block_height": 9048918,
  "block_time": "2026-09-23T15:08:18.000Z",
  "tx_id": "0xc342cb7616498a84a8dcfac62a991c31c037d8c6f98a11f987f0719fd619e672",
  "tx_index": 2,
  "event_index": 3,
  "event_type": "ft_transfer",
  "contract_id": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token",
  "asset_identifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
  "sender": "SP1A27KFY4XERQCCRCARCYD1CC5N7M6688BSYADJ7.v0-8-market",
  "recipient": "SP1A27KFY4XERQCCRCARCYD1CC5N7M6688BSYADJ7.v0-vault-sbtc",
  "amount": "15000"
}
```

## The NFT transfer object

One NFT transfer.

| Field | Type | Description |
| --- | --- | --- |
| `cursor` | string | `<block_height>:<event_index>`. Pass as `cursor`. |
| `block_height` | integer | Stacks block the row landed in. |
| `block_time` | string · date-time · nullable | That block's timestamp, ISO 8601 UTC. |
| `tx_id` | string | Transaction id, `0x`-prefixed hex. |
| `tx_index` | integer | The transaction's position in its block. |
| `event_index` | integer | The event's position in the block. With `block_height`, the cursor. |
| `event_type` | enum | Always `nft_transfer`. Values: `nft_transfer`. |
| `contract_id` | string | NFT contract. |
| `asset_identifier` | string | `<contract>::<asset>`. |
| `sender` | string | Sending principal. |
| `recipient` | string | Receiving principal. |
| `value` | string | The token id as hex-encoded Clarity. |

```json
{
  "cursor": "9048916:0",
  "block_height": 9048916,
  "block_time": "2026-09-23T15:07:41.000Z",
  "tx_id": "0x269d7877002b998ad3a3ee35f661610cc8f2be6eec604905311d52cf1cf80caa",
  "tx_index": 0,
  "event_index": 0,
  "event_type": "nft_transfer",
  "contract_id": "SPVD6CE8RW90BGGKJZTKCSMGKS7HP0K8364TFR48.bitcoin-faces-airdrop",
  "asset_identifier": "SPVD6CE8RW90BGGKJZTKCSMGKS7HP0K8364TFR48.bitcoin-faces-airdrop::bitcoin-faces",
  "sender": "SP3KMHQ80ZGT037R2FCHV1F7Z0X25EV6GVEYNAB87",
  "recipient": "SP37YJNKVKP3KD5SFK71NW7DC25RRF4HKWWQRH3NT",
  "value": "0x010000000000000000000000000008331e"
}
```

## The Contract call object

One top-level contract-call transaction, arguments and result decoded to JSON. Clarity integers come back as decimal strings.

| Field | Type | Description |
| --- | --- | --- |
| `cursor` | string | `<block_height>:<tx_index>`. Pass as `cursor`. |
| `block_height` | integer | Stacks block the row landed in. |
| `block_time` | string · date-time · nullable | That block's timestamp, ISO 8601 UTC. |
| `tx_id` | string | Transaction id, `0x`-prefixed hex. |
| `tx_index` | integer | The transaction's position in its block. |
| `contract_id` | string | Called contract. |
| `function_name` | string | Called function. |
| `sender` | string | Transaction sender principal. |
| `status` | string | Execution status, e.g. `success` or `abort_by_response`. |
| `args` | array of object | Decoded arguments, in call order. |
| `result` | object | Decoded result. `null` when there is none. |
| `result_hex` | string · nullable | The result as hex-encoded Clarity. |

```json
{
  "cursor": "9048502:1",
  "block_height": 9048502,
  "block_time": "2026-09-23T13:14:42.000Z",
  "tx_id": "0x198d642edb1104a1757a8c353ad61231d0422ecb6ba83aa1ce45d05df44157e0",
  "tx_index": 1,
  "contract_id": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token",
  "function_name": "transfer",
  "sender": "SP15T1W26JTNS26VG17HM468KW7TQD3124KTYA9EJ",
  "status": "success",
  "args": [
    "300",
    "SP15T1W26JTNS26VG17HM468KW7TQD3124KTYA9EJ",
    "SPD3SCZDZ52X4ZX1878XWBFBJ29YRX0MFW9Z9W4V",
    null
  ],
  "result": true,
  "result_hex": "0x0703"
}
```

## The Canonical block object

One height of the canonical chain: the hash, its parent, and the Bitcoin anchor. Every row is canonical.

| Field | Type | Description |
| --- | --- | --- |
| `cursor` | string | `<block_height>:0`. Pass as `cursor`. |
| `block_height` | integer | Stacks block height. |
| `block_hash` | string | The block's hash. |
| `parent_hash` | string | The parent block's hash, to check linkage. |
| `burn_block_height` | integer | The Bitcoin block it anchors to. |
| `burn_block_hash` | string · nullable | That Bitcoin block's hash. |

```json
{
  "cursor": "9048503:0",
  "block_height": 9048503,
  "block_hash": "0x5560590afefe994adbe2188586a44ba3668e8964a7e64bb137c0df216531da0c",
  "parent_hash": "0x2a20974330e301b65086d3cde9b0006c9b7471479ba9a3fc2ca72621479655cc",
  "burn_block_height": 968276,
  "burn_block_hash": "0x000000000000000000019d25cd76d06a9eea796a906ac88300cd70fd70ccdb57"
}
```

## The State write object

One MARF write a block committed, as the node's `state_writes` observer key delivered it.

| Field | Type | Description |
| --- | --- | --- |
| `cursor` | string | `<block_height>:<ordinal>`. Pass as `cursor`. |
| `block_height` | integer | Stacks block height. |
| `ordinal` | integer | Position in the block's write order, from 0. A separate clock from event_index. |
| `tx_index` | integer · nullable | The writing transaction's index in the block; null for block-level writes. |
| `key` | string | The full MARF key, e.g. `vm::<contract>::0::map::<hex>` or `vm-account::<address>::19`. |
| `value_hex` | string | The value written, hex as the node sent it. |
| `tx_id` | string · nullable | Writing transaction's id. Only with `tx_context=true`; null for block-level writes. |
| `tx_sender` | string · nullable | Writing transaction's sender. Only with `tx_context=true`. |
| `tx_type` | string · nullable | Writing transaction's type. Only with `tx_context=true`. |
| `tx_status` | string · nullable | Writing transaction's status. Only with `tx_context=true`. |
| `tx_contract_id` | string · nullable | Contract the writing transaction called or deployed. Only with `tx_context=true`. |
| `tx_function_name` | string · nullable | Function the writing transaction called. Only with `tx_context=true`. |

```json
{
  "cursor": "100:1",
  "block_height": 100,
  "ordinal": 1,
  "tx_index": 0,
  "key": "vm::ST3FEXKRAY93SR2MERXNSAXWGV9XSDNM1MVEFY5TH.store::0::map::0d0000000568656c6c6f",
  "value_hex": "30313030303030303030303030303030303030303030303030303030303030303031"
}
```

## The Block object

A Stacks block: chain linkage and Bitcoin anchor. Only what ingest stores; no miner, tx count or cost data.

| Field | Type | Description |
| --- | --- | --- |
| `cursor` | string | `<block_height>:0`. Pass as `cursor`. |
| `block_height` | integer | Stacks block height. |
| `block_hash` | string | The block's hash. |
| `parent_hash` | string | The parent block's hash. |
| `burn_block_height` | integer | The Bitcoin block it anchors to. |
| `burn_block_hash` | string · nullable | That Bitcoin block's hash. |
| `index_block_hash` | string · nullable | Nakamoto block id. Pass it as `?tip=` to a node read-only call to pin the read to this block. `null` on rows ingested before it was stored. |
| `block_time` | string · date-time · nullable | That block's timestamp, ISO 8601 UTC. |
| `canonical` | boolean | Whether the block is on the canonical chain. Always `true` in the list; a hash lookup can return an orphan. |

```json
{
  "cursor": "9048502:0",
  "block_height": 9048502,
  "block_hash": "0x2a20974330e301b65086d3cde9b0006c9b7471479ba9a3fc2ca72621479655cc",
  "parent_hash": "0x10606e958095ef9b964e3c3f2d9eb75bf8577fb8c09c24ee492b767f7504abea",
  "burn_block_height": 968276,
  "burn_block_hash": "0x000000000000000000019d25cd76d06a9eea796a906ac88300cd70fd70ccdb57",
  "index_block_hash": "0x990152a894e12288960c8b68e5790ad7994f9d261e9ad8adc13c72e76f914339",
  "block_time": "2026-09-23T13:14:42.000Z",
  "canonical": true
}
```

## The Transaction object

A confirmed transaction: stored columns plus what decodes from the raw bytes. Payload objects appear only for the matching `tx_type`. Decoded fields are `null` when the raw bytes do not decode (burnchain ops).

| Field | Type | Description |
| --- | --- | --- |
| `cursor` | string | `<block_height>:<tx_index>`. Pass as `cursor`. |
| `tx_id` | string | Transaction id, `0x`-prefixed hex. |
| `block_height` | integer | Stacks block the row landed in. |
| `block_time` | string · date-time · nullable | That block's timestamp, ISO 8601 UTC. |
| `burn_block_height` | integer · nullable | The Bitcoin block its Stacks block anchors to. |
| `tx_index` | integer | The transaction's position in its block. |
| `status` | string | Execution status, e.g. `success` or `abort_by_response`. |
| `tx_type` | string | `token_transfer`, `contract_call`, `smart_contract`, `coinbase`, `tenure_change` or `poison_microblock`, decoded from the raw bytes. Falls back to the stored type when they do not decode. |
| `sender` | string | Sender principal. |
| `fee` | string · nullable | Fee in microSTX as a decimal string. `null` when the raw bytes do not decode. |
| `nonce` | string · nullable | Sender nonce as a decimal string. |
| `sponsored` | boolean · nullable | Whether a sponsor paid the fee. |
| `anchor_mode` | enum · nullable | Anchor mode from the raw bytes. Values: `on_chain_only`, `off_chain_only`, `any`. |
| `post_condition_mode` | enum · nullable | Post-condition mode from the raw bytes. Values: `allow`, `deny`. |
| `post_conditions` | array of PostCondition | Decoded post-conditions. Empty when there are none. |
| `token_transfer` | object | `token_transfer` only. |
| `coinbase` | object | `coinbase` only. |
| `tenure_change` | object | `tenure_change` only. |
| `contract_call` | object | `contract_call` only. |
| `smart_contract` | object | `smart_contract` only. |

```json
{
  "cursor": "9048502:1",
  "tx_id": "0x198d642edb1104a1757a8c353ad61231d0422ecb6ba83aa1ce45d05df44157e0",
  "block_height": 9048502,
  "block_time": "2026-09-23T13:14:42.000Z",
  "burn_block_height": 968276,
  "tx_index": 1,
  "tx_type": "contract_call",
  "sender": "SP15T1W26JTNS26VG17HM468KW7TQD3124KTYA9EJ",
  "status": "success",
  "fee": "3000",
  "nonce": "265",
  "sponsored": false,
  "anchor_mode": "any",
  "post_condition_mode": "deny",
  "post_conditions": [
    {
      "type": "ft",
      "principal": "SP15T1W26JTNS26VG17HM468KW7TQD3124KTYA9EJ",
      "asset_identifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
      "condition_code": 1,
      "condition_code_name": "sent_eq",
      "amount": "300"
    }
  ],
  "contract_call": {
    "contract_id": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token",
    "function_name": "transfer",
    "function_args": [
      "300",
      "SP15T1W26JTNS26VG17HM468KW7TQD3124KTYA9EJ",
      "SPD3SCZDZ52X4ZX1878XWBFBJ29YRX0MFW9Z9W4V",
      null
    ],
    "function_args_hex": [
      "010000000000000000000000000000012c",
      "05164ba0f04696ab911b7009e3421913e1f5768c2224",
      "05161a3cb3edf945d27fa141d1de2deb9093ec74147f",
      "09"
    ],
    "result": true,
    "result_hex": "0x0703"
  }
}
```

## The Transaction proof object

Everything needed to verify inclusion offline. Hex values have no `0x` prefix.

| Field | Type | Description |
| --- | --- | --- |
| `txid` | string | Transaction id. Recompute it from `raw_tx`. |
| `index_block_hash` | string | Nakamoto block id. Recompute it from `raw_header`. |
| `block_height` | integer | Stacks block height. |
| `tx_index` | integer | The transaction's position in the block; the merkle leaf index. |
| `raw_tx` | string | The serialized transaction. |
| `raw_header` | string | The serialized Nakamoto block header, signer signatures included. |
| `tx_merkle_path` | array of object | Sibling hashes from the leaf up to the header's tx merkle root. |
| `consensus` | object | The reward cycle and its signer set, to check the header's signatures pass 70% of signer weight. Absent when the node cannot resolve the set. |

```json
{
  "txid": "198d642edb1104a1757a8c353ad61231d0422ecb6ba83aa1ce45d05df44157e0",
  "index_block_hash": "990152a894e12288960c8b68e5790ad7994f9d261e9ad8adc13c72e76f914339",
  "block_height": 9048502,
  "tx_index": 1,
  "raw_tx": "000000000104004ba0f04696ab911b7009e3421913e1f5768c222400000000000001090000000000000bb8000123767ba956a0545b2543f059d003500767330fad1f4e6cce3d734c8719bfca9226c07dc6e64edb83078cb34e83c6b430d4a260628af821041f51eb5f21e444390302000000010102164ba0f04696ab911b7009e3421913e1f5768c222414f6decc7cfff2a413bd7cd4f53c25ad7fd1899acc0a736274632d746f6b656e0a736274632d746f6b656e01000000000000012c0214f6decc7cfff2a413bd7cd4f53c25ad7fd1899acc0a736274632d746f6b656e087472616e7366657200000004010000000000000000000000000000012c05164ba0f04696ab911b7009e3421913e1f5768c222405161a3cb3edf945d27fa141d1de2deb9093ec74147f09",
  "raw_header": "0100000000008a11b60000006643539084f394038c2267d08e5e5c52b9da2d99f4360c91a5314c3cac40ac15f4397b92360ccaa5adaeda6ebc5587511f33b1495fe1d1f66d10b0bfc1ef83918390f25ad4f0b7f5ddf76fa79782e92da2315856590664d9c179ccc0143e35f99140069ea4517ab4ca79e39f0e54b406b1b68b5f8c29e25750000000006ab3d0c2012ae8acc3a85100ae9eecf07468c694fee3cb585c423a0df3054991c77f4fa3fb4fc7b300f1b332f02ec0488c4033c7eb72abec17999c01326d222225f430e56d000000130016f1a9fb8ef3c85b04bd863818f4b47680de8f33e375b2872fbdd5b94d1664624488458f13383478becec32e9ce8ac9b2d0292d2773b37bf1c5943e87cf35b31006dbcd0a22c1eaf81d34dd1c2f25a1e05b7d6e5fdd3c5d8c7e4b96aa4472088e14d8b7ece3427f77fbcfd4027e7bd27be1388ddb68efb8debf8dfe48da558987400e3df41a2cc852d41b6db8f67c8e6c7bf1c31d185c87b019d349a5d6713c951ca43837c05b62a9f0b58ac4403a98233f1e1573e031fae90306ccd618dff1ce8eb01f4bba3a39010779d4870906d1f7e66115b8146f6b70b4e4fee3a402fa484c1d40146832b8fb21ab3328d7588e3861d9c614522692bd14fb763c77a2203d0b86a010075c0105d54deac9a6388734e0519d7ba90024727f02d65b8fd2cd788eca9350c6f6c7ff52200e41cefbf19873bcb9c17068ca9b093fd032dd68683348e95550083c787948e7bd477341747c8769ce2e9c8a420cb137d3609b5e90163f0c56ac3747219c113b0b5018aefa1dc9bd415b21b48173cb9c42f96f19cf143a77261340092179e100c58cfc2b3ee3271eca056ad98c986a1b434296eb1bb00986f3c1a7e70d89fdcbb983f216b9b9d5b5bf21093f58bf2e022fb0010f6706010ebe8c09f01bf06697f3e93883ebf59f83a98bc032d7b3da70886b6e44309cd77855dc1bf8c099084b40609c0a4e3d04806a0fd23391ad79745ba52b122838e3edf6359aa2d006d529e1f590e58b3d1519b719d1149e1babfa1cf20b164167199da2804dec7004159c9f6cb90f265b0d7c3338905de677087bbf9e40a7f06159a4697014756c501726fe445f8a4c876138ff40581e34a659463f97d7b2d1cc843b82d5c8ec9cd2e326ab34b509f4c193f424ebd2ddd729f007687c88b66af59ddda1036b1c7da8700d9a1b7f528df90661c608fd1935d95abb0f638b780573101837863353daf50c529e02703891e885ffa2603ca19a8ab1724c07795e6ab458e93abdca27ac67c3600c4202f95645edb945c2659345fd1dc5043d9fd178c398ddb2ed55ffc5bb9c62f2a61ada05578631cd6523fadf4a47be96133216b5cd421222a1289914ec41d5600ae2119da699f9eea1a8c49b0cf63c2ed5191d141491f11893869e04f24ad14d20dabc52b11e4418cd46610709c7d6456ff0a235760dc39c7e4c057fc8802fa4101813c588a7730569283f517b53949f67aa46a214530dab382da6ec3afc7014ede023706361ec717f8487a88f9d8ae9f912b3033124fccf8c98e5bcf382208febf016ad6d2d6a0c77ae937dc663b7111ab145e4df0248bcb1da80dce6b5bf0061539705f37412cf59476a22ee6677c20c9b980f263f8c1d4c79acd8d7166ff1a7b4f00aecd8f47fee2d6819993aa0d2e3e4872e2a95550c7403491f9764ebfa832f38b61704739abda8cfb0e05d8020efa5e49a5704dbd82f79fd3638b61005c37867c00025ea0925e7583f2f43c9ad4ff6ceaa8ef6bb62cb15265ebf684ca9005f5bb9312009b7e7bc171291c4c2530b74b275e57fc53072cd381a1cb27dc63232c0835007a5b80bc0b5af6848568468cc2a10f74789e1a1e0bcf6f18bb45f8df4e7a19f55c26f593b23aa76c2b7171aff4b062a58f6611da097071298621735521c7ee110062254da4170c6e66963e8a74c9a4fb5797e4247f5ccd8784c82d70ae96f5f01644e6b7733ca93f0e065fc165e152b4abd602687f2f081ab39c8f0943d3b51ea90001000000010100000000",
  "tx_merkle_path": [
    {
      "position": "left",
      "hash": "ea5ecd867b9945b06aecdf77117de46ae9fd055b137be1fecc1dee38f56b19af"
    },
    {
      "position": "right",
      "hash": "398e676d798b758c66c50e21a83b4c6c8c20a5ffc8552b61d1df160f36a0c23c"
    },
    {
      "position": "right",
      "hash": "d1315b8f5f61861868657a0ab2c8c62332bb77a74a833567427a9f95ab86c1eb"
    }
  ],
  "consensus": {
    "reward_cycle": 143,
    "reward_set": {
      "signers": [
        {
          "signing_key": "0209130bec93e83b23d3366adfcbe0a1641057b9c48102572d892fe8f78de2bee7",
          "weight": 646
        },
        {
          "signing_key": "0216436e7785e1dc55cf4350ac1f90e45daf0ec5b57bce13a32551a513446e71d1",
          "weight": 9
        }
      ],
      "total_weight": 4000
    }
  }
}
```

## The Mempool transaction object

A pending transaction: the transaction document before it has a block, so no height, index, status or result, plus when this instance first saw it.

| Field | Type | Description |
| --- | --- | --- |
| `cursor` | string | Opaque. Pass it back as `cursor` unchanged. |
| `tx_id` | string | Transaction id, `0x`-prefixed hex. |
| `received_at` | string · date-time · nullable | When this instance's node first saw it, ISO 8601 UTC. |
| `tx_type` | string | `token_transfer`, `contract_call`, `smart_contract`, `coinbase`, `tenure_change` or `poison_microblock`, decoded from the raw bytes. Falls back to the stored type when they do not decode. |
| `sender` | string | Sender principal. |
| `fee` | string · nullable | Fee in microSTX as a decimal string. `null` when the raw bytes do not decode. |
| `nonce` | string · nullable | Sender nonce as a decimal string. |
| `sponsored` | boolean · nullable | Whether a sponsor paid the fee. |
| `anchor_mode` | enum · nullable | Anchor mode from the raw bytes. Values: `on_chain_only`, `off_chain_only`, `any`. |
| `post_condition_mode` | enum · nullable | Post-condition mode from the raw bytes. Values: `allow`, `deny`. |
| `post_conditions` | array of PostCondition | Decoded post-conditions. Empty when there are none. |
| `token_transfer` | object | `token_transfer` only. |
| `coinbase` | object | `coinbase` only. |
| `tenure_change` | object | `tenure_change` only. |
| `contract_call` | object | `contract_call` only. No result yet. |
| `smart_contract` | object | `smart_contract` only. |

```json
{
  "cursor": "bToyNjg4Mjc0",
  "tx_id": "0x00a5cd852c25c5bb5cc7f97c60b88a6a635c001ad431e2133f366be85d5ce0f0",
  "tx_type": "token_transfer",
  "sender": "SM1Z09F6431EQT8GCTEF496TCT5JDTPJ3VFF9Q766",
  "received_at": "2026-09-23T15:11:05.629Z",
  "fee": "1732",
  "nonce": "37",
  "sponsored": false,
  "anchor_mode": "any",
  "post_condition_mode": "deny",
  "post_conditions": [],
  "token_transfer": {
    "recipient": "SMJWA5YRH2DRANX3V96Z6JJSFBE03Z9Y0WHN3XSQ",
    "amount": "79559850769",
    "memo": "572826509"
  }
}
```

## The Print schema object

A contract's print payloads, grouped by `topic` and typed field by field from the events sampled.

| Field | Type | Description |
| --- | --- | --- |
| `contract_id` | string | The contract read. |
| `topics` | array of object | One entry per `topic` value. Prints with no string `topic` group under `*`. |
| `sampled` | boolean | `true` when the contract has more print events than were read. |
| `total_events` | integer | Canonical print events for the contract, capped at 50000. |
| `total_events_capped` | boolean | `true` when `total_events` hit the cap. |
| `sample` | object | What was read. |
| `tip` | Tip |  |

```json
{
  "contract_id": "SP2BM6AQSMQ04CX8KDE62QBFVZTDZ2ZX80GZJSBZ4.zc-claim-helper-v2",
  "topics": [
    {
      "topic": "zc-claim-ok",
      "count": 2,
      "first_height": 9004646,
      "last_height": 9004646,
      "non_tuple": false,
      "fields": [
        {
          "name": "earned",
          "camel_name": "earned",
          "clarity_type": "uint",
          "ts_type": "bigint",
          "column_type": "uint",
          "always_present": true
        },
        {
          "name": "reward-cycle",
          "camel_name": "rewardCycle",
          "clarity_type": "uint",
          "ts_type": "bigint",
          "column_type": "uint",
          "always_present": true
        },
        {
          "name": "staker",
          "camel_name": "staker",
          "clarity_type": "principal",
          "ts_type": "string",
          "column_type": "principal",
          "always_present": true
        },
        {
          "name": "withdrawal-request",
          "camel_name": "withdrawalRequest",
          "clarity_type": "(optional ?)",
          "ts_type": "unknown | null",
          "column_type": "jsonb",
          "always_present": true,
          "optional_some_rate": 0
        }
      ]
    },
    {
      "topic": "claim-many",
      "count": 1,
      "first_height": 9004646,
      "last_height": 9004646,
      "non_tuple": false,
      "fields": [
        {
          "name": "err-count",
          "camel_name": "errCount",
          "clarity_type": "uint",
          "ts_type": "bigint",
          "column_type": "uint",
          "always_present": true
        },
        {
          "name": "manager",
          "camel_name": "manager",
          "clarity_type": "principal",
          "ts_type": "string",
          "column_type": "principal",
          "always_present": true
        },
        {
          "name": "ok-count",
          "camel_name": "okCount",
          "clarity_type": "uint",
          "ts_type": "bigint",
          "column_type": "uint",
          "always_present": true
        },
        {
          "name": "submitted",
          "camel_name": "submitted",
          "clarity_type": "uint",
          "ts_type": "bigint",
          "column_type": "uint",
          "always_present": true
        }
      ]
    }
  ],
  "sampled": false,
  "total_events": 3,
  "total_events_capped": false,
  "sample": {
    "size": 3,
    "newest_height": 9004646,
    "oldest_height": 9004646
  },
  "tip": {
    "block_height": 9048938,
    "finalized_height": 9048671,
    "lag_seconds": 23,
    "source_block_height": 9048938
  }
}
```

## The PoX-4 stacking action object

One decoded PoX-4 contract call. Which columns are set depends on `function_name`; a failed call sets only the base fields.

| Field | Type | Description |
| --- | --- | --- |
| `cursor` | string | `<block_height>:<tx_index>`. Pass as `cursor`. |
| `block_height` | integer | Stacks block the call landed in. |
| `block_time` | string · date-time · nullable | That block's timestamp. |
| `burn_block_height` | integer | Bitcoin block that Stacks block anchors to. |
| `tx_id` | string | The calling transaction. |
| `tx_index` | integer | Its position in the block. With `block_height`, the cursor. |
| `function_name` | string | PoX-4 function called, e.g. `stack-stx`. |
| `caller` | string | Transaction sender. |
| `stacker` | string · nullable | Stacker the call acts on, on delegate-side calls. Null on solo `stack-stx`, where `caller` is the stacker. |
| `delegate_to` | string · nullable | Pool operator, on `delegate-stx`. |
| `amount_ustx` | string · nullable | microSTX as a decimal string (bigint-safe): the amount locked, delegated or increased by. |
| `lock_period` | integer · nullable | Cycles locked, or extended by. |
| `pox_addr` | object | Bitcoin reward address. Every field is null when the call names none. |
| `start_cycle` | integer · nullable | First reward cycle the lock covers. |
| `end_cycle` | integer · nullable | Last reward cycle the lock covers. |
| `reward_cycle` | integer · nullable | Reward cycle named by aggregation and signer-key calls. What `/v1/index/pox/cycles` groups on. |
| `signer_key` | string · nullable | Signer public key, `0x`-prefixed hex. |
| `result_ok` | boolean | Whether the call returned `ok`. |

```json
{
  "cursor": "8647822:1",
  "block_height": 8647822,
  "block_time": "2026-07-27T18:39:09.000Z",
  "burn_block_height": 959869,
  "tx_id": "0x995fb653065c74596b63e6cc9c10fb8fa7b8f3574d5ea089b45d3e11d9e7336e",
  "tx_index": 1,
  "function_name": "stack-stx",
  "caller": "SP14S259H3AC9PXWJ8EVX9ER5NZ58R03GR4QAKHCB",
  "stacker": null,
  "delegate_to": null,
  "amount_ustx": "4491100000000",
  "lock_period": 1,
  "pox_addr": {
    "version": 6,
    "hashbytes": "0x6f0fbc92d6ad56a9d7783b7420ae6731434831e3484d82ad54afe32d7fc7ad6d",
    "btc": "bc1pdu8meykk44t2n4mc8d6zptn8x9p5sv0rfpxc9t254l3j6l7844ksz4jjcs"
  },
  "start_cycle": 139,
  "end_cycle": 139,
  "reward_cycle": null,
  "signer_key": "0x031b91a32ab90b80d40715e8875fb3dfe996aa1e6f5e62ba05d34542aa58ad43b5",
  "result_ok": true
}
```

## The PoX-5 reward cycle object

Materialized per-cycle rollup of pox-5.clar's own state (plan 078), exact to the node.

| Field | Type | Description |
| --- | --- | --- |
| `reward_cycle` | integer | The reward cycle. |
| `start_burn_height` | integer | Bitcoin block at which this cycle starts. |
| `prepare_start_burn_height` | integer | Bitcoin block at which this cycle's totals froze: the start of the prior cycle's prepare phase. |
| `end_burn_height` | integer | Bitcoin block at which this cycle ends. |
| `total_stacked_ustx` | string | STX + bond ustx delegated to signers this cycle, as a decimal string. |
| `reward_eligible_ustx` | string | STX actually counted toward rewards. Signers below the 50,000 STX threshold don't count, even though they're in `total_stacked_ustx`. |
| `stakers` | integer | Distinct stakers (STX and bond) with a position this cycle. |
| `signers_in_set` | integer | Signers that met the 50,000 STX delegation threshold this cycle. |
| `bond_sats` | object | Bond index (as a string key) -> sats staked for this cycle. |
| `bond_total_sats` | string | Sum of `bond_sats` for this cycle. |
| `sbtc_custodied_sats` | string | sBTC currently custodied by pox-5 (non-L1 bonds only). A running total, not scoped to this cycle: the contract's `get-total-sbtc-staked` takes no cycle argument. |
| `rewards_allocated_stx` | string | STX rewards allocated to this cycle's distributions so far. |
| `rewards_allocated_bond` | string | Bond (sBTC) rewards allocated to this cycle's distributions so far. |
| `reserve_deposit` | string | STX deposited to the protocol reserve from this cycle's distributions. |
| `rewards_per_token_stx` | string · nullable | Cumulative STX rewards per micro-STX for this cycle. `null` before the first distribution. |
| `rewards_per_token_bond` | object | Bond index (as a string key) -> cumulative rewards-per-sat for this cycle. |
| `distributions` | integer | Distributions run for this cycle so far (0, 1, or 2). |
| `rewards_claimed` | string | Rewards claimed for this cycle so far, across every signer. |
| `computed_through_height` | integer | Stacks block height this rollup was last computed through. |
| `is_current` | boolean | True for the cycle containing the tip's burn height. |
| `is_frozen` | boolean | True once totals can no longer change: the tip has reached `prepare_start_burn_height`. Closed and current cycles are always frozen; only a not-yet-started future cycle is open. |

```json
{
  "reward_cycle": 141,
  "start_burn_height": 962150,
  "prepare_start_burn_height": 962050,
  "end_burn_height": 964249,
  "total_stacked_ustx": "392447554847960",
  "reward_eligible_ustx": "392447554847960",
  "stakers": 3035,
  "signers_in_set": 36,
  "bond_sats": {
    "1": "23017037628"
  },
  "bond_total_sats": "23017037628",
  "sbtc_custodied_sats": "16016587628",
  "rewards_allocated_stx": "297713728",
  "rewards_allocated_bond": "13810222",
  "reserve_deposit": "52537716",
  "rewards_per_token_stx": "758607677183",
  "rewards_per_token_bond": {
    "1": "599999974940302"
  },
  "distributions": 2,
  "rewards_claimed": "162018760",
  "computed_through_height": 8665732,
  "is_current": false,
  "is_frozen": true
}
```

## The PoX-5 event object

One print event from the pox-5 boot contract, decoded. Promoted columns cover the common filters; `data` always holds the complete tuple, so no topic loses fields.

| Field | Type | Description |
| --- | --- | --- |
| `cursor` | string | `<block_height>:<event_index>`. Pass as `cursor`. |
| `block_height` | integer | Stacks block the print landed in. |
| `block_time` | string · date-time · nullable | That block's timestamp. |
| `tx_id` | string | Transaction that emitted the print. |
| `tx_index` | integer | Its position in the block. |
| `event_index` | integer | The event's position in the block. With `block_height`, the cursor. |
| `topic` | enum | The print topic. Decides which columns are set. Values: `set-bond-admin`, `set-pause-admin`, `pause-rewards`, `setup-bond`, `add-to-allowlist`, `register-for-bond`, `update-bond-registration`, `register-signer`, `stake`, `stake-update`, `announce-l1-early-exit`, `unstake-sbtc`, `unstake`, `calculate-rewards`, `bond-distribution`, `claim-rewards`, `claim-staker-rewards-for-signer`, `grant-signer-key`, `revoke-signer-grant`. |
| `staker` | string · nullable | Staker principal. |
| `signer` | string · nullable | Signer-manager contract, on stake-side topics. |
| `signer_manager` | string · nullable | Signer-manager contract, on claim and grant topics. |
| `bond_index` | integer · nullable | The bond this event belongs to, on bond topics. |
| `amount_ustx` | string · nullable | microSTX as a decimal string (bigint-safe). |
| `amount_sats` | string · nullable | Satoshis as a decimal string (bigint-safe). |
| `reward_cycle` | integer · nullable | Reward cycle the event refers to, on reward and claim topics. |
| `first_reward_cycle` | integer · nullable | First cycle a stake earns in. |
| `unlock_cycle` | integer · nullable | Cycle the stake unlocks at. |
| `unlock_burn_height` | integer · nullable | Bitcoin block height the stake unlocks at. |
| `is_l1_lock` | boolean · nullable | Bond registrations: whether the lockup is on Bitcoin L1. |
| `signer_key` | string · nullable | Signer public key, on key-grant topics. |
| `data` | object | The full decoded print tuple. Nested shapes (btc-lockup, bond-rewards, bond-periods) come through intact. |

```json
{
  "cursor": "9048679:0",
  "block_height": 9048679,
  "block_time": "2026-09-23T13:55:35.000Z",
  "tx_id": "0xba5c3e57f65a7e92f917c0e8c32bdf11cedb31baefe708c8a3b9177437420ea9",
  "tx_index": 0,
  "event_index": 0,
  "topic": "stake",
  "staker": "SP19YK5M97734AJZAA2X8W6J9GGNVM9JFZ751H0F1",
  "signer": "SP8HK160YD5GHXP69VGA0TC7AQJ1X4CDW3XVERSE.xverse-signer-manager-3",
  "signer_manager": null,
  "bond_index": null,
  "amount_ustx": "550000000",
  "amount_sats": null,
  "reward_cycle": null,
  "first_reward_cycle": 144,
  "unlock_cycle": 240,
  "unlock_burn_height": 1170050,
  "is_l1_lock": null,
  "signer_key": null,
  "data": {
    "topic": "stake",
    "signer": "SP8HK160YD5GHXP69VGA0TC7AQJ1X4CDW3XVERSE.xverse-signer-manager-3",
    "staker": "SP19YK5M97734AJZAA2X8W6J9GGNVM9JFZ751H0F1",
    "num-cycles": "96",
    "amount-ustx": "550000000",
    "unlock-cycle": "240",
    "first-reward-cycle": "144",
    "unlock-burn-height": "1170050"
  }
}
```

## The Rune entry object

One etched rune's full entry — identity, premine/mint/burn counters, computed supply, and mint terms.

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | `<block>:<tx>` of the etch, e.g. `840000:3`. |
| `number` | string | Etch sequence number (u128 decimal string) — 0 is genesis. |
| `name` | string | The rune's name with spacers stripped, uppercase. |
| `spaced_name` | string | The name as etched, spacers (`•`) included. |
| `symbol` | string · nullable | The etched display symbol, if any. |
| `divisibility` | integer | Decimal places a whole unit divides into. |
| `premine` | string | Premined amount, u128 decimal string. |
| `supply` | string | Computed: `premine + mints × terms.amount` (0 with no terms). u128 decimal string. |
| `burned` | string | Total burned, u128 decimal string. |
| `mints` | string | Total mint transactions, u128 decimal string. |
| `turbo` | boolean | Whether the etch opted into future protocol upgrades. |
| `etching_txid` | string | The etch transaction's Bitcoin txid. |
| `etched_height` | integer | Bitcoin block the etch landed in. |
| `etched_tx_index` | integer | The etch transaction's position in that block. |
| `terms` | object · nullable | Mint terms, or `null` for a premine-only rune that was never mintable. |

```json
{
  "id": "840000:3",
  "number": "3",
  "name": "DOGGOTOTHEMOON",
  "spaced_name": "DOG•GO•TO•THE•MOON",
  "symbol": "🐕",
  "divisibility": 5,
  "premine": "10000000000000000",
  "supply": "10000000000000000",
  "burned": "2440680717188",
  "mints": "0",
  "turbo": false,
  "etching_txid": "e79134080a83fe3e0e06ed6990c5a9b63b362313341745707a2bff7d788a1375",
  "etched_height": 840000,
  "etched_tx_index": 3,
  "terms": null
}
```

## The Rune event object

One etch/mint/transfer/burn event from `rune_events`, with the rune's identity embedded so an activity feed never needs a second lookup to show a name.

| Field | Type | Description |
| --- | --- | --- |
| `cursor` | string | `<block_height>:<event_index>`. Pass as `cursor`. |
| `block_height` | integer | Bitcoin block the event landed in. |
| `tx_index` | integer | The transaction's position in that block. |
| `txid` | string | The Bitcoin transaction's txid. |
| `event_index` | integer | The event's position in the block. With `block_height`, the cursor. |
| `kind` | enum | What happened. Values: `rune_etch`, `rune_mint`, `rune_transfer`, `rune_burn`. |
| `amount` | string | u128 decimal string. |
| `vout` | integer · nullable | The output the amount landed on, when this event has one. |
| `address` | string · nullable | The output's mainnet address. Only ever set on a `rune_transfer` event. |
| `rune` | RuneRefSummary |  |

```json
{
  "cursor": "968919:3",
  "block_height": 968919,
  "tx_index": 144,
  "txid": "77c95d193e680429fe7f801a68488bd30919d011f23542141a7fae5244b002b4",
  "event_index": 3,
  "kind": "rune_transfer",
  "amount": "2933553300",
  "vout": 2,
  "address": "bc1pjku9h7ckjt8802mu79zt7vv8d6mhp0x57gkppcls57ex4rkq9cxqs52pr3",
  "rune": {
    "id": "840000:3",
    "name": "DOGGOTOTHEMOON",
    "spaced_name": "DOG•GO•TO•THE•MOON",
    "symbol": "🐕",
    "divisibility": 5
  }
}
```

## The Rune balance object

A rune's balance on one unspent output, with the rune's identity embedded.

| Field | Type | Description |
| --- | --- | --- |
| `rune` | RuneRefSummary |  |
| `address` | string · nullable | The output's mainnet address, or `null` for a non-standard script. |
| `txid` | string | The outpoint's Bitcoin txid. |
| `vout` | integer | The outpoint's output index. |
| `amount` | string | u128 decimal string. |

```json
{
  "rune": {
    "id": "840000:3",
    "name": "DOGGOTOTHEMOON",
    "spaced_name": "DOG•GO•TO•THE•MOON",
    "symbol": "🐕",
    "divisibility": 5
  },
  "address": "bc1phnp234rd5hel0h8ulqvnq3zrww2td0a8q48hjcg7mv67j97jnxfq9ayczm",
  "txid": "2d2b54008790a44c9c3e630faa521a50f8ab122625c4eb8fb84705051c4fc611",
  "vout": 0,
  "amount": "88980600"
}
```

## The sBTC event object

One decoded `sbtc-registry` print. Every topic shares this shape; `topic` decides which columns are set.

| Field | Type | Description |
| --- | --- | --- |
| `cursor` | string | `<block_height>:<event_index>`. Pass as `cursor`. |
| `block_height` | integer | Stacks block the print landed in. |
| `block_time` | string · date-time · nullable | That block's timestamp. |
| `tx_id` | string | Transaction that emitted the print. |
| `tx_index` | integer | Its position in the block. |
| `event_index` | integer | The event's position in the block. With `block_height`, the cursor. |
| `topic` | enum | The print topic. Values: `completed-deposit`, `withdrawal-create`, `withdrawal-accept`, `withdrawal-reject`, `key-rotation`, `update-protocol-contract`. |
| `request_id` | integer · nullable | Withdrawal request id, on withdrawal topics. |
| `amount` | string · nullable | Sats as a decimal string, on deposits and withdrawal requests. |
| `sender` | string · nullable | Stacks principal, e.g. the withdrawal requester. |
| `recipient_btc_version` | integer · nullable | Bitcoin address type of the recipient: 0 p2pkh, 1 p2sh, 2 p2sh-p2wpkh, 3 p2sh-p2wsh, 4 p2wpkh, 5 p2wsh, 6 p2tr. |
| `recipient_btc_hashbytes` | string · nullable | Recipient's hash bytes, `0x`-prefixed hex. |
| `bitcoin_txid` | string · nullable | Bitcoin txid, `0x`-prefixed hex: the deposit transaction, or the sweep on an accept. |
| `output_index` | integer · nullable | Output within that Bitcoin transaction. |
| `sweep_txid` | string · nullable | Signers' sweep transaction on Bitcoin. |
| `burn_hash` | string · nullable | Bitcoin block hash the signers anchored the event to. |
| `burn_height` | integer · nullable | That Bitcoin block's height. |
| `signer_bitmap` | string · nullable | Bitmap of the signers' votes, as a decimal string. |
| `max_fee` | string · nullable | Most the requester will pay in BTC fees, in sats. |
| `fee` | string · nullable | BTC fee charged on an accepted withdrawal, in sats. |
| `governance_contract_type` | integer · nullable | Which protocol contract changed, on `update-protocol-contract`. |
| `governance_new_contract` | string · nullable | The new contract, on `update-protocol-contract`. |
| `signer_aggregate_pubkey` | string · nullable | The registry's aggregate-key field, on `key-rotation` (33 bytes, hex). In v1 it is the signers' aggregate public key. Once DKG is disabled (no height set yet) it is the marker `0xFF || bitcoin block hash`, not a key, so do not derive addresses from it. |
| `signer_threshold` | integer · nullable | New signature threshold, on `key-rotation`. |
| `signer_address` | string · nullable | New signer principal, on `key-rotation`. |
| `signer_keys_count` | integer · nullable | Number of signer keys in the new set, on `key-rotation`. |

```json
{
  "cursor": "9048486:2",
  "block_height": 9048486,
  "block_time": "2026-09-23T13:10:19.000Z",
  "tx_id": "0x0e09fd3cf7628a2cc81083aa9dbb7eebc7bae5cbe277c370e0f8bfc3d1fe00a9",
  "tx_index": 2,
  "event_index": 2,
  "topic": "withdrawal-create",
  "request_id": 3406,
  "amount": "3127",
  "sender": "SP2ADCJ6606CFHERS3HKABWB6PF5XBANXBQ4GSE86",
  "recipient_btc_version": 4,
  "recipient_btc_hashbytes": "0x9c32e45a19cf9c028bccf12816e62bbfc8eb5fb8",
  "bitcoin_txid": null,
  "output_index": null,
  "sweep_txid": null,
  "burn_hash": null,
  "burn_height": null,
  "signer_bitmap": null,
  "max_fee": "1500",
  "fee": null,
  "governance_contract_type": null,
  "governance_new_contract": null,
  "signer_aggregate_pubkey": null,
  "signer_threshold": null,
  "signer_address": null,
  "signer_keys_count": null
}
```

## The sBTC deposit object

A completed peg-in: one `completed-deposit` event. Keyed by the Bitcoin txid; deposits carry no request id.

| Field | Type | Description |
| --- | --- | --- |
| `cursor` | string | `<block_height>:<event_index>`. Pass as `cursor`. |
| `block_height` | integer | Stacks block the deposit completed in. |
| `block_time` | string · date-time · nullable | That block's timestamp. |
| `tx_id` | string | Stacks transaction that completed the deposit. |
| `tx_index` | integer | Its position in the block. |
| `event_index` | integer | The event's position in the block. With `block_height`, the cursor. |
| `amount` | string · nullable | Sats minted, as a decimal string. |
| `sender` | string · nullable | Stacks principal on the event. Null on mainnet deposits today. |
| `bitcoin_txid` | string · nullable | The deposit's Bitcoin txid, `0x`-prefixed hex. |
| `output_index` | integer · nullable | The deposit output within that Bitcoin transaction. |
| `recipient_btc_version` | integer · nullable | Bitcoin address type of the recipient: 0 p2pkh, 1 p2sh, 2 p2sh-p2wpkh, 3 p2sh-p2wsh, 4 p2wpkh, 5 p2wsh, 6 p2tr. |
| `recipient_btc_hashbytes` | string · nullable | Recipient's hash bytes, `0x`-prefixed hex. |

```json
{
  "cursor": "9048911:4",
  "block_height": 9048911,
  "block_time": "2026-09-23T15:06:29.000Z",
  "tx_id": "0x8600af4a0b2a911e6494d1feea0691068d8b329be0ef2c0d11a7d5aed8f9f4e5",
  "tx_index": 2,
  "event_index": 4,
  "amount": "79856",
  "sender": null,
  "bitcoin_txid": "0x2c11f9e20d21c96daed5d851c07d0e7c4361e552ebd78a8385f06fbb2f9892d3",
  "output_index": 1,
  "recipient_btc_version": null,
  "recipient_btc_hashbytes": null
}
```

## The sBTC withdrawal object

A peg-out rolled up to one row per `request_id`: the request, its current status, and the Bitcoin settlement of its sweep.

| Field | Type | Description |
| --- | --- | --- |
| `cursor` | string | The request event's `<block_height>:<event_index>`. Pass as `cursor`. |
| `request_id` | integer | Withdrawal request id. |
| `status` | enum | `REQUESTED` until the signers act, then `ACCEPTED` or `REJECTED` from the latest resolution event. Values: `REQUESTED`, `ACCEPTED`, `REJECTED`. |
| `amount` | string · nullable | Sats requested, as a decimal string. |
| `sender` | string · nullable | Requester's Stacks principal. |
| `recipient_btc_version` | integer · nullable | Bitcoin address type of the recipient: 0 p2pkh, 1 p2sh, 2 p2sh-p2wpkh, 3 p2sh-p2wsh, 4 p2wpkh, 5 p2wsh, 6 p2tr. |
| `recipient_btc_hashbytes` | string · nullable | Recipient's hash bytes, `0x`-prefixed hex. |
| `sweep_txid` | string · nullable | Bitcoin sweep the signers committed to on accept. Null until then. |
| `settlement_confirmed` | boolean · nullable | True once the sweep has confirmed on Bitcoin, false while it is pending, null when there is no sweep or it has not been seen yet. |
| `btc_confirmations` | integer · nullable | Bitcoin confirmations on the sweep at the last check. |
| `btc_block_height` | integer · nullable | Bitcoin block the sweep landed in. |
| `confirmed_at` | string · date-time · nullable | When the sweep reached the confirmation threshold. |
| `requested_at` | string · date-time · nullable | Block time of the request. |
| `resolved_at` | string · date-time · nullable | Block time of the accept or reject. Null while requested. |

```json
{
  "cursor": "9048225:2",
  "request_id": 3405,
  "status": "ACCEPTED",
  "amount": "221653",
  "sender": "SP2WRMQD3G4G8BR0120Y390CT6A8BTE6MB0JQ5EHA",
  "recipient_btc_version": 4,
  "recipient_btc_hashbytes": "0xa7d5ce640208ed037950d4c849c805e881209d6a",
  "sweep_txid": "0x0efad88eb01e248fd8321ada99cb74f7258948226eae106f8be5a5917d7473e4",
  "settlement_confirmed": true,
  "btc_confirmations": 8,
  "btc_block_height": 968278,
  "confirmed_at": "2026-09-23T14:35:25.473Z",
  "requested_at": "2026-09-23T12:01:45.000Z",
  "resolved_at": "2026-09-23T13:24:47.000Z"
}
```

## Index discovery

`GET /v1/index`

Lists every Index route with its accepted filters, plus the per-`event_type` column and filter vocabulary for `/v1/index/events`. Open: it runs before auth, so it never needs a token.

### Returns

One `IndexDiscovery` object.

### Responses

- `200` Index routes and filters

### Example

```bash
curl https://api.secondlayer.tools/v1/index \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "routes": [
    {
      "path": "/v1/index/ft-transfers",
      "method": "GET",
      "description": "Alias for /events?event_type=ft_transfer. Fungible token transfers, decoded + filterable.",
      "filters": [
        "limit",
        "cursor",
        "from_cursor",
        "from_height",
        "to_height",
        "order",
        "contract_id",
        "sender",
        "recipient",
        "asset_identifier",
        "fields"
      ]
    },
    {
      "path": "/v1/index/blocks",
      "method": "GET",
      "description": "Canonical blocks, cursor-paginated. Returns blocks[] ({block_height, block_hash, parent_hash, burn_block_height, burn_block_hash, block_time, canonical}), next_cursor, tip.",
      "filters": [
        "limit",
        "cursor",
        "from_cursor",
        "from_height",
        "to_height"
      ]
    }
  ],
  "cursor": {
    "format": "<block_height>:<event_index>",
    "semantics": "opaque resume token; pass back unchanged to continue. Equals last event's cursor (inclusive on output, exclusive on input). Under order=desc it carries a `desc:` prefix and resumes only under order=desc."
  }
}
```

## Decoded events by event_type

`GET /v1/index/events`

One `event_type` per request, oldest first unless `order=desc`. Which filters and `fields` apply depends on the type; anything a type does not accept is refused.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `event_type` | enum, required | The event type to read. Required, unless you pass `types` instead. Values: `ft_transfer`, `nft_transfer`, `stx_transfer`, `stx_mint`, `stx_burn`, `stx_lock`, `ft_mint`, `ft_burn`, `nft_mint`, `nft_burn`, `print`. |
| `types` | string | Alias for `event_type`, the Streams spelling. Takes one value; a comma-separated set is refused. Mutually exclusive with `event_type`. |
| `order` | enum | `asc` (default) reads oldest first. `desc` reads newest first: with no height range, page one is the newest rows of the last day. A `desc` page's `next_cursor` starts with `desc:` and pages older; a cursor resumes only under the order that issued it, and the other order is refused with 400. Values: `asc`, `desc`. |
| `contract_id` | string | Contract principal, or a comma-separated set of up to 20 (e.g. `SP1.sbtc-token,SP1.sbtc-registry`). Mutually exclusive with `trait`. |
| `trait` | enum | Only contracts that declare or implement this SIP standard, resolved as of the page's `to_height`. Mutually exclusive with `contract_id`. Values: `sip-009`, `sip-010`, `sip-013`. |
| `asset_identifier` | string | Asset identifier (`<contract>::<asset>`). ft and nft transfer, mint and burn types only. |
| `sender` | string | Sending principal. Types with a sender only; on `stx_lock` it is the locked address. |
| `recipient` | string | Receiving principal. Types with a recipient only. |
| `tx_context` | boolean | `true` adds the submitting transaction's `tx_sender`, `tx_type`, `tx_status`, `tx_contract_id` and `tx_function_name` to each row. |
| `wait` | integer | Seconds (max 25) to hold the response open when there's nothing new past your cursor/from_height, instead of answering immediately. Returns early the moment new data commits, or at the timeout — either way you get a normal response, just later. Omit for the old poll-and-retry behavior. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Page size; capped at 1000. |
| `cursor` | string | `<block_height>:<event_index>` from a previous page's `next_cursor`. Resumes after it. |
| `from_cursor` | string | Same as `cursor`. Mutually exclusive with it and with `from_height`. |
| `from_height` | integer | First block height to read. Mutually exclusive with a cursor. With neither, the read starts one day behind the tip. |
| `to_height` | integer | Last block height to read, inclusive. |
| `fields` | string | Comma-separated columns to return: the universal ones, the type's own columns, and the `tx_*` columns when `tx_context=true`. `cursor`, `block_height` and `event_type` always come back; an unknown name is refused. Leaving out `block_time` skips a join. |

### Returns

A cursor envelope; `events` holds `IndexEvent` objects.

### Responses

- `200` Cursor-paginated envelope
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/events \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "events": [
    {
      "cursor": "9048921:2",
      "block_height": 9048921,
      "block_time": "2026-09-23T15:09:05.000Z",
      "tx_id": "0x55249ff4ec221ff6852b7fa37cf3729c7c50bc63f0da760f96e6886e2e8a911e",
      "tx_index": 4,
      "event_index": 2,
      "event_type": "stx_transfer",
      "contract_id": null,
      "sender": "SPX4KR3G5Z1SZT4RVF4ATHV4ZP4KXPWSZ4424W6K",
      "recipient": "SP2Z2CBMGWB9MQZAF5Z8X56KS69XRV3SJF4WKJ7J9",
      "amount": "50",
      "memo": null
    }
  ],
  "next_cursor": "7960000:42",
  "tip": {
    "block_height": 9048690,
    "finalized_height": 9048457,
    "lag_seconds": 4,
    "source_block_height": 9048692
  },
  "reorgs": [
    {
      "id": "3f16c2c5-9551-4ae9-adb0-923668e5c65e",
      "detected_at": "2026-09-15T15:39:38.129Z",
      "fork_point_height": 8996511,
      "old_index_block_hash": "0x45ba0195071488eda13afa66d8ce8612558a739f781be5a032d96332797c4174",
      "new_index_block_hash": "0xa686ff58e7c5ccb26c850a7eaffa7e7274fcbc081c706344eb6f1e1446023df0",
      "orphaned_range": {
        "from": "8996511:0",
        "to": "8996512:2701"
      },
      "new_canonical_tip": "8996511:0"
    }
  ]
}
```

## Fungible token transfers

`GET /v1/index/ft-transfers`

Same rows as `/v1/index/events?event_type=ft_transfer`, oldest first unless `order=desc`, with a typed row. Filters combine with AND.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `order` | enum | `asc` (default) reads oldest first. `desc` reads newest first: with no height range, page one is the newest rows of the last day. A `desc` page's `next_cursor` starts with `desc:` and pages older; a cursor resumes only under the order that issued it, and the other order is refused with 400. Values: `asc`, `desc`. |
| `contract_id` | string | Token contract. One principal; no comma-separated set here. |
| `asset_identifier` | string | Asset identifier (`<contract>::<asset>`). |
| `sender` | string | Sending principal. |
| `recipient` | string | Receiving principal. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Page size; capped at 1000. |
| `cursor` | string | `<block_height>:<event_index>` from a previous page's `next_cursor`. Resumes after it. |
| `from_cursor` | string | Same as `cursor`. Mutually exclusive with it and with `from_height`. |
| `from_height` | integer | First block height to read. Mutually exclusive with a cursor. With neither, the read starts one day behind the tip. |
| `to_height` | integer | Last block height to read, inclusive. |
| `fields` | string | Comma-separated columns to return, from `block_time`, `tx_id`, `tx_index`, `event_index`, `contract_id`, `asset_identifier`, `sender`, `recipient`, `amount` and `event_type`. `cursor` and `block_height` always come back; an unknown name is refused. |

### Returns

A cursor envelope; `events` holds `FtTransfer` objects.

### Responses

- `200` Cursor-paginated envelope
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/ft-transfers \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "events": [
    {
      "cursor": "9048918:3",
      "block_height": 9048918,
      "block_time": "2026-09-23T15:08:18.000Z",
      "tx_id": "0xc342cb7616498a84a8dcfac62a991c31c037d8c6f98a11f987f0719fd619e672",
      "tx_index": 2,
      "event_index": 3,
      "event_type": "ft_transfer",
      "contract_id": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token",
      "asset_identifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
      "sender": "SP1A27KFY4XERQCCRCARCYD1CC5N7M6688BSYADJ7.v0-8-market",
      "recipient": "SP1A27KFY4XERQCCRCARCYD1CC5N7M6688BSYADJ7.v0-vault-sbtc",
      "amount": "15000"
    }
  ],
  "next_cursor": "7960000:42",
  "tip": {
    "block_height": 9048690,
    "finalized_height": 9048457,
    "lag_seconds": 4,
    "source_block_height": 9048692
  },
  "reorgs": [
    {
      "id": "3f16c2c5-9551-4ae9-adb0-923668e5c65e",
      "detected_at": "2026-09-15T15:39:38.129Z",
      "fork_point_height": 8996511,
      "old_index_block_hash": "0x45ba0195071488eda13afa66d8ce8612558a739f781be5a032d96332797c4174",
      "new_index_block_hash": "0xa686ff58e7c5ccb26c850a7eaffa7e7274fcbc081c706344eb6f1e1446023df0",
      "orphaned_range": {
        "from": "8996511:0",
        "to": "8996512:2701"
      },
      "new_canonical_tip": "8996511:0"
    }
  ]
}
```

## NFT transfers

`GET /v1/index/nft-transfers`

Same rows as `/v1/index/events?event_type=nft_transfer`, oldest first unless `order=desc`, with a typed row. Filters combine with AND.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `order` | enum | `asc` (default) reads oldest first. `desc` reads newest first: with no height range, page one is the newest rows of the last day. A `desc` page's `next_cursor` starts with `desc:` and pages older; a cursor resumes only under the order that issued it, and the other order is refused with 400. Values: `asc`, `desc`. |
| `contract_id` | string | NFT contract. One principal; no comma-separated set here. |
| `asset_identifier` | string | Asset identifier (`<contract>::<asset>`). |
| `sender` | string | Sending principal. |
| `recipient` | string | Receiving principal. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Page size; capped at 1000. |
| `cursor` | string | `<block_height>:<event_index>` from a previous page's `next_cursor`. Resumes after it. |
| `from_cursor` | string | Same as `cursor`. Mutually exclusive with it and with `from_height`. |
| `from_height` | integer | First block height to read. Mutually exclusive with a cursor. With neither, the read starts one day behind the tip. |
| `to_height` | integer | Last block height to read, inclusive. |
| `fields` | string | Comma-separated columns to return, from `block_time`, `tx_id`, `tx_index`, `event_index`, `contract_id`, `asset_identifier`, `sender`, `recipient`, `value` and `event_type`. `cursor` and `block_height` always come back; an unknown name is refused. |

### Returns

A cursor envelope; `events` holds `NftTransfer` objects.

### Responses

- `200` Cursor-paginated envelope
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/nft-transfers \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "events": [
    {
      "cursor": "9048916:0",
      "block_height": 9048916,
      "block_time": "2026-09-23T15:07:41.000Z",
      "tx_id": "0x269d7877002b998ad3a3ee35f661610cc8f2be6eec604905311d52cf1cf80caa",
      "tx_index": 0,
      "event_index": 0,
      "event_type": "nft_transfer",
      "contract_id": "SPVD6CE8RW90BGGKJZTKCSMGKS7HP0K8364TFR48.bitcoin-faces-airdrop",
      "asset_identifier": "SPVD6CE8RW90BGGKJZTKCSMGKS7HP0K8364TFR48.bitcoin-faces-airdrop::bitcoin-faces",
      "sender": "SP3KMHQ80ZGT037R2FCHV1F7Z0X25EV6GVEYNAB87",
      "recipient": "SP37YJNKVKP3KD5SFK71NW7DC25RRF4HKWWQRH3NT",
      "value": "0x010000000000000000000000000008331e"
    }
  ],
  "next_cursor": "7960000:42",
  "tip": {
    "block_height": 9048690,
    "finalized_height": 9048457,
    "lag_seconds": 4,
    "source_block_height": 9048692
  },
  "reorgs": [
    {
      "id": "3f16c2c5-9551-4ae9-adb0-923668e5c65e",
      "detected_at": "2026-09-15T15:39:38.129Z",
      "fork_point_height": 8996511,
      "old_index_block_hash": "0x45ba0195071488eda13afa66d8ce8612558a739f781be5a032d96332797c4174",
      "new_index_block_hash": "0xa686ff58e7c5ccb26c850a7eaffa7e7274fcbc081c706344eb6f1e1446023df0",
      "orphaned_range": {
        "from": "8996511:0",
        "to": "8996512:2701"
      },
      "new_canonical_tip": "8996511:0"
    }
  ]
}
```

## Decoded contract-call transactions

`GET /v1/index/contract-calls`

Top-level `contract_call` transactions in canonical blocks, oldest first, with arguments and result decoded. Failed calls are included; check `status`. The cursor is `<block_height>:<tx_index>`. `reorgs` covers every height on the page.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `contract_id` | string | Contract principal, or a comma-separated set of up to 20 (e.g. `SP1.sbtc-token,SP1.sbtc-registry`). Mutually exclusive with `trait`. |
| `trait` | enum | Only contracts that declare or implement this SIP standard, resolved as of the page's `to_height`. Mutually exclusive with `contract_id`. Values: `sip-009`, `sip-010`, `sip-013`. |
| `function_name` | string | Called function. Exact match. |
| `sender` | string | Transaction sender principal. Exact match. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Page size; capped at 1000. |
| `cursor` | string | `<block_height>:<tx_index>` from a previous page's `next_cursor`. Resumes after it. |
| `from_cursor` | string | Same as `cursor`. Mutually exclusive with it and with `from_height`. |
| `from_height` | integer | First block height to read. Mutually exclusive with a cursor. With neither, the read starts one day behind the tip. |
| `to_height` | integer | Last block height to read, inclusive. |

### Returns

A cursor envelope; `contract_calls` holds `ContractCall` objects.

### Responses

- `200` Cursor-paginated envelope
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/contract-calls \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "contract_calls": [
    {
      "cursor": "9048502:1",
      "block_height": 9048502,
      "block_time": "2026-09-23T13:14:42.000Z",
      "tx_id": "0x198d642edb1104a1757a8c353ad61231d0422ecb6ba83aa1ce45d05df44157e0",
      "tx_index": 1,
      "contract_id": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token",
      "function_name": "transfer",
      "sender": "SP15T1W26JTNS26VG17HM468KW7TQD3124KTYA9EJ",
      "status": "success",
      "args": [
        "300",
        "SP15T1W26JTNS26VG17HM468KW7TQD3124KTYA9EJ",
        "SPD3SCZDZ52X4ZX1878XWBFBJ29YRX0MFW9Z9W4V",
        null
      ],
      "result": true,
      "result_hex": "0x0703"
    }
  ],
  "next_cursor": "7960000:42",
  "tip": {
    "block_height": 9048690,
    "finalized_height": 9048457,
    "lag_seconds": 4,
    "source_block_height": 9048692
  },
  "reorgs": [
    {
      "id": "3f16c2c5-9551-4ae9-adb0-923668e5c65e",
      "detected_at": "2026-09-15T15:39:38.129Z",
      "fork_point_height": 8996511,
      "old_index_block_hash": "0x45ba0195071488eda13afa66d8ce8612558a739f781be5a032d96332797c4174",
      "new_index_block_hash": "0xa686ff58e7c5ccb26c850a7eaffa7e7274fcbc081c706344eb6f1e1446023df0",
      "orphaned_range": {
        "from": "8996511:0",
        "to": "8996512:2701"
      },
      "new_canonical_tip": "8996511:0"
    }
  ]
}
```

## Canonical block-hash map

`GET /v1/index/canonical`

One row per height on the canonical chain, oldest first. Orphaned blocks never appear, so you can check your own chain against it. Lean by design; `/v1/index/blocks` is the full block. Not metered.

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Page size; capped at 1000. |
| `cursor` | string | `<block_height>:<event_index>` from a previous page's `next_cursor`. Resumes after it. |
| `from_cursor` | string | Same as `cursor`. Mutually exclusive with it and with `from_height`. |
| `from_height` | integer | First block height to read. Mutually exclusive with a cursor. With neither, the read starts one day behind the tip. |
| `to_height` | integer | Last block height to read, inclusive. |

### Returns

A cursor envelope; `canonical` holds `CanonicalBlock` objects.

### Responses

- `200` Cursor-paginated envelope
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/canonical \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "canonical": [
    {
      "cursor": "9048503:0",
      "block_height": 9048503,
      "block_hash": "0x5560590afefe994adbe2188586a44ba3668e8964a7e64bb137c0df216531da0c",
      "parent_hash": "0x2a20974330e301b65086d3cde9b0006c9b7471479ba9a3fc2ca72621479655cc",
      "burn_block_height": 968276,
      "burn_block_hash": "0x000000000000000000019d25cd76d06a9eea796a906ac88300cd70fd70ccdb57"
    }
  ],
  "next_cursor": "7960000:42",
  "tip": {
    "block_height": 9048690,
    "finalized_height": 9048457,
    "lag_seconds": 4,
    "source_block_height": 9048692
  }
}
```

## State writes

`GET /v1/index/state-writes`

The exact MARF writes each canonical block committed, in the node's write order: every changed leaf of the block's state trie, named. Oldest first; the cursor is `<block_height>:<ordinal>`. `block_height` reads one block. Present only from the height this instance's node subscribed to the `state_writes` observer key, with no earlier history. The window follows the ingest tip. `reorgs` overlaps by height.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `block_height` | integer | Read one block: the same as `from_height` and `to_height` both set to it, which it excludes. A cursor with it must sit at that height. |
| `contract_id` | string | One contract, `<address>.<contract-name>`: only keys under `vm::<contract_id>::` (its data maps, data vars and token balances). Exact, no wildcards. |
| `tx_context` | boolean | `true` adds the writing transaction's `tx_id`, `tx_sender`, `tx_type`, `tx_status`, `tx_contract_id` and `tx_function_name` to each row; null for block-level writes. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Page size; capped at 1000. |
| `cursor` | string | `<block_height>:<ordinal>` from a previous page's `next_cursor`. Resumes after it. |
| `from_cursor` | string | Same as `cursor`. Mutually exclusive with it and with `from_height`. |
| `from_height` | integer | First block height to read. Mutually exclusive with a cursor. With neither, the read starts one day behind the tip. |
| `to_height` | integer | Last block height to read, inclusive. |

### Returns

A cursor envelope; `state_writes` holds `StateWrite` objects.

### Responses

- `200` Cursor-paginated envelope
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/state-writes \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "state_writes": [
    {
      "cursor": "100:1",
      "block_height": 100,
      "ordinal": 1,
      "tx_index": 0,
      "key": "vm::ST3FEXKRAY93SR2MERXNSAXWGV9XSDNM1MVEFY5TH.store::0::map::0d0000000568656c6c6f",
      "value_hex": "30313030303030303030303030303030303030303030303030303030303030303031"
    }
  ],
  "next_cursor": "7960000:42",
  "tip": {
    "block_height": 9048690,
    "finalized_height": 9048457,
    "lag_seconds": 4,
    "source_block_height": 9048692
  },
  "reorgs": [
    {
      "id": "3f16c2c5-9551-4ae9-adb0-923668e5c65e",
      "detected_at": "2026-09-15T15:39:38.129Z",
      "fork_point_height": 8996511,
      "old_index_block_hash": "0x45ba0195071488eda13afa66d8ce8612558a739f781be5a032d96332797c4174",
      "new_index_block_hash": "0xa686ff58e7c5ccb26c850a7eaffa7e7274fcbc081c706344eb6f1e1446023df0",
      "orphaned_range": {
        "from": "8996511:0",
        "to": "8996512:2701"
      },
      "new_canonical_tip": "8996511:0"
    }
  ]
}
```

## Canonical blocks

`GET /v1/index/blocks`

Canonical blocks, oldest first. The cursor is `<block_height>:0`. The window follows the ingest tip, so it can run ahead of the decoded `tip.block_height`.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `wait` | integer | Seconds (max 25) to hold the response open when there's nothing new past your cursor/from_height, instead of answering immediately. Returns early the moment new data commits, or at the timeout — either way you get a normal response, just later. Omit for the old poll-and-retry behavior. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Page size; capped at 1000. |
| `cursor` | string | `<block_height>:<event_index>` from a previous page's `next_cursor`. Resumes after it. |
| `from_cursor` | string | Same as `cursor`. Mutually exclusive with it and with `from_height`. |
| `from_height` | integer | First block height to read. Mutually exclusive with a cursor. With neither, the read starts one day behind the tip. |
| `to_height` | integer | Last block height to read, inclusive. |

### Returns

A cursor envelope; `blocks` holds `Block` objects.

### Responses

- `200` Cursor-paginated envelope
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/blocks \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "blocks": [
    {
      "cursor": "9048502:0",
      "block_height": 9048502,
      "block_hash": "0x2a20974330e301b65086d3cde9b0006c9b7471479ba9a3fc2ca72621479655cc",
      "parent_hash": "0x10606e958095ef9b964e3c3f2d9eb75bf8577fb8c09c24ee492b767f7504abea",
      "burn_block_height": 968276,
      "burn_block_hash": "0x000000000000000000019d25cd76d06a9eea796a906ac88300cd70fd70ccdb57",
      "index_block_hash": "0x990152a894e12288960c8b68e5790ad7994f9d261e9ad8adc13c72e76f914339",
      "block_time": "2026-09-23T13:14:42.000Z",
      "canonical": true
    }
  ],
  "next_cursor": "7960000:42",
  "tip": {
    "block_height": 9048690,
    "finalized_height": 9048457,
    "lag_seconds": 4,
    "source_block_height": 9048692
  }
}
```

## Block by height or hash

`GET /v1/index/blocks/{height_or_hash}`

A height returns the canonical block there. A hash returns that block whether or not it is canonical, so check `canonical` to spot an orphan. Not metered.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `height_or_hash` | string, required | A block height (digits only) or a `0x`-prefixed block hash. |

### Responses

- `200` The block and the tip it was read against
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No block at that height or with that hash (`Block not found`)
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/blocks/9048502 \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "block": {
    "cursor": "9048502:0",
    "block_height": 9048502,
    "block_hash": "0x2a20974330e301b65086d3cde9b0006c9b7471479ba9a3fc2ca72621479655cc",
    "parent_hash": "0x10606e958095ef9b964e3c3f2d9eb75bf8577fb8c09c24ee492b767f7504abea",
    "burn_block_height": 968276,
    "burn_block_hash": "0x000000000000000000019d25cd76d06a9eea796a906ac88300cd70fd70ccdb57",
    "index_block_hash": "0x990152a894e12288960c8b68e5790ad7994f9d261e9ad8adc13c72e76f914339",
    "block_time": "2026-09-23T13:14:42.000Z",
    "canonical": true
  },
  "tip": {
    "block_height": 9048938,
    "finalized_height": 9048671,
    "lag_seconds": 23,
    "source_block_height": 9048938
  }
}
```

## Full transaction documents

`GET /v1/index/transactions`

Transactions in canonical blocks, oldest first, with fee, nonce, post-conditions and payload detail decoded from the raw bytes. Filters combine with AND. The cursor is `<block_height>:<tx_index>`. `reorgs` covers every height on the page.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `type` | string | Transaction type as stored, e.g. `contract_call`, `token_transfer`, `smart_contract`, `coinbase`, `tenure_change`. Exact match. |
| `sender` | string | Sender principal. Exact match. |
| `contract_id` | string | Called or deployed contract. One principal. Exact match. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Page size; capped at 1000. |
| `cursor` | string | `<block_height>:<tx_index>` from a previous page's `next_cursor`. Resumes after it. |
| `from_cursor` | string | Same as `cursor`. Mutually exclusive with it and with `from_height`. |
| `from_height` | integer | First block height to read. Mutually exclusive with a cursor. With neither, the read starts one day behind the tip. |
| `to_height` | integer | Last block height to read, inclusive. |
| `fields` | string | Comma-separated columns to return, from `block_time`, `burn_block_height`, `tx_index`, `tx_type`, `sender`, `status`, `fee`, `nonce`, `sponsored`, `anchor_mode`, `post_condition_mode`, `post_conditions`, `contract_call`, `smart_contract`, `token_transfer`. `cursor`, `block_height` and `tx_id` always come back; an unknown name is refused. |

### Returns

A cursor envelope; `transactions` holds `Transaction` objects.

### Responses

- `200` Cursor-paginated envelope
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/transactions \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "transactions": [
    {
      "cursor": "9048502:1",
      "tx_id": "0x198d642edb1104a1757a8c353ad61231d0422ecb6ba83aa1ce45d05df44157e0",
      "block_height": 9048502,
      "block_time": "2026-09-23T13:14:42.000Z",
      "burn_block_height": 968276,
      "tx_index": 1,
      "tx_type": "contract_call",
      "sender": "SP15T1W26JTNS26VG17HM468KW7TQD3124KTYA9EJ",
      "status": "success",
      "fee": "3000",
      "nonce": "265",
      "sponsored": false,
      "anchor_mode": "any",
      "post_condition_mode": "deny",
      "post_conditions": [
        {
          "type": "ft",
          "principal": "SP15T1W26JTNS26VG17HM468KW7TQD3124KTYA9EJ",
          "asset_identifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
          "condition_code": 1,
          "condition_code_name": "sent_eq",
          "amount": "300"
        }
      ],
      "contract_call": {
        "contract_id": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token",
        "function_name": "transfer",
        "function_args": [
          "300",
          "SP15T1W26JTNS26VG17HM468KW7TQD3124KTYA9EJ",
          "SPD3SCZDZ52X4ZX1878XWBFBJ29YRX0MFW9Z9W4V",
          null
        ],
        "function_args_hex": [
          "010000000000000000000000000000012c",
          "05164ba0f04696ab911b7009e3421913e1f5768c2224",
          "05161a3cb3edf945d27fa141d1de2deb9093ec74147f",
          "09"
        ],
        "result": true,
        "result_hex": "0x0703"
      }
    }
  ],
  "next_cursor": "7960000:42",
  "tip": {
    "block_height": 9048690,
    "finalized_height": 9048457,
    "lag_seconds": 4,
    "source_block_height": 9048692
  },
  "reorgs": [
    {
      "id": "3f16c2c5-9551-4ae9-adb0-923668e5c65e",
      "detected_at": "2026-09-15T15:39:38.129Z",
      "fork_point_height": 8996511,
      "old_index_block_hash": "0x45ba0195071488eda13afa66d8ce8612558a739f781be5a032d96332797c4174",
      "new_index_block_hash": "0xa686ff58e7c5ccb26c850a7eaffa7e7274fcbc081c706344eb6f1e1446023df0",
      "orphaned_range": {
        "from": "8996511:0",
        "to": "8996512:2701"
      },
      "new_canonical_tip": "8996511:0"
    }
  ]
}
```

## Transaction by tx_id

`GET /v1/index/transactions/{tx_id}`

One transaction document, only if it sits in a canonical block. A transaction still pending is at `/v1/index/mempool/{tx_id}`. Not metered.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `tx_id` | string, required | Transaction id, `0x`-prefixed hex. |

### Responses

- `200` The transaction and the tip it was read against
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No canonical transaction with that id (`Transaction not found`)
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/transactions/{tx_id} \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "transaction": {
    "cursor": "9048502:1",
    "tx_id": "0x198d642edb1104a1757a8c353ad61231d0422ecb6ba83aa1ce45d05df44157e0",
    "block_height": 9048502,
    "block_time": "2026-09-23T13:14:42.000Z",
    "burn_block_height": 968276,
    "tx_index": 1,
    "tx_type": "contract_call",
    "sender": "SP15T1W26JTNS26VG17HM468KW7TQD3124KTYA9EJ",
    "status": "success",
    "fee": "3000",
    "nonce": "265",
    "sponsored": false,
    "anchor_mode": "any",
    "post_condition_mode": "deny",
    "post_conditions": [
      {
        "type": "ft",
        "principal": "SP15T1W26JTNS26VG17HM468KW7TQD3124KTYA9EJ",
        "asset_identifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
        "condition_code": 1,
        "condition_code_name": "sent_eq",
        "amount": "300"
      }
    ],
    "contract_call": {
      "contract_id": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token",
      "function_name": "transfer",
      "function_args": [
        "300",
        "SP15T1W26JTNS26VG17HM468KW7TQD3124KTYA9EJ",
        "SPD3SCZDZ52X4ZX1878XWBFBJ29YRX0MFW9Z9W4V",
        null
      ],
      "function_args_hex": [
        "010000000000000000000000000000012c",
        "05164ba0f04696ab911b7009e3421913e1f5768c2224",
        "05161a3cb3edf945d27fa141d1de2deb9093ec74147f",
        "09"
      ],
      "result": true,
      "result_hex": "0x0703"
    }
  },
  "tip": {
    "block_height": 9048938,
    "finalized_height": 9048671,
    "lag_seconds": 23,
    "source_block_height": 9048938
  }
}
```

## Transaction inclusion proof

`GET /v1/index/transactions/{tx_id}/proof`

What you need to check, without trusting this API, that a transaction is in a signed Nakamoto block: the raw tx, the raw block header, and the tx merkle path. `consensus` adds the cycle's signer set when the node can resolve it. The SDK's `verifyTransactionProof` runs every check. The instance must reach its Stacks node. Immutable once served.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `tx_id` | string, required | Transaction id, `0x`-prefixed hex. |

### Returns

One `TransactionProof` object.

### Responses

- `200` The inclusion proof
- `401` This bind requires the instance token, and it was missing or wrong
- `404` The transaction or its block is not indexed or not on the node (`PROOF_UNAVAILABLE`)
- `429` Rate limited; retry after `Retry-After` seconds
- `503` The Stacks node is unreachable (`PROOF_NODE_UNAVAILABLE`), or the stored transactions do not reproduce the block's merkle root (`PROOF_TX_SET_INCOMPLETE`)

### Example

```bash
curl https://api.secondlayer.tools/v1/index/transactions/{tx_id}/proof \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "txid": "198d642edb1104a1757a8c353ad61231d0422ecb6ba83aa1ce45d05df44157e0",
  "index_block_hash": "990152a894e12288960c8b68e5790ad7994f9d261e9ad8adc13c72e76f914339",
  "block_height": 9048502,
  "tx_index": 1,
  "raw_tx": "000000000104004ba0f04696ab911b7009e3421913e1f5768c222400000000000001090000000000000bb8000123767ba956a0545b2543f059d003500767330fad1f4e6cce3d734c8719bfca9226c07dc6e64edb83078cb34e83c6b430d4a260628af821041f51eb5f21e444390302000000010102164ba0f04696ab911b7009e3421913e1f5768c222414f6decc7cfff2a413bd7cd4f53c25ad7fd1899acc0a736274632d746f6b656e0a736274632d746f6b656e01000000000000012c0214f6decc7cfff2a413bd7cd4f53c25ad7fd1899acc0a736274632d746f6b656e087472616e7366657200000004010000000000000000000000000000012c05164ba0f04696ab911b7009e3421913e1f5768c222405161a3cb3edf945d27fa141d1de2deb9093ec74147f09",
  "raw_header": "0100000000008a11b60000006643539084f394038c2267d08e5e5c52b9da2d99f4360c91a5314c3cac40ac15f4397b92360ccaa5adaeda6ebc5587511f33b1495fe1d1f66d10b0bfc1ef83918390f25ad4f0b7f5ddf76fa79782e92da2315856590664d9c179ccc0143e35f99140069ea4517ab4ca79e39f0e54b406b1b68b5f8c29e25750000000006ab3d0c2012ae8acc3a85100ae9eecf07468c694fee3cb585c423a0df3054991c77f4fa3fb4fc7b300f1b332f02ec0488c4033c7eb72abec17999c01326d222225f430e56d000000130016f1a9fb8ef3c85b04bd863818f4b47680de8f33e375b2872fbdd5b94d1664624488458f13383478becec32e9ce8ac9b2d0292d2773b37bf1c5943e87cf35b31006dbcd0a22c1eaf81d34dd1c2f25a1e05b7d6e5fdd3c5d8c7e4b96aa4472088e14d8b7ece3427f77fbcfd4027e7bd27be1388ddb68efb8debf8dfe48da558987400e3df41a2cc852d41b6db8f67c8e6c7bf1c31d185c87b019d349a5d6713c951ca43837c05b62a9f0b58ac4403a98233f1e1573e031fae90306ccd618dff1ce8eb01f4bba3a39010779d4870906d1f7e66115b8146f6b70b4e4fee3a402fa484c1d40146832b8fb21ab3328d7588e3861d9c614522692bd14fb763c77a2203d0b86a010075c0105d54deac9a6388734e0519d7ba90024727f02d65b8fd2cd788eca9350c6f6c7ff52200e41cefbf19873bcb9c17068ca9b093fd032dd68683348e95550083c787948e7bd477341747c8769ce2e9c8a420cb137d3609b5e90163f0c56ac3747219c113b0b5018aefa1dc9bd415b21b48173cb9c42f96f19cf143a77261340092179e100c58cfc2b3ee3271eca056ad98c986a1b434296eb1bb00986f3c1a7e70d89fdcbb983f216b9b9d5b5bf21093f58bf2e022fb0010f6706010ebe8c09f01bf06697f3e93883ebf59f83a98bc032d7b3da70886b6e44309cd77855dc1bf8c099084b40609c0a4e3d04806a0fd23391ad79745ba52b122838e3edf6359aa2d006d529e1f590e58b3d1519b719d1149e1babfa1cf20b164167199da2804dec7004159c9f6cb90f265b0d7c3338905de677087bbf9e40a7f06159a4697014756c501726fe445f8a4c876138ff40581e34a659463f97d7b2d1cc843b82d5c8ec9cd2e326ab34b509f4c193f424ebd2ddd729f007687c88b66af59ddda1036b1c7da8700d9a1b7f528df90661c608fd1935d95abb0f638b780573101837863353daf50c529e02703891e885ffa2603ca19a8ab1724c07795e6ab458e93abdca27ac67c3600c4202f95645edb945c2659345fd1dc5043d9fd178c398ddb2ed55ffc5bb9c62f2a61ada05578631cd6523fadf4a47be96133216b5cd421222a1289914ec41d5600ae2119da699f9eea1a8c49b0cf63c2ed5191d141491f11893869e04f24ad14d20dabc52b11e4418cd46610709c7d6456ff0a235760dc39c7e4c057fc8802fa4101813c588a7730569283f517b53949f67aa46a214530dab382da6ec3afc7014ede023706361ec717f8487a88f9d8ae9f912b3033124fccf8c98e5bcf382208febf016ad6d2d6a0c77ae937dc663b7111ab145e4df0248bcb1da80dce6b5bf0061539705f37412cf59476a22ee6677c20c9b980f263f8c1d4c79acd8d7166ff1a7b4f00aecd8f47fee2d6819993aa0d2e3e4872e2a95550c7403491f9764ebfa832f38b61704739abda8cfb0e05d8020efa5e49a5704dbd82f79fd3638b61005c37867c00025ea0925e7583f2f43c9ad4ff6ceaa8ef6bb62cb15265ebf684ca9005f5bb9312009b7e7bc171291c4c2530b74b275e57fc53072cd381a1cb27dc63232c0835007a5b80bc0b5af6848568468cc2a10f74789e1a1e0bcf6f18bb45f8df4e7a19f55c26f593b23aa76c2b7171aff4b062a58f6611da097071298621735521c7ee110062254da4170c6e66963e8a74c9a4fb5797e4247f5ccd8784c82d70ae96f5f01644e6b7733ca93f0e065fc165e152b4abd602687f2f081ab39c8f0943d3b51ea90001000000010100000000",
  "tx_merkle_path": [
    {
      "position": "left",
      "hash": "ea5ecd867b9945b06aecdf77117de46ae9fd055b137be1fecc1dee38f56b19af"
    },
    {
      "position": "right",
      "hash": "398e676d798b758c66c50e21a83b4c6c8c20a5ffc8552b61d1df160f36a0c23c"
    },
    {
      "position": "right",
      "hash": "d1315b8f5f61861868657a0ab2c8c62332bb77a74a833567427a9f95ab86c1eb"
    }
  ],
  "consensus": {
    "reward_cycle": 143,
    "reward_set": {
      "signers": [
        {
          "signing_key": "0209130bec93e83b23d3366adfcbe0a1641057b9c48102572d892fe8f78de2bee7",
          "weight": 646
        },
        {
          "signing_key": "0216436e7785e1dc55cf4350ac1f90e45daf0ec5b57bce13a32551a513446e71d1",
          "weight": 9
        }
      ],
      "total_weight": 4000
    }
  }
}
```

## Pending transactions

`GET /v1/index/mempool`

Pending transactions this instance's node has seen since the observer connected, in arrival order. A single-node, go-forward view: the backlog from before the observer connected is never replayed. Confirmed transactions leave the list. No height window. Never cached.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `sender` | string | Sender principal. Exact match. |
| `type` | string | Transaction type as stored, e.g. `contract_call`, `token_transfer`. Exact match. |
| `contract_id` | string | Called contract. Exact match. |
| `function_name` | string | Called function. Exact match. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Page size; capped at 1000. |
| `cursor` | string | Opaque `next_cursor` from a previous page. Pass it back unchanged. Mutually exclusive with `from_cursor`. |
| `from_cursor` | string | Same as `cursor`. |

### Returns

A cursor envelope; `mempool` holds `MempoolTransaction` objects.

### Responses

- `200` Cursor-paginated envelope
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/mempool \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "mempool": [
    {
      "cursor": "bToyNjg4Mjc0",
      "tx_id": "0x00a5cd852c25c5bb5cc7f97c60b88a6a635c001ad431e2133f366be85d5ce0f0",
      "tx_type": "token_transfer",
      "sender": "SM1Z09F6431EQT8GCTEF496TCT5JDTPJ3VFF9Q766",
      "received_at": "2026-09-23T15:11:05.629Z",
      "fee": "1732",
      "nonce": "37",
      "sponsored": false,
      "anchor_mode": "any",
      "post_condition_mode": "deny",
      "post_conditions": [],
      "token_transfer": {
        "recipient": "SMJWA5YRH2DRANX3V96Z6JJSFBE03Z9Y0WHN3XSQ",
        "amount": "79559850769",
        "memo": "572826509"
      }
    }
  ],
  "next_cursor": "bToyNjg4Mjc0",
  "tip": {
    "block_height": 9048690,
    "finalized_height": 9048457,
    "lag_seconds": 4,
    "source_block_height": 9048692
  }
}
```

## Pending transaction by tx_id

`GET /v1/index/mempool/{tx_id}`

One pending transaction. It 404s once the transaction confirms (read it from `/v1/index/transactions/{tx_id}`) or drops. Never cached.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `tx_id` | string, required | Transaction id, `0x`-prefixed hex. |

### Responses

- `200` The pending transaction and the current tip
- `401` This bind requires the instance token, and it was missing or wrong
- `404` Not pending on this instance: confirmed, dropped, or never seen (`Pending transaction not found`)
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/mempool/{tx_id} \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "transaction": {
    "cursor": "bToyNjg4Mjc0",
    "tx_id": "0x00a5cd852c25c5bb5cc7f97c60b88a6a635c001ad431e2133f366be85d5ce0f0",
    "tx_type": "token_transfer",
    "sender": "SM1Z09F6431EQT8GCTEF496TCT5JDTPJ3VFF9Q766",
    "received_at": "2026-09-23T15:11:05.629Z",
    "fee": "1732",
    "nonce": "37",
    "sponsored": false,
    "anchor_mode": "any",
    "post_condition_mode": "deny",
    "post_conditions": [],
    "token_transfer": {
      "recipient": "SMJWA5YRH2DRANX3V96Z6JJSFBE03Z9Y0WHN3XSQ",
      "amount": "79559850769",
      "memo": "572826509"
    }
  },
  "tip": {
    "block_height": 9048938,
    "finalized_height": 9048671,
    "lag_seconds": 23,
    "source_block_height": 9048938
  }
}
```

## Print payload schemas for a contract

`GET /v1/index/contracts/{contract_id}/print-schema`

Infers the shape of a contract's `print` payloads from its canonical print events, grouped by the tuple's `topic` field. Reads the newest 1500 and oldest 500 events, so fields the contract stopped emitting still show. Takes no query parameters. Held in memory for 5 minutes per contract. Not metered.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `contract_id` | string, required | Contract principal, `<address>.<name>`. Anything that does not look like one is refused. |

### Returns

One `PrintSchema` object.

### Responses

- `200` Inferred print schemas
- `400` `contract_id` is not a contract principal, or a query parameter was sent (`VALIDATION_ERROR`)
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/contracts/SP2BM6AQSMQ04CX8KDE62QBFVZTDZ2ZX80GZJSBZ4.zc-claim-helper-v2/print-schema \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "contract_id": "SP2BM6AQSMQ04CX8KDE62QBFVZTDZ2ZX80GZJSBZ4.zc-claim-helper-v2",
  "topics": [
    {
      "topic": "zc-claim-ok",
      "count": 2,
      "first_height": 9004646,
      "last_height": 9004646,
      "non_tuple": false,
      "fields": [
        {
          "name": "earned",
          "camel_name": "earned",
          "clarity_type": "uint",
          "ts_type": "bigint",
          "column_type": "uint",
          "always_present": true
        },
        {
          "name": "reward-cycle",
          "camel_name": "rewardCycle",
          "clarity_type": "uint",
          "ts_type": "bigint",
          "column_type": "uint",
          "always_present": true
        },
        {
          "name": "staker",
          "camel_name": "staker",
          "clarity_type": "principal",
          "ts_type": "string",
          "column_type": "principal",
          "always_present": true
        },
        {
          "name": "withdrawal-request",
          "camel_name": "withdrawalRequest",
          "clarity_type": "(optional ?)",
          "ts_type": "unknown | null",
          "column_type": "jsonb",
          "always_present": true,
          "optional_some_rate": 0
        }
      ]
    },
    {
      "topic": "claim-many",
      "count": 1,
      "first_height": 9004646,
      "last_height": 9004646,
      "non_tuple": false,
      "fields": [
        {
          "name": "err-count",
          "camel_name": "errCount",
          "clarity_type": "uint",
          "ts_type": "bigint",
          "column_type": "uint",
          "always_present": true
        },
        {
          "name": "manager",
          "camel_name": "manager",
          "clarity_type": "principal",
          "ts_type": "string",
          "column_type": "principal",
          "always_present": true
        },
        {
          "name": "ok-count",
          "camel_name": "okCount",
          "clarity_type": "uint",
          "ts_type": "bigint",
          "column_type": "uint",
          "always_present": true
        },
        {
          "name": "submitted",
          "camel_name": "submitted",
          "clarity_type": "uint",
          "ts_type": "bigint",
          "column_type": "uint",
          "always_present": true
        }
      ]
    }
  ],
  "sampled": false,
  "total_events": 3,
  "total_events_capped": false,
  "sample": {
    "size": 3,
    "newest_height": 9004646,
    "oldest_height": 9004646
  },
  "tip": {
    "block_height": 9048938,
    "finalized_height": 9048671,
    "lag_seconds": 23,
    "source_block_height": 9048938
  }
}
```

## PoX-4 stacking actions

`GET /v1/index/stacking`

Decoded PoX-4 contract calls (`stack-stx`, `delegate-stx`, `stack-aggregation-commit-indexed`, …), one row per call, oldest first. Pages on `<block_height>:<tx_index>`. With no cursor or `from_height`, the read covers the last day. PoX-4 stopped at the epoch 4.0 fork, so read history with `from_height` and use `/v1/index/pox5/events` for what came after.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `function_name` | string | PoX-4 function name, e.g. `stack-stx` or `delegate-stack-stx`. Exact match. |
| `stacker` | string | Stacker principal named in the call. Exact match. Solo `stack-stx` calls leave it null, so filter those by `caller`. |
| `caller` | string | Transaction sender. Exact match. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Page size; capped at 1000. |
| `cursor` | string | `<block_height>:<tx_index>` from a previous page's `next_cursor`. Resumes after it. |
| `from_cursor` | string | Same as `cursor`. Mutually exclusive with it and with `from_height`. |
| `from_height` | integer | First block height to read. Mutually exclusive with a cursor. With neither, the read starts one day behind the tip. |
| `to_height` | integer | Last block height to read, inclusive. |

### Returns

A cursor envelope; `stacking` holds `StackingAction` objects.

### Responses

- `200` Cursor-paginated envelope
- `304` The page is past finality and matches your `If-None-Match` ETag.
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/stacking \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "stacking": [
    {
      "cursor": "8647822:1",
      "block_height": 8647822,
      "block_time": "2026-07-27T18:39:09.000Z",
      "burn_block_height": 959869,
      "tx_id": "0x995fb653065c74596b63e6cc9c10fb8fa7b8f3574d5ea089b45d3e11d9e7336e",
      "tx_index": 1,
      "function_name": "stack-stx",
      "caller": "SP14S259H3AC9PXWJ8EVX9ER5NZ58R03GR4QAKHCB",
      "stacker": null,
      "delegate_to": null,
      "amount_ustx": "4491100000000",
      "lock_period": 1,
      "pox_addr": {
        "version": 6,
        "hashbytes": "0x6f0fbc92d6ad56a9d7783b7420ae6731434831e3484d82ad54afe32d7fc7ad6d",
        "btc": "bc1pdu8meykk44t2n4mc8d6zptn8x9p5sv0rfpxc9t254l3j6l7844ksz4jjcs"
      },
      "start_cycle": 139,
      "end_cycle": 139,
      "reward_cycle": null,
      "signer_key": "0x031b91a32ab90b80d40715e8875fb3dfe996aa1e6f5e62ba05d34542aa58ad43b5",
      "result_ok": true
    }
  ],
  "next_cursor": "7960000:42",
  "tip": {
    "block_height": 9048690,
    "finalized_height": 9048457,
    "lag_seconds": 4,
    "source_block_height": 9048692
  },
  "reorgs": [
    {
      "id": "3f16c2c5-9551-4ae9-adb0-923668e5c65e",
      "detected_at": "2026-09-15T15:39:38.129Z",
      "fork_point_height": 8996511,
      "old_index_block_hash": "0x45ba0195071488eda13afa66d8ce8612558a739f781be5a032d96332797c4174",
      "new_index_block_hash": "0xa686ff58e7c5ccb26c850a7eaffa7e7274fcbc081c706344eb6f1e1446023df0",
      "orphaned_range": {
        "from": "8996511:0",
        "to": "8996512:2701"
      },
      "new_canonical_tip": "8996511:0"
    }
  ]
}
```

## Reward cycles of the current PoX (PoX-5)

`GET /v1/index/pox/cycles`

One row per pox-5 reward cycle, descending, exact to the node: total stacked, reward-eligible STX, bond sats, sBTC custodied, rewards allocated/claimed, and cumulative rewards-per-token. Without a cursor, your list starts at the next reward cycle (the current one plus one) and goes back in time. Pass a higher `cursor` to reach cycles further in the future, which PoX-5 bonds can lock many cycles ahead. `pox_version` marks the era; at the next PoX fork this endpoint follows the chain and reports the new PoX instead. PoX-4 history is final and not served here; rebuild it from `/v1/index/stacking` on a self-hosted instance. Frozen cycles cache for an hour; the still-open current cycle caches briefly.

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 100 | Cycles per page, 1 to 100. Defaults to 20; anything else is refused. |
| `cursor` | integer | `next_cursor` from the previous page, or any reward cycle (exclusive) to page from. Returns cycles below it. Omit it and the list starts at the next reward cycle. |

### Returns

A cursor envelope; `cycles` holds `Pox5Cycle` objects.

### Responses

- `200` Reward cycles, descending from the next reward cycle
- `400` `limit` or `cursor` is out of range, or an unknown parameter was sent
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/pox/cycles \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "pox_version": 5,
  "cycles": [
    {
      "reward_cycle": 141,
      "start_burn_height": 962150,
      "prepare_start_burn_height": 962050,
      "end_burn_height": 964249,
      "total_stacked_ustx": "392447554847960",
      "reward_eligible_ustx": "392447554847960",
      "stakers": 3035,
      "signers_in_set": 36,
      "bond_sats": {
        "1": "23017037628"
      },
      "bond_total_sats": "23017037628",
      "sbtc_custodied_sats": "16016587628",
      "rewards_allocated_stx": "297713728",
      "rewards_allocated_bond": "13810222",
      "reserve_deposit": "52537716",
      "rewards_per_token_stx": "758607677183",
      "rewards_per_token_bond": {
        "1": "599999974940302"
      },
      "distributions": 2,
      "rewards_claimed": "162018760",
      "computed_through_height": 8665732,
      "is_current": false,
      "is_frozen": true
    }
  ],
  "next_cursor": 141,
  "tip": {
    "block_height": 9048690,
    "finalized_height": 9048457,
    "lag_seconds": 4,
    "source_block_height": 9048692
  }
}
```

## A PoX-5 reward cycle by cycle number

`GET /v1/index/pox/cycles/{reward_cycle}`

One pox-5 reward cycle, the same shape as a row of `/v1/index/pox/cycles` plus its per-signer breakdown. 404 when the cycle doesn't exist yet. A cycle number below 141 (pox-5's first) 404s with a note that PoX-4 cycles are final and not served here.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `reward_cycle` | integer, required | Reward cycle number. |

### Responses

- `200` The cycle, with its signers
- `400` `reward_cycle` is not a non-negative integer
- `401` This bind requires the instance token, and it was missing or wrong
- `404` The cycle doesn't exist yet, or (below 141) is PoX-4 history: final and not served here; rebuild it from /v1/index/stacking on a self-hosted instance
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/pox/cycles/141 \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "pox_version": 5,
  "cycle": {
    "reward_cycle": 141,
    "start_burn_height": 962150,
    "prepare_start_burn_height": 962050,
    "end_burn_height": 964249,
    "total_stacked_ustx": "392447554847960",
    "reward_eligible_ustx": "392447554847960",
    "stakers": 3035,
    "signers_in_set": 36,
    "bond_sats": {
      "1": "23017037628"
    },
    "bond_total_sats": "23017037628",
    "sbtc_custodied_sats": "16016587628",
    "rewards_allocated_stx": "297713728",
    "rewards_allocated_bond": "13810222",
    "reserve_deposit": "52537716",
    "rewards_per_token_stx": "758607677183",
    "rewards_per_token_bond": {
      "1": "599999974940302"
    },
    "distributions": 2,
    "rewards_claimed": "162018760",
    "computed_through_height": 8665732,
    "is_current": false,
    "is_frozen": true,
    "signers": [
      {
        "signer": "SPMPMA1V6P430M8C91QS1G9XJ95S59JS1TZFZ4Q4.fastpool-max500-signer-manager",
        "delegated_ustx": "51000000000000",
        "stx_only_ustx": "51000000000000",
        "reward_shares_ustx": "51000000000000",
        "in_set": true,
        "rewards_claimed": "19011164"
      }
    ]
  },
  "tip": {
    "block_height": 9048929,
    "finalized_height": 9048671,
    "lag_seconds": 22,
    "source_block_height": 9048929
  }
}
```

## Decoded PoX-5 boot-contract events

`GET /v1/index/pox5/events`

Decoded print log of the pox-5 boot contract (SIP-045 Bitcoin Staking) — all 19 topics (stake, stake-update, register-signer, unstake, claim-rewards, etc.), one row per print. Starts at the epoch 4.0 hard fork, where /v1/index/stacking's pox-4 feed ends.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `topic` | enum | One print topic. Omit for all of them. Values: `set-bond-admin`, `set-pause-admin`, `pause-rewards`, `setup-bond`, `add-to-allowlist`, `register-for-bond`, `update-bond-registration`, `register-signer`, `stake`, `stake-update`, `announce-l1-early-exit`, `unstake-sbtc`, `unstake`, `calculate-rewards`, `bond-distribution`, `claim-rewards`, `claim-staker-rewards-for-signer`, `grant-signer-key`, `revoke-signer-grant`. |
| `staker` | string | Staker principal. Exact match. |
| `signer` | string | The signer: a signer-manager contract. Set on every topic that involves a signer, so `signer=` returns a pool's full activity. |
| `signer_manager` | string | The same principal, as printed on claim and grant topics (`claim-rewards`, `claim-staker-rewards-for-signer`, `grant-signer-key`, `revoke-signer-grant`). Kept for compatibility; `signer` covers it. |
| `bond_index` | integer | Bond index. Exact match. |
| `reward_cycle` | integer | Reward cycle. Exact match. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Page size; capped at 1000. |
| `cursor` | string | `<block_height>:<event_index>` from a previous page's `next_cursor`. Resumes after it. |
| `from_cursor` | string | Same as `cursor`. Mutually exclusive with it and with `from_height`. |
| `from_height` | integer | First block height to read. Mutually exclusive with a cursor. With neither, the read starts one day behind the tip. |
| `to_height` | integer | Last block height to read, inclusive. |
| `confirmed` | boolean | `true` stops the page at the finality boundary, so every row is past the reorg margin. |
| `fields` | string | Comma-separated columns to return. `cursor`, `block_height` and `topic` always come back; an unknown name is refused. |

### Returns

A cursor envelope; `events` holds `Pox5Event` objects.

### Responses

- `200` Cursor-paginated envelope
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl -G https://api.secondlayer.tools/v1/index/pox5/events \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d topic=stake \
  -d signer=SP8HK160YD5GHXP69VGA0TC7AQJ1X4CDW3XVERSE.xverse-signer-manager-3
```

```json
{
  "events": [
    {
      "cursor": "9048679:0",
      "block_height": 9048679,
      "block_time": "2026-09-23T13:55:35.000Z",
      "tx_id": "0xba5c3e57f65a7e92f917c0e8c32bdf11cedb31baefe708c8a3b9177437420ea9",
      "tx_index": 0,
      "event_index": 0,
      "topic": "stake",
      "staker": "SP19YK5M97734AJZAA2X8W6J9GGNVM9JFZ751H0F1",
      "signer": "SP8HK160YD5GHXP69VGA0TC7AQJ1X4CDW3XVERSE.xverse-signer-manager-3",
      "signer_manager": null,
      "bond_index": null,
      "amount_ustx": "550000000",
      "amount_sats": null,
      "reward_cycle": null,
      "first_reward_cycle": 144,
      "unlock_cycle": 240,
      "unlock_burn_height": 1170050,
      "is_l1_lock": null,
      "signer_key": null,
      "data": {
        "topic": "stake",
        "signer": "SP8HK160YD5GHXP69VGA0TC7AQJ1X4CDW3XVERSE.xverse-signer-manager-3",
        "staker": "SP19YK5M97734AJZAA2X8W6J9GGNVM9JFZ751H0F1",
        "num-cycles": "96",
        "amount-ustx": "550000000",
        "unlock-cycle": "240",
        "first-reward-cycle": "144",
        "unlock-burn-height": "1170050"
      }
    }
  ],
  "next_cursor": "7960000:42",
  "tip": {
    "block_height": 9048690,
    "finalized_height": 9048457,
    "lag_seconds": 4,
    "source_block_height": 9048692
  },
  "reorgs": [
    {
      "id": "3f16c2c5-9551-4ae9-adb0-923668e5c65e",
      "detected_at": "2026-09-15T15:39:38.129Z",
      "fork_point_height": 8996511,
      "old_index_block_hash": "0x45ba0195071488eda13afa66d8ce8612558a739f781be5a032d96332797c4174",
      "new_index_block_hash": "0xa686ff58e7c5ccb26c850a7eaffa7e7274fcbc081c706344eb6f1e1446023df0",
      "orphaned_range": {
        "from": "8996511:0",
        "to": "8996512:2701"
      },
      "new_canonical_tip": "8996511:0"
    }
  ]
}
```

## Rune catalog

`GET /v1/index/runes`

Every etched rune, with the identity bundle (id, name, spaced_name, symbol, divisibility) every Runes response carries plus its computed supply, mint/burn counters and mint terms. `search` prefix-matches the name (spacers and case ignored); `sort` is etch order (`number`, default) or busiest-first (`mints`).

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `search` | string | Name prefix, spacers and case ignored (e.g. `dog` matches `DOG•GO•TO•THE•MOON`). |
| `sort` | enum | `number` (etch order, oldest first — default) or `mints` (busiest first). Values: `number`, `mints`. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `cursor` | string | Opaque token from a previous page's `next_cursor`. Resumes after it — pass back unchanged. |
| `limit` | integer · 1 to 1000 | Page size; capped at 1000. |

### Returns

A cursor envelope; `runes` holds `RuneEntry` objects.

### Responses

- `200` Cursor-paginated envelope
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds
- `503` The Bitcoin (Runes) database is configured but unreachable (`BITCOIN_DATA_UNAVAILABLE`). Retry shortly

### Example

```bash
curl https://api.secondlayer.tools/v1/index/runes \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "runes": [
    {
      "id": "840000:3",
      "number": "3",
      "name": "DOGGOTOTHEMOON",
      "spaced_name": "DOG•GO•TO•THE•MOON",
      "symbol": "🐕",
      "divisibility": 5,
      "premine": "10000000000000000",
      "supply": "10000000000000000",
      "burned": "2440680717188",
      "mints": "0",
      "turbo": false,
      "etching_txid": "e79134080a83fe3e0e06ed6990c5a9b63b362313341745707a2bff7d788a1375",
      "etched_height": 840000,
      "etched_tx_index": 3,
      "terms": null
    }
  ],
  "tip": {
    "block_height": 968925,
    "finalized_height": 968919,
    "lag_seconds": 210
  },
  "reorgs": [
    {
      "id": "7",
      "detected_at": "2026-08-11T04:22:10.000Z",
      "fork_point_height": 851203,
      "old_hash": "0000000000000000000123abc1230000000000000000000000000000000000",
      "new_hash": "0000000000000000000456def4560000000000000000000000000000000000",
      "orphaned_from": 851203,
      "orphaned_to": 851204,
      "new_tip_height": 851205
    }
  ]
}
```

## A single rune's entry

`GET /v1/index/runes/{rune}`

One rune's full entry by id or name, with computed `supply` (`premine + mints × terms.amount`). 404 when no rune matches.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `rune` | string, required | A rune id (`<block>:<tx>`, e.g. `840000:3`) or name — spacers and case are ignored, so `DOG•GO•TO•THE•MOON`, `dog.go.to.the.moon` and `doggotothemoon` are the same rune. |

### Responses

- `200` The rune
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No rune matches this id/name, or Runes isn't configured on this instance (`code: NOT_FOUND`, the latter case also carries `details.notes`)
- `429` Rate limited; retry after `Retry-After` seconds
- `503` The Bitcoin (Runes) database is configured but unreachable (`BITCOIN_DATA_UNAVAILABLE`). Retry shortly

### Example

```bash
curl https://api.secondlayer.tools/v1/index/runes/840000:3 \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "rune": {
    "id": "840000:3",
    "number": "3",
    "name": "DOGGOTOTHEMOON",
    "spaced_name": "DOG•GO•TO•THE•MOON",
    "symbol": "🐕",
    "divisibility": 5,
    "premine": "10000000000000000",
    "supply": "10000000000000000",
    "burned": "2440680717188",
    "mints": "0",
    "turbo": false,
    "etching_txid": "e79134080a83fe3e0e06ed6990c5a9b63b362313341745707a2bff7d788a1375",
    "etched_height": 840000,
    "etched_tx_index": 3,
    "terms": null
  },
  "tip": {
    "block_height": 968925,
    "finalized_height": 968919,
    "lag_seconds": 210
  }
}
```

## Rune etches, mints, transfers and burns

`GET /v1/index/runes/activity`

Every rune-moving event — etch, mint, transfer, burn — filterable by rune/address/kind/txid and cursor-paginated. Returns events[], next_cursor, tip, reorgs[]. Cursor: <block_height>:<event_index>, same shape as every other decoded-event feed.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `rune` | string | A rune id (`<block>:<tx>`, e.g. `840000:3`) or name — spacers and case are ignored, so `DOG•GO•TO•THE•MOON`, `dog.go.to.the.moon` and `doggotothemoon` are the same rune. |
| `address` | string | Mainnet Bitcoin address. Exact match. |
| `kind` | string | One or more of `rune_etch`, `rune_mint`, `rune_transfer`, `rune_burn`, comma-separated. |
| `txid` | string | Bitcoin txid, hex with no `0x` prefix (Rune txids come back as stored, unlike Stacks ones). Exact match. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Page size; capped at 1000. |
| `cursor` | string | `<block_height>:<event_index>` from a previous page's `next_cursor`. Resumes after it. |
| `from_cursor` | string | Same as `cursor`. Mutually exclusive with it and with `from_height`. |
| `from_height` | integer | First block height to read. Mutually exclusive with a cursor. With neither, the read starts one day behind the tip. |
| `to_height` | integer | Last block height to read, inclusive. |

### Returns

A cursor envelope; `events` holds `RuneEvent` objects.

### Responses

- `200` Cursor-paginated envelope
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds
- `503` The Bitcoin (Runes) database is configured but unreachable (`BITCOIN_DATA_UNAVAILABLE`). Retry shortly

### Example

```bash
curl -G https://api.secondlayer.tools/v1/index/runes/activity \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d rune=840000:3 \
  -d kind=rune_mint,rune_transfer
```

```json
{
  "events": [
    {
      "cursor": "968919:3",
      "block_height": 968919,
      "tx_index": 144,
      "txid": "77c95d193e680429fe7f801a68488bd30919d011f23542141a7fae5244b002b4",
      "event_index": 3,
      "kind": "rune_transfer",
      "amount": "2933553300",
      "vout": 2,
      "address": "bc1pjku9h7ckjt8802mu79zt7vv8d6mhp0x57gkppcls57ex4rkq9cxqs52pr3",
      "rune": {
        "id": "840000:3",
        "name": "DOGGOTOTHEMOON",
        "spaced_name": "DOG•GO•TO•THE•MOON",
        "symbol": "🐕",
        "divisibility": 5
      }
    }
  ],
  "tip": {
    "block_height": 968925,
    "finalized_height": 968919,
    "lag_seconds": 210
  },
  "reorgs": [
    {
      "id": "7",
      "detected_at": "2026-08-11T04:22:10.000Z",
      "fork_point_height": 851203,
      "old_hash": "0000000000000000000123abc1230000000000000000000000000000000000",
      "new_hash": "0000000000000000000456def4560000000000000000000000000000000000",
      "orphaned_from": 851203,
      "orphaned_to": 851204,
      "new_tip_height": 851205
    }
  ]
}
```

## Rune balances by address or outpoint

`GET /v1/index/runes/balances`

Current per-outpoint rune balances. Exactly one of `address` or `outpoint` is required; `rune` narrows either to one rune. Returns balances[], next_cursor, tip.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `address` | string | Mainnet Bitcoin address. Mutually exclusive with `outpoint`; exactly one is required. |
| `outpoint` | string | `<txid>:<vout>`. Mutually exclusive with `address`; exactly one is required. |
| `rune` | string | A rune id (`<block>:<tx>`, e.g. `840000:3`) or name — spacers and case are ignored, so `DOG•GO•TO•THE•MOON`, `dog.go.to.the.moon` and `doggotothemoon` are the same rune. Narrows the result to one rune. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `cursor` | string | Opaque token from a previous page's `next_cursor`. Resumes after it — pass back unchanged. |
| `limit` | integer · 1 to 1000 | Page size; capped at 1000. |

### Returns

A cursor envelope; `balances` holds `RuneBalance` objects.

### Responses

- `200` Cursor-paginated envelope
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds
- `503` The Bitcoin (Runes) database is configured but unreachable (`BITCOIN_DATA_UNAVAILABLE`). Retry shortly

### Example

```bash
curl -G https://api.secondlayer.tools/v1/index/runes/balances \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d outpoint=2d2b54008790a44c9c3e630faa521a50f8ab122625c4eb8fb84705051c4fc611:0 \
  -d rune=840000:3
```

```json
{
  "balances": [
    {
      "rune": {
        "id": "840000:3",
        "name": "DOGGOTOTHEMOON",
        "spaced_name": "DOG•GO•TO•THE•MOON",
        "symbol": "🐕",
        "divisibility": 5
      },
      "address": "bc1phnp234rd5hel0h8ulqvnq3zrww2td0a8q48hjcg7mv67j97jnxfq9ayczm",
      "txid": "2d2b54008790a44c9c3e630faa521a50f8ab122625c4eb8fb84705051c4fc611",
      "vout": 0,
      "amount": "88980600"
    }
  ],
  "tip": {
    "block_height": 968925,
    "finalized_height": 968919,
    "lag_seconds": 210
  },
  "reorgs": [
    {
      "id": "7",
      "detected_at": "2026-08-11T04:22:10.000Z",
      "fork_point_height": 851203,
      "old_hash": "0000000000000000000123abc1230000000000000000000000000000000000",
      "new_hash": "0000000000000000000456def4560000000000000000000000000000000000",
      "orphaned_from": 851203,
      "orphaned_to": 851204,
      "new_tip_height": 851205
    }
  ]
}
```

## Decoded sBTC peg events

`GET /v1/index/sbtc/events`

Every decoded `sbtc-registry` print, all topics, one row per event, oldest first: deposits, withdrawal create/accept/reject, signer key rotations and protocol-contract updates. With no cursor or `from_height`, the read covers the last day.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `topic` | enum | One print topic. Omit for all of them; an unknown topic is refused. Values: `completed-deposit`, `withdrawal-create`, `withdrawal-accept`, `withdrawal-reject`, `key-rotation`, `update-protocol-contract`. |
| `sender` | string | Stacks principal on the event (the withdrawal requester on `withdrawal-create`). Exact match. |
| `request_id` | integer | Withdrawal request id. Exact match. |
| `bitcoin_txid` | string | Bitcoin txid on the event, `0x`-prefixed hex. Exact match. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Page size; capped at 1000. |
| `cursor` | string | `<block_height>:<event_index>` from a previous page's `next_cursor`. Resumes after it. |
| `from_cursor` | string | Same as `cursor`. Mutually exclusive with it and with `from_height`. |
| `from_height` | integer | First block height to read. Mutually exclusive with a cursor. With neither, the read starts one day behind the tip. |
| `to_height` | integer | Last block height to read, inclusive. |
| `confirmed` | boolean | `true` stops the page at the finality boundary, so every row is past the reorg margin. |

### Returns

A cursor envelope; `events` holds `SbtcEvent` objects.

### Responses

- `200` Cursor-paginated envelope
- `304` The page is past finality and matches your `If-None-Match` ETag.
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/sbtc/events \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "events": [
    {
      "cursor": "9048486:2",
      "block_height": 9048486,
      "block_time": "2026-09-23T13:10:19.000Z",
      "tx_id": "0x0e09fd3cf7628a2cc81083aa9dbb7eebc7bae5cbe277c370e0f8bfc3d1fe00a9",
      "tx_index": 2,
      "event_index": 2,
      "topic": "withdrawal-create",
      "request_id": 3406,
      "amount": "3127",
      "sender": "SP2ADCJ6606CFHERS3HKABWB6PF5XBANXBQ4GSE86",
      "recipient_btc_version": 4,
      "recipient_btc_hashbytes": "0x9c32e45a19cf9c028bccf12816e62bbfc8eb5fb8",
      "bitcoin_txid": null,
      "output_index": null,
      "sweep_txid": null,
      "burn_hash": null,
      "burn_height": null,
      "signer_bitmap": null,
      "max_fee": "1500",
      "fee": null,
      "governance_contract_type": null,
      "governance_new_contract": null,
      "signer_aggregate_pubkey": null,
      "signer_threshold": null,
      "signer_address": null,
      "signer_keys_count": null
    }
  ],
  "next_cursor": "7960000:42",
  "tip": {
    "block_height": 9048690,
    "finalized_height": 9048457,
    "lag_seconds": 4,
    "source_block_height": 9048692
  },
  "reorgs": [
    {
      "id": "3f16c2c5-9551-4ae9-adb0-923668e5c65e",
      "detected_at": "2026-09-15T15:39:38.129Z",
      "fork_point_height": 8996511,
      "old_index_block_hash": "0x45ba0195071488eda13afa66d8ce8612558a739f781be5a032d96332797c4174",
      "new_index_block_hash": "0xa686ff58e7c5ccb26c850a7eaffa7e7274fcbc081c706344eb6f1e1446023df0",
      "orphaned_range": {
        "from": "8996511:0",
        "to": "8996512:2701"
      },
      "new_canonical_tip": "8996511:0"
    }
  ]
}
```

## Completed sBTC deposits

`GET /v1/index/sbtc/deposits`

Completed deposits, one row per `completed-deposit` event, oldest first. The event fires once the signers have swept the BTC in, so every row is final on the peg side. With no cursor or `from_height`, the read covers the last day.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `sender` | string | Stacks principal on the event. Exact match. Mainnet deposits currently carry none. |
| `bitcoin_txid` | string | Deposit's Bitcoin txid, `0x`-prefixed hex. Exact match. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Page size; capped at 1000. |
| `cursor` | string | `<block_height>:<event_index>` from a previous page's `next_cursor`. Resumes after it. |
| `from_cursor` | string | Same as `cursor`. Mutually exclusive with it and with `from_height`. |
| `from_height` | integer | First block height to read. Mutually exclusive with a cursor. With neither, the read starts one day behind the tip. |
| `to_height` | integer | Last block height to read, inclusive. |
| `confirmed` | boolean | `true` stops the page at the finality boundary, so every row is past the reorg margin. |
| `fields` | string | Comma-separated columns to return, from: `block_time`, `tx_id`, `tx_index`, `event_index`, `amount`, `sender`, `bitcoin_txid`, `output_index`, `recipient_btc_version`, `recipient_btc_hashbytes`. `cursor` and `block_height` always come back; an unknown name is refused. |

### Returns

A cursor envelope; `deposits` holds `SbtcDeposit` objects.

### Responses

- `200` Cursor-paginated envelope
- `304` The page is past finality and matches your `If-None-Match` ETag.
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/sbtc/deposits \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "deposits": [
    {
      "cursor": "9048911:4",
      "block_height": 9048911,
      "block_time": "2026-09-23T15:06:29.000Z",
      "tx_id": "0x8600af4a0b2a911e6494d1feea0691068d8b329be0ef2c0d11a7d5aed8f9f4e5",
      "tx_index": 2,
      "event_index": 4,
      "amount": "79856",
      "sender": null,
      "bitcoin_txid": "0x2c11f9e20d21c96daed5d851c07d0e7c4361e552ebd78a8385f06fbb2f9892d3",
      "output_index": 1,
      "recipient_btc_version": null,
      "recipient_btc_hashbytes": null
    }
  ],
  "next_cursor": "7960000:42",
  "tip": {
    "block_height": 9048690,
    "finalized_height": 9048457,
    "lag_seconds": 4,
    "source_block_height": 9048692
  },
  "reorgs": [
    {
      "id": "3f16c2c5-9551-4ae9-adb0-923668e5c65e",
      "detected_at": "2026-09-15T15:39:38.129Z",
      "fork_point_height": 8996511,
      "old_index_block_hash": "0x45ba0195071488eda13afa66d8ce8612558a739f781be5a032d96332797c4174",
      "new_index_block_hash": "0xa686ff58e7c5ccb26c850a7eaffa7e7274fcbc081c706344eb6f1e1446023df0",
      "orphaned_range": {
        "from": "8996511:0",
        "to": "8996512:2701"
      },
      "new_canonical_tip": "8996511:0"
    }
  ]
}
```

## sBTC peg-in by Bitcoin txid

`GET /v1/index/sbtc/deposits/{bitcoin_txid}`

One completed deposit, looked up by the Bitcoin txid of the deposit. Past finality it is cached for good and answers `If-None-Match` with 304.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `bitcoin_txid` | string, required | Deposit's Bitcoin txid, `0x`-prefixed hex, as it appears in `bitcoin_txid`. |

### Responses

- `200` The deposit
- `304` The resource is past finality and matches your `If-None-Match` ETag.
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No completed deposit has this Bitcoin txid
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/sbtc/deposits/0x2c11f9e20d21c96daed5d851c07d0e7c4361e552ebd78a8385f06fbb2f9892d3 \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "deposit": {
    "cursor": "9048911:4",
    "block_height": 9048911,
    "block_time": "2026-09-23T15:06:29.000Z",
    "tx_id": "0x8600af4a0b2a911e6494d1feea0691068d8b329be0ef2c0d11a7d5aed8f9f4e5",
    "tx_index": 2,
    "event_index": 4,
    "amount": "79856",
    "sender": null,
    "bitcoin_txid": "0x2c11f9e20d21c96daed5d851c07d0e7c4361e552ebd78a8385f06fbb2f9892d3",
    "output_index": 1,
    "recipient_btc_version": null,
    "recipient_btc_hashbytes": null,
    "status": "COMPLETED"
  },
  "tip": {
    "block_height": 9048929,
    "finalized_height": 9048671,
    "lag_seconds": 22,
    "source_block_height": 9048929
  }
}
```

## sBTC withdrawals, one per request

`GET /v1/index/sbtc/withdrawals`

Withdrawals, one row per `request_id`, ordered by when they were requested. Each row carries its current status from the latest accept or reject, the BTC sweep the signers committed to, and that sweep's Bitcoin settlement. The height window and cursor apply to the request event. Status can change after a row is final, so pages are never long-cached.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `status` | enum | Current lifecycle status. Values: `REQUESTED`, `ACCEPTED`, `REJECTED`. |
| `sender` | string | Requester's Stacks principal. Exact match. |
| `request_id` | integer | Withdrawal request id. Exact match. |
| `settlement_confirmed` | boolean | `true` returns withdrawals whose sweep has confirmed on Bitcoin. `false` returns the rest, including ones with no sweep yet. |
| `confirmed_after` | string · date-time | ISO 8601 timestamp. Returns only withdrawals whose sweep confirmed on Bitcoin strictly after it. Combine with `from_height=0` to scan all history. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Page size; capped at 1000. |
| `cursor` | string | `<block_height>:<event_index>` from a previous page's `next_cursor`. Resumes after it. |
| `from_cursor` | string | Same as `cursor`. Mutually exclusive with it and with `from_height`. |
| `from_height` | integer | First block height to read. Mutually exclusive with a cursor. With neither, the read starts one day behind the tip. |
| `to_height` | integer | Last block height to read, inclusive. |
| `confirmed` | boolean | `true` stops the page at the finality boundary, so every row is past the reorg margin. |
| `fields` | string | Comma-separated columns to return, from: `status`, `amount`, `sender`, `recipient_btc_version`, `recipient_btc_hashbytes`, `sweep_txid`, `settlement_confirmed`, `btc_confirmations`, `btc_block_height`, `confirmed_at`, `requested_at`, `resolved_at`. `cursor` and `request_id` always come back; an unknown name is refused. |

### Returns

A cursor envelope; `withdrawals` holds `SbtcWithdrawal` objects.

### Responses

- `200` Cursor-paginated envelope
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/sbtc/withdrawals \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "withdrawals": [
    {
      "cursor": "9048225:2",
      "request_id": 3405,
      "status": "ACCEPTED",
      "amount": "221653",
      "sender": "SP2WRMQD3G4G8BR0120Y390CT6A8BTE6MB0JQ5EHA",
      "recipient_btc_version": 4,
      "recipient_btc_hashbytes": "0xa7d5ce640208ed037950d4c849c805e881209d6a",
      "sweep_txid": "0x0efad88eb01e248fd8321ada99cb74f7258948226eae106f8be5a5917d7473e4",
      "settlement_confirmed": true,
      "btc_confirmations": 8,
      "btc_block_height": 968278,
      "confirmed_at": "2026-09-23T14:35:25.473Z",
      "requested_at": "2026-09-23T12:01:45.000Z",
      "resolved_at": "2026-09-23T13:24:47.000Z"
    }
  ],
  "next_cursor": "7960000:42",
  "tip": {
    "block_height": 9048690,
    "finalized_height": 9048457,
    "lag_seconds": 4,
    "source_block_height": 9048692
  },
  "reorgs": [
    {
      "id": "3f16c2c5-9551-4ae9-adb0-923668e5c65e",
      "detected_at": "2026-09-15T15:39:38.129Z",
      "fork_point_height": 8996511,
      "old_index_block_hash": "0x45ba0195071488eda13afa66d8ce8612558a739f781be5a032d96332797c4174",
      "new_index_block_hash": "0xa686ff58e7c5ccb26c850a7eaffa7e7274fcbc081c706344eb6f1e1446023df0",
      "orphaned_range": {
        "from": "8996511:0",
        "to": "8996512:2701"
      },
      "new_canonical_tip": "8996511:0"
    }
  ]
}
```

## sBTC peg-out lifecycle by request_id

`GET /v1/index/sbtc/withdrawals/{request_id}`

One withdrawal's full lifecycle: the request, the accept or reject, and the Bitcoin settlement of its sweep. Once it is accepted or rejected and past finality, it is cached for good and answers `If-None-Match` with 304.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `request_id` | integer, required | Withdrawal request id. |

### Responses

- `200` The withdrawal's lifecycle
- `304` The resource is past finality and matches your `If-None-Match` ETag.
- `400` `request_id` is not a non-negative integer
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No withdrawal request has this id
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/sbtc/withdrawals/3405 \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "withdrawal": {
    "request_id": 3405,
    "status": "ACCEPTED",
    "amount": "221653",
    "sender": "SP2WRMQD3G4G8BR0120Y390CT6A8BTE6MB0JQ5EHA",
    "recipient_btc_version": 4,
    "recipient_btc_hashbytes": "0xa7d5ce640208ed037950d4c849c805e881209d6a",
    "requested": {
      "block_height": 9048225,
      "block_time": "2026-09-23T12:01:45.000Z",
      "tx_id": "0x0e132d74a062d9327bbf25b371dd3fa7171f5e3479131b046106c1f6086a9780"
    },
    "accepted": {
      "block_height": 9048547,
      "block_time": "2026-09-23T13:24:47.000Z",
      "tx_id": "0xddedcfe67fe6c63339781619867816a69fc054e4a83b7358a8f07e983f2a1b68",
      "sweep_txid": "0x0efad88eb01e248fd8321ada99cb74f7258948226eae106f8be5a5917d7473e4",
      "signer_bitmap": "0"
    },
    "rejected": null,
    "settlement": {
      "sweep_txid": "0x0efad88eb01e248fd8321ada99cb74f7258948226eae106f8be5a5917d7473e4",
      "btc_confirmations": 8,
      "settlement_confirmed": true,
      "btc_block_height": 968278,
      "confirmed_at": "2026-09-23T14:35:25.473Z"
    },
    "finalized": true
  },
  "tip": {
    "block_height": 9048929,
    "finalized_height": 9048671,
    "lag_seconds": 22,
    "source_block_height": 9048929
  }
}
```

## sBTC peg summary scoreboard

`GET /v1/index/sbtc/summary`

All-time totals for the whole bridge: deposit and withdrawal counts, net peg flow, and circulating sBTC supply read from `sbtc-token` on this instance's node. Takes no parameters.

### Responses

- `200` The bridge totals
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/index/sbtc/summary \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "summary": {
    "total_deposits": 28077,
    "total_withdrawals_requested": 3408,
    "total_withdrawals_accepted": 3328,
    "total_withdrawals_rejected": 78,
    "net_peg_flow_sats": "247872312898",
    "total_locked_sats": "247872312898",
    "unmatched_withdrawal_accepts": 0,
    "sbtc_supply_sats": "247872312898"
  },
  "tip": {
    "block_height": 9048929,
    "finalized_height": 9048671,
    "lag_seconds": 22,
    "source_block_height": 9048929
  }
}
```

---

# streams

Raw event firehose, chain tip, and reorg history

## The Streams discovery object

Catalog of the Streams routes and filters.

| Field | Type | Description |
| --- | --- | --- |
| `routes` | array of object | Each route with its method, a one-line description, and for the event feed its event types and filters. |
| `cursor` | object | Cursor `format` and resume `semantics`. |
| `reorgs_shape` | object | Field names and types of a reorg record. |

```json
{
  "routes": [
    {
      "path": "/v1/streams/events",
      "method": "GET",
      "description": "Raw event firehose. Cursor-paginated. Returns events[], next_cursor, tip, reorgs[].",
      "event_types": [
        "stx_transfer",
        "stx_mint",
        "stx_burn",
        "stx_lock",
        "ft_transfer",
        "ft_mint",
        "ft_burn",
        "nft_transfer",
        "nft_mint",
        "nft_burn",
        "print"
      ],
      "filters": [
        {
          "name": "types",
          "type": "event_type[]",
          "description": "Event types to include"
        },
        {
          "name": "contract_id",
          "type": "principal | comma-list"
        },
        {
          "name": "limit",
          "type": "number (max 1000)"
        }
      ],
      "auth": "bearer required, metered per row"
    },
    {
      "path": "/v1/streams/tip",
      "method": "GET",
      "description": "Current chain tip: { block_height, block_hash, burn_block_height, finalized_height, lag_seconds }."
    }
  ],
  "cursor": {
    "format": "<block_height>:<event_index>",
    "semantics": "opaque resume token; pass back unchanged to continue. Equals last event's cursor (inclusive on output, exclusive on input)."
  },
  "reorgs_shape": {
    "detected_at": "ISO 8601",
    "new_canonical_tip": "<block_height>:<event_index>",
    "new_canonical_height": "number",
    "new_canonical_event_index": "number"
  }
}
```

## The Streams event object

One raw chain event. `payload` is the node's event body; its fields depend on `event_type`.

| Field | Type | Description |
| --- | --- | --- |
| `cursor` | string | `<block_height>:<event_index>`. Pass as `cursor` or `from_cursor`. |
| `block_height` | integer | Stacks block the event landed in. |
| `block_hash` | string | That block's hash. |
| `burn_block_height` | integer | Bitcoin block that block anchors to. |
| `tx_id` | string | Transaction that emitted the event. |
| `tx_index` | integer | Its position in the block. |
| `event_index` | integer | The event's position among the block's Streams events, from 0. |
| `event_type` | enum | The event's type. Values: `stx_transfer`, `stx_mint`, `stx_burn`, `stx_lock`, `ft_transfer`, `ft_mint`, `ft_burn`, `nft_transfer`, `nft_mint`, `nft_burn`, `print`. |
| `contract_id` | string · nullable | Emitting contract for `print`, asset contract for ft/nft events, `null` otherwise. |
| `payload` | object | The event body. Prints carry `topic`, `value` and `contract_id`; transfers carry `sender`, `recipient`, `amount` or `value`, and `asset_identifier`. |
| `ts` | string · date-time | The block's timestamp. |
| `canonical` | boolean | Always `true`: the firehose serves the canonical chain only. |
| `matched` | array of string | Labels from `filters` this event satisfied. Present only when the request used `filters`. |
| `finalized` | boolean | `true` when the block is at or below `tip.finalized_height` and can no longer reorg. |

```json
{
  "cursor": "9048876:3",
  "block_height": 9048876,
  "block_hash": "0xba0676b375e8d5fc1495a02847cc3cc32b5f7c2113a5e73999d94ee8d75ebde4",
  "burn_block_height": 968284,
  "tx_id": "0xb9edea3443a9adb603d856bfed2d1e630146bed128b07aaaa259dc97e3eae2ba",
  "tx_index": 2,
  "event_index": 3,
  "event_type": "ft_transfer",
  "contract_id": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token",
  "payload": {
    "amount": "416515709472",
    "sender": "SP3R4NKXMGW6YXA44X2ESZPKJNV25X4ZN7DPW0RXR",
    "recipient": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.alex-vault",
    "asset_identifier": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token::alex"
  },
  "ts": "2026-09-23T14:56:36.000Z",
  "canonical": true,
  "finalized": false
}
```

## The Reorg object

A fork the indexer rolled back. Rows inside `orphaned_range` were replaced; undo anything you committed from them.

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Reorg id. |
| `detected_at` | string · date-time | When the indexer saw the fork. |
| `fork_point_height` | integer | First block height the new fork replaced. Undo rows at or above it. |
| `old_index_block_hash` | string · nullable | Index block hash of the abandoned tip. |
| `new_index_block_hash` | string · nullable | Index block hash of the winning tip. |
| `orphaned_range` | object | First and last cursor of the rows that were replaced. |
| `new_canonical_tip` | string | Cursor of the new canonical tip. |

```json
{
  "id": "3f16c2c5-9551-4ae9-adb0-923668e5c65e",
  "detected_at": "2026-09-15T15:39:38.129Z",
  "fork_point_height": 8996511,
  "old_index_block_hash": "0x45ba0195071488eda13afa66d8ce8612558a739f781be5a032d96332797c4174",
  "new_index_block_hash": "0xa686ff58e7c5ccb26c850a7eaffa7e7274fcbc081c706344eb6f1e1446023df0",
  "orphaned_range": {
    "from": "8996511:0",
    "to": "8996512:2701"
  },
  "new_canonical_tip": "8996511:0"
}
```

## The Streams canonical block object

The canonical Stacks block at a height.

| Field | Type | Description |
| --- | --- | --- |
| `block_height` | integer | Stacks block height. |
| `block_hash` | string | Stacks block hash. |
| `burn_block_height` | integer | Bitcoin block it anchors to. |
| `burn_block_hash` | string · nullable | That Bitcoin block's hash. |
| `is_canonical` | boolean | Always `true`. |

```json
{
  "block_height": 9048876,
  "block_hash": "0xba0676b375e8d5fc1495a02847cc3cc32b5f7c2113a5e73999d94ee8d75ebde4",
  "burn_block_height": 968284,
  "burn_block_hash": "0x00000000000000000001b1cadea50180db28922325872c9f16ca19edda3f8bae",
  "is_canonical": true
}
```

## The Streams tip object

The newest canonical block and how far back this caller can read.

| Field | Type | Description |
| --- | --- | --- |
| `block_height` | integer | Highest canonical Stacks block this instance has indexed. |
| `block_hash` | string | That block's hash. |
| `burn_block_height` | integer | The Bitcoin block it anchors to. |
| `finalized_height` | integer | Highest Stacks block whose Bitcoin anchor has 6 confirmations. Rows at or below it are immutable. |
| `lag_seconds` | integer | Seconds since the tip block was produced. |
| `oldest_seekable_height` | integer · nullable | Always `null` — every account reads full history. Rows past the monthly allowance are a paid read, not a blocked one. |
| `oldest_cursor` | string · nullable | Always `null` — see `oldest_seekable_height`. |

```json
{
  "block_height": 9048928,
  "block_hash": "0xe79ae38b78b37ad1f5d16c7afd04b4e202efe8d983d981803b785ca64257933f",
  "burn_block_height": 968284,
  "finalized_height": 9048671,
  "lag_seconds": 4,
  "oldest_seekable_height": null,
  "oldest_cursor": null
}
```

## Streams discovery

`GET /v1/streams`

Lists the Streams routes, the event types and filters `/v1/streams/events` accepts, and the cursor format. Served ahead of Streams auth, so it never needs a credential.

### Returns

One `StreamsDiscovery` object.

### Responses

- `200` Route and filter catalog

### Example

```bash
curl https://api.secondlayer.tools/v1/streams \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "routes": [
    {
      "path": "/v1/streams/events",
      "method": "GET",
      "description": "Raw event firehose. Cursor-paginated. Returns events[], next_cursor, tip, reorgs[].",
      "event_types": [
        "stx_transfer",
        "stx_mint",
        "stx_burn",
        "stx_lock",
        "ft_transfer",
        "ft_mint",
        "ft_burn",
        "nft_transfer",
        "nft_mint",
        "nft_burn",
        "print"
      ],
      "filters": [
        {
          "name": "types",
          "type": "event_type[]",
          "description": "Event types to include"
        },
        {
          "name": "contract_id",
          "type": "principal | comma-list"
        },
        {
          "name": "limit",
          "type": "number (max 1000)"
        }
      ],
      "auth": "bearer required, metered per row"
    },
    {
      "path": "/v1/streams/tip",
      "method": "GET",
      "description": "Current chain tip: { block_height, block_hash, burn_block_height, finalized_height, lag_seconds }."
    }
  ],
  "cursor": {
    "format": "<block_height>:<event_index>",
    "semantics": "opaque resume token; pass back unchanged to continue. Equals last event's cursor (inclusive on output, exclusive on input)."
  },
  "reorgs_shape": {
    "detected_at": "ISO 8601",
    "new_canonical_tip": "<block_height>:<event_index>",
    "new_canonical_height": "number",
    "new_canonical_event_index": "number"
  }
}
```

## Raw event firehose

`GET /v1/streams/events`

Canonical chain events in chain order: block height, then transaction position, then event position. Pages are capped at the tip minus a 2-block reorg margin. With no cursor or `from_height`, the read starts 1000 blocks behind the tip. An empty filtered page still returns a `next_cursor` past `to_height`, so a consumer never stalls. Pages at or below `tip.finalized_height` are immutable: they carry an `ETag` and answer `If-None-Match` with 304.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `chain` | enum | `stacks` (default) is the existing Stacks feed, byte-identical to before. `bitcoin` serves Runes events (rune_etch, rune_mint, rune_transfer, rune_burn — see the RuneStreamsEvent schema) on their own cursor space: a bitcoin cursor only resumes with chain=bitcoin. Stacks-only filters (contract_id, sender, recipient, asset_identifier, filters, clock) 400 on chain=bitcoin; rune and address are bitcoin-only. Values: `stacks`, `bitcoin`. |
| `types` | array of enum | Comma-separated event types to include. Default: all of them. |
| `event_type` | enum | One event type, as an alias for `types`. Mutually exclusive with `types`; a set must use `types`. Values: `stx_transfer`, `stx_mint`, `stx_burn`, `stx_lock`, `ft_transfer`, `ft_mint`, `ft_burn`, `nft_transfer`, `nft_mint`, `nft_burn`, `print`. |
| `not_types` | array of enum | Comma-separated event types to exclude, applied after `types`. |
| `contract_id` | string | Contract principal, or a comma-separated set. Matches print events by the emitting contract and ft/nft events by the asset's contract. Other event types never match. |
| `sender` | string | Principal, or a comma-separated set, matched against the payload's `sender`. Event types without a sender never match. |
| `recipient` | string | Principal, or a comma-separated set, matched against the payload's `recipient`. |
| `asset_identifier` | string | Exact `<contract>::<asset>` match on the payload. |
| `filters` | string | JSON object of up to 8 labelled filter groups: `{ "<label>": { types?, contractId?, sender?, recipient?, assetIdentifier? } }`. Each value is a string or array of strings. Groups OR together, fields inside a group AND, and the flat filters above still apply to the whole scan. Each event lists the labels it matched in `matched`. Labels are letters, digits, `-` and `_`, 32 characters at most. |
| `rune` | string | chain=bitcoin only. A RuneRef: an id (`840000:3`) or a name, spacers/case ignored (`DOG•GO•TO•THE•MOON`, `dog.go.to.the.moon`, `doggotothemoon`). |
| `address` | string | chain=bitcoin only. A mainnet address. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Events per page. Default 100. Values above 1000 are clamped to 1000. |
| `cursor` | string | `<block_height>:<event_index>` from a previous page's `next_cursor`. Resumes strictly after it. Mutually exclusive with `from_cursor` and `from_height`. |
| `from_cursor` | string | Same as `cursor`. `0:0` starts at genesis. |
| `from_height` | integer | First block height to read. Mutually exclusive with a cursor. With neither, the read starts 1000 blocks behind the tip. |
| `to_height` | integer | Last block height to read, inclusive. Always clamped to the tip minus a 2-block reorg margin, which is also the default. |

### Returns

A cursor envelope; `events` holds `StreamsEvent` objects.

### Responses

- `200` A page of events, the cursor to resume from, the tip it was read against, and any reorgs that touched it
- `304` Finalized page unchanged since the `ETag` sent in `If-None-Match`
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited on the hosted API (50/s per IP without a key, 10/s on a free key). Self-hosted instances do not throttle Streams. Retry after `Retry-After` seconds
- `503` No canonical block indexed yet (`CHAIN_DATA_UNAVAILABLE`): wait for the indexer to ingest its first block. With `chain=bitcoin`, the Bitcoin database is configured but unreachable (`BITCOIN_DATA_UNAVAILABLE`): retry shortly

### Example

```bash
curl -G https://api.secondlayer.tools/v1/streams/events \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d contract_id=SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token \
  -d asset_identifier=SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token::alex \
  -d filters={"alex":{"types":["ft_transfer"],"contractId":"SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token"},"stx":{"types":"stx_transfer"}}
```

```json
{
  "events": [
    {
      "cursor": "9048876:3",
      "block_height": 9048876,
      "block_hash": "0xba0676b375e8d5fc1495a02847cc3cc32b5f7c2113a5e73999d94ee8d75ebde4",
      "burn_block_height": 968284,
      "tx_id": "0xb9edea3443a9adb603d856bfed2d1e630146bed128b07aaaa259dc97e3eae2ba",
      "tx_index": 2,
      "event_index": 3,
      "event_type": "ft_transfer",
      "contract_id": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token",
      "payload": {
        "amount": "416515709472",
        "sender": "SP3R4NKXMGW6YXA44X2ESZPKJNV25X4ZN7DPW0RXR",
        "recipient": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.alex-vault",
        "asset_identifier": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token::alex"
      },
      "ts": "2026-09-23T14:56:36.000Z",
      "canonical": true,
      "finalized": false
    }
  ],
  "next_cursor": "7960000:42",
  "tip": {
    "block_height": 9048690,
    "finalized_height": 9048457,
    "lag_seconds": 4,
    "source_block_height": 9048692
  },
  "reorgs": [
    {
      "id": "3f16c2c5-9551-4ae9-adb0-923668e5c65e",
      "detected_at": "2026-09-15T15:39:38.129Z",
      "fork_point_height": 8996511,
      "old_index_block_hash": "0x45ba0195071488eda13afa66d8ce8612558a739f781be5a032d96332797c4174",
      "new_index_block_hash": "0xa686ff58e7c5ccb26c850a7eaffa7e7274fcbc081c706344eb6f1e1446023df0",
      "orphaned_range": {
        "from": "8996511:0",
        "to": "8996512:2701"
      },
      "new_canonical_tip": "8996511:0"
    }
  ]
}
```

## Tail the event firehose over SSE

`GET /v1/streams/events/stream`

Server-sent events over the same read as `/v1/streams/events`, with the same parameters. The server polls every 1.5 seconds by default (`STREAMS_SSE_POLL_MS`) and writes one frame per event: `id` is the event's cursor and `data` is `{ "event": <StreamsEvent> }`, or `{ "event", "sig", "key_id" }` with an ed25519 signature over the event's JSON when the instance has a signing key. After 20 seconds with no events it writes `event: ping` with empty data; ignore it. With no `cursor`, `from_cursor` or `from_height`, the tail starts at the current tip minus the reorg margin. `Last-Event-ID` is not read; to resume after a disconnect, reconnect with `from_cursor` set to the last `id` you processed.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `chain` | enum | `stacks` (default) is the existing Stacks feed, byte-identical to before. `bitcoin` serves Runes events (rune_etch, rune_mint, rune_transfer, rune_burn — see the RuneStreamsEvent schema) on their own cursor space: a bitcoin cursor only resumes with chain=bitcoin. Stacks-only filters (contract_id, sender, recipient, asset_identifier, filters, clock) 400 on chain=bitcoin; rune and address are bitcoin-only. Values: `stacks`, `bitcoin`. |
| `types` | array of enum | Comma-separated event types to include. Default: all of them. |
| `event_type` | enum | One event type, as an alias for `types`. Mutually exclusive with `types`; a set must use `types`. Values: `stx_transfer`, `stx_mint`, `stx_burn`, `stx_lock`, `ft_transfer`, `ft_mint`, `ft_burn`, `nft_transfer`, `nft_mint`, `nft_burn`, `print`. |
| `not_types` | array of enum | Comma-separated event types to exclude, applied after `types`. |
| `contract_id` | string | Contract principal, or a comma-separated set. Matches print events by the emitting contract and ft/nft events by the asset's contract. Other event types never match. |
| `sender` | string | Principal, or a comma-separated set, matched against the payload's `sender`. Event types without a sender never match. |
| `recipient` | string | Principal, or a comma-separated set, matched against the payload's `recipient`. |
| `asset_identifier` | string | Exact `<contract>::<asset>` match on the payload. |
| `filters` | string | JSON object of up to 8 labelled filter groups: `{ "<label>": { types?, contractId?, sender?, recipient?, assetIdentifier? } }`. Each value is a string or array of strings. Groups OR together, fields inside a group AND, and the flat filters above still apply to the whole scan. Each event lists the labels it matched in `matched`. Labels are letters, digits, `-` and `_`, 32 characters at most. |
| `rune` | string | chain=bitcoin only. A RuneRef: an id (`840000:3`) or a name, spacers/case ignored (`DOG•GO•TO•THE•MOON`, `dog.go.to.the.moon`, `doggotothemoon`). |
| `address` | string | chain=bitcoin only. A mainnet address. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Most events written per poll. Default 100. Values above 1000 are clamped to 1000. |
| `cursor` | string | `<block_height>:<event_index>` from a previous page's `next_cursor`. Resumes strictly after it. Mutually exclusive with `from_cursor` and `from_height`. |
| `from_cursor` | string | Same as `cursor`. `0:0` starts at genesis. |
| `from_height` | integer | First block height to read. Mutually exclusive with a cursor. With neither, the read starts 1000 blocks behind the tip. |
| `to_height` | integer | Last block height to read, inclusive. Always clamped to the tip minus a 2-block reorg margin, which is also the default. |

### Responses

- `200` An open `text/event-stream`. Frames: `id: <cursor>` plus `data: {"event":{...}}` per event, and `event: ping` after 20 seconds idle
- `400` An unknown query parameter. Other bad values close the stream without a frame
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited on the hosted API (50/s per IP without a key, 10/s on a free key). Self-hosted instances do not throttle Streams. Retry after `Retry-After` seconds

### Example

```bash
curl -G https://api.secondlayer.tools/v1/streams/events/stream \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d contract_id=SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token \
  -d asset_identifier=SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token::alex \
  -d filters={"alex":{"types":["ft_transfer"],"contractId":"SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.age000-governance-token"},"stx":{"types":"stx_transfer"}}
```

## Chain reorg history

`GET /v1/streams/reorgs`

Reorgs the indexer recorded, oldest first. Page forward by passing `next_since` back as `since`.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `chain` | enum | `stacks` (default) is the existing Stacks feed, byte-identical to before. `bitcoin` serves Runes events (rune_etch, rune_mint, rune_transfer, rune_burn — see the RuneStreamsEvent schema) on their own cursor space: a bitcoin cursor only resumes with chain=bitcoin. Stacks-only filters (contract_id, sender, recipient, asset_identifier, filters, clock) 400 on chain=bitcoin; rune and address are bitcoin-only. Values: `stacks`, `bitcoin`. |
| `since` | string, required | Where to start. An ISO timestamp returns reorgs detected after it. A `<block_height>:<event_index>` cursor returns reorgs whose orphaned range ends at or after it. A `next_since` value (`<detected_at>~<id>`) resumes after the last reorg delivered. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer · 1 to 1000 | Reorgs per page. Default 100. Values above 1000 are clamped to 1000. |

### Returns

A cursor envelope; `reorgs` holds `Reorg` objects.

### Responses

- `200` Reorgs, oldest first
- `400` `since` is missing or is not a timestamp, cursor or `next_since` value, or `limit` is not a positive integer
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited on the hosted API (50/s per IP without a key, 10/s on a free key). Self-hosted instances do not throttle Streams. Retry after `Retry-After` seconds
- `503` The Bitcoin (Runes) database is configured but unreachable (`BITCOIN_DATA_UNAVAILABLE`). Retry shortly

### Example

```bash
curl -G https://api.secondlayer.tools/v1/streams/reorgs \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d since=2026-09-01T00:00:00Z
```

```json
{
  "reorgs": [
    {
      "id": "3f16c2c5-9551-4ae9-adb0-923668e5c65e",
      "detected_at": "2026-09-15T15:39:38.129468Z",
      "fork_point_height": 8996511,
      "old_index_block_hash": "0x45ba0195071488eda13afa66d8ce8612558a739f781be5a032d96332797c4174",
      "new_index_block_hash": "0xa686ff58e7c5ccb26c850a7eaffa7e7274fcbc081c706344eb6f1e1446023df0",
      "orphaned_range": {
        "from": "8996511:0",
        "to": "8996512:2701"
      },
      "new_canonical_tip": "8996511:0"
    }
  ],
  "next_since": "2026-09-15T15:39:38.129468Z~3f16c2c5-9551-4ae9-adb0-923668e5c65e"
}
```

## Canonical block by height

`GET /v1/streams/canonical/{height}`

The canonical Stacks block at one height and the Bitcoin block it anchors to. Finalized heights are cached for good and answer `If-None-Match` with 304; the `ETag` is the block hash.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `height` | integer, required | Block height (Stacks by default, Bitcoin with chain=bitcoin). |

### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `chain` | enum | `stacks` (default) is the existing Stacks feed, byte-identical to before. `bitcoin` serves Runes events (rune_etch, rune_mint, rune_transfer, rune_burn — see the RuneStreamsEvent schema) on their own cursor space: a bitcoin cursor only resumes with chain=bitcoin. Stacks-only filters (contract_id, sender, recipient, asset_identifier, filters, clock) 400 on chain=bitcoin; rune and address are bitcoin-only. Values: `stacks`, `bitcoin`. |

### Returns

One `StreamsCanonicalBlock` object.

### Responses

- `200` The canonical block. chain=bitcoin returns a StreamsBitcoinCanonicalBlock (btc block hash, no burn-chain fields) instead.
- `304` Block unchanged since the `ETag` sent in `If-None-Match`
- `400` `height` is not a non-negative integer
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No canonical block at that height yet
- `429` Rate limited on the hosted API (50/s per IP without a key, 10/s on a free key). Self-hosted instances do not throttle Streams. Retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/streams/canonical/9048876 \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "block_height": 9048876,
  "block_hash": "0xba0676b375e8d5fc1495a02847cc3cc32b5f7c2113a5e73999d94ee8d75ebde4",
  "burn_block_height": 968284,
  "burn_block_hash": "0x00000000000000000001b1cadea50180db28922325872c9f16ca19edda3f8bae",
  "is_canonical": true
}
```

## Current chain tip

`GET /v1/streams/tip`

The newest canonical block this instance has indexed, the finality boundary, and how far back this caller can seek. Never cached.

### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `chain` | enum | `stacks` (default) is the existing Stacks feed, byte-identical to before. `bitcoin` serves Runes events (rune_etch, rune_mint, rune_transfer, rune_burn — see the RuneStreamsEvent schema) on their own cursor space: a bitcoin cursor only resumes with chain=bitcoin. Stacks-only filters (contract_id, sender, recipient, asset_identifier, filters, clock) 400 on chain=bitcoin; rune and address are bitcoin-only. Values: `stacks`, `bitcoin`. |

### Returns

One `StreamsTip` object.

### Responses

- `200` The tip. chain=bitcoin returns a StreamsBitcoinTip (no burn-chain or retention fields) instead.
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Rate limited on the hosted API (50/s per IP without a key, 10/s on a free key). Self-hosted instances do not throttle Streams. Retry after `Retry-After` seconds
- `503` No canonical block indexed yet (`CHAIN_DATA_UNAVAILABLE`): wait for the indexer to ingest its first block. With `chain=bitcoin`, the Bitcoin database is configured but unreachable (`BITCOIN_DATA_UNAVAILABLE`): retry shortly

### Example

```bash
curl https://api.secondlayer.tools/v1/streams/tip \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "block_height": 9048928,
  "block_hash": "0xe79ae38b78b37ad1f5d16c7afd04b4e202efe8d983d981803b785ca64257933f",
  "burn_block_height": 968284,
  "finalized_height": 9048671,
  "lag_seconds": 4,
  "oldest_seekable_height": null,
  "oldest_cursor": null
}
```

---

# proofs

Chain proofs to check Index rows without trusting this API: state witnesses, signed blocks, MARF proofs, burn-block preimages, Bitcoin headers. Free.

## The Burn block proof object

A sortition's consensus-hash preimage and the Bitcoin block it commits to.

| Field | Type | Description |
| --- | --- | --- |
| `consensus_hash` | string | The consensus hash asked about, hex. |
| `burn_height` | integer | Bitcoin height of the sortition. |
| `bitcoin_block_hash` | string | Bitcoin block hash, display order. Equals preimage bytes 4..36. |
| `preimage` | string | Hex bytes whose RIPEMD160(SHA256(·)) is `consensus_hash`. |

```json
{
  "consensus_hash": "e55512fc4a1fd7b3c6b0bcd1b0b2d28a7e3f9c11",
  "burn_height": 970269,
  "bitcoin_block_hash": "00000000000000000001b6b3c5a9e2b5d8f8f1c0a7e4d3b2a1908f7e6d5c4b3a",
  "preimage": "0100000000000000000000000001b6b3c5a9e2b5d8f8f1c0a7e4d3b2a1908f7e6d5c4b3a…"
}
```

## The Bitcoin headers object

Consecutive raw Bitcoin block headers.

| Field | Type | Description |
| --- | --- | --- |
| `from` | integer | Bitcoin height of the first header. |
| `headers` | array of string | 80-byte headers, hex, in height order. |

```json
{
  "from": 0,
  "headers": [
    "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c"
  ]
}
```

## The Bitcoin transaction proof object

A Bitcoin transaction, the merkle branch to its block's root, and that block's header.

| Field | Type | Description |
| --- | --- | --- |
| `txid` | string | The txid, display order, hex. |
| `block_hash` | string | Hash of `header`, display order, hex. |
| `block_height` | integer | Bitcoin height of the confirming block. |
| `header` | string | The block's 80-byte header, hex. |
| `raw_tx` | string | The serialized transaction, witness included, hex. |
| `tx_index` | integer | Position of the transaction in the block. |
| `tx_count` | integer | Number of transactions in the block. |
| `siblings` | array of string | Merkle branch from the leaf up, internal byte order, hex: `ceil(log2(tx_count))` hashes. |

```json
{
  "txid": "f80a188d012dbb733748db003b987cc98fc4a636cbd071734decb298c1bad80b",
  "block_hash": "0000000000000000000179d46b321695682e26183ee0b1257bb3ea17483be0f9",
  "block_height": 968461,
  "header": "0000003…",
  "raw_tx": "02000000000101…",
  "tx_index": 2156,
  "tx_count": 4371,
  "siblings": [
    "4c1a…"
  ]
}
```

## The Block state writes object

One block's MARF writes, whole, in write order.

| Field | Type | Description |
| --- | --- | --- |
| `block_height` | integer | Stacks block height. |
| `state_writes` | array of object | Every write of the block, ordinal 0 first. |

```json
{
  "block_height": 9137005,
  "state_writes": [
    {
      "ordinal": 0,
      "tx_index": 0,
      "key": "vm::SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-vault-v2-01::1::paused",
      "value_hex": "3033"
    }
  ]
}
```

## The MARF proof object

A MARF value and its inclusion proof, as the node returns them.

| Field | Type | Description |
| --- | --- | --- |
| `data` | string | The stored value, hex (`0x`-prefixed). For keys with no stored value (`__MARF_*`), the raw 40-byte leaf value instead. |
| `proof` | string | Serialized MARF proof, hex (`0x`-prefixed). Proof bytes are malleable: hash the parsed content, never use them as an id. |

```json
{
  "data": "0x0100000000000000000000000000000064",
  "proof": "0x00…"
}
```

## State witness for a block

`GET /v1/proofs/witness/{index_block_hash}`

The block's MARF state witness (wire v3) from the proof sidecar: the trie nodes the block wrote, ancestor roots, and the names of every write. Recompute the root and compare it with the header's `state_index_root`; every changed leaf is a named write, a carried value, or an internal key, so a hidden write fails. Extracted on demand, so it has its own lower rate limit. Free and keyless: never metered, no account needed. Hosted limits: 10 requests per second per IP (1 for witnesses), or 20 (2) per account with a key. Bytes are passed through unchanged; verify them, do not trust them.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `index_block_hash` | string, required | The block's `index_block_hash`: 64 hex characters, `0x` optional. |

### Responses

- `200` The witness bytes. Immutable.
- `400` A path or query parameter is malformed (`VALIDATION_ERROR`): hashes must be exact-length hex, heights non-negative integers
- `401` This bind requires the instance token, and it was missing or wrong
- `404` The sidecar has no block with that `index_block_hash`
- `429` Rate limited; retry after `Retry-After` seconds
- `502` The node or proof sidecar could not be reached, or answered with an unexpected status (`PROOF_SOURCE_ERROR`)
- `503` `PROOFS_UNAVAILABLE`: this instance has no proof sidecar (`PROOF_SIDECAR_URL` unset). `PROOF_SOURCE_BUSY`: the sidecar is busy; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/proofs/witness/990152a894e12288960c8b68e5790ad7994f9d261e9ad8adc13c72e76f914339 \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

## Burn block behind a consensus hash

`GET /v1/proofs/burn/{consensus_hash}`

The sortition preimage that hashes to a block header's `consensus_hash` (RIPEMD160(SHA256(preimage))). Bytes 4..36 of the preimage are the Bitcoin block hash, which pins the header's burn height against Bitcoin proof of work. Free and keyless: never metered, no account needed. Hosted limits: 10 requests per second per IP (1 for witnesses), or 20 (2) per account with a key. Bytes are passed through unchanged; verify them, do not trust them.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `consensus_hash` | string, required | The header's `consensus_hash`: 40 hex characters, `0x` optional. |

### Returns

One `ProofBurnBlock` object.

### Responses

- `200` The preimage and the burn block it names
- `400` A path or query parameter is malformed (`VALIDATION_ERROR`): hashes must be exact-length hex, heights non-negative integers
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No sortition with that consensus hash
- `429` Rate limited; retry after `Retry-After` seconds
- `502` The node or proof sidecar could not be reached, or answered with an unexpected status (`PROOF_SOURCE_ERROR`)
- `503` `PROOFS_UNAVAILABLE`: this instance has no proof sidecar (`PROOF_SIDECAR_URL` unset). `PROOF_SOURCE_BUSY`: the sidecar is busy; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/proofs/burn/e55512fc4a1fd7b3c6b0bcd1b0b2d28a7e3f9c11 \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "consensus_hash": "e55512fc4a1fd7b3c6b0bcd1b0b2d28a7e3f9c11",
  "burn_height": 970269,
  "bitcoin_block_hash": "00000000000000000001b6b3c5a9e2b5d8f8f1c0a7e4d3b2a1908f7e6d5c4b3a",
  "preimage": "0100000000000000000000000001b6b3c5a9e2b5d8f8f1c0a7e4d3b2a1908f7e6d5c4b3a…"
}
```

## Bitcoin block headers

`GET /v1/proofs/bitcoin-headers`

Raw 80-byte Bitcoin headers from `from`, at most 2016 (one difficulty period) per call. Check proof of work and linkage yourself from a checkpoint you trust. Free and keyless: never metered, no account needed. Hosted limits: 10 requests per second per IP (1 for witnesses), or 20 (2) per account with a key. Bytes are passed through unchanged; verify them, do not trust them.

### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `from` | integer, required | First Bitcoin height to return. |
| `count` | integer · 1 to 2016, required | Number of headers, 1 to 2016. |

### Returns

One `BitcoinHeaders` object.

### Responses

- `200` Consecutive headers starting at `from`
- `400` A path or query parameter is malformed (`VALIDATION_ERROR`): hashes must be exact-length hex, heights non-negative integers
- `401` This bind requires the instance token, and it was missing or wrong
- `404` The sidecar has no header at `from`
- `429` Rate limited; retry after `Retry-After` seconds
- `502` The node or proof sidecar could not be reached, or answered with an unexpected status (`PROOF_SOURCE_ERROR`)
- `503` `PROOFS_UNAVAILABLE`: this instance has no proof sidecar (`PROOF_SIDECAR_URL` unset). `PROOF_SOURCE_BUSY`: the sidecar is busy; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/proofs/bitcoin-headers \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "from": 0,
  "headers": [
    "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c"
  ]
}
```

## Bitcoin transaction proof

`GET /v1/proofs/bitcoin-tx/{txid}`

A confirmed Bitcoin transaction with its merkle branch (internal byte order) and block header, from the instance's own Bitcoin node. Recompute the txid from `raw_tx`, fold the branch to the header's merkle root, and check the header against a Bitcoin chain you trust, such as a checkpoint plus proof of work. Short-cached: a shallow block can reorg. Free and keyless: never metered, no account needed. Hosted limits: 10 requests per second per IP (1 for witnesses), or 20 (2) per account with a key. Bytes are passed through unchanged; verify them, do not trust them.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `txid` | string, required | The Bitcoin txid, display order: 64 hex characters, `0x` optional. |

### Returns

One `BitcoinTxProof` object.

### Responses

- `200` The raw transaction, merkle branch and block header
- `400` A path or query parameter is malformed (`VALIDATION_ERROR`): hashes must be exact-length hex, heights non-negative integers
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No confirmed transaction with that txid
- `429` Rate limited; retry after `Retry-After` seconds
- `502` The node or proof sidecar could not be reached, or answered with an unexpected status (`PROOF_SOURCE_ERROR`)
- `503` `PROOFS_UNAVAILABLE`: this instance has no Bitcoin node (`BITCOIN_RPC_URL` unset)

### Example

```bash
curl https://api.secondlayer.tools/v1/proofs/bitcoin-tx/f80a188d012dbb733748db003b987cc98fc4a636cbd071734decb298c1bad80b \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "txid": "f80a188d012dbb733748db003b987cc98fc4a636cbd071734decb298c1bad80b",
  "block_hash": "0000000000000000000179d46b321695682e26183ee0b1257bb3ea17483be0f9",
  "block_height": 968461,
  "header": "0000003…",
  "raw_tx": "02000000000101…",
  "tx_index": 2156,
  "tx_count": 4371,
  "siblings": [
    "4c1a…"
  ]
}
```

## Signed block by id

`GET /v1/proofs/block/{index_block_hash}`

The consensus-serialized Nakamoto block from the Stacks node (`/v3/blocks/{id}`): header with signer signatures, then transactions. Recompute the block id from the header and check the signatures against the cycle's signer set. Free and keyless: never metered, no account needed. Hosted limits: 10 requests per second per IP (1 for witnesses), or 20 (2) per account with a key. Bytes are passed through unchanged; verify them, do not trust them.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `index_block_hash` | string, required | The block's `index_block_hash`: 64 hex characters, `0x` optional. |

### Responses

- `200` The block bytes. Immutable.
- `400` A path or query parameter is malformed (`VALIDATION_ERROR`): hashes must be exact-length hex, heights non-negative integers
- `401` This bind requires the instance token, and it was missing or wrong
- `404` The node has no block with that `index_block_hash`
- `429` Rate limited; retry after `Retry-After` seconds
- `502` The node or proof sidecar could not be reached, or answered with an unexpected status (`PROOF_SOURCE_ERROR`)

### Example

```bash
curl https://api.secondlayer.tools/v1/proofs/block/990152a894e12288960c8b68e5790ad7994f9d261e9ad8adc13c72e76f914339 \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

## Signed block by height

`GET /v1/proofs/block/height/{height}`

The node's block at a Stacks height (`/v3/blocks/height/{height}`), same bytes as the by-id route. Near the tip the block at a height can change, so this is short-cached; prefer the by-id route once you know the id. Free and keyless: never metered, no account needed. Hosted limits: 10 requests per second per IP (1 for witnesses), or 20 (2) per account with a key. Bytes are passed through unchanged; verify them, do not trust them.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `height` | integer, required | Stacks block height. |

### Responses

- `200` The block bytes.
- `400` A path or query parameter is malformed (`VALIDATION_ERROR`): hashes must be exact-length hex, heights non-negative integers
- `401` This bind requires the instance token, and it was missing or wrong
- `404` The node has no block at that height
- `429` Rate limited; retry after `Retry-After` seconds
- `502` The node or proof sidecar could not be reached, or answered with an unexpected status (`PROOF_SOURCE_ERROR`)

### Example

```bash
curl https://api.secondlayer.tools/v1/proofs/block/height/9048502 \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

## Epoch 2.x block header by id

`GET /v1/proofs/epoch2-header/{index_block_hash}`

A pre-Nakamoto block's header from the Stacks node (`/v2/headers/1?tip={id}`), as the node's one-element array: `header` (consensus-serialized StacksBlockHeader, hex), `consensus_hash` and `parent_block_id`. The id is sha512/256(sha512/256(header) || consensus_hash): recompute it, then check state against the header's `state_index_root`. Nakamoto blocks are a 404 here; use the block route. Free and keyless: never metered, no account needed. Hosted limits: 10 requests per second per IP (1 for witnesses), or 20 (2) per account with a key. Bytes are passed through unchanged; verify them, do not trust them.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `index_block_hash` | string, required | The block's `index_block_hash`: 64 hex characters, `0x` optional. |

### Responses

- `200` The block's header and consensus hash
- `400` A path or query parameter is malformed (`VALIDATION_ERROR`): hashes must be exact-length hex, heights non-negative integers
- `401` This bind requires the instance token, and it was missing or wrong
- `404` The node has no epoch 2.x block with that `index_block_hash`
- `429` Rate limited; retry after `Retry-After` seconds
- `502` The node or proof sidecar could not be reached, or answered with an unexpected status (`PROOF_SOURCE_ERROR`)

### Example

```bash
curl https://api.secondlayer.tools/v1/proofs/epoch2-header/990152a894e12288960c8b68e5790ad7994f9d261e9ad8adc13c72e76f914339 \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
[
  {
    "consensus_hash": "0898651bac6711e8c7679d76bdf5ffb11e9d6f7c",
    "header": "070000004b4c5f4fb400000000000249f0…",
    "parent_block_id": "d281730546550e66a5b734c49cba0867c3e379c6d39f7e819be44c64a9c03b9f"
  }
]
```

## State writes of a block

`GET /v1/proofs/writes/{height}`

Every MARF write the canonical block at `height` committed, in the node's write order, from this instance's indexed `state_writes`: the names a verifier needs to label each written leaf of the block's witness. A write the witness does not hold, or a leaf no write names, fails that check, so the rows are checked, not trusted. 404 when the instance has no writes for the block (its node did not subscribe to `state_writes` at that height); verifiers then report the diff unnamed. Near the tip the block at a height can change, so this is short-cached. Free and keyless: never metered, no account needed. Hosted limits: 10 requests per second per IP (1 for witnesses), or 20 (2) per account with a key. Bytes are passed through unchanged; verify them, do not trust them.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `height` | integer, required | Stacks block height. |

### Returns

One `BlockStateWrites` object.

### Responses

- `200` The block's writes
- `400` A path or query parameter is malformed (`VALIDATION_ERROR`): hashes must be exact-length hex, heights non-negative integers
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No `state_writes` for a canonical block at that height
- `429` Rate limited; retry after `Retry-After` seconds
- `502` The node or proof sidecar could not be reached, or answered with an unexpected status (`PROOF_SOURCE_ERROR`)

### Example

```bash
curl https://api.secondlayer.tools/v1/proofs/writes/9137005 \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "block_height": 9137005,
  "state_writes": [
    {
      "ordinal": 0,
      "tx_index": 0,
      "key": "vm::SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-vault-v2-01::1::paused",
      "value_hex": "3033"
    }
  ]
}
```

## MARF inclusion proof

`GET /v1/proofs/marf/{path}`

A value and its MARF inclusion proof as of block `tip`, from the node (`/v2/clarity/marf/{path}?proof=1`). Keys the node cannot serve because they hold no stored value string, such as the MARF's own `__MARF_BLOCK_HEIGHT_TO_HASH::<height>`, are proven by the proof sidecar from the MARF itself: same proof bytes, and `data` is the raw 40-byte leaf value. Fold the proof to a root and compare it with `tip`'s `state_index_root`. Inclusion only: a missing key is a 404, not a proof of absence. Free and keyless: never metered, no account needed. Hosted limits: 10 requests per second per IP (1 for witnesses), or 20 (2) per account with a key. Bytes are passed through unchanged; verify them, do not trust them.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `path` | string, required | The MARF key hash: 64 hex characters, `0x` optional. |

### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `tip` | string, required | `index_block_hash` of the block to prove against, 64 hex characters, `0x` optional. Pinned, so the answer is immutable. |

### Returns

One `MarfProof` object.

### Responses

- `200` The value and its proof
- `400` A path or query parameter is malformed (`VALIDATION_ERROR`): hashes must be exact-length hex, heights non-negative integers
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No value at that path as of `tip`
- `429` Rate limited; retry after `Retry-After` seconds
- `502` The node or proof sidecar could not be reached, or answered with an unexpected status (`PROOF_SOURCE_ERROR`)

### Example

```bash
curl -G https://api.secondlayer.tools/v1/proofs/marf/5c8d9e1f6c1e0f4a2a6a7f0b0f5f1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d tip=990152a894e12288960c8b68e5790ad7994f9d261e9ad8adc13c72e76f914339
```

```json
{
  "data": "0x0100000000000000000000000000000064",
  "proof": "0x00…"
}
```

---

# bitcoin

Reads answered live from this instance's own Bitcoin node: whether an output is spent, and by what.

## The Bitcoin output spend object

One output's spend status. An unspent output carries only `txid`, `vout` and `spent`.

| Field | Type | Description |
| --- | --- | --- |
| `txid` | string | The output's transaction, display order, no `0x`. |
| `vout` | integer | The output's index. |
| `spent` | boolean | True once any transaction spends it, in the mempool or in a block. |
| `spending_txid` | string · nullable | The spending transaction. Null for a confirmed spend deeper than the blocks a lookup reads; `note` says so. |
| `spending_vin` | integer · nullable | The index of the spending input in that transaction. |
| `confirmed` | boolean | True when the spend is in a block of the active chain. |
| `mempool` | boolean | True when the spend is only in the mempool so far. |
| `block_height` | integer · nullable | Height of the block with the spend. Null in the mempool. |
| `block_hash` | string · nullable | Hash of the block with the spend. Null in the mempool. |
| `note` | string | Present when the answer is incomplete: why the spender is not named. |

```json
{
  "txid": "f80a188d012dbb733748db003b987cc98fc4a636cbd071734decb298c1bad80b",
  "vout": 0,
  "spent": true,
  "spending_txid": "3b7a6f0c2d9e1a4b5c8d7e6f0a1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3",
  "spending_vin": 0,
  "confirmed": true,
  "mempool": false,
  "block_height": 968470,
  "block_hash": "00000000000000000001c3b0d1a9f5e2e5b2a9c2d0f6c7e1b8a4d3c2b1a09f8e"
}
```

## Bitcoin output spend status

`GET /v1/bitcoin/outspend/{txid}/{vout}`

Whether one Bitcoin output is spent, and by which transaction, read live from this instance's Bitcoin node. A spend in the mempool shows on the next read, with `mempool: true`. A confirmed spend names its block. The spender of a confirmed spend is found by reading blocks back from the tip, up to 144 of them. A spend deeper than that answers `spent: true` with a null `spending_txid` and a `note`. Answers can change with the mempool and with reorgs, so they are cached for 2 seconds at most. Hosted: one row per answer, 20 requests per second per key. Self-host needs Bitcoin Core 24 or later with `txindex=1` at `BITCOIN_RPC_URL`.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `txid` | string, required | The txid of the transaction that created the output, display order: 64 hex characters, `0x` optional. |
| `vout` | integer, required | The output's index in that transaction, from 0. |

### Returns

One `BitcoinOutspend` object.

### Responses

- `200` The output's spend status
- `400` The txid is not 64 hex characters, or vout is not a non-negative integer (`VALIDATION_ERROR`)
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No transaction with that txid, no output at that index, or an `OP_RETURN` output, which can never be spent
- `429` Rate limited; retry after `Retry-After` seconds
- `502` The Bitcoin node could not be read, or lacks a method this needs (`BITCOIN_SOURCE_ERROR`)
- `503` `BITCOIN_UNAVAILABLE`: this instance has no Bitcoin node (`BITCOIN_RPC_URL` unset). `BITCOIN_BUSY`: block reads are at capacity; retry after `Retry-After` seconds

### Example

```bash
curl https://api.secondlayer.tools/v1/bitcoin/outspend/f80a188d012dbb733748db003b987cc98fc4a636cbd071734decb298c1bad80b/0 \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "txid": "f80a188d012dbb733748db003b987cc98fc4a636cbd071734decb298c1bad80b",
  "vout": 0,
  "spent": true,
  "spending_txid": "3b7a6f0c2d9e1a4b5c8d7e6f0a1b2c3d4e5f60718293a4b5c6d7e8f9a0b1c2d3",
  "spending_vin": 0,
  "confirmed": true,
  "mempool": false,
  "block_height": 968470,
  "block_hash": "00000000000000000001c3b0d1a9f5e2e5b2a9c2d0f6c7e1b8a4d3c2b1a09f8e"
}
```

---

# subgraphs

Reads over deployed subgraphs — rows, counts, aggregates, generated schema and docs

## The Subgraph directory object

| Field | Type | Description |
| --- | --- | --- |
| `subgraphs` | array of SubgraphSummary | Every subgraph you can read. Not paginated. |
| `tip` | object | The chain tip this list was read against. |
| `envelope` | object | Fixed reminder of the row-read shape and cursor rules, for agents reading the directory cold. |

```json
{
  "subgraphs": [
    {
      "name": "sbtc-token",
      "description": null,
      "status": "active",
      "version": "1.0.0",
      "created_at": "2026-09-01T12:00:00.000Z",
      "total_rows": 48138,
      "sources": [
        "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token"
      ],
      "last_processed_block": 9048931,
      "blocks_behind": 0,
      "tables": [
        "transfers"
      ],
      "url": "/v1/subgraphs/sbtc-token"
    }
  ],
  "tip": {
    "block_height": 9048931
  },
  "envelope": {
    "rows": "GET /v1/subgraphs/:name/:table → { rows, next_cursor, tip }",
    "cursor": "_id keyset by default, or ?_sort=<column>&_order=asc|desc for a composite keyset; pass ?cursor=<next_cursor> to resume"
  }
}
```

## The Subgraph object

Metadata for one subgraph.

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Subgraph name. |
| `description` | string · nullable | The definition's `description`, if it has one. |
| `version` | string | Deployed version. |
| `status` | string | Indexing state: `active` (following the chain), `reindexing`, `paused`, or `error`. |
| `created_at` | string · date-time | First deploy. |
| `sources` | array of string | Contracts the definition reads from: each source's `contractId`, and the contract half of each `assetIdentifier`. |
| `start_block` | integer | Block indexing starts from. 0 means genesis. |
| `tables` | object | Each table by name. |
| `tip` | SubgraphTip |  |
| `docs` | object | Generated docs for this subgraph. |

```json
{
  "name": "sbtc-token",
  "description": null,
  "version": "1.0.0",
  "status": "active",
  "created_at": "2026-09-01T12:00:00.000Z",
  "sources": [
    "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token"
  ],
  "start_block": 0,
  "tables": {
    "transfers": {
      "endpoint": "/v1/subgraphs/sbtc-token/transfers",
      "columns": [
        "sender",
        "recipient",
        "amount",
        "asset_identifier"
      ],
      "column_types": {
        "sender": "principal",
        "recipient": "principal",
        "amount": "uint",
        "asset_identifier": "text"
      }
    }
  },
  "tip": {
    "block_height": 9048931,
    "subgraph_height": 9048931,
    "blocks_behind": 0
  },
  "docs": {
    "openapi": "/v1/subgraphs/sbtc-token/openapi.json",
    "schema": "/v1/subgraphs/sbtc-token/schema.json",
    "markdown": "/v1/subgraphs/sbtc-token/docs.md"
  }
}
```

## The Subgraph OpenAPI document object

OpenAPI 3.1 document generated from the definition.

| Field | Type | Description |
| --- | --- | --- |
| `openapi` | string | Always `3.1.0`. |
| `info` | object | Title, the subgraph's version, and its description. |
| `servers` | array of object | The `server` param, or this request's origin. |
| `paths` | object | A list path and a count path per table. |
| `components` | object | A `<table>Row` schema per table. |
| `x-secondlayer-subgraph` | string | Subgraph name. |
| `x-secondlayer-version` | string | Deployed version. |
| `x-secondlayer-schema-hash` | string | sha256 of the definition's name, schema and sources. Changes only when they do. |
| `x-secondlayer-pin` | string | sha256 over the schema hash, bundled handler, startBlock, network and runtime. Absent for subgraphs deployed before pins existed. |
| `x-secondlayer-generated-at` | string · date-time | When this document was generated. |
| `x-secondlayer-sources` | object | The definition's `sources`, as written. |
| `x-secondlayer-tables` | array of string | Table names. |

```json
{
  "openapi": "3.1.0",
  "info": {
    "title": "sbtc-token Subgraph API",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "http://127.0.0.1:3800"
    }
  ],
  "paths": {
    "/v1/subgraphs/sbtc-token/transfers": {
      "get": {
        "summary": "Query sbtc-token.transfers",
        "operationId": "query_sbtc_token_transfers",
        "parameters": [
          {
            "name": "_limit",
            "in": "query",
            "required": false,
            "description": "Maximum rows to return.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 1000
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Resume token from next_cursor (keyset pagination on _id).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_order",
            "in": "query",
            "required": false,
            "description": "Sort direction (_id keyset).",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            }
          },
          {
            "name": "_fields",
            "in": "query",
            "required": false,
            "description": "Comma-separated columns to include.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender",
            "in": "query",
            "required": false,
            "description": "Filter sender by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender.neq",
            "in": "query",
            "required": false,
            "description": "Filter sender by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for sender.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient",
            "in": "query",
            "required": false,
            "description": "Filter recipient by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient.neq",
            "in": "query",
            "required": false,
            "description": "Filter recipient by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for recipient.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount",
            "in": "query",
            "required": false,
            "description": "Filter amount by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.neq",
            "in": "query",
            "required": false,
            "description": "Filter amount by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.gt",
            "in": "query",
            "required": false,
            "description": "Filter amount with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.gte",
            "in": "query",
            "required": false,
            "description": "Filter amount with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.lt",
            "in": "query",
            "required": false,
            "description": "Filter amount with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.lte",
            "in": "query",
            "required": false,
            "description": "Filter amount with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier",
            "in": "query",
            "required": false,
            "description": "Filter asset_identifier by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier.neq",
            "in": "query",
            "required": false,
            "description": "Filter asset_identifier by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for asset_identifier.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id",
            "in": "query",
            "required": false,
            "description": "Filter _id by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.neq",
            "in": "query",
            "required": false,
            "description": "Filter _id by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.gt",
            "in": "query",
            "required": false,
            "description": "Filter _id with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.gte",
            "in": "query",
            "required": false,
            "description": "Filter _id with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.lt",
            "in": "query",
            "required": false,
            "description": "Filter _id with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.lte",
            "in": "query",
            "required": false,
            "description": "Filter _id with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height",
            "in": "query",
            "required": false,
            "description": "Filter _block_height by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.neq",
            "in": "query",
            "required": false,
            "description": "Filter _block_height by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.gt",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.gte",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.lt",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.lte",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id",
            "in": "query",
            "required": false,
            "description": "Filter _tx_id by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id.neq",
            "in": "query",
            "required": false,
            "description": "Filter _tx_id by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for _tx_id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at",
            "in": "query",
            "required": false,
            "description": "Filter _created_at by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.neq",
            "in": "query",
            "required": false,
            "description": "Filter _created_at by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.gt",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.gte",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.lt",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.lte",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for _created_at.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rows returned from the subgraph table.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/transfersRow"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "tip": {
                      "type": "object",
                      "properties": {
                        "block_height": {
                          "type": "integer"
                        },
                        "subgraph_height": {
                          "type": "integer"
                        },
                        "blocks_behind": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/subgraphs/sbtc-token/transfers/count": {
      "get": {
        "summary": "Count sbtc-token.transfers",
        "operationId": "count_sbtc_token_transfers",
        "parameters": [
          {
            "name": "_limit",
            "in": "query",
            "required": false,
            "description": "Maximum rows to return.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 1000
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Resume token from next_cursor (keyset pagination on _id).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_order",
            "in": "query",
            "required": false,
            "description": "Sort direction (_id keyset).",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            }
          },
          {
            "name": "_fields",
            "in": "query",
            "required": false,
            "description": "Comma-separated columns to include.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender",
            "in": "query",
            "required": false,
            "description": "Filter sender by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender.neq",
            "in": "query",
            "required": false,
            "description": "Filter sender by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for sender.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient",
            "in": "query",
            "required": false,
            "description": "Filter recipient by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient.neq",
            "in": "query",
            "required": false,
            "description": "Filter recipient by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for recipient.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount",
            "in": "query",
            "required": false,
            "description": "Filter amount by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.neq",
            "in": "query",
            "required": false,
            "description": "Filter amount by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.gt",
            "in": "query",
            "required": false,
            "description": "Filter amount with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.gte",
            "in": "query",
            "required": false,
            "description": "Filter amount with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.lt",
            "in": "query",
            "required": false,
            "description": "Filter amount with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.lte",
            "in": "query",
            "required": false,
            "description": "Filter amount with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier",
            "in": "query",
            "required": false,
            "description": "Filter asset_identifier by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier.neq",
            "in": "query",
            "required": false,
            "description": "Filter asset_identifier by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for asset_identifier.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id",
            "in": "query",
            "required": false,
            "description": "Filter _id by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.neq",
            "in": "query",
            "required": false,
            "description": "Filter _id by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.gt",
            "in": "query",
            "required": false,
            "description": "Filter _id with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.gte",
            "in": "query",
            "required": false,
            "description": "Filter _id with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.lt",
            "in": "query",
            "required": false,
            "description": "Filter _id with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.lte",
            "in": "query",
            "required": false,
            "description": "Filter _id with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height",
            "in": "query",
            "required": false,
            "description": "Filter _block_height by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.neq",
            "in": "query",
            "required": false,
            "description": "Filter _block_height by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.gt",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.gte",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.lt",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.lte",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id",
            "in": "query",
            "required": false,
            "description": "Filter _tx_id by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id.neq",
            "in": "query",
            "required": false,
            "description": "Filter _tx_id by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for _tx_id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at",
            "in": "query",
            "required": false,
            "description": "Filter _created_at by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.neq",
            "in": "query",
            "required": false,
            "description": "Filter _created_at by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.gt",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.gte",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.lt",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.lte",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for _created_at.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Row count for the filtered table query.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "count"
                  ],
                  "example": {
                    "count": 48138
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "transfersRow": {
        "type": "object",
        "properties": {
          "sender": {
            "type": "string"
          },
          "recipient": {
            "type": "string"
          },
          "amount": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$"
          },
          "asset_identifier": {
            "type": "string"
          },
          "_id": {
            "type": "integer"
          },
          "_block_height": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$"
          },
          "_tx_id": {
            "type": "string"
          },
          "_created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "sender",
          "recipient",
          "amount",
          "asset_identifier",
          "_id",
          "_block_height",
          "_tx_id",
          "_created_at"
        ],
        "example": {
          "sender": "SP000000000000000000002Q6VF78",
          "recipient": "SP000000000000000000002Q6VF78",
          "amount": "1000",
          "asset_identifier": "example",
          "_id": 1,
          "_block_height": "1000",
          "_tx_id": "example",
          "_created_at": "2026-01-01T00:00:00.000Z"
        }
      }
    }
  },
  "x-secondlayer-subgraph": "sbtc-token",
  "x-secondlayer-version": "1.0.0",
  "x-secondlayer-schema-hash": "fffc99aeb7e11b465bf2b91ff67f3b4fcafc7bfbd447fe4d1d36d3bb4b867c2f",
  "x-secondlayer-generated-at": "2026-09-23T15:12:00.000Z",
  "x-secondlayer-sources": {
    "transfers": {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  },
  "x-secondlayer-tables": [
    "transfers"
  ]
}
```

## The Subgraph agent schema object

Compact description of a subgraph for agents and tools.

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Subgraph name. |
| `version` | string | Deployed version. |
| `description` | string | The definition's `description`. Absent without one. |
| `schemaHash` | string | sha256 of the definition's name, schema and sources. |
| `pin` | string | sha256 over the schema hash, bundled handler, startBlock, network and runtime. |
| `generatedAt` | string · date-time | When this schema was generated. |
| `serverUrl` | string | The `server` param, or this request's origin. |
| `sources` | object | The definition's `sources`, as written. |
| `tables` | object | Per table: absolute endpoints, exact `rowCount`, columns including system columns, accepted `query` params and filters, and example requests. |

```json
{
  "name": "sbtc-token",
  "version": "1.0.0",
  "schemaHash": "fffc99aeb7e11b465bf2b91ff67f3b4fcafc7bfbd447fe4d1d36d3bb4b867c2f",
  "generatedAt": "2026-09-23T15:12:00.000Z",
  "serverUrl": "http://127.0.0.1:3800",
  "sources": {
    "transfers": {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  },
  "tables": {
    "transfers": {
      "endpoint": "http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers",
      "countEndpoint": "http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers/count",
      "aggregateEndpoint": "http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers/aggregate",
      "streamEndpoint": "http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers/stream",
      "rowCount": 48138,
      "columns": {
        "sender": {
          "type": "principal"
        },
        "recipient": {
          "type": "principal"
        },
        "amount": {
          "type": "uint"
        },
        "asset_identifier": {
          "type": "text",
          "indexed": true
        },
        "_id": {
          "type": "serial"
        },
        "_block_height": {
          "type": "bigint"
        },
        "_tx_id": {
          "type": "text"
        },
        "_created_at": {
          "type": "timestamp"
        }
      },
      "query": {
        "parameters": [
          "_limit",
          "cursor",
          "_order",
          "_fields"
        ],
        "sortable": [],
        "selectable": [
          "sender",
          "recipient",
          "amount",
          "asset_identifier",
          "_id",
          "_block_height",
          "_tx_id",
          "_created_at"
        ],
        "searchable": [],
        "filters": [
          "sender",
          "sender.neq",
          "sender.like",
          "recipient",
          "recipient.neq",
          "recipient.like",
          "amount",
          "amount.neq",
          "amount.gt",
          "amount.gte",
          "amount.lt",
          "amount.lte",
          "asset_identifier",
          "asset_identifier.neq",
          "asset_identifier.like",
          "_id",
          "_id.neq",
          "_id.gt",
          "_id.gte",
          "_id.lt",
          "_id.lte",
          "_block_height",
          "_block_height.neq",
          "_block_height.gt",
          "_block_height.gte",
          "_block_height.lt",
          "_block_height.lte",
          "_tx_id",
          "_tx_id.neq",
          "_tx_id.like",
          "_created_at",
          "_created_at.neq",
          "_created_at.gt",
          "_created_at.gte",
          "_created_at.lt",
          "_created_at.lte",
          "_created_at.like"
        ]
      },
      "examples": {
        "list": {
          "sender": "SP000000000000000000002Q6VF78",
          "recipient": "SP000000000000000000002Q6VF78",
          "amount": "1000",
          "asset_identifier": "example",
          "_id": 1,
          "_block_height": "1000",
          "_tx_id": "example",
          "_created_at": "2026-01-01T00:00:00.000Z"
        },
        "count": {
          "count": 48138
        },
        "curl": "curl 'http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers?_limit=10&_order=desc'"
      }
    }
  }
}
```

## The Subgraph rows page object

One page of rows from `GET /v1/subgraphs/{name}/{table}`.

| Field | Type | Description |
| --- | --- | --- |
| `rows` | array of SubgraphRow | The page, in `_id` order or `_sort` order. |
| `next_cursor` | string · nullable | Pass back as `cursor` to resume after this page. Set on every page that returned rows, including a short last page; `null` only on an empty page, which means you are caught up. To tail, keep the last cursor you got: `cursor = next_cursor ?? cursor`. Without `_sort` it is the last row's `_id`; with `_sort` it is an opaque token. |
| `tip` | SubgraphTip |  |

```json
{
  "rows": [
    {
      "_id": "48138",
      "_block_height": "9048926",
      "_tx_id": "0x926f5ff4326155dea077126d21810fc7954568647898dbfb2f66a28b880dc5c9",
      "_created_at": "2026-09-23T15:10:29.114Z",
      "sender": "SP1TPYSSG52FKCDHHN8Y76ABMA12P069BZBFAYEAF",
      "recipient": "SP3EWCDA3V8HCP64CSETSNYXZ25WC4AJ95EC0ZEST.zr-d-0",
      "amount": "14000",
      "asset_identifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  ],
  "next_cursor": "48138",
  "tip": {
    "block_height": 9048931,
    "subgraph_height": 9048931,
    "blocks_behind": 0
  }
}
```

## The Subgraph row count object

| Field | Type | Description |
| --- | --- | --- |
| `count` | integer | Rows matching the filters, or the planner's estimate for an unfiltered `_count=estimate`. |

```json
{
  "count": 48138
}
```

## The Subgraph aggregates object

Only the aggregates you asked for are present. Sums, minimums and maximums are strings so large values stay exact.

| Field | Type | Description |
| --- | --- | --- |
| `count` | integer | Rows matching the filters. Present with `_count`, or when no other aggregate was asked for. |
| `countDistinct` | object | Distinct values per `_countDistinct` column. |
| `sum` | object | Sum per `_sum` column. `"0"` over no rows. |
| `min` | object | Minimum per `_min` column. `null` over no rows. |
| `max` | object | Maximum per `_max` column. `null` over no rows. |

```json
{
  "count": 3,
  "countDistinct": {
    "sender": 3
  },
  "sum": {
    "amount": "42000"
  },
  "max": {
    "amount": "14000"
  }
}
```

## List the subgraphs this instance serves

`GET /v1/subgraphs`

Every subgraph deployed on this instance, with its tables, sync position, and approximate row count. Hosted: your account's subgraphs, then every public one. The anonymous list is cached for 30 seconds and carries an `ETag`; send `If-None-Match` for a 304.

### Returns

One `SubgraphDirectory` object.

### Responses

- `200` The subgraph directory
- `304` Anonymous request whose `If-None-Match` matches the current `ETag`; no body
- `401` This bind requires the instance token, and it was missing or wrong
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

### Example

```bash
curl https://api.secondlayer.tools/v1/subgraphs \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "subgraphs": [
    {
      "name": "sbtc-token",
      "description": null,
      "status": "active",
      "version": "1.0.0",
      "created_at": "2026-09-01T12:00:00.000Z",
      "total_rows": 48138,
      "sources": [
        "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token"
      ],
      "last_processed_block": 9048931,
      "blocks_behind": 0,
      "tables": [
        "transfers"
      ],
      "url": "/v1/subgraphs/sbtc-token"
    }
  ],
  "tip": {
    "block_height": 9048931
  },
  "envelope": {
    "rows": "GET /v1/subgraphs/:name/:table → { rows, next_cursor, tip }",
    "cursor": "_id keyset by default, or ?_sort=<column>&_order=asc|desc for a composite keyset; pass ?cursor=<next_cursor> to resume"
  }
}
```

## Get one subgraph's tables, columns, and sync tip

`GET /v1/subgraphs/{name}`

Metadata for one subgraph: each table's read endpoint and declared columns, how far indexing has reached, and links to its generated docs. `columns` lists only the definition's columns; the system columns `_id`, `_block_height`, `_tx_id` and `_created_at` exist on every table too.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed (the `name` in its definition). |

### Returns

One `Subgraph` object.

### Responses

- `200` Subgraph metadata
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No readable subgraph by that name (`SUBGRAPH_NOT_FOUND`). Hosted: another account's private subgraph reads as missing
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

### Example

```bash
curl https://api.secondlayer.tools/v1/subgraphs/{name} \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "name": "sbtc-token",
  "description": null,
  "version": "1.0.0",
  "status": "active",
  "created_at": "2026-09-01T12:00:00.000Z",
  "sources": [
    "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token"
  ],
  "start_block": 0,
  "tables": {
    "transfers": {
      "endpoint": "/v1/subgraphs/sbtc-token/transfers",
      "columns": [
        "sender",
        "recipient",
        "amount",
        "asset_identifier"
      ],
      "column_types": {
        "sender": "principal",
        "recipient": "principal",
        "amount": "uint",
        "asset_identifier": "text"
      }
    }
  },
  "tip": {
    "block_height": 9048931,
    "subgraph_height": 9048931,
    "blocks_behind": 0
  },
  "docs": {
    "openapi": "/v1/subgraphs/sbtc-token/openapi.json",
    "schema": "/v1/subgraphs/sbtc-token/schema.json",
    "markdown": "/v1/subgraphs/sbtc-token/docs.md"
  }
}
```

## Get a subgraph's OpenAPI document

`GET /v1/subgraphs/{name}/openapi.json`

An OpenAPI 3.1 document generated from the subgraph's definition: one list path and one count path per table, a row schema per table, and the column filters each table accepts. Feed it to a client generator.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed (the `name` in its definition). |

### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `server` | string | Base URL written into the document's endpoints and examples. Defaults to this request's origin, honoring `X-Forwarded-Proto`. |

### Returns

One `SubgraphOpenApiDocument` object.

### Responses

- `200` The generated OpenAPI document
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No readable subgraph by that name (`SUBGRAPH_NOT_FOUND`). Hosted: another account's private subgraph reads as missing
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

### Example

```bash
curl https://api.secondlayer.tools/v1/subgraphs/{name}/openapi.json \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "openapi": "3.1.0",
  "info": {
    "title": "sbtc-token Subgraph API",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "http://127.0.0.1:3800"
    }
  ],
  "paths": {
    "/v1/subgraphs/sbtc-token/transfers": {
      "get": {
        "summary": "Query sbtc-token.transfers",
        "operationId": "query_sbtc_token_transfers",
        "parameters": [
          {
            "name": "_limit",
            "in": "query",
            "required": false,
            "description": "Maximum rows to return.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 1000
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Resume token from next_cursor (keyset pagination on _id).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_order",
            "in": "query",
            "required": false,
            "description": "Sort direction (_id keyset).",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            }
          },
          {
            "name": "_fields",
            "in": "query",
            "required": false,
            "description": "Comma-separated columns to include.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender",
            "in": "query",
            "required": false,
            "description": "Filter sender by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender.neq",
            "in": "query",
            "required": false,
            "description": "Filter sender by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for sender.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient",
            "in": "query",
            "required": false,
            "description": "Filter recipient by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient.neq",
            "in": "query",
            "required": false,
            "description": "Filter recipient by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for recipient.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount",
            "in": "query",
            "required": false,
            "description": "Filter amount by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.neq",
            "in": "query",
            "required": false,
            "description": "Filter amount by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.gt",
            "in": "query",
            "required": false,
            "description": "Filter amount with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.gte",
            "in": "query",
            "required": false,
            "description": "Filter amount with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.lt",
            "in": "query",
            "required": false,
            "description": "Filter amount with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.lte",
            "in": "query",
            "required": false,
            "description": "Filter amount with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier",
            "in": "query",
            "required": false,
            "description": "Filter asset_identifier by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier.neq",
            "in": "query",
            "required": false,
            "description": "Filter asset_identifier by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for asset_identifier.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id",
            "in": "query",
            "required": false,
            "description": "Filter _id by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.neq",
            "in": "query",
            "required": false,
            "description": "Filter _id by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.gt",
            "in": "query",
            "required": false,
            "description": "Filter _id with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.gte",
            "in": "query",
            "required": false,
            "description": "Filter _id with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.lt",
            "in": "query",
            "required": false,
            "description": "Filter _id with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.lte",
            "in": "query",
            "required": false,
            "description": "Filter _id with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height",
            "in": "query",
            "required": false,
            "description": "Filter _block_height by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.neq",
            "in": "query",
            "required": false,
            "description": "Filter _block_height by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.gt",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.gte",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.lt",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.lte",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id",
            "in": "query",
            "required": false,
            "description": "Filter _tx_id by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id.neq",
            "in": "query",
            "required": false,
            "description": "Filter _tx_id by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for _tx_id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at",
            "in": "query",
            "required": false,
            "description": "Filter _created_at by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.neq",
            "in": "query",
            "required": false,
            "description": "Filter _created_at by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.gt",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.gte",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.lt",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.lte",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for _created_at.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rows returned from the subgraph table.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/transfersRow"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "tip": {
                      "type": "object",
                      "properties": {
                        "block_height": {
                          "type": "integer"
                        },
                        "subgraph_height": {
                          "type": "integer"
                        },
                        "blocks_behind": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/subgraphs/sbtc-token/transfers/count": {
      "get": {
        "summary": "Count sbtc-token.transfers",
        "operationId": "count_sbtc_token_transfers",
        "parameters": [
          {
            "name": "_limit",
            "in": "query",
            "required": false,
            "description": "Maximum rows to return.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 1000
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Resume token from next_cursor (keyset pagination on _id).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_order",
            "in": "query",
            "required": false,
            "description": "Sort direction (_id keyset).",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            }
          },
          {
            "name": "_fields",
            "in": "query",
            "required": false,
            "description": "Comma-separated columns to include.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender",
            "in": "query",
            "required": false,
            "description": "Filter sender by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender.neq",
            "in": "query",
            "required": false,
            "description": "Filter sender by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sender.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for sender.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient",
            "in": "query",
            "required": false,
            "description": "Filter recipient by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient.neq",
            "in": "query",
            "required": false,
            "description": "Filter recipient by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recipient.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for recipient.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount",
            "in": "query",
            "required": false,
            "description": "Filter amount by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.neq",
            "in": "query",
            "required": false,
            "description": "Filter amount by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.gt",
            "in": "query",
            "required": false,
            "description": "Filter amount with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.gte",
            "in": "query",
            "required": false,
            "description": "Filter amount with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.lt",
            "in": "query",
            "required": false,
            "description": "Filter amount with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount.lte",
            "in": "query",
            "required": false,
            "description": "Filter amount with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier",
            "in": "query",
            "required": false,
            "description": "Filter asset_identifier by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier.neq",
            "in": "query",
            "required": false,
            "description": "Filter asset_identifier by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "asset_identifier.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for asset_identifier.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id",
            "in": "query",
            "required": false,
            "description": "Filter _id by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.neq",
            "in": "query",
            "required": false,
            "description": "Filter _id by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.gt",
            "in": "query",
            "required": false,
            "description": "Filter _id with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.gte",
            "in": "query",
            "required": false,
            "description": "Filter _id with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.lt",
            "in": "query",
            "required": false,
            "description": "Filter _id with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_id.lte",
            "in": "query",
            "required": false,
            "description": "Filter _id with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height",
            "in": "query",
            "required": false,
            "description": "Filter _block_height by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.neq",
            "in": "query",
            "required": false,
            "description": "Filter _block_height by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.gt",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.gte",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.lt",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_block_height.lte",
            "in": "query",
            "required": false,
            "description": "Filter _block_height with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id",
            "in": "query",
            "required": false,
            "description": "Filter _tx_id by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id.neq",
            "in": "query",
            "required": false,
            "description": "Filter _tx_id by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_tx_id.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for _tx_id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at",
            "in": "query",
            "required": false,
            "description": "Filter _created_at by equality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.neq",
            "in": "query",
            "required": false,
            "description": "Filter _created_at by inequality.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.gt",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with gt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.gte",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with gte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.lt",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with lt.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.lte",
            "in": "query",
            "required": false,
            "description": "Filter _created_at with lte.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "_created_at.like",
            "in": "query",
            "required": false,
            "description": "Case-insensitive contains filter for _created_at.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Row count for the filtered table query.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "count"
                  ],
                  "example": {
                    "count": 48138
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "transfersRow": {
        "type": "object",
        "properties": {
          "sender": {
            "type": "string"
          },
          "recipient": {
            "type": "string"
          },
          "amount": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$"
          },
          "asset_identifier": {
            "type": "string"
          },
          "_id": {
            "type": "integer"
          },
          "_block_height": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$"
          },
          "_tx_id": {
            "type": "string"
          },
          "_created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "sender",
          "recipient",
          "amount",
          "asset_identifier",
          "_id",
          "_block_height",
          "_tx_id",
          "_created_at"
        ],
        "example": {
          "sender": "SP000000000000000000002Q6VF78",
          "recipient": "SP000000000000000000002Q6VF78",
          "amount": "1000",
          "asset_identifier": "example",
          "_id": 1,
          "_block_height": "1000",
          "_tx_id": "example",
          "_created_at": "2026-01-01T00:00:00.000Z"
        }
      }
    }
  },
  "x-secondlayer-subgraph": "sbtc-token",
  "x-secondlayer-version": "1.0.0",
  "x-secondlayer-schema-hash": "fffc99aeb7e11b465bf2b91ff67f3b4fcafc7bfbd447fe4d1d36d3bb4b867c2f",
  "x-secondlayer-generated-at": "2026-09-23T15:12:00.000Z",
  "x-secondlayer-sources": {
    "transfers": {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  },
  "x-secondlayer-tables": [
    "transfers"
  ]
}
```

## Get a subgraph's agent schema

`GET /v1/subgraphs/{name}/schema.json`

A compact JSON description of the subgraph for agents and tools: per table, its endpoints, exact row count, columns, the query params and filters it accepts, and example requests.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed (the `name` in its definition). |

### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `server` | string | Base URL written into the document's endpoints and examples. Defaults to this request's origin, honoring `X-Forwarded-Proto`. |

### Returns

One `SubgraphAgentSchema` object.

### Responses

- `200` The generated agent schema
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No readable subgraph by that name (`SUBGRAPH_NOT_FOUND`). Hosted: another account's private subgraph reads as missing
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

### Example

```bash
curl https://api.secondlayer.tools/v1/subgraphs/{name}/schema.json \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "name": "sbtc-token",
  "version": "1.0.0",
  "schemaHash": "fffc99aeb7e11b465bf2b91ff67f3b4fcafc7bfbd447fe4d1d36d3bb4b867c2f",
  "generatedAt": "2026-09-23T15:12:00.000Z",
  "serverUrl": "http://127.0.0.1:3800",
  "sources": {
    "transfers": {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  },
  "tables": {
    "transfers": {
      "endpoint": "http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers",
      "countEndpoint": "http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers/count",
      "aggregateEndpoint": "http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers/aggregate",
      "streamEndpoint": "http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers/stream",
      "rowCount": 48138,
      "columns": {
        "sender": {
          "type": "principal"
        },
        "recipient": {
          "type": "principal"
        },
        "amount": {
          "type": "uint"
        },
        "asset_identifier": {
          "type": "text",
          "indexed": true
        },
        "_id": {
          "type": "serial"
        },
        "_block_height": {
          "type": "bigint"
        },
        "_tx_id": {
          "type": "text"
        },
        "_created_at": {
          "type": "timestamp"
        }
      },
      "query": {
        "parameters": [
          "_limit",
          "cursor",
          "_order",
          "_fields"
        ],
        "sortable": [],
        "selectable": [
          "sender",
          "recipient",
          "amount",
          "asset_identifier",
          "_id",
          "_block_height",
          "_tx_id",
          "_created_at"
        ],
        "searchable": [],
        "filters": [
          "sender",
          "sender.neq",
          "sender.like",
          "recipient",
          "recipient.neq",
          "recipient.like",
          "amount",
          "amount.neq",
          "amount.gt",
          "amount.gte",
          "amount.lt",
          "amount.lte",
          "asset_identifier",
          "asset_identifier.neq",
          "asset_identifier.like",
          "_id",
          "_id.neq",
          "_id.gt",
          "_id.gte",
          "_id.lt",
          "_id.lte",
          "_block_height",
          "_block_height.neq",
          "_block_height.gt",
          "_block_height.gte",
          "_block_height.lt",
          "_block_height.lte",
          "_tx_id",
          "_tx_id.neq",
          "_tx_id.like",
          "_created_at",
          "_created_at.neq",
          "_created_at.gt",
          "_created_at.gte",
          "_created_at.lt",
          "_created_at.lte",
          "_created_at.like"
        ]
      },
      "examples": {
        "list": {
          "sender": "SP000000000000000000002Q6VF78",
          "recipient": "SP000000000000000000002Q6VF78",
          "amount": "1000",
          "asset_identifier": "example",
          "_id": 1,
          "_block_height": "1000",
          "_tx_id": "example",
          "_created_at": "2026-01-01T00:00:00.000Z"
        },
        "count": {
          "count": 48138
        },
        "curl": "curl 'http://127.0.0.1:3800/v1/subgraphs/sbtc-token/transfers?_limit=10&_order=desc'"
      }
    }
  }
}
```

## Get a subgraph's markdown docs

`GET /v1/subgraphs/{name}/docs.md`

The agent schema rendered as markdown: a section per table with its endpoints, row count, column table, query params, filters, and a curl example. Served as `text/markdown`.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed (the `name` in its definition). |

### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `server` | string | Base URL written into the document's endpoints and examples. Defaults to this request's origin, honoring `X-Forwarded-Proto`. |

### Responses

- `200` The generated markdown
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No readable subgraph by that name (`SUBGRAPH_NOT_FOUND`). Hosted: another account's private subgraph reads as missing
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

### Example

```bash
curl https://api.secondlayer.tools/v1/subgraphs/{name}/docs.md \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

## List a table's rows

`GET /v1/subgraphs/{name}/{table}`

Rows from one subgraph table, keyset-paginated. By default pages walk `_id`; `_sort` walks one column instead, with `_id` as the tiebreaker. `next_cursor` is set on every page that returned rows and is `null` only on an empty page: pass it back as `cursor` to continue, or tail new rows with `cursor = next_cursor ?? cursor`. `_offset` is refused (400): deep OFFSET scans hurt on big tables. `_limit` defaults to 50.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed (the `name` in its definition). |
| `table` | string, required | Table name, one of the keys of the definition's `schema`. `GET /v1/subgraphs/{name}` lists them. |

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `_search` | string | Case-insensitive contains match across every column marked `search: true` in the definition, ORed. 400 on a table with no searchable column. |
| `filters` | object | Column filters, one query param each, ANDed. Any table column or system column (`_id`, `_block_height`, `_tx_id`, `_created_at`) works. `col=value` is equality; `col.op=value` applies an operator, one of `gte`, `lte`, `gt`, `lt`, `neq`, `like`, `in`, `notIn`. `like` is a case-insensitive contains match. `in` and `notIn` take a comma-separated list. An unknown column is 400 `INVALID_COLUMN`; an unknown operator, or the misplaced form `col=op.value`, is 400 `VALIDATION_ERROR`. Bare `limit`, `offset`, `sort`, `order`, `fields` and `search` are 400: control params take a leading underscore. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `_limit` | integer · 1 to 1000 | Page size, 1 to 1000. A non-integer, 0, a negative, or anything over 1000 is 400, not clamped. |
| `cursor` | string | The previous page's `next_cursor`, verbatim. Opaque: its shape depends on `_sort`, so never build one. A cursor issued under one `_sort`/`_order` is 400 under another. |
| `_sort` | string | One column to sort by. A comma list and `jsonb` columns are 400. Omit to page by `_id`. |
| `_order` | enum | Direction of the `_sort` column, or of `_id` without `_sort`. Anything but `asc` or `desc` is 400. Values: `asc`, `desc`. |
| `_fields` | string | Comma-separated columns to return. An unknown name is 400 `INVALID_COLUMN`. The cursor still works: `_id` and the `_sort` column are read for it and dropped from rows you did not ask them for. |

### Returns

One `SubgraphRowsPage` object.

### Responses

- `200` One page of rows
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No readable subgraph by that name (`SUBGRAPH_NOT_FOUND`), or it has no such table (`TABLE_NOT_FOUND`)
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

### Example

```bash
curl -G https://api.secondlayer.tools/v1/subgraphs/{name}/{table} \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d filters=[object Object]
```

```json
{
  "rows": [
    {
      "_id": "48138",
      "_block_height": "9048926",
      "_tx_id": "0x926f5ff4326155dea077126d21810fc7954568647898dbfb2f66a28b880dc5c9",
      "_created_at": "2026-09-23T15:10:29.114Z",
      "sender": "SP1TPYSSG52FKCDHHN8Y76ABMA12P069BZBFAYEAF",
      "recipient": "SP3EWCDA3V8HCP64CSETSNYXZ25WC4AJ95EC0ZEST.zr-d-0",
      "amount": "14000",
      "asset_identifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  ],
  "next_cursor": "48138",
  "tip": {
    "block_height": 9048931,
    "subgraph_height": 9048931,
    "blocks_behind": 0
  }
}
```

## Count a table's rows

`GET /v1/subgraphs/{name}/{table}/count`

How many rows match the filters. Takes the same column filters and `_search` as the row list.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed (the `name` in its definition). |
| `table` | string, required | Table name, one of the keys of the definition's `schema`. `GET /v1/subgraphs/{name}` lists them. |

### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `_count` | enum | `exact` runs COUNT(*). `estimate` reads the planner's row estimate instead, with no table scan, but only when there are no filters and no `_search`; otherwise it counts exactly. The estimate can be -1 before the table's first ANALYZE. Values: `exact`, `estimate`. |
| `_search` | string | Case-insensitive contains match across every column marked `search: true` in the definition, ORed. 400 on a table with no searchable column. |
| `filters` | object | Column filters, one query param each, ANDed. Any table column or system column (`_id`, `_block_height`, `_tx_id`, `_created_at`) works. `col=value` is equality; `col.op=value` applies an operator, one of `gte`, `lte`, `gt`, `lt`, `neq`, `like`, `in`, `notIn`. `like` is a case-insensitive contains match. `in` and `notIn` take a comma-separated list. An unknown column is 400 `INVALID_COLUMN`; an unknown operator, or the misplaced form `col=op.value`, is 400 `VALIDATION_ERROR`. Bare `limit`, `offset`, `sort`, `order`, `fields` and `search` are 400: control params take a leading underscore. |

### Returns

One `SubgraphRowCount` object.

### Responses

- `200` The count
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No readable subgraph by that name (`SUBGRAPH_NOT_FOUND`), or it has no such table (`TABLE_NOT_FOUND`)
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

### Example

```bash
curl -G https://api.secondlayer.tools/v1/subgraphs/{name}/{table}/count \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d filters=[object Object]
```

```json
{
  "count": 48138
}
```

## Aggregate a table's rows

`GET /v1/subgraphs/{name}/{table}/aggregate`

Scalar aggregates over the rows that match the filters: count, distinct counts, and sum, min and max of numeric columns. With no aggregate param it returns `{ "count": n }`. Up to 32 aggregate columns per request. Takes the same column filters and `_search` as the row list.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed (the `name` in its definition). |
| `table` | string, required | Table name, one of the keys of the definition's `schema`. `GET /v1/subgraphs/{name}` lists them. |

### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `_count` | string | Include `count`. Any value but `false` turns it on. `count` is also returned when no other aggregate is asked for. |
| `_countDistinct` | string | Comma-separated columns to count distinct values of. Any column. |
| `_sum` | string | Comma-separated columns to sum. Numeric only: `uint` or `int` columns, or `_block_height`; others are 400 `NON_NUMERIC_COLUMN`. |
| `_min` | string | Comma-separated numeric columns to take the minimum of. |
| `_max` | string | Comma-separated numeric columns to take the maximum of. |
| `_search` | string | Case-insensitive contains match across every column marked `search: true` in the definition, ORed. 400 on a table with no searchable column. |
| `filters` | object | Column filters, one query param each, ANDed. Any table column or system column (`_id`, `_block_height`, `_tx_id`, `_created_at`) works. `col=value` is equality; `col.op=value` applies an operator, one of `gte`, `lte`, `gt`, `lt`, `neq`, `like`, `in`, `notIn`. `like` is a case-insensitive contains match. `in` and `notIn` take a comma-separated list. An unknown column is 400 `INVALID_COLUMN`; an unknown operator, or the misplaced form `col=op.value`, is 400 `VALIDATION_ERROR`. Bare `limit`, `offset`, `sort`, `order`, `fields` and `search` are 400: control params take a leading underscore. |

### Returns

One `SubgraphAggregates` object.

### Responses

- `200` The aggregates
- `400` A column or filter was refused: `INVALID_COLUMN`, `NON_NUMERIC_COLUMN`, `TOO_MANY_AGGREGATES`, or `VALIDATION_ERROR`
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No readable subgraph by that name (`SUBGRAPH_NOT_FOUND`), or it has no such table (`TABLE_NOT_FOUND`)
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

### Example

```bash
curl -G https://api.secondlayer.tools/v1/subgraphs/{name}/{table}/aggregate \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d filters=[object Object]
```

```json
{
  "count": 3,
  "countDistinct": {
    "sender": 3
  },
  "sum": {
    "amount": "42000"
  },
  "max": {
    "amount": "14000"
  }
}
```

## Stream a table's new rows over SSE

`GET /v1/subgraphs/{name}/{table}/stream`

Server-Sent Events of rows as they are indexed. Starts at the table's newest row and sends only rows after it; `since` replays from a block height first, then keeps tailing. The server polls every 1.5 seconds, sends each matching row in `_id` order, and sends a `ping` event after 20 idle seconds. Column filters and `_search` apply; `_limit`, `_sort`, `_order` and `_fields` are checked but have no effect, and rows always come back whole. Nothing is stored between connections, and `Last-Event-ID` is not read: to resume, reconnect with `since`.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed (the `name` in its definition). |
| `table` | string, required | Table name, one of the keys of the definition's `schema`. `GET /v1/subgraphs/{name}` lists them. |

### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `since` | integer | Block height to replay from: rows with `_block_height` at or above it, then the live tail. A value that is not a number is ignored. With nothing at or above it yet, the stream tails live. |
| `_search` | string | Case-insensitive contains match across every column marked `search: true` in the definition, ORed. 400 on a table with no searchable column. |
| `filters` | object | Column filters, one query param each, ANDed. Any table column or system column (`_id`, `_block_height`, `_tx_id`, `_created_at`) works. `col=value` is equality; `col.op=value` applies an operator, one of `gte`, `lte`, `gt`, `lt`, `neq`, `like`, `in`, `notIn`. `like` is a case-insensitive contains match. `in` and `notIn` take a comma-separated list. An unknown column is 400 `INVALID_COLUMN`; an unknown operator, or the misplaced form `col=op.value`, is 400 `VALIDATION_ERROR`. Bare `limit`, `offset`, `sort`, `order`, `fields` and `search` are 400: control params take a leading underscore. |

### Responses

- `200` An open event stream. Each row arrives as one message: `id` is the row's `_id`, `data` is the row as JSON, in the same shape as the row list. Idle keepalives are `event: ping` with empty data.
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No readable subgraph by that name (`SUBGRAPH_NOT_FOUND`), or it has no such table (`TABLE_NOT_FOUND`)
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

### Example

```bash
curl -G https://api.secondlayer.tools/v1/subgraphs/{name}/{table}/stream \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  -d since=9048900 \
  -d filters=[object Object]
```

## Get one row by id

`GET /v1/subgraphs/{name}/{table}/{id}`

One row by its `_id`, wrapped as `{ data }`. An id that isn't an integer, or no row with it, is a 404.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed (the `name` in its definition). |
| `table` | string, required | Table name, one of the keys of the definition's `schema`. `GET /v1/subgraphs/{name}` lists them. |
| `id` | string, required | The row's `_id`. |

### Responses

- `200` The row.
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph, table, or row with that name or id (`SUBGRAPH_NOT_FOUND`, `TABLE_NOT_FOUND`, `ROW_NOT_FOUND`)
- `429` Hosted only: rate limited; retry after `Retry-After` seconds. Self-hosted reads are not rate limited

### Example

```bash
curl https://api.secondlayer.tools/v1/subgraphs/{name}/{table}/48138 \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "data": {
    "_id": "48138",
    "_block_height": "9048926",
    "_tx_id": "0x926f5ff4326155dea077126d21810fc7954568647898dbfb2f66a28b880dc5c9",
    "_created_at": "2026-09-23T15:10:29.114Z",
    "sender": "SP1TPYSSG52FKCDHHN8Y76ABMA12P069BZBFAYEAF",
    "recipient": "SP3EWCDA3V8HCP64CSETSNYXZ25WC4AJ95EC0ZEST.zr-d-0",
    "amount": "14000",
    "asset_identifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
  }
}
```

---

# archive

Compare this instance against a signed archive. Read-only; nothing is uploaded but the manifest URL. Identity digests only.

## The Archive verification object

The outcome of an archive comparison.

| Field | Type | Description |
| --- | --- | --- |
| `status` | enum | `clean`: every range matched. `diverged`: at least one did not. `unanchored`: the comparison could not be trusted or run; see `reason`. Values: `clean`, `diverged`, `unanchored`. |
| `target` | string | The target that ran, normalised (`raw`, `all`, `decode:<name>`, `subgraph:<name>`). |
| `against` | string | The manifest URL, as sent. |
| `signature` | object | Whether the manifest signature verified, and why not when it did not. |
| `coverage` | object | Lowest and highest height compared. Absent when nothing was compared. |
| `ranges` | array of object | One row per compared range. `status` is `match`, `digest-mismatch`, `count-mismatch`, or `missing` when this instance has no rows there. Digests are `null` for an empty range. |
| `reason` | string | Why the result is `unanchored`: `no public key`, the fetch error, the signature failure, or `no digests`. |

```json
{
  "status": "diverged",
  "target": "raw",
  "against": "https://archive.secondlayer.tools/latest.json",
  "signature": {
    "verified": true
  },
  "coverage": {
    "from_block": 0,
    "to_block": 99999
  },
  "ranges": [
    {
      "dataset": "blocks",
      "from_block": 0,
      "to_block": 49999,
      "status": "match",
      "expected_digest": "5d41402abc4b2a76b9719d911017c592",
      "actual_digest": "5d41402abc4b2a76b9719d911017c592"
    },
    {
      "dataset": "blocks",
      "from_block": 50000,
      "to_block": 99999,
      "status": "digest-mismatch",
      "expected_digest": "7d793037a0760186574b0282f2f435e7",
      "actual_digest": "e2fc714c4727ee9395f324cd2e7f331f"
    }
  ]
}
```

## Verify this instance against a signed archive

`POST /v1/archive/verify`

Compares identity digests of this instance's blocks, transactions and events against the range digests in a signed archive manifest. Read-only: the instance fetches the manifest, nothing is uploaded. Semantic replay (`--deep`) is CLI only. Without `from_block` or `to_block`, a manifest with more than 40 matching ranges is refused with 400. A missing key, an unreachable manifest, a failed signature or no matching digests answer 200 `unanchored` with a `reason`, never `clean`.

### Body

| Field | Type | Description |
| --- | --- | --- |
| `against` | string · uri, required | https URL of `latest.json` or a snapshot manifest. Local paths and plain http are refused. |
| `target` | string | `raw` (default) or `all` check blocks, transactions and events. `decode:<name>` and `subgraph:<name>` parse, but only raw datasets have digests, so they answer `unanchored` with `no digests`. |
| `from_block` | integer | First height to check. Needed, with or without `to_block`, when the manifest has more than 40 matching ranges. |
| `to_block` | integer | Last height to check. Must be at least `from_block`. |
| `insecure` | boolean | Compare even when the signature does not verify. `signature.verified` stays `false`. |
| `public_key_pem` | string | Archive signing key to verify with. Without it the instance uses `ARCHIVE_SIGNING_PUBLIC_KEY` (or `STREAMS_SIGNING_PUBLIC_KEY`); the hosted API also fetches the published key. |

### Returns

One `ArchiveVerifyResult` object.

### Responses

- `200` The comparison, or `unanchored` with the reason it could not run
- `400` The body is not JSON (`INVALID_JSON`), `against` is missing or not https, a field has the wrong type, `target` is not recognised, `to_block` is below `from_block`, or more than 40 ranges matched without a height window (`VALIDATION_ERROR`)
- `401` This bind requires the instance token, and it was missing or wrong

### Example

```bash
curl -X POST https://api.secondlayer.tools/v1/archive/verify \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  --json '{
  "against": "https://archive.secondlayer.tools/latest.json",
  "target": "raw",
  "from_block": 0,
  "to_block": 99999
}'
```

```json
{
  "status": "diverged",
  "target": "raw",
  "against": "https://archive.secondlayer.tools/latest.json",
  "signature": {
    "verified": true
  },
  "coverage": {
    "from_block": 0,
    "to_block": 99999
  },
  "ranges": [
    {
      "dataset": "blocks",
      "from_block": 0,
      "to_block": 49999,
      "status": "match",
      "expected_digest": "5d41402abc4b2a76b9719d911017c592",
      "actual_digest": "5d41402abc4b2a76b9719d911017c592"
    },
    {
      "dataset": "blocks",
      "from_block": 50000,
      "to_block": 99999,
      "status": "digest-mismatch",
      "expected_digest": "7d793037a0760186574b0282f2f435e7",
      "actual_digest": "e2fc714c4727ee9395f324cd2e7f331f"
    }
  ]
}
```

---

# deployments

Deploy, reindex, backfill, stop, and delete subgraphs on this instance (write plane, `/api`)

## The Deployed subgraph object

One deployed subgraph, as the list returns it.

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Subgraph name. |
| `version` | string | Deployed version. |
| `status` | enum | Processor state. Values: `active`, `reindexing`, `paused`, `error`. |
| `lastProcessedBlock` | integer | Highest block processed. |
| `totalProcessed` | integer | Events handled since deploy. |
| `totalRows` | integer | Approximate rows across tables, from table statistics. |
| `totalErrors` | integer | Handler errors since deploy. |
| `tables` | array of string | Table names. |
| `chainTip` | integer | Progress denominator: the chain tip, or the reindex target while reindexing. |
| `sourceChainTip` | integer | The instance's chain tip. |
| `targetBlock` | integer | Height this sync or reindex is walking toward. |
| `progress` | number · 0 to 1 | Fraction of blocks processed, 0 to 1. |
| `blocksRemaining` | integer | Blocks left to `targetBlock`. |
| `syncMode` | enum | `reindex` while a reindex walk runs. Values: `sync`, `reindex`. |
| `gapCount` | integer | Open gaps. |
| `integrity` | enum | `gaps_detected` when any gap is open. Values: `complete`, `gaps_detected`. |
| `lastError` | string · nullable | Most recent processing error. |
| `lastErrorAt` | string · date-time · nullable | When it happened. |
| `updatedAt` | string · date-time · nullable | Last registry update. |
| `createdAt` | string · date-time | First deploy. |
| `health` | SubgraphHealth | Hourly health, present only when the list is requested with `health`. |

```json
{
  "name": "sbtc-flows",
  "version": "1.0.2",
  "status": "active",
  "lastProcessedBlock": 9048712,
  "totalProcessed": 48210,
  "totalRows": 48138,
  "totalErrors": 0,
  "tables": [
    "transfers"
  ],
  "chainTip": 9048712,
  "sourceChainTip": 9048712,
  "targetBlock": 9048712,
  "progress": 1,
  "blocksRemaining": 0,
  "syncMode": "sync",
  "gapCount": 0,
  "integrity": "complete",
  "lastError": null,
  "lastErrorAt": null,
  "updatedAt": "2026-09-23T14:02:11.000Z",
  "createdAt": "2026-09-21T09:14:52.000Z"
}
```

## The Subgraph bundle object

A compiled subgraph. Everything but `ok` and `bundleSize` is the body for `POST /api/subgraphs`.

| Field | Type | Description |
| --- | --- | --- |
| `ok` | enum | Always `true`. Values: `true`. |
| `name` | string | Name from the definition. |
| `description` | string · nullable | Description from the definition, if set. |
| `sources` | object | Event sources by name. |
| `schema` | object | Table definitions by name. |
| `handlerCode` | string | The bundled handler module. |
| `sourceCode` | string | The `code` you sent, echoed back. |
| `bundleSize` | integer | Size of `handlerCode` in bytes. |

```json
{
  "ok": true,
  "name": "sbtc-flows",
  "description": null,
  "sources": {
    "transfers": {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  },
  "schema": {
    "transfers": {
      "columns": {
        "amount": {
          "type": "uint"
        },
        "sender": {
          "type": "principal"
        },
        "recipient": {
          "type": "principal"
        }
      }
    }
  },
  "handlerCode": "// secondlayer-stub:@secondlayer/subgraphs\nfunction defineSubgraph(def) {\n  return def;\n}\n\n// <stdin>\nvar stdin_default = defineSubgraph({\n  name: \"sbtc-flows\",\n  startBlock: 8e6,\n  sources: {\n    transfers: {\n      type: \"ft_transfer\",\n      assetIdentifier: \"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token\"\n    }\n  },\n  schema: {\n    transfers: {\n      columns: {\n        amount: { type: \"uint\" },\n        sender: { type: \"principal\" },\n        recipient: { type: \"principal\" }\n      }\n    }\n  },\n  handlers: {\n    transfers: (event, ctx) => {\n      ctx.insert(\"transfers\", { amount: event.amount, sender: event.sender, recipient: event.recipient });\n    }\n  }\n});\nexport {\n  stdin_default as default\n};\n",
  "sourceCode": "import { defineSubgraph } from \"@secondlayer/subgraphs\";\n\nexport default defineSubgraph({\n  name: \"sbtc-flows\",\n  startBlock: 8000000,\n  sources: {\n    transfers: {\n      type: \"ft_transfer\",\n      assetIdentifier: \"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token\",\n    },\n  },\n  schema: {\n    transfers: {\n      columns: {\n        amount: { type: \"uint\" },\n        sender: { type: \"principal\" },\n        recipient: { type: \"principal\" },\n      },\n    },\n  },\n  handlers: {\n    transfers: (event, ctx) => {\n      ctx.insert(\"transfers\", { amount: event.amount, sender: event.sender, recipient: event.recipient });\n    },\n  },\n});\n",
  "bundleSize": 724
}
```

## The Deployed subgraph detail object

One deployed subgraph in full.

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Subgraph name. |
| `version` | string | Deployed version. |
| `schemaHash` | string | Hash of the table schema. Changes when the schema does. |
| `pin` | string · nullable | Hash of everything that shapes rows: schema hash, bundled handler, startBlock, network, runtime. Null for subgraphs deployed before pins existed. |
| `verification` | object | Derived verification level. Null for subgraphs deployed before levels were derived. |
| `status` | enum | Processor state. Values: `active`, `reindexing`, `paused`, `error`. |
| `lastProcessedBlock` | integer | Highest block processed. |
| `description` | string | From the definition, when set. |
| `sources` | object | Event sources by name, from the definition. |
| `definition` | object · nullable | The stored definition: `name`, `description`, `startBlock`, `sources`, `schema`. |
| `health` | object | `totalProcessed` and `totalErrors` since deploy, `errorRate`, the last error and when, `emptyMapping` (events matched but every table is empty), and `violationCount` (print violations kept). |
| `sync` | DeployedSubgraphSync |  |
| `tables` | object | By table name: read `endpoint`, `columns` (with the system columns `_id`, `_block_height`, `_tx_id`, `_created_at`), exact `rowCount`, an `example` query, and `indexes` and `uniqueKeys` when declared. |
| `createdAt` | string · date-time | First deploy. |
| `updatedAt` | string · date-time | Last registry update. |

```json
{
  "name": "sbtc-flows",
  "version": "1.0.2",
  "schemaHash": "4b9e1d27c83f0a65e2d7b14c9f06a38e5d21c7b90f4e6a83d15c2b7e09f4a6d1",
  "pin": "7f3c0d5e9b21a4c86f0e3d17b5a92c4e8d61f07a3b9c25e4d8f1a06c7b3e5a91e",
  "verification": {
    "level": "events",
    "reasons": [
      "ft_transfer source \"transfers\" needs event proofs"
    ],
    "unproven": []
  },
  "status": "active",
  "lastProcessedBlock": 9048712,
  "sources": {
    "transfers": {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  },
  "definition": {
    "name": "sbtc-flows",
    "startBlock": 8000000,
    "sources": {
      "transfers": {
        "type": "ft_transfer",
        "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
      }
    },
    "schema": {
      "transfers": {
        "columns": {
          "amount": {
            "type": "uint"
          },
          "sender": {
            "type": "principal"
          },
          "recipient": {
            "type": "principal"
          }
        }
      }
    }
  },
  "health": {
    "totalProcessed": 48210,
    "totalErrors": 0,
    "errorRate": 0,
    "lastError": null,
    "lastErrorAt": null,
    "emptyMapping": false,
    "violationCount": 0
  },
  "sync": {
    "status": "synced",
    "mode": "sync",
    "startBlock": 8000000,
    "lastProcessedBlock": 9048712,
    "chainTip": 9048712,
    "sourceChainTip": 9048712,
    "targetBlock": 9048712,
    "blocksRemaining": 0,
    "processedBlocks": 1048713,
    "totalBlocks": 1048713,
    "progress": 1,
    "gaps": {
      "count": 0,
      "totalMissingBlocks": 0,
      "ranges": []
    },
    "integrity": "complete"
  },
  "tables": {
    "transfers": {
      "endpoint": "/subgraphs/sbtc-flows/transfers",
      "columns": {
        "amount": {
          "type": "uint"
        },
        "sender": {
          "type": "principal"
        },
        "recipient": {
          "type": "principal"
        },
        "_id": {
          "type": "serial"
        },
        "_block_height": {
          "type": "bigint"
        },
        "_tx_id": {
          "type": "text"
        },
        "_created_at": {
          "type": "timestamp"
        }
      },
      "rowCount": 48138,
      "example": "/subgraphs/sbtc-flows/transfers?_sort=_block_height&_order=desc&_limit=10"
    }
  },
  "createdAt": "2026-09-21T09:14:52.000Z",
  "updatedAt": "2026-09-23T14:02:11.000Z"
}
```

## The Queued operation object

A reindex or backfill accepted into the queue.

| Field | Type | Description |
| --- | --- | --- |
| `message` | string | One-line summary. |
| `fromBlock` | integer | First block. For a reindex, the subgraph's start block. |
| `toBlock` | integer | Last block. For a reindex, the string `"chain tip"`: it runs to whatever the tip is when it gets there. |
| `operationId` | string · uuid | Poll `/api/subgraphs/{name}/operations/{operation_id}`. |
| `status` | enum | Always `queued`. Values: `queued`. |

```json
{
  "message": "Backfill queued for subgraph \"sbtc-flows\"",
  "fromBlock": 8950000,
  "toBlock": 9000000,
  "operationId": "9a4e7c21-0b3d-4f6a-8c15-e2d7b9f0a461",
  "status": "queued"
}
```

## The Subgraph operation object

A reindex or backfill. One per subgraph can be queued or running at a time.

| Field | Type | Description |
| --- | --- | --- |
| `id` | string · uuid | Operation id. |
| `subgraphName` | string | Subgraph it runs on. |
| `kind` | enum | `reindex` drops and rebuilds; `backfill` re-runs a range in place. Values: `reindex`, `backfill`. |
| `status` | enum | `queued` until a processor claims it. `completed`, `failed` and `cancelled` are final. Values: `queued`, `running`, `completed`, `failed`, `cancelled`. |
| `weight` | enum | `light` for contract-scoped sources, `heavy` for broad ones. The processor limits how many heavy operations run at once. Values: `light`, `heavy`. |
| `fromBlock` | integer · nullable | First block. `null` on a reindex queued from `/reindex`, which starts at the subgraph's start block. |
| `toBlock` | integer · nullable | Last block. `null` means the chain tip. |
| `processedBlocks` | integer · nullable | Blocks covered. Set when the operation finishes. |
| `cursorBlock` | integer · nullable | Backfills only: last committed block. A requeued backfill resumes after it. |
| `estimatedEvents` | integer · nullable | Events expected, computed at enqueue for contract-scoped sources. |
| `processedEvents` | integer · nullable | Events processed so far. |
| `progress` | number · 0 to 1 · nullable | Fraction done, 0 to 1, from events when estimated, else blocks. `null` before any progress is reported. |
| `queuePosition` | integer | Queued only: approximate 1-based position. Show it as `~N`. |
| `error` | string · nullable | Why it failed. |
| `startedAt` | string · date-time · nullable | When a processor claimed it. |
| `finishedAt` | string · date-time · nullable | When it reached a final status. |
| `createdAt` | string · date-time | When it was queued. |
| `updatedAt` | string · date-time | Last change. |

```json
{
  "id": "3f1c9a52-7d4e-4b8a-9e21-5c0d8f6a2b17",
  "subgraphName": "sbtc-flows",
  "kind": "reindex",
  "status": "running",
  "weight": "light",
  "fromBlock": 8000000,
  "toBlock": 9048712,
  "processedBlocks": null,
  "cursorBlock": null,
  "estimatedEvents": 48210,
  "processedEvents": 24105,
  "progress": 0.5,
  "error": null,
  "startedAt": "2026-09-21T09:15:04.000Z",
  "finishedAt": null,
  "createdAt": "2026-09-21T09:14:53.000Z",
  "updatedAt": "2026-09-21T09:18:37.000Z"
}
```

## The Subgraph gap object

A block range the subgraph did not process.

| Field | Type | Description |
| --- | --- | --- |
| `start` | integer | First missing block. |
| `end` | integer | Last missing block, inclusive. |
| `size` | integer | Blocks in the range. |
| `reason` | enum | `block_missing`: the block was not in the index. `processing_error`: a handler threw. Values: `block_missing`, `processing_error`. |
| `detectedAt` | string · date-time | When it was recorded. |
| `resolvedAt` | string · date-time · nullable | When a later walk covered it. `null` while open. |

```json
{
  "start": 8412207,
  "end": 8412207,
  "size": 1,
  "reason": "processing_error",
  "detectedAt": "2026-09-22T18:40:03.000Z",
  "resolvedAt": null
}
```

## The Subgraph health object

Hourly health buckets and the newest 24 hours' handler errors.

| Field | Type | Description |
| --- | --- | --- |
| `hours` | array of object | One bucket per hour, oldest first. |
| `errors24h` | integer | Handler errors across the newest 24 hours. |

```json
{
  "hours": [
    {
      "hour": "2026-10-16T13:00:00.000Z",
      "lagBlocks": 0,
      "rowsWritten": 1840,
      "errors": 0,
      "eventsHandled": 2213
    }
  ],
  "errors24h": 0
}
```

## The Print validation skip object

A print event skipped because its payload did not match the declared `prints` schema.

| Field | Type | Description |
| --- | --- | --- |
| `id` | string · uuid | Violation id. |
| `sourceName` | string | Source whose `prints` schema refused it. |
| `blockHeight` | integer | Block of the event. |
| `txId` | string | Transaction of the event. |
| `reason` | string | Which field was missing or had the wrong type. |
| `samplePayload` | object | The decoded print, as `topic` and `data`. |
| `seenAt` | string · date-time | When it was skipped. |

```json
{
  "id": "c41d8e27-6f0a-4b93-a5e2-7d19b3c8f056",
  "sourceName": "swaps",
  "blockHeight": 9031477,
  "txId": "0x8d2f6a91c4e07b35d1a8f2e69c0b47d3a5e18f62b9c04d7e3a16f58b2c9d0e41",
  "reason": "missing declared field \"amount-in\"",
  "samplePayload": {
    "topic": "swap",
    "data": {
      "amount-out": "1500000",
      "sender": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9"
    }
  },
  "seenAt": "2026-09-23T11:27:45.000Z"
}
```

## List deployed subgraphs

`GET /api/subgraphs`

Every subgraph on this instance with its status, sync progress, row count and open gap count. Row counts come from Postgres table statistics, so they are approximate; the detail read counts exactly.

### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `health` | integer | Also return each subgraph's hourly health for this many hours, the same shape as the health read. 1 to 168. |

### Returns

A cursor envelope; `data` holds `DeployedSubgraph` objects.

### Responses

- `200` Every deployed subgraph, unpaginated
- `401` This bind requires the instance token, and it was missing or wrong

### Example

```bash
curl https://api.secondlayer.tools/api/subgraphs \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "data": [
    {
      "name": "sbtc-flows",
      "version": "1.0.2",
      "status": "active",
      "lastProcessedBlock": 9048712,
      "totalProcessed": 48210,
      "totalRows": 48138,
      "totalErrors": 0,
      "tables": [
        "transfers"
      ],
      "chainTip": 9048712,
      "sourceChainTip": 9048712,
      "targetBlock": 9048712,
      "progress": 1,
      "blocksRemaining": 0,
      "syncMode": "sync",
      "gapCount": 0,
      "integrity": "complete",
      "lastError": null,
      "lastErrorAt": null,
      "updatedAt": "2026-09-23T14:02:11.000Z",
      "createdAt": "2026-09-21T09:14:52.000Z"
    }
  ]
}
```

## Deploy or redeploy a subgraph

`POST /api/subgraphs`

Takes bundled handler code (see `/api/subgraphs/bundle`) plus the schema and sources extracted from it. The server reads the definition from the bundle without running it, validates it, lints print fields, then applies the schema. A new subgraph, or a breaking schema change, queues a reindex and returns its `operationId`. Redeploying the same name updates in place; `action` says what happened. With `dryRun` it returns the DDL plan and writes nothing.

### Body

| Field | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name. Lowercase letters, digits and hyphens. Deploying an existing name updates it. |
| `sources` | object, required | Event sources keyed by name, as read from the handler module. At least one. |
| `schema` | object, required | Table definitions keyed by table name. |
| `handlerCode` | string, required | The bundled handler module. 1 MB max. |
| `startBlock` | integer | Overrides the definition's `startBlock` for this deploy and forces a reindex from it. Refused past the chain tip (`START_BLOCK_PAST_TIP`). |
| `description` | string | One line on what the subgraph indexes. |
| `sourceCode` | string | The unbundled TypeScript source, kept so `secondlayer subgraphs source` can return it. 1 MB max. |
| `dryRun` | boolean | Validate and return the DDL plan without writing anything. |

### Responses

- `200` Redeployed in place (`action` is anything but `created`), or the dry-run plan when `dryRun` is set
- `201` Created. A reindex from `start_block` is queued; poll `operationId`
- `400` The body failed validation (here `error` is an object of field errors, not a string), the bundle could not be read (`HANDLER_IMPORT_FAILED`), the definition is invalid, or `startBlock` is past the chain tip (`START_BLOCK_PAST_TIP`)
- `401` This bind requires the instance token, and it was missing or wrong
- `409` A reindex or backfill is already queued or running for this subgraph (`OPERATION_IN_PROGRESS`). Wait for it, or stop it first.
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`
- `422` The definition deploys but would index wrong: a declared print field is never seen on-chain (`PRINT_FIELD_MISMATCH`), handlers write 0 rows against observed prints (`EMPTY_MAPPING`), a tip-first deploy has delta handlers (`TIP_FIRST_NON_REPLAYABLE_HANDLER`) or a breaking schema change (`TIP_FIRST_BREAKING_CHANGE`), or a subgraph with provable sources has a nondeterministic handler (`NONDETERMINISTIC_HANDLER`, with `verification.reasons` naming each use)

### Example

```bash
curl -X POST https://api.secondlayer.tools/api/subgraphs \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  --json '{
  "name": "sbtc-flows",
  "sources": {
    "transfers": {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  },
  "schema": {
    "transfers": {
      "columns": {
        "amount": {
          "type": "uint"
        },
        "sender": {
          "type": "principal"
        },
        "recipient": {
          "type": "principal"
        }
      }
    }
  },
  "handlerCode": "// secondlayer-stub:@secondlayer/subgraphs\nfunction defineSubgraph(def) {\n  return def;\n}\n\n// <stdin>\nvar stdin_default = defineSubgraph({\n  name: \"sbtc-flows\",\n  startBlock: 8e6,\n  sources: {\n    transfers: {\n      type: \"ft_transfer\",\n      assetIdentifier: \"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token\"\n    }\n  },\n  schema: {\n    transfers: {\n      columns: {\n        amount: { type: \"uint\" },\n        sender: { type: \"principal\" },\n        recipient: { type: \"principal\" }\n      }\n    }\n  },\n  handlers: {\n    transfers: (event, ctx) => {\n      ctx.insert(\"transfers\", { amount: event.amount, sender: event.sender, recipient: event.recipient });\n    }\n  }\n});\nexport {\n  stdin_default as default\n};\n",
  "sourceCode": "import { defineSubgraph } from \"@secondlayer/subgraphs\";\n\nexport default defineSubgraph({\n  name: \"sbtc-flows\",\n  startBlock: 8000000,\n  sources: {\n    transfers: {\n      type: \"ft_transfer\",\n      assetIdentifier: \"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token\",\n    },\n  },\n  schema: {\n    transfers: {\n      columns: {\n        amount: { type: \"uint\" },\n        sender: { type: \"principal\" },\n        recipient: { type: \"principal\" },\n      },\n    },\n  },\n  handlers: {\n    transfers: (event, ctx) => {\n      ctx.insert(\"transfers\", { amount: event.amount, sender: event.sender, recipient: event.recipient });\n    },\n  },\n});\n"
}'
```

```json
{
  "action": "handler_updated",
  "subgraphId": "b7e2d4a1-5c3f-4e8b-a0d9-2f6c1e8b7a34",
  "version": "1.0.3",
  "start_block": 8000000,
  "message": "Subgraph \"sbtc-flows\" handler_updated",
  "warnings": [
    "handler changed but schema did not: new logic applies from the current tip only. Rows already indexed were computed by the previous handler. To recompute history, redeploy with --reindex (or backfill a range)."
  ]
}
```

## Bundle subgraph source into deployable handler code

`POST /api/subgraphs/bundle`

Compiles a TypeScript subgraph module on the server and returns the bundle plus the metadata read from it, ready to POST to `/api/subgraphs`. Deploys nothing. The CLI and MCP bundle locally; this is for callers that cannot run esbuild.

### Body

| Field | Type | Description |
| --- | --- | --- |
| `code` | string, required | The subgraph module's TypeScript source, with `defineSubgraph` as the default export. |

### Returns

One `SubgraphBundle` object.

### Responses

- `200` Bundled
- `400` `code` is missing or empty, or the module did not compile (`BUNDLE_FAILED`)
- `401` This bind requires the instance token, and it was missing or wrong
- `413` The bundle exceeds the size limit (`BUNDLE_TOO_LARGE`); `actualBytes` and `maxBytes` say by how much
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

### Example

```bash
curl -X POST https://api.secondlayer.tools/api/subgraphs/bundle \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  --json '{
  "code": "import { defineSubgraph } from \"@secondlayer/subgraphs\";\n\nexport default defineSubgraph({\n  name: \"sbtc-flows\",\n  startBlock: 8000000,\n  sources: {\n    transfers: {\n      type: \"ft_transfer\",\n      assetIdentifier: \"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token\",\n    },\n  },\n  schema: {\n    transfers: {\n      columns: {\n        amount: { type: \"uint\" },\n        sender: { type: \"principal\" },\n        recipient: { type: \"principal\" },\n      },\n    },\n  },\n  handlers: {\n    transfers: (event, ctx) => {\n      ctx.insert(\"transfers\", { amount: event.amount, sender: event.sender, recipient: event.recipient });\n    },\n  },\n});\n"
}'
```

```json
{
  "ok": true,
  "name": "sbtc-flows",
  "description": null,
  "sources": {
    "transfers": {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  },
  "schema": {
    "transfers": {
      "columns": {
        "amount": {
          "type": "uint"
        },
        "sender": {
          "type": "principal"
        },
        "recipient": {
          "type": "principal"
        }
      }
    }
  },
  "handlerCode": "// secondlayer-stub:@secondlayer/subgraphs\nfunction defineSubgraph(def) {\n  return def;\n}\n\n// <stdin>\nvar stdin_default = defineSubgraph({\n  name: \"sbtc-flows\",\n  startBlock: 8e6,\n  sources: {\n    transfers: {\n      type: \"ft_transfer\",\n      assetIdentifier: \"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token\"\n    }\n  },\n  schema: {\n    transfers: {\n      columns: {\n        amount: { type: \"uint\" },\n        sender: { type: \"principal\" },\n        recipient: { type: \"principal\" }\n      }\n    }\n  },\n  handlers: {\n    transfers: (event, ctx) => {\n      ctx.insert(\"transfers\", { amount: event.amount, sender: event.sender, recipient: event.recipient });\n    }\n  }\n});\nexport {\n  stdin_default as default\n};\n",
  "sourceCode": "import { defineSubgraph } from \"@secondlayer/subgraphs\";\n\nexport default defineSubgraph({\n  name: \"sbtc-flows\",\n  startBlock: 8000000,\n  sources: {\n    transfers: {\n      type: \"ft_transfer\",\n      assetIdentifier: \"SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token\",\n    },\n  },\n  schema: {\n    transfers: {\n      columns: {\n        amount: { type: \"uint\" },\n        sender: { type: \"principal\" },\n        recipient: { type: \"principal\" },\n      },\n    },\n  },\n  handlers: {\n    transfers: (event, ctx) => {\n      ctx.insert(\"transfers\", { amount: event.amount, sender: event.sender, recipient: event.recipient });\n    },\n  },\n});\n",
  "bundleSize": 724
}
```

## Get a deployed subgraph

`GET /api/subgraphs/{name}`

Definition, health, sync progress and per-table columns and exact row counts. While a reindex or backfill is active, `sync` carries its queue position, event progress and ETA.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

### Returns

One `DeployedSubgraphDetail` object.

### Responses

- `200` The subgraph
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name on this instance (`SUBGRAPH_NOT_FOUND`)

### Example

```bash
curl https://api.secondlayer.tools/api/subgraphs/{name} \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "name": "sbtc-flows",
  "version": "1.0.2",
  "schemaHash": "4b9e1d27c83f0a65e2d7b14c9f06a38e5d21c7b90f4e6a83d15c2b7e09f4a6d1",
  "pin": "7f3c0d5e9b21a4c86f0e3d17b5a92c4e8d61f07a3b9c25e4d8f1a06c7b3e5a91e",
  "verification": {
    "level": "events",
    "reasons": [
      "ft_transfer source \"transfers\" needs event proofs"
    ],
    "unproven": []
  },
  "status": "active",
  "lastProcessedBlock": 9048712,
  "sources": {
    "transfers": {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
    }
  },
  "definition": {
    "name": "sbtc-flows",
    "startBlock": 8000000,
    "sources": {
      "transfers": {
        "type": "ft_transfer",
        "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
      }
    },
    "schema": {
      "transfers": {
        "columns": {
          "amount": {
            "type": "uint"
          },
          "sender": {
            "type": "principal"
          },
          "recipient": {
            "type": "principal"
          }
        }
      }
    }
  },
  "health": {
    "totalProcessed": 48210,
    "totalErrors": 0,
    "errorRate": 0,
    "lastError": null,
    "lastErrorAt": null,
    "emptyMapping": false,
    "violationCount": 0
  },
  "sync": {
    "status": "synced",
    "mode": "sync",
    "startBlock": 8000000,
    "lastProcessedBlock": 9048712,
    "chainTip": 9048712,
    "sourceChainTip": 9048712,
    "targetBlock": 9048712,
    "blocksRemaining": 0,
    "processedBlocks": 1048713,
    "totalBlocks": 1048713,
    "progress": 1,
    "gaps": {
      "count": 0,
      "totalMissingBlocks": 0,
      "ranges": []
    },
    "integrity": "complete"
  },
  "tables": {
    "transfers": {
      "endpoint": "/subgraphs/sbtc-flows/transfers",
      "columns": {
        "amount": {
          "type": "uint"
        },
        "sender": {
          "type": "principal"
        },
        "recipient": {
          "type": "principal"
        },
        "_id": {
          "type": "serial"
        },
        "_block_height": {
          "type": "bigint"
        },
        "_tx_id": {
          "type": "text"
        },
        "_created_at": {
          "type": "timestamp"
        }
      },
      "rowCount": 48138,
      "example": "/subgraphs/sbtc-flows/transfers?_sort=_block_height&_order=desc&_limit=10"
    }
  },
  "createdAt": "2026-09-21T09:14:52.000Z",
  "updatedAt": "2026-09-23T14:02:11.000Z"
}
```

## Delete a subgraph

`DELETE /api/subgraphs/{name}`

Requests cancellation of any queued or running operation, waits up to 30 seconds for the processor to release it, then drops the Postgres schema and the registry row. Irreversible.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `force` | boolean | Accepted and logged. The delete proceeds after the 30-second wait either way. |

### Responses

- `200` Deleted
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name on this instance (`SUBGRAPH_NOT_FOUND`)
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

### Example

```bash
curl -X DELETE https://api.secondlayer.tools/api/subgraphs/{name} \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "message": "Subgraph \"sbtc-flows\" deleted"
}
```

## Queue a full reindex

`POST /api/subgraphs/{name}/reindex`

Drops the subgraph's tables and rebuilds them from its start block to the chain tip. Takes no body. A `fromBlock`/`toBlock` is refused with 400 `REINDEX_RANGE_NOT_SUPPORTED` rather than ignored, since a ranged walk would destroy everything outside the range; use `backfill` for a range. Poll the returned `operationId`.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

### Returns

One `SubgraphOperationQueued` object.

### Responses

- `200` Reindex queued
- `400` The body carried `fromBlock` or `toBlock` (`REINDEX_RANGE_NOT_SUPPORTED`)
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name on this instance (`SUBGRAPH_NOT_FOUND`)
- `409` A reindex or backfill is already queued or running for this subgraph (`OPERATION_IN_PROGRESS`). Wait for it, or stop it first.
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

### Example

```bash
curl -X POST https://api.secondlayer.tools/api/subgraphs/{name}/reindex \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "message": "Backfill queued for subgraph \"sbtc-flows\"",
  "fromBlock": 8950000,
  "toBlock": 9000000,
  "operationId": "9a4e7c21-0b3d-4f6a-8c15-e2d7b9f0a461",
  "status": "queued"
}
```

## Queue a backfill over a block range

`POST /api/subgraphs/{name}/backfill`

Re-runs handlers over `fromBlock` to `toBlock`, inclusive, without dropping data. A backfill that overlaps an earlier failed or cancelled one resumes from its last committed block. Refused when the handlers apply deltas (`ctx.increment`, `ctx.update`) or read-modify-write rows (`ctx.findOne`, `ctx.findMany`), which would double-count blocks already processed; reindex those instead.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

### Body

| Field | Type | Description |
| --- | --- | --- |
| `fromBlock` | integer, required | First block height to re-run, inclusive. |
| `toBlock` | integer, required | Last block height to re-run, inclusive. |

### Returns

One `SubgraphOperationQueued` object.

### Responses

- `200` Backfill queued
- `400` `fromBlock` or `toBlock` is missing, not a number, or 0 (`VALIDATION_ERROR`)
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name on this instance (`SUBGRAPH_NOT_FOUND`)
- `409` A reindex or backfill is already queued or running for this subgraph (`OPERATION_IN_PROGRESS`). Wait for it, or stop it first.
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`
- `422` The handlers apply deltas and cannot be replayed (`BACKFILL_NON_REPLAYABLE_HANDLER`)

### Example

```bash
curl -X POST https://api.secondlayer.tools/api/subgraphs/{name}/backfill \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  --json '{
  "fromBlock": 8950000,
  "toBlock": 9000000
}'
```

```json
{
  "message": "Backfill queued for subgraph \"sbtc-flows\"",
  "fromBlock": 8950000,
  "toBlock": 9000000,
  "operationId": "9a4e7c21-0b3d-4f6a-8c15-e2d7b9f0a461",
  "status": "queued"
}
```

## Stop the active reindex or backfill

`POST /api/subgraphs/{name}/stop`

Takes no body. Flags the queued or running operation for cancellation; the processor stops it at its next checkpoint and marks it `cancelled`. A stopped backfill resumes from its checkpoint when the same range is queued again.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

### Responses

- `200` Stop requested
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name (`SUBGRAPH_NOT_FOUND`), or nothing queued or running to stop (`NO_OPERATION`)
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

### Example

```bash
curl -X POST https://api.secondlayer.tools/api/subgraphs/{name}/stop \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "message": "Stop requested for \"sbtc-flows\"",
  "operationId": "3f1c9a52-7d4e-4b8a-9e21-5c0d8f6a2b17",
  "status": "cancel_requested"
}
```

## List a subgraph's operations

`GET /api/subgraphs/{name}/operations`

The 20 most recent reindex and backfill operations, newest first, with progress. Queued ones carry an approximate `queuePosition`.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

### Returns

A cursor envelope; `operations` holds `SubgraphOperation` objects.

### Responses

- `200` Recent operations
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name on this instance (`SUBGRAPH_NOT_FOUND`)

### Example

```bash
curl https://api.secondlayer.tools/api/subgraphs/{name}/operations \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "operations": [
    {
      "id": "3f1c9a52-7d4e-4b8a-9e21-5c0d8f6a2b17",
      "subgraphName": "sbtc-flows",
      "kind": "reindex",
      "status": "running",
      "weight": "light",
      "fromBlock": 8000000,
      "toBlock": 9048712,
      "processedBlocks": null,
      "cursorBlock": null,
      "estimatedEvents": 48210,
      "processedEvents": 24105,
      "progress": 0.5,
      "error": null,
      "startedAt": "2026-09-21T09:15:04.000Z",
      "finishedAt": null,
      "createdAt": "2026-09-21T09:14:53.000Z",
      "updatedAt": "2026-09-21T09:18:37.000Z"
    }
  ]
}
```

## Get an operation

`GET /api/subgraphs/{name}/operations/{operation_id}`

One reindex or backfill by id. Poll it after a deploy, reindex or backfill until `status` is `completed`, `failed` or `cancelled`.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |
| `operation_id` | string, required | Operation id (UUID) from a deploy, reindex or backfill response, or from the operations list. |

### Returns

One `SubgraphOperation` object.

### Responses

- `200` The operation
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name (`SUBGRAPH_NOT_FOUND`), or no operation with this id on it (`OPERATION_NOT_FOUND`)

### Example

```bash
curl https://api.secondlayer.tools/api/subgraphs/{name}/operations/{operation_id} \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "id": "3f1c9a52-7d4e-4b8a-9e21-5c0d8f6a2b17",
  "subgraphName": "sbtc-flows",
  "kind": "reindex",
  "status": "running",
  "weight": "light",
  "fromBlock": 8000000,
  "toBlock": 9048712,
  "processedBlocks": null,
  "cursorBlock": null,
  "estimatedEvents": 48210,
  "processedEvents": 24105,
  "progress": 0.5,
  "error": null,
  "startedAt": "2026-09-21T09:15:04.000Z",
  "finishedAt": null,
  "createdAt": "2026-09-21T09:14:53.000Z",
  "updatedAt": "2026-09-21T09:18:37.000Z"
}
```

## List a subgraph's gaps

`GET /api/subgraphs/{name}/gaps`

Block ranges the subgraph failed to process, ordered by start height. Open gaps only by default.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `_offset` | integer | Gaps to skip. Default 0. |
| `resolved` | enum | `true` or `all` include resolved gaps with the open ones. Omit for open gaps only. Values: `true`, `all`. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `_limit` | integer | Page size, 1 to 1000. Default 50. |

### Returns

A cursor envelope; `data` holds `SubgraphGap` objects.

### Responses

- `200` A page of gaps
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name on this instance (`SUBGRAPH_NOT_FOUND`)

### Example

```bash
curl https://api.secondlayer.tools/api/subgraphs/{name}/gaps \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "data": [
    {
      "start": 8412207,
      "end": 8412207,
      "size": 1,
      "reason": "processing_error",
      "detectedAt": "2026-09-22T18:40:03.000Z",
      "resolvedAt": null
    }
  ],
  "meta": {
    "total": 1,
    "totalMissingBlocks": 1,
    "limit": 50,
    "offset": 0
  }
}
```

## Get a subgraph's hourly health

`GET /api/subgraphs/{name}/health`

Hourly buckets from the snapshot the subgraph records every minute: blocks behind the chain tip, rows written, handler errors and events handled. Snapshots are kept 7 days. Oldest hour first; an hour with no snapshots has `lagBlocks: null` and zero counts.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `hours` | integer | Hours to return, 1 to 168. Default 24. |

### Returns

One `SubgraphHealth` object.

### Responses

- `200` Hourly health
- `400` `hours` is not an integer from 1 to 168 (`VALIDATION_ERROR`)
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name on this instance (`SUBGRAPH_NOT_FOUND`)

### Example

```bash
curl https://api.secondlayer.tools/api/subgraphs/{name}/health \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "hours": [
    {
      "hour": "2026-10-16T13:00:00.000Z",
      "lagBlocks": 0,
      "rowsWritten": 1840,
      "errors": 0,
      "eventsHandled": 2213
    }
  ],
  "errors24h": 0
}
```

## List a subgraph's handler errors

`GET /api/subgraphs/{name}/errors`

Handler errors grouped by message and stack frame, most frequent first. A bug that throws on every event is one group with a count, plus a sample transaction to reproduce it.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer | Groups to return, 1 to 50. Default 20. |

### Responses

- `200` Error groups
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name on this instance (`SUBGRAPH_NOT_FOUND`)

### Example

```bash
curl https://api.secondlayer.tools/api/subgraphs/{name}/errors \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "data": [
    {
      "message": "Cannot read properties of undefined (reading 'amount')",
      "frame": "handlers/swap.ts:14:22",
      "eventType": "print",
      "sourceName": "swaps",
      "count": 312,
      "sampleTxId": "0x8d2f6a91c4e07b35d1a8f2e69c0b47d3a5e18f62b9c04d7e3a16f58b2c9d0e41",
      "sampleBlockHeight": 9031477,
      "firstSeen": "2026-10-16T09:02:11.000Z",
      "lastSeen": "2026-10-16T13:41:56.000Z"
    }
  ]
}
```

## List a subgraph's print violations

`GET /api/subgraphs/{name}/violations`

Print events a handler skipped because the payload did not match the source's declared `prints` schema, newest first. The instance keeps the last 100 per subgraph.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `name` | string, required | Subgraph name, as deployed. Lowercase letters, digits and hyphens. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `limit` | integer | Page size, 1 to 100. Default 50. |

### Returns

A cursor envelope; `data` holds `SubgraphViolation` objects.

### Responses

- `200` Recent violations
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No subgraph by this name on this instance (`SUBGRAPH_NOT_FOUND`)

### Example

```bash
curl https://api.secondlayer.tools/api/subgraphs/{name}/violations \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "data": [
    {
      "id": "c41d8e27-6f0a-4b93-a5e2-7d19b3c8f056",
      "sourceName": "swaps",
      "blockHeight": 9031477,
      "txId": "0x8d2f6a91c4e07b35d1a8f2e69c0b47d3a5e18f62b9c04d7e3a16f58b2c9d0e41",
      "reason": "missing declared field \"amount-in\"",
      "samplePayload": {
        "topic": "swap",
        "data": {
          "amount-out": "1500000",
          "sender": "SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9"
        }
      },
      "seenAt": "2026-09-23T11:27:45.000Z"
    }
  ],
  "meta": {
    "total": 1,
    "limit": 50
  }
}
```

---

# webhooks

Webhooks: register a URL, we match, sign, retry and POST (write plane, `/api`)

## The Webhook summary object

A webhook as it appears in the list.

| Field | Type | Description |
| --- | --- | --- |
| `id` | string · uuid | Webhook id. |
| `name` | string | Unique name on this instance. |
| `status` | enum | `active` delivers. `paused` holds new events in the outbox until resumed; the circuit breaker also pauses a webhook after 20 straight failures. `error` is reserved; nothing sets it today. Values: `active`, `paused`, `error`. |
| `kind` | enum | Always `chain`: webhooks fire on raw chain events matched by `triggers`. Values: `chain`. |
| `format` | enum | Payload and signing format. `standard-webhooks` signs with the webhook's secret; the others shape the body for that receiver. Values: `standard-webhooks`, `inngest`, `trigger`, `cloudflare`, `cloudevents`, `raw`. |
| `url` | string | Delivery target. |
| `lastDeliveryAt` | string · date-time · nullable | Last delivery attempt, successful or not. |
| `lastSuccessAt` | string · date-time · nullable | Last delivery the target answered with a 2xx. |
| `circuitOpenedAt` | string · date-time · nullable | When the circuit breaker paused this webhook. Cleared on resume. |
| `circuitFailures` | integer | Consecutive failed deliveries; a success resets it. The circuit opens at 20; resume resets it. |
| `managedBy` | string · nullable | Product that owns this webhook's name, derived from a reserved name prefix (for example `sentinel:` is Sentinel). When set, renaming returns 409 `WEBHOOK_NAME_MANAGED`. `null` for ordinary webhooks. |
| `createdAt` | string · date-time | When the webhook was created. |
| `updatedAt` | string · date-time | Last config or status change. |

```json
{
  "id": "3f1c2a9e-8b7d-4c21-9a55-6e0d4b2f7c18",
  "name": "sbtc-moves",
  "status": "active",
  "kind": "chain",
  "format": "standard-webhooks",
  "url": "https://example.com/webhooks/sbtc",
  "lastDeliveryAt": "2026-09-22T14:03:11.402Z",
  "lastSuccessAt": "2026-09-22T14:03:11.402Z",
  "circuitOpenedAt": null,
  "circuitFailures": 0,
  "managedBy": null,
  "createdAt": "2026-09-20T09:12:45.118Z",
  "updatedAt": "2026-09-20T09:12:45.118Z"
}
```

## The Webhook with its signing secret object

Returned by create and rotate-secret only. The one place the signing secret appears.

| Field | Type | Description |
| --- | --- | --- |
| `webhook` | Webhook |  |
| `signingSecret` | string | Plaintext signing secret, 64 hex characters. Shown once; store it server-side to verify deliveries. |

```json
{
  "webhook": {
    "id": "3f1c2a9e-8b7d-4c21-9a55-6e0d4b2f7c18",
    "name": "sbtc-moves",
    "status": "active",
    "kind": "chain",
    "format": "standard-webhooks",
    "url": "https://example.com/webhooks/sbtc",
    "lastDeliveryAt": "2026-09-22T14:03:11.402Z",
    "lastSuccessAt": "2026-09-22T14:03:11.402Z",
    "circuitOpenedAt": null,
    "circuitFailures": 0,
    "managedBy": null,
    "createdAt": "2026-09-20T09:12:45.118Z",
    "updatedAt": "2026-09-20T09:12:45.118Z",
    "triggers": [
      {
        "type": "ft_transfer",
        "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
        "minAmount": "100000"
      },
      {
        "type": "print_event",
        "contractId": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry"
      }
    ],
    "auth": {
      "type": "none",
      "headerNames": [],
      "hasSecret": false
    },
    "maxRetries": 7,
    "timeoutMs": 10000,
    "concurrency": 4,
    "lastError": null,
    "receiverCertExpiresAt": "2026-12-25T22:56:35.000Z",
    "warning": null
  },
  "signingSecret": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
}
```

## The Webhook object

A webhook's full config and delivery health. Never includes the signing secret.

| Field | Type | Description |
| --- | --- | --- |
| `id` | string · uuid | Webhook id. |
| `name` | string | Unique name on this instance. |
| `status` | enum | `active` delivers. `paused` holds new events in the outbox until resumed; the circuit breaker also pauses a webhook after 20 straight failures. `error` is reserved; nothing sets it today. Values: `active`, `paused`, `error`. |
| `kind` | enum | Always `chain`: webhooks fire on raw chain events matched by `triggers`. Values: `chain`. |
| `format` | enum | Payload and signing format. `standard-webhooks` signs with the webhook's secret; the others shape the body for that receiver. Values: `standard-webhooks`, `inngest`, `trigger`, `cloudflare`, `cloudevents`, `raw`. |
| `url` | string | Delivery target. |
| `lastDeliveryAt` | string · date-time · nullable | Last delivery attempt, successful or not. |
| `lastSuccessAt` | string · date-time · nullable | Last delivery the target answered with a 2xx. |
| `circuitOpenedAt` | string · date-time · nullable | When the circuit breaker paused this webhook. Cleared on resume. |
| `circuitFailures` | integer | Consecutive failed deliveries; a success resets it. The circuit opens at 20; resume resets it. |
| `managedBy` | string · nullable | Product that owns this webhook's name, derived from a reserved name prefix (for example `sentinel:` is Sentinel). When set, renaming returns 409 `WEBHOOK_NAME_MANAGED`. `null` for ordinary webhooks. |
| `createdAt` | string · date-time | When the webhook was created. |
| `updatedAt` | string · date-time | Last config or status change. |
| `triggers` | array of ChainTrigger | Chain triggers. |
| `auth` | object | Receiver credentials, redacted — set with `authConfig` on create or update, but never returned. `authConfig` is write-only. |
| `maxRetries` | integer · 0 to 100 | Attempts before an event moves to the dead-letter queue. Default 7. |
| `timeoutMs` | integer · 100 to 300000 | Per-attempt HTTP timeout in milliseconds. Default 10000. |
| `concurrency` | integer · 1 to 100 | Deliveries in flight at once for this webhook. Default 4. |
| `lastError` | string · nullable | Error from the last failed delivery. |
| `receiverCertExpiresAt` | string · date-time · nullable | When the receiver's TLS certificate expires, from a periodic probe. `null` for http receivers, unreachable hosts, and webhooks not probed yet. |
| `warning` | string · nullable | Set when this instance's chain-trigger evaluator isn't running (`SUBGRAPH_SOURCE` != `streams-index`) — the webhook will never fire until that's fixed. `null` otherwise. |

```json
{
  "id": "3f1c2a9e-8b7d-4c21-9a55-6e0d4b2f7c18",
  "name": "sbtc-moves",
  "status": "active",
  "kind": "chain",
  "format": "standard-webhooks",
  "url": "https://example.com/webhooks/sbtc",
  "lastDeliveryAt": "2026-09-22T14:03:11.402Z",
  "lastSuccessAt": "2026-09-22T14:03:11.402Z",
  "circuitOpenedAt": null,
  "circuitFailures": 0,
  "managedBy": null,
  "createdAt": "2026-09-20T09:12:45.118Z",
  "updatedAt": "2026-09-20T09:12:45.118Z",
  "triggers": [
    {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
      "minAmount": "100000"
    },
    {
      "type": "print_event",
      "contractId": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry"
    }
  ],
  "auth": {
    "type": "none",
    "headerNames": [],
    "hasSecret": false
  },
  "maxRetries": 7,
  "timeoutMs": 10000,
  "concurrency": 4,
  "lastError": null,
  "receiverCertExpiresAt": "2026-12-25T22:56:35.000Z",
  "warning": null
}
```

## The Webhook test result object

Outcome of one test delivery.

| Field | Type | Description |
| --- | --- | --- |
| `ok` | boolean | `true` when the target answered 2xx. |
| `statusCode` | integer · nullable | HTTP status. `null` when no response came back. |
| `error` | string · nullable | Why the delivery failed. `null` on success. |
| `durationMs` | integer | Round trip in milliseconds. |
| `deliveryId` | string · uuid | The logged attempt, as it appears under deliveries. |

```json
{
  "ok": true,
  "statusCode": 200,
  "error": null,
  "durationMs": 138,
  "deliveryId": "c7a0e5d2-1f4b-4e8a-9d36-0b5f2a7c9e14"
}
```

## The Webhook delivery object

One HTTP attempt to deliver an event.

| Field | Type | Description |
| --- | --- | --- |
| `id` | string · uuid | Attempt id. |
| `seq` | integer | Position in the webhook's lifetime log. The newest attempt equals `total`. |
| `attempt` | integer | Which try this was for its event, from 1. |
| `statusCode` | integer · nullable | HTTP status. `null` when no response came back. |
| `blockHeight` | integer · nullable | Block of the delivered event. `null` for test deliveries and for events already compacted out of the outbox. |
| `blockTime` | string · date-time · nullable | When the delivered event's block was produced. `null` for test deliveries, events already compacted out of the outbox, and rows written before this column existed. |
| `errorMessage` | string · nullable | Why the attempt failed. `null` on success. |
| `durationMs` | integer · nullable | Round trip in milliseconds. |
| `responseBody` | string · nullable | Start of the target's response body. |
| `dispatchedAt` | string · date-time | When the attempt was sent. |

```json
{
  "id": "9b2e41c7-5d0a-4f3e-8c61-2a7f90d4e3b5",
  "seq": 4182,
  "attempt": 1,
  "statusCode": 200,
  "blockHeight": 8700076,
  "blockTime": "2026-09-22T14:02:58.000Z",
  "errorMessage": null,
  "durationMs": 142,
  "responseBody": "{\"received\":true}",
  "dispatchedAt": "2026-09-22T14:03:11.402Z"
}
```

## The Dead-lettered webhook event object

An event that used up its retries without a 2xx.

| Field | Type | Description |
| --- | --- | --- |
| `id` | string · uuid | Outbox id. Pass it as `outbox_id` to requeue. |
| `eventType` | string | Delivered `type`, e.g. `chain.ft_transfer.apply`. |
| `attempt` | integer | Attempts made before it was dead-lettered. |
| `blockHeight` | integer | Block the event came from. |
| `txId` | string · nullable | Transaction behind the event, when there is one. |
| `payload` | object | The event body that failed to deliver. |
| `failedAt` | string · date-time · nullable | When the last attempt failed. |
| `createdAt` | string · date-time | When the event was queued. |

```json
{
  "id": "5e8d3b17-a2c4-4f90-b6e1-7d0c29f4a853",
  "eventType": "chain.ft_transfer.apply",
  "attempt": 7,
  "blockHeight": 8700076,
  "txId": "0x839ef4720020f5aba341630e8965851f8537bc8fc99a856091093780c886a8be",
  "payload": {
    "action": "apply",
    "block_hash": "0xf9ff01e876f9276abc9f0fc79a4fbefc500bf2b93ed1effd0ef637ef84e10604",
    "block_height": 8700076,
    "block_time": "2026-05-01T11:59:48.000Z",
    "tx_id": "0x839ef4720020f5aba341630e8965851f8537bc8fc99a856091093780c886a8be",
    "canonical": true,
    "trigger": "ft_transfer",
    "event": {
      "tx_id": "0x839ef4720020f5aba341630e8965851f8537bc8fc99a856091093780c886a8be",
      "type": "ft_transfer_event",
      "event_index": 45,
      "data": {
        "amount": "764749",
        "sender": "SM1FKXGNZJWSTWDWXQZJNF7B5TV5ZB235JTCXYXKD.dlmm-pool-stx-sbtc-v-1-bps-15",
        "recipient": "SP1GBRTAXY96ZDYRQY4GR0M9JTXVYD2FGFRGV60FJ",
        "asset_identifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
      }
    }
  },
  "failedAt": "2026-09-22T16:41:02.877Z",
  "createdAt": "2026-09-22T14:03:10.915Z"
}
```

## The Webhook replay object

What a replay queued.

| Field | Type | Description |
| --- | --- | --- |
| `replayId` | string | 16 hex characters, derived from webhook, range and `force`. The same inputs give the same id. |
| `enqueuedCount` | integer | Events queued. Lower than matches when part of the range was replayed before. |
| `scannedCount` | integer | Rows or events examined in the range. |

```json
{
  "replayId": "a41f09c3d27e8b65",
  "enqueuedCount": 312,
  "scannedCount": 4870
}
```

## The Webhook replay progress object

Live state of one replay. While it runs, `delivered` and `gaveUp` are `null`; they are set once no event is left pending.

| Field | Type | Description |
| --- | --- | --- |
| `replayId` | string | 16 hex characters. |
| `webhookId` | string | Webhook id (UUID). |
| `status` | enum | `scanning` while the range is read, `running` while events deliver, `done` when none are pending, `failed` if the scan broke. Values: `scanning`, `running`, `done`, `failed`. |
| `fromBlock` | integer | First block, inclusive. |
| `toBlock` | integer | Last block, inclusive. |
| `total` | integer | Events the replay queued. |
| `processed` | integer | Events delivered or given up on: `total` minus `pending`. |
| `pending` | integer | Events still waiting to be delivered or retried. |
| `delivered` | integer · nullable | Events delivered. `null` until the replay is done. |
| `gaveUp` | integer · nullable | Events that used up their retries. `null` until the replay is done. |
| `startedAt` | string | ISO 8601 time it started. |
| `finishedAt` | string · nullable | ISO 8601 time it finished. `null` while it runs. |
| `error` | string · nullable | Why the scan failed. `null` otherwise. |

```json
{
  "replayId": "a41f09c3d27e8b65",
  "webhookId": "3f1c2a9e-8b7d-4c21-9a55-6e0d4b2f7c18",
  "status": "done",
  "fromBlock": 8650000,
  "toBlock": 8700000,
  "total": 312,
  "processed": 312,
  "pending": 0,
  "delivered": 309,
  "gaveUp": 3,
  "startedAt": "2026-09-22T14:00:02.000Z",
  "finishedAt": "2026-09-22T14:06:40.000Z",
  "error": null
}
```

## List webhooks

`GET /api/webhooks`

Every webhook on this instance, newest first, with status and last delivery times. Paged by offset.

### Filters

| Name | Type | Description |
| --- | --- | --- |
| `_offset` | integer | Rows to skip. Default 0. |

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `_limit` | integer | Page size, 1 to 200. Default 50. |

### Returns

A cursor envelope; `data` holds `WebhookSummary` objects.

### Responses

- `200` One page of webhooks
- `401` This bind requires the instance token, and it was missing or wrong

### Example

```bash
curl https://api.secondlayer.tools/api/webhooks \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "data": [
    {
      "id": "3f1c2a9e-8b7d-4c21-9a55-6e0d4b2f7c18",
      "name": "sbtc-moves",
      "status": "active",
      "kind": "chain",
      "format": "standard-webhooks",
      "url": "https://example.com/webhooks/sbtc",
      "lastDeliveryAt": "2026-09-22T14:03:11.402Z",
      "lastSuccessAt": "2026-09-22T14:03:11.402Z",
      "circuitOpenedAt": null,
      "circuitFailures": 0,
      "managedBy": null,
      "createdAt": "2026-09-20T09:12:45.118Z",
      "updatedAt": "2026-09-20T09:12:45.118Z"
    }
  ]
}
```

## Create a webhook

`POST /api/webhooks`

Takes `triggers` and starts at the chain tip, with no backfill. The response carries the signing secret. This is the only time it is returned, so store it now; `rotate-secret` issues a new one.

### Body

| Field | Type | Description |
| --- | --- | --- |
| `name` | string, required | Unique name on this instance. |
| `url` | string, required | Delivery target, `http://` or `https://`. Private addresses are refused at delivery time. |
| `triggers` | array of ChainTrigger, required | 1 to 50 triggers. An event matching any of them is delivered. |
| `format` | enum | Payload and signing format. `standard-webhooks` signs with the webhook's secret; the others shape the body for that receiver. |
| `authConfig` | object | Per-format receiver credentials: `token` / `tokenEnc` for `trigger` and `cloudflare`, `headers` or `basicAuth` for `raw`. Write-only — never returned; a read gets `auth` instead. |
| `maxRetries` | integer · 0 to 100 | Attempts before an event moves to the dead-letter queue. Default 7. |
| `timeoutMs` | integer · 100 to 300000 | Per-attempt HTTP timeout in milliseconds. Default 10000. |
| `concurrency` | integer · 1 to 100 | Deliveries in flight at once for this webhook. Default 4. |

### Returns

One `WebhookWithSecret` object.

### Responses

- `201` Created. The body carries the signing secret, once
- `400` A parameter, filter, cursor or body field was refused; `code` says which. A body carrying `subgraphName`, `tableName` or `filter` is answered with `SUBGRAPH_WEBHOOKS_REMOVED`: subgraph webhooks were removed
- `401` This bind requires the instance token, and it was missing or wrong
- `409` A webhook with that name already exists
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`
- `500` The webhook could not be stored (`INTERNAL_ERROR`)

### Example

```bash
curl -X POST https://api.secondlayer.tools/api/webhooks \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  --json '{
  "name": "sbtc-moves",
  "url": "https://example.com/webhooks/sbtc",
  "triggers": [
    {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
      "minAmount": "100000"
    },
    {
      "type": "print_event",
      "contractId": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry"
    }
  ]
}'
```

```json
{
  "webhook": {
    "id": "3f1c2a9e-8b7d-4c21-9a55-6e0d4b2f7c18",
    "name": "sbtc-moves",
    "status": "active",
    "kind": "chain",
    "format": "standard-webhooks",
    "url": "https://example.com/webhooks/sbtc",
    "lastDeliveryAt": "2026-09-22T14:03:11.402Z",
    "lastSuccessAt": "2026-09-22T14:03:11.402Z",
    "circuitOpenedAt": null,
    "circuitFailures": 0,
    "managedBy": null,
    "createdAt": "2026-09-20T09:12:45.118Z",
    "updatedAt": "2026-09-20T09:12:45.118Z",
    "triggers": [
      {
        "type": "ft_transfer",
        "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
        "minAmount": "100000"
      },
      {
        "type": "print_event",
        "contractId": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry"
      }
    ],
    "auth": {
      "type": "none",
      "headerNames": [],
      "hasSecret": false
    },
    "maxRetries": 7,
    "timeoutMs": 10000,
    "concurrency": 4,
    "lastError": null,
    "receiverCertExpiresAt": "2026-12-25T22:56:35.000Z",
    "warning": null
  },
  "signingSecret": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
}
```

## Get a webhook

`GET /api/webhooks/{id}`

Full config and delivery health for one webhook. Never includes the signing secret.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Webhook id (UUID), as returned by create or list. |

### Returns

One `Webhook` object.

### Responses

- `200` The webhook
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No webhook with this id

### Example

```bash
curl https://api.secondlayer.tools/api/webhooks/{id} \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "id": "3f1c2a9e-8b7d-4c21-9a55-6e0d4b2f7c18",
  "name": "sbtc-moves",
  "status": "active",
  "kind": "chain",
  "format": "standard-webhooks",
  "url": "https://example.com/webhooks/sbtc",
  "lastDeliveryAt": "2026-09-22T14:03:11.402Z",
  "lastSuccessAt": "2026-09-22T14:03:11.402Z",
  "circuitOpenedAt": null,
  "circuitFailures": 0,
  "managedBy": null,
  "createdAt": "2026-09-20T09:12:45.118Z",
  "updatedAt": "2026-09-20T09:12:45.118Z",
  "triggers": [
    {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
      "minAmount": "100000"
    },
    {
      "type": "print_event",
      "contractId": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry"
    }
  ],
  "auth": {
    "type": "none",
    "headerNames": [],
    "hasSecret": false
  },
  "maxRetries": 7,
  "timeoutMs": 10000,
  "concurrency": 4,
  "lastError": null,
  "receiverCertExpiresAt": "2026-12-25T22:56:35.000Z",
  "warning": null
}
```

## Update a webhook

`PATCH /api/webhooks/{id}`

Change any subset of fields; at least one is required. `triggers` are fixed at create: to change them, create a new webhook.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Webhook id (UUID), as returned by create or list. |

### Body

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | New unique name. |
| `url` | string | New delivery target, `http://` or `https://`. |
| `format` | enum | Payload and signing format. `standard-webhooks` signs with the webhook's secret; the others shape the body for that receiver. |
| `authConfig` | object | Per-format receiver credentials: `token` / `tokenEnc` for `trigger` and `cloudflare`, `headers` or `basicAuth` for `raw`. Write-only — never returned; a read gets `auth` instead. |
| `maxRetries` | integer · 0 to 100 | Attempts before an event moves to the dead-letter queue. Default 7. |
| `timeoutMs` | integer · 100 to 300000 | Per-attempt HTTP timeout in milliseconds. Default 10000. |
| `concurrency` | integer · 1 to 100 | Deliveries in flight at once for this webhook. Default 4. |

### Returns

One `Webhook` object.

### Responses

- `200` The updated webhook
- `400` A parameter, filter, cursor or body field was refused; `code` says which. A body carrying `subgraphName`, `tableName` or `filter` is answered with `SUBGRAPH_WEBHOOKS_REMOVED`: subgraph webhooks were removed
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No webhook with this id
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

### Example

```bash
curl -X PATCH https://api.secondlayer.tools/api/webhooks/{id} \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  --json '{
  "url": "https://example.com/webhooks/sbtc-v2",
  "maxRetries": 10
}'
```

```json
{
  "id": "3f1c2a9e-8b7d-4c21-9a55-6e0d4b2f7c18",
  "name": "sbtc-moves",
  "status": "active",
  "kind": "chain",
  "format": "standard-webhooks",
  "url": "https://example.com/webhooks/sbtc",
  "lastDeliveryAt": "2026-09-22T14:03:11.402Z",
  "lastSuccessAt": "2026-09-22T14:03:11.402Z",
  "circuitOpenedAt": null,
  "circuitFailures": 0,
  "managedBy": null,
  "createdAt": "2026-09-20T09:12:45.118Z",
  "updatedAt": "2026-09-20T09:12:45.118Z",
  "triggers": [
    {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
      "minAmount": "100000"
    },
    {
      "type": "print_event",
      "contractId": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry"
    }
  ],
  "auth": {
    "type": "none",
    "headerNames": [],
    "hasSecret": false
  },
  "maxRetries": 7,
  "timeoutMs": 10000,
  "concurrency": 4,
  "lastError": null,
  "receiverCertExpiresAt": "2026-12-25T22:56:35.000Z",
  "warning": null
}
```

## Delete a webhook

`DELETE /api/webhooks/{id}`

Removes the webhook. Nothing more is delivered to its URL. Cannot be undone.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Webhook id (UUID), as returned by create or list. |

### Responses

- `200` Deleted
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No webhook with this id
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

### Example

```bash
curl -X DELETE https://api.secondlayer.tools/api/webhooks/{id} \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "ok": true
}
```

## Pause a webhook

`POST /api/webhooks/{id}/pause`

Sets `status` to `paused`. Matched events keep queueing and deliver on resume. Takes no body.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Webhook id (UUID), as returned by create or list. |

### Returns

One `Webhook` object.

### Responses

- `200` The paused webhook
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No webhook with this id
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

### Example

```bash
curl -X POST https://api.secondlayer.tools/api/webhooks/{id}/pause \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "id": "3f1c2a9e-8b7d-4c21-9a55-6e0d4b2f7c18",
  "name": "sbtc-moves",
  "status": "active",
  "kind": "chain",
  "format": "standard-webhooks",
  "url": "https://example.com/webhooks/sbtc",
  "lastDeliveryAt": "2026-09-22T14:03:11.402Z",
  "lastSuccessAt": "2026-09-22T14:03:11.402Z",
  "circuitOpenedAt": null,
  "circuitFailures": 0,
  "managedBy": null,
  "createdAt": "2026-09-20T09:12:45.118Z",
  "updatedAt": "2026-09-20T09:12:45.118Z",
  "triggers": [
    {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
      "minAmount": "100000"
    },
    {
      "type": "print_event",
      "contractId": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry"
    }
  ],
  "auth": {
    "type": "none",
    "headerNames": [],
    "hasSecret": false
  },
  "maxRetries": 7,
  "timeoutMs": 10000,
  "concurrency": 4,
  "lastError": null,
  "receiverCertExpiresAt": "2026-12-25T22:56:35.000Z",
  "warning": null
}
```

## Resume a webhook

`POST /api/webhooks/{id}/resume`

Sets `status` to `active` and resets the circuit breaker (`circuitFailures`, `circuitOpenedAt`). Queued events start delivering. Takes no body.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Webhook id (UUID), as returned by create or list. |

### Returns

One `Webhook` object.

### Responses

- `200` The resumed webhook
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No webhook with this id
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

### Example

```bash
curl -X POST https://api.secondlayer.tools/api/webhooks/{id}/resume \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "id": "3f1c2a9e-8b7d-4c21-9a55-6e0d4b2f7c18",
  "name": "sbtc-moves",
  "status": "active",
  "kind": "chain",
  "format": "standard-webhooks",
  "url": "https://example.com/webhooks/sbtc",
  "lastDeliveryAt": "2026-09-22T14:03:11.402Z",
  "lastSuccessAt": "2026-09-22T14:03:11.402Z",
  "circuitOpenedAt": null,
  "circuitFailures": 0,
  "managedBy": null,
  "createdAt": "2026-09-20T09:12:45.118Z",
  "updatedAt": "2026-09-20T09:12:45.118Z",
  "triggers": [
    {
      "type": "ft_transfer",
      "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
      "minAmount": "100000"
    },
    {
      "type": "print_event",
      "contractId": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry"
    }
  ],
  "auth": {
    "type": "none",
    "headerNames": [],
    "hasSecret": false
  },
  "maxRetries": 7,
  "timeoutMs": 10000,
  "concurrency": 4,
  "lastError": null,
  "receiverCertExpiresAt": "2026-12-25T22:56:35.000Z",
  "warning": null
}
```

## Rotate the signing secret

`POST /api/webhooks/{id}/rotate-secret`

Issues a new signing secret and returns it, once. Deliveries signed with the old secret stop verifying immediately, so update your receiver right away. Takes no body.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Webhook id (UUID), as returned by create or list. |

### Returns

One `WebhookWithSecret` object.

### Responses

- `200` The webhook and its new signing secret
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No webhook with this id
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

### Example

```bash
curl -X POST https://api.secondlayer.tools/api/webhooks/{id}/rotate-secret \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "webhook": {
    "id": "3f1c2a9e-8b7d-4c21-9a55-6e0d4b2f7c18",
    "name": "sbtc-moves",
    "status": "active",
    "kind": "chain",
    "format": "standard-webhooks",
    "url": "https://example.com/webhooks/sbtc",
    "lastDeliveryAt": "2026-09-22T14:03:11.402Z",
    "lastSuccessAt": "2026-09-22T14:03:11.402Z",
    "circuitOpenedAt": null,
    "circuitFailures": 0,
    "managedBy": null,
    "createdAt": "2026-09-20T09:12:45.118Z",
    "updatedAt": "2026-09-20T09:12:45.118Z",
    "triggers": [
      {
        "type": "ft_transfer",
        "assetIdentifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token",
        "minAmount": "100000"
      },
      {
        "type": "print_event",
        "contractId": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-registry"
      }
    ],
    "auth": {
      "type": "none",
      "headerNames": [],
      "hasSecret": false
    },
    "maxRetries": 7,
    "timeoutMs": 10000,
    "concurrency": 4,
    "lastError": null,
    "receiverCertExpiresAt": "2026-12-25T22:56:35.000Z",
    "warning": null
  },
  "signingSecret": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
}
```

## Send a test delivery

`POST /api/webhooks/{id}/test`

Posts one sample event, built and signed in the webhook's format, to its URL through the SSRF guard, and logs it under deliveries. The body has `data.test: true`, so receivers can tell it from a live event. Works while paused. Takes no body.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Webhook id (UUID), as returned by create or list. |

### Returns

One `WebhookTestResult` object.

### Responses

- `200` What the target answered. A failed delivery still returns 200; check `ok`
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No webhook with this id
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

### Example

```bash
curl -X POST https://api.secondlayer.tools/api/webhooks/{id}/test \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "ok": true,
  "statusCode": 200,
  "error": null,
  "durationMs": 138,
  "deliveryId": "c7a0e5d2-1f4b-4e8a-9d36-0b5f2a7c9e14"
}
```

## List delivery attempts

`GET /api/webhooks/{id}/deliveries`

The last 100 delivery attempts, newest first, including test deliveries. `total` counts every attempt ever logged for the webhook. With `from` and `to`, only attempts dispatched in that window; `total` counts the window.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Webhook id (UUID), as returned by create or list. |

### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `from` | string | Window start, inclusive, ISO 8601. Needs `to`; at most 31 days before it. |
| `to` | string | Window end, exclusive, ISO 8601. Needs `from`. |

### Returns

A cursor envelope; `data` holds `WebhookDelivery` objects.

### Responses

- `200` Recent delivery attempts
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No webhook with this id

### Example

```bash
curl https://api.secondlayer.tools/api/webhooks/{id}/deliveries \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "total": 4182,
  "data": [
    {
      "id": "9b2e41c7-5d0a-4f3e-8c61-2a7f90d4e3b5",
      "seq": 4182,
      "attempt": 1,
      "statusCode": 200,
      "blockHeight": 8700076,
      "blockTime": "2026-09-22T14:02:58.000Z",
      "errorMessage": null,
      "durationMs": 142,
      "responseBody": "{\"received\":true}",
      "dispatchedAt": "2026-09-22T14:03:11.402Z"
    }
  ]
}
```

## List dead-lettered events

`GET /api/webhooks/{id}/dead`

Events that used up `maxRetries` without a 2xx, newest failure first, up to 100. Requeue one to try it again.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Webhook id (UUID), as returned by create or list. |

### Returns

A cursor envelope; `data` holds `WebhookDeadEvent` objects.

### Responses

- `200` Dead-lettered events
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No webhook with this id

### Example

```bash
curl https://api.secondlayer.tools/api/webhooks/{id}/dead \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "data": [
    {
      "id": "5e8d3b17-a2c4-4f90-b6e1-7d0c29f4a853",
      "eventType": "chain.ft_transfer.apply",
      "attempt": 7,
      "blockHeight": 8700076,
      "txId": "0x839ef4720020f5aba341630e8965851f8537bc8fc99a856091093780c886a8be",
      "payload": {
        "action": "apply",
        "block_hash": "0xf9ff01e876f9276abc9f0fc79a4fbefc500bf2b93ed1effd0ef637ef84e10604",
        "block_height": 8700076,
        "block_time": "2026-05-01T11:59:48.000Z",
        "tx_id": "0x839ef4720020f5aba341630e8965851f8537bc8fc99a856091093780c886a8be",
        "canonical": true,
        "trigger": "ft_transfer",
        "event": {
          "tx_id": "0x839ef4720020f5aba341630e8965851f8537bc8fc99a856091093780c886a8be",
          "type": "ft_transfer_event",
          "event_index": 45,
          "data": {
            "amount": "764749",
            "sender": "SM1FKXGNZJWSTWDWXQZJNF7B5TV5ZB235JTCXYXKD.dlmm-pool-stx-sbtc-v-1-bps-15",
            "recipient": "SP1GBRTAXY96ZDYRQY4GR0M9JTXVYD2FGFRGV60FJ",
            "asset_identifier": "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token::sbtc-token"
          }
        }
      },
      "failedAt": "2026-09-22T16:41:02.877Z",
      "createdAt": "2026-09-22T14:03:10.915Z"
    }
  ]
}
```

## Requeue a dead event

`POST /api/webhooks/{id}/dead/{outbox_id}/requeue`

Moves one dead event back to pending with its attempt count reset. It delivers at live priority, even if it came from a replay. Takes no body.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Webhook id (UUID), as returned by create or list. |
| `outbox_id` | string, required | Dead event id (UUID), the `id` from the dead-letter list. |

### Responses

- `200` Requeued
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No webhook with this id, or no dead event with this id on it
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

### Example

```bash
curl -X POST https://api.secondlayer.tools/api/webhooks/{id}/dead/{outbox_id}/requeue \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "ok": true
}
```

## Replay a block range

`POST /api/webhooks/{id}/replay`

Queues historical events in a block range for redelivery, capped at 100,000 blocks. Replays are idempotent: the same range yields the same `replayId` and delivers nothing twice unless `force` is set. They drain through a 10% share of the outbox so live traffic keeps priority, and never move the live cursor. Replays deliver the normal `chain.<type>.apply` envelope. Watch one with `GET /api/webhooks/{id}/replays/{replay_id}`.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Webhook id (UUID), as returned by create or list. |

### Body

| Field | Type | Description |
| --- | --- | --- |
| `fromBlock` | integer, required | First block, inclusive. |
| `toBlock` | integer, required | Last block, inclusive. At most 100,000 blocks after `fromBlock`. |
| `force` | string | Any string, up to 64 characters. Makes a new `replayId`, so an already-replayed range delivers again. |

### Returns

One `WebhookReplayResult` object.

### Responses

- `202` Replay queued
- `400` A parameter, filter, cursor or body field was refused; `code` says which. Also: `fromBlock` after `toBlock`, or a range over 100,000 blocks
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No webhook with this id
- `409` A replay of this webhook is still scanning its range
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`
- `500` The replay could not be queued (`INTERNAL_ERROR`)

### Example

```bash
curl -X POST https://api.secondlayer.tools/api/webhooks/{id}/replay \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  --json '{
  "fromBlock": 8650000,
  "toBlock": 8700000
}'
```

```json
{
  "replayId": "a41f09c3d27e8b65",
  "enqueuedCount": 312,
  "scannedCount": 4870
}
```

## List recent replays across webhooks

`GET /api/webhooks/replays`

Replays still scanning or delivering, plus any started in the last 24 hours, across every webhook on this instance. Newest first.

### Range and paging

| Name | Type | Description |
| --- | --- | --- |
| `_limit` | integer | Page size, 1 to 50. Default 20. |

### Returns

A cursor envelope; `data` holds `WebhookReplayProgress` objects.

### Responses

- `200` Recent replays with live progress
- `401` This bind requires the instance token, and it was missing or wrong

### Example

```bash
curl https://api.secondlayer.tools/api/webhooks/replays \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "data": [
    {
      "replayId": "a41f09c3d27e8b65",
      "webhookId": "3f1c2a9e-8b7d-4c21-9a55-6e0d4b2f7c18",
      "status": "done",
      "fromBlock": 8650000,
      "toBlock": 8700000,
      "total": 312,
      "processed": 312,
      "pending": 0,
      "delivered": 309,
      "gaveUp": 3,
      "startedAt": "2026-09-22T14:00:02.000Z",
      "finishedAt": "2026-09-22T14:06:40.000Z",
      "error": null
    }
  ]
}
```

## List a webhook's replays and replay ranges

`GET /api/webhooks/{id}/replays`

The webhook's last 10 replays with live progress, newest first, and the block ranges a new replay can start from. Ranges count back from the last block the evaluator processed.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Webhook id (UUID), as returned by create or list. |

### Returns

A cursor envelope; `data` holds `WebhookReplayProgress` objects.

### Responses

- `200` Replays and ranges for this webhook
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No webhook with this id

### Example

```bash
curl https://api.secondlayer.tools/api/webhooks/{id}/replays \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "data": [
    {
      "replayId": "a41f09c3d27e8b65",
      "webhookId": "3f1c2a9e-8b7d-4c21-9a55-6e0d4b2f7c18",
      "status": "done",
      "fromBlock": 8650000,
      "toBlock": 8700000,
      "total": 312,
      "processed": 312,
      "pending": 0,
      "delivered": 309,
      "gaveUp": 3,
      "startedAt": "2026-09-22T14:00:02.000Z",
      "finishedAt": "2026-09-22T14:06:40.000Z",
      "error": null
    }
  ],
  "ranges": {
    "tip": 8700076,
    "presets": [
      {
        "blocks": 100,
        "fromBlock": 8699977,
        "toBlock": 8700076,
        "knownEvents": 4
      },
      {
        "blocks": 1000,
        "fromBlock": 8699077,
        "toBlock": 8700076,
        "knownEvents": 31
      },
      {
        "blocks": 10000,
        "fromBlock": 8690077,
        "toBlock": 8700076,
        "knownEvents": 305
      }
    ],
    "deadSinceFailing": {
      "since": "2026-09-22T13:10:00.000Z",
      "count": 3
    }
  }
}
```

## Get a replay's progress

`GET /api/webhooks/{id}/replays/{replay_id}`

Where one replay stands: how many events it queued, how many are done, and once nothing is left pending, how many delivered and how many gave up. Poll it while the replay drains.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Webhook id (UUID), as returned by create or list. |
| `replay_id` | string, required | Replay id, the `replayId` from the replay response (16 hex characters). |

### Returns

One `WebhookReplayProgress` object.

### Responses

- `200` The replay's progress
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No webhook with this id, or no replay with this id on it

### Example

```bash
curl https://api.secondlayer.tools/api/webhooks/{id}/replays/{replay_id} \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "replayId": "a41f09c3d27e8b65",
  "webhookId": "3f1c2a9e-8b7d-4c21-9a55-6e0d4b2f7c18",
  "status": "done",
  "fromBlock": 8650000,
  "toBlock": 8700000,
  "total": 312,
  "processed": 312,
  "pending": 0,
  "delivered": 309,
  "gaveUp": 3,
  "startedAt": "2026-09-22T14:00:02.000Z",
  "finishedAt": "2026-09-22T14:06:40.000Z",
  "error": null
}
```

## Requeue dead events in bulk

`POST /api/webhooks/{id}/dead/requeue`

Moves dead events back to pending with their attempt counts reset, at live priority. With no body it requeues every dead event of the webhook; `replayId` narrows it to one replay and `since` to events that failed at or after that time. A replay that gets events back returns to running, so its progress tracks the retry.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Webhook id (UUID), as returned by create or list. |

### Body

| Field | Type | Description |
| --- | --- | --- |
| `replayId` | string | Only dead events created by this replay. |
| `since` | string · date-time | Only dead events that failed at or after this time. |

### Responses

- `200` Events requeued
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No webhook with this id
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

### Example

```bash
curl -X POST https://api.secondlayer.tools/api/webhooks/{id}/dead/requeue \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  --json '{
  "replayId": "a41f09c3d27e8b65"
}'
```

```json
{
  "requeued": 14
}
```

## List insights

`GET /api/insights`

Ranked findings about this instance's webhooks and subgraphs: losing events first, then about to fail, then costing money, then recently recovered. Dismissed and expected insights are left out.

### Query parameters

| Name | Type | Description |
| --- | --- | --- |
| `resource` | string | Only one resource, as `<kind>:<id>`, such as `webhook:<id>` or `subgraph:<name>`. |

### Responses

- `200` Insights in rank order
- `400` `resource` is not `<kind>:<id>` (`VALIDATION_ERROR`)
- `401` This bind requires the instance token, and it was missing or wrong

### Example

```bash
curl https://api.secondlayer.tools/api/insights \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "data": [
    {
      "id": "9b0f6f1e-3c1a-4f55-8d64-0c2f7a5e1b30",
      "rule": "receiver_down",
      "severity": "losing",
      "impact": 214,
      "resource": {
        "kind": "webhook",
        "id": "3f1c2a9e-8b7d-4c21-9a55-6e0d4b2f7c18",
        "name": "sbtc-moves"
      },
      "line": "sbtc-moves has failed 12 deliveries in a row",
      "meta": "since 16:06 UTC",
      "lead": "Your receiver is down, not us.",
      "body": "hooks.example.dev returns HTTP 502 on every delivery since 16:06 UTC. 214 deliveries queued to retry.",
      "evidence": [
        {
          "kind": "shapes",
          "title": "What the receiver sent back",
          "shapes": [
            {
              "code": "502",
              "body": "Bad Gateway",
              "count": 12,
              "isNew": true
            }
          ]
        }
      ],
      "handoff": {
        "prompt": "My Secondlayer webhook \"sbtc-moves\" has been failing since 16:06 UTC. The receiver hooks.example.dev returns HTTP 502 on every delivery. Check the service behind hooks.example.dev. Then run sl webhooks doctor sbtc-moves.",
        "commands": [
          "sl webhooks doctor sbtc-moves"
        ]
      },
      "how": {
        "detector": "the newest 5 or more delivery attempts are 5xx or got no response",
        "facts": [
          [
            "consecutive failures",
            "12"
          ]
        ]
      },
      "createdAt": "2026-10-15T16:07:00.000Z",
      "resolvedAt": null
    }
  ]
}
```

## Dismiss an insight

`POST /api/insights/{id}/dismiss`

Hides this incident. If the same rule fires again after it clears, the new incident shows. Takes no body.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Insight id (UUID), as returned by the insights list. |

### Responses

- `200` Dismissed
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No insight with this id
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

### Example

```bash
curl -X POST https://api.secondlayer.tools/api/insights/{id}/dismiss \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "ok": true
}
```

## Undo dismissing an insight

`DELETE /api/insights/{id}/dismiss`

Shows a dismissed insight again.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Insight id (UUID), as returned by the insights list. |

### Responses

- `200` Shown again
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No insight with this id
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

### Example

```bash
curl -X DELETE https://api.secondlayer.tools/api/insights/{id}/dismiss \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "ok": true
}
```

## Snooze an insight until a time

`POST /api/insights/{id}/snooze`

Hides this insight until the time you give, at most 7 days ahead. It shows again then, or earlier if you remove the snooze.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Insight id (UUID), as returned by the insights list. |

### Body

| Field | Type | Description |
| --- | --- | --- |
| `until` | string · date-time, required | When it shows again. Must be in the next 7 days. |

### Responses

- `200` Snoozed
- `400` `until` is not a time in the next 7 days (`VALIDATION_ERROR`)
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No insight with this id
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

### Example

```bash
curl -X POST https://api.secondlayer.tools/api/insights/{id}/snooze \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY" \
  --json '{
  "until": "2026-10-19T09:00:00.000Z"
}'
```

```json
{
  "ok": true
}
```

## Remove an insight's snooze

`DELETE /api/insights/{id}/snooze`

Shows a snoozed insight again now.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Insight id (UUID), as returned by the insights list. |

### Responses

- `200` Shown again
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No insight with this id
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

### Example

```bash
curl -X DELETE https://api.secondlayer.tools/api/insights/{id}/snooze \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "ok": true
}
```

## Mark an insight expected

`POST /api/insights/{id}/expected`

Mutes this rule for this resource, including future incidents, until you remove the mute. Takes no body.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Insight id (UUID), as returned by the insights list. |

### Responses

- `200` Muted
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No insight with this id
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

### Example

```bash
curl -X POST https://api.secondlayer.tools/api/insights/{id}/expected \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "ok": true
}
```

## Unmark an insight as expected

`DELETE /api/insights/{id}/expected`

Removes the mute for this rule and resource, so it shows again whenever it fires.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `id` | string, required | Insight id (UUID), as returned by the insights list. |

### Responses

- `200` Unmuted
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong
- `404` No insight with this id
- `415` Missing or non-JSON `Content-Type` (`UNSUPPORTED_MEDIA_TYPE`) — writes must send `Content-Type: application/json`

### Example

```bash
curl -X DELETE https://api.secondlayer.tools/api/insights/{id}/expected \
  -H "Authorization: Bearer $SECONDLAYER_API_KEY"
```

```json
{
  "ok": true
}
```

---

# node

Proxy to the Stacks node this instance follows

## The Contract ABI object

A Clarity contract interface, exactly as the Stacks node's `/v2/contracts/interface` returns it.

| Field | Type | Description |
| --- | --- | --- |
| `functions` | array of object | Every function: `name`, `access` (`public`, `read_only` or `private`), `args` and `outputs`. |
| `variables` | array of object | Data vars and constants: `name`, `type`, and `access` (`variable` or `constant`). |
| `maps` | array of object | Data maps: `name`, `key` type and `value` type. |
| `fungible_tokens` | array of object | Fungible tokens the contract defines, by `name`. |
| `non_fungible_tokens` | array of object | Non-fungible tokens the contract defines: `name` and `type`. |
| `epoch` | string | Stacks epoch the contract was deployed in, e.g. `Epoch2_05`. |
| `clarity_version` | string | Clarity version it was deployed with, e.g. `Clarity1`. |

```json
{
  "functions": [
    {
      "name": "check-is-owner",
      "access": "private",
      "args": [],
      "outputs": {
        "type": {
          "response": {
            "ok": "bool",
            "error": "uint128"
          }
        }
      }
    },
    {
      "name": "start-zeus",
      "access": "public",
      "args": [
        {
          "name": "dx",
          "type": "uint128"
        },
        {
          "name": "min-dy",
          "type": "uint128"
        },
        {
          "name": "offset",
          "type": "uint128"
        }
      ],
      "outputs": {
        "type": {
          "response": {
            "ok": "bool",
            "error": "uint128"
          }
        }
      }
    }
  ],
  "variables": [
    {
      "name": "ERR-NOT-AUTHORIZED",
      "type": {
        "response": {
          "ok": "none",
          "error": "uint128"
        }
      },
      "access": "constant"
    },
    {
      "name": "contract-owner",
      "type": "principal",
      "access": "variable"
    }
  ],
  "maps": [],
  "fungible_tokens": [],
  "non_fungible_tokens": [],
  "epoch": "Epoch2_05",
  "clarity_version": "Clarity1"
}
```

## Contract ABI from the local Stacks node

`GET /api/node/contracts/{contract_id}/abi` (self-hosted instances only)

Fetches the contract's interface from the Stacks node this instance follows (`/v2/contracts/interface`) and returns it unchanged. Nothing is cached. Self-hosted only; needs the instance token whenever one is set.

### Path parameters

| Name | Type | Description |
| --- | --- | --- |
| `contract_id` | string, required | Contract principal, `<address>.<name>`. A value without a `.` does not match the route and gets 404. |

### Returns

One `ContractAbi` object.

### Responses

- `200` The contract's ABI as the node reports it
- `401` This bind requires the instance token, and it was missing or wrong
- `404` The node has no contract with that id, or the id has no `.`
- `502` The node could not be reached or answered with an error; `error` carries its message

### Example

```bash
curl http://127.0.0.1:3800/api/node/contracts/SP3D23QJG4S1G6TBXSPFR0N3PMF53WXA50XTX9W0M.zeus-a/abi \
  -H "Authorization: Bearer $INSTANCE_TOKEN"
```

```json
{
  "functions": [
    {
      "name": "check-is-owner",
      "access": "private",
      "args": [],
      "outputs": {
        "type": {
          "response": {
            "ok": "bool",
            "error": "uint128"
          }
        }
      }
    },
    {
      "name": "start-zeus",
      "access": "public",
      "args": [
        {
          "name": "dx",
          "type": "uint128"
        },
        {
          "name": "min-dy",
          "type": "uint128"
        },
        {
          "name": "offset",
          "type": "uint128"
        }
      ],
      "outputs": {
        "type": {
          "response": {
            "ok": "bool",
            "error": "uint128"
          }
        }
      }
    }
  ],
  "variables": [
    {
      "name": "ERR-NOT-AUTHORIZED",
      "type": {
        "response": {
          "ok": "none",
          "error": "uint128"
        }
      },
      "access": "constant"
    },
    {
      "name": "contract-owner",
      "type": "principal",
      "access": "variable"
    }
  ],
  "maps": [],
  "fungible_tokens": [],
  "non_fungible_tokens": [],
  "epoch": "Epoch2_05",
  "clarity_version": "Clarity1"
}
```

---

# instance

What this instance is, holds, and has enabled

## The Instance catalog object

What this instance is and holds.

| Field | Type | Description |
| --- | --- | --- |
| `mode` | enum | The mode the operator declared in `INSTANCE_MODE`. Values: `oss`, `platform`, `archive`. |
| `network` | string | `mainnet`, `testnet` or `devnet`. |
| `instance_id` | string · nullable | This instance's id, or `null` before first boot. |
| `features` | object | Same manifest as `/v1/instance/features`. |
| `scope` | object · nullable | The history this instance claims: `start_height`, optional `target_height`, and how it was acquired (`bootstrap.source` is `archive`, `genesis` or `import`, with the archive manifest digest when restored). `null` on an instance older than the scope table. |
| `subgraphs` | array of object | Deployed subgraphs: `name`, `status`, `start_block`, `last_processed_block`. |
| `webhooks` | array of object | Webhooks: `name`, `status`, `kind` (`chain`). |
| `console` | object | Console flags. Always false on a self-hosted instance. |

```json
{
  "mode": "oss",
  "network": "mainnet",
  "instance_id": "0b6f1c9e-4a52-4f0e-9d8a-2c1e7b3d5a90",
  "features": {
    "index": true,
    "subgraphs": true,
    "webhooks": true
  },
  "scope": {
    "network": "mainnet",
    "start_height": 8650000,
    "target_height": null,
    "bootstrap": {
      "source": "archive",
      "manifest_digest": "d1db4af398e9a0418095a48beef1c6c595070df3398f92ef16ef0fdb10daed1b",
      "genesis_hash": null
    }
  },
  "subgraphs": [
    {
      "name": "fastpool-signers",
      "status": "active",
      "start_block": 8665568,
      "last_processed_block": 9048707
    }
  ],
  "webhooks": [
    {
      "name": "pox5-stakes",
      "status": "active",
      "kind": "chain"
    }
  ],
  "console": {
    "signup": false,
    "pricing": false,
    "publicDirectory": false
  }
}
```

## The Instance feature manifest object

The declared mode and the modules this instance runs.

| Field | Type | Description |
| --- | --- | --- |
| `mode` | enum | The mode the operator declared in `INSTANCE_MODE`. Values: `oss`, `platform`, `archive`. |
| `features` | object | Module flags. `protocolDatasets` says which protocol decoders are on; `signup`, `pricing` and `publicDirectory` are always false here. |

```json
{
  "mode": "oss",
  "features": {
    "rawRest": true,
    "rawSse": true,
    "index": true,
    "subgraphs": true,
    "webhooks": true,
    "contractDiscovery": true,
    "verification": true,
    "protocolDatasets": {
      "sbtc": true,
      "pox": true,
      "bns": false
    },
    "signup": false,
    "pricing": false,
    "publicDirectory": false,
    "unsignedWebhooks": false
  }
}
```

## The Instance metrics object

Operational vitals.

| Field | Type | Description |
| --- | --- | --- |
| `uptime_s` | integer | Seconds since the API process started. |
| `db_size_bytes` | integer · nullable | Postgres database size, or `null` if it can't be read. |
| `deliveries_24h` | object · nullable | Webhook deliveries in the last 24 hours: `total`, `failed` (no status or 400+), and `dlq` (events currently dead-lettered). |
| `rows_series` | array of object | Rows processed across all subgraphs, per hour over the last 24 hours: `{ t, rows }`. |

```json
{
  "uptime_s": 86400,
  "db_size_bytes": 61203865600,
  "deliveries_24h": {
    "total": 460,
    "failed": 2,
    "dlq": 0
  },
  "rows_series": [
    {
      "t": "2026-09-23 13:00:00+00",
      "rows": 3202
    }
  ]
}
```

## Local instance catalog

`GET /v1/instance` (self-hosted instances only)

What this instance is and holds: its mode and network, the feature manifest, the history it claims, and the subgraphs and webhooks deployed on it. No signup or pricing, ever.

### Returns

One `InstanceCatalog` object.

### Responses

- `200` The catalog. If the database is unreachable, lists come back empty rather than failing.
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong

### Example

```bash
curl http://127.0.0.1:3800/v1/instance
```

```json
{
  "mode": "oss",
  "network": "mainnet",
  "instance_id": "0b6f1c9e-4a52-4f0e-9d8a-2c1e7b3d5a90",
  "features": {
    "index": true,
    "subgraphs": true,
    "webhooks": true
  },
  "scope": {
    "network": "mainnet",
    "start_height": 8650000,
    "target_height": null,
    "bootstrap": {
      "source": "archive",
      "manifest_digest": "d1db4af398e9a0418095a48beef1c6c595070df3398f92ef16ef0fdb10daed1b",
      "genesis_hash": null
    }
  },
  "subgraphs": [
    {
      "name": "fastpool-signers",
      "status": "active",
      "start_block": 8665568,
      "last_processed_block": 9048707
    }
  ],
  "webhooks": [
    {
      "name": "pox5-stakes",
      "status": "active",
      "kind": "chain"
    }
  ],
  "console": {
    "signup": false,
    "pricing": false,
    "publicDirectory": false
  }
}
```

## Default feature manifest

`GET /v1/instance/features` (self-hosted instances only)

Which modules this instance runs, including which protocol decoders are on.

### Returns

One `InstanceFeatures` object.

### Responses

- `200` The declared mode and feature manifest.
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong

### Example

```bash
curl http://127.0.0.1:3800/v1/instance/features
```

```json
{
  "mode": "oss",
  "features": {
    "rawRest": true,
    "rawSse": true,
    "index": true,
    "subgraphs": true,
    "webhooks": true,
    "contractDiscovery": true,
    "verification": true,
    "protocolDatasets": {
      "sbtc": true,
      "pox": true,
      "bns": false
    },
    "signup": false,
    "pricing": false,
    "publicDirectory": false,
    "unsignedWebhooks": false
  }
}
```

## Operational vitals

`GET /v1/instance/metrics` (self-hosted instances only)

Process uptime, database size, the last 24 hours of webhook delivery outcomes, and rows processed per hour. A value that can't be measured comes back `null` instead of failing the request.

### Returns

One `InstanceMetrics` object.

### Responses

- `200` Current vitals.
- `400` A parameter, filter, cursor or body field was refused; `code` says which
- `401` This bind requires the instance token, and it was missing or wrong

### Example

```bash
curl http://127.0.0.1:3800/v1/instance/metrics
```

```json
{
  "uptime_s": 86400,
  "db_size_bytes": 61203865600,
  "deliveries_24h": {
    "total": 460,
    "failed": 2,
    "dlq": 0
  },
  "rows_series": [
    {
      "t": "2026-09-23 13:00:00+00",
      "rows": 3202
    }
  ]
}
```

## Notes for agents

- Every endpoint and object is generated from the OpenAPI spec at 'https://www.secondlayer.tools/docs/api-reference.md'; the pages by job are 'https://www.secondlayer.tools/docs/api/<index|streams|proofs|subgraphs|webhooks|instance>' (each with a .md twin).
- One endpoint as markdown: 'https://www.secondlayer.tools/docs/api-reference/<anchor>.md', where <anchor> is its operationId in kebab case (e.g. list-pox5-events).
- Base URL: hosted needs SECONDLAYER_API_KEY; a loopback instance needs no key. Page by passing next_cursor back as cursor, and undo rows named in reorgs[].
- Live spec: 'GET $SECONDLAYER_API_URL/v1/openapi.json'. operationIds are stable, so keep them as method names; auth is an optional bearer.
- Prefer @secondlayer/sdk over hand-rolled paging: walk() follows cursors, consume() runs a checkpointed sweep.

## Setup

Ensure setup once, skipping any step already done:
- Skill: `bunx skills add ryanwaits/secondlayer --skill secondlayer -y`
- CLI: `bun add -g @secondlayer/cli`
- Instance: `secondlayer init` writes `INSTANCE_TOKEN`. Loopback `/v1` reads need no key.
- Hosted/archive: export `SECONDLAYER_API_KEY` (`sk-sl_*`) or buy credits with `secondlayer credits buy` (`balance`, `refill`), then `secondlayer whoami`
