SDK
Talk to Secondlayer, hosted or your own instance, from TypeScript without hand-writing cursor walks, reorg rewind, or signature checks.
Every export, with signatures: SDK reference.
3
The client reads SECONDLAYER_API_URL for the host. Leave apiKey out and the key follows the host:
| Host | Key read |
|---|---|
api.secondlayer.tools | SECONDLAYER_API_KEY |
| anywhere else | INSTANCE_TOKEN (Self-host) |
accountKey is for archive operations only. See Keys and billing.
One client, every surface
sl.streams: raw, ordered chain events (cursor-paginated, replayable).sl.index: decoded rows: FT/NFT transfers, all event types, contract calls, andprintSchema(contractId).sl.contracts: find deployed contracts by trait (SIP-009/010/013).sl.subgraphs: your app-specific tables, plus open/v1reads (rows).sl.webhooks: create and manage webhooks on raw chain events.
Each surface documents its own calls:
| Task | Call | Documented in |
|---|---|---|
| Webhook on raw chain events | sl.webhooks.create({ name, url, triggers }) | Webhooks |
| Re-deliver history | sl.webhooks.replay(id, { fromBlock, toBlock }) | Replay |
| Check a delivery signature | verifyWebhookSignature, verifySecondlayerSignature | Receive and verify |
| Prove a transaction is in a block | verifyTransactionProof, fetchRewardSet | Verify |
| Tail the live firehose | sl.streams.events.subscribe(options) | Streams |
| Read subgraph tables | sl.subgraphs.rows, sl.subgraphs.typed | Subgraphs |
| Stream or roll up rows | subgraph.<table>.subscribe, .aggregate | Read rows |
| Let a sink own the transaction | kyselySink, drizzleSink, bunSqliteSink | Sinks |
| Describe one event for every surface | on.ftMint(...), .toIndexParams() | Filters |
- A failed call throws
ApiError. ApiError.requestIdis therequest_idto quote when you report a problem.
trigger.* is not on.*
trigger.*(from@secondlayer/sdk) configures chain webhooks.on.*(from@secondlayer/stacks) configures subgraph sources in a handler config.@secondlayer/stacksis the typed chain client, documented at stacks.secondlayer.tools.
Checkpointed consumer
consume polls and commits a cursor so you never miss or double-process. Return the committed cursor from onBatch.
Available on:
sl.streams.eventssl.index.events(takeseventType)sl.index.contractCalls
- Reorgs rewind the cursor to the fork point and fire
onReorg. - Fold running balances in a sink's
onRollback, not there: Sinks. finalizedOnlyholds delivery to rows at or belowtip.finalized_height.fromHeight: 0backfills as far as your instance holds (archive restore is metered past that).- Full options and the batch envelope: Index.
Verifying responses
createStreamsClient({ apiKey })verifies both REST reads (X-Signature) and SSE frames.- The key is fetched once from
/public/streams/signing-key; a rotatedX-Signature-KeyIdtriggers a single refresh. - Blocks, transactions, and subgraph rows: Verify.
verify | Behavior |
|---|---|
| default (lenient) | Verify signed responses; pass unsigned through; throw on invalid |
true (strict) | A missing signature throws too |
{ publicKey } | Pin a known PEM |
false | Disable verification |
verify lives on createStreamsClient
new SecondLayer() does not accept verify.
Use createStreamsClient when you need it.
Filters
Describe a chain event once and get the exact shape Index, Streams, Webhooks, and Subgraphs each expect.
The surfaces disagree, and a spread hides it. Each projection converts explicitly:
| Difference | Where |
|---|---|
Print event is print on Index, print_event on Subgraphs | Index, Subgraphs |
contract_call and contract_deploy live on a different Index endpoint than events | Index |
Amounts are strings: a bigint in a JSON.stringify body throws at runtime, so toChainTrigger() turns 100_000n into "100000" | Webhooks |
trait scoping exists | Index, Subgraphs, not Streams |
Unsupported members don't exist
A filter type a surface doesn't support lacks that method, so the error names the problem. Field gating comes second: trait is absent from toStreamsParams(), and wildcards are Webhooks and Subgraphs only.
Principals are validated
A contract id where an asset identifier belongs is the most common copy-paste mistake, and it used to return zero rows. The factory now throws at construction, naming the field. The fields are still typed string, so it fails when the filter is built, not as a red squiggle.
Subgraph sources
toSubgraphSource() and fromSubgraphSource() round-trip a source.
The abi literal survives, so event.input stays typed in a handler.