Guide
WebSocket events
Follow finalized cuts, addresses, the mempool and single transactions live over the three Tenrai WebSocket endpoints, and recover after a reconnect.
On this page
Three sockets push what would otherwise need polling. All of them speak JSON text frames of the form {"type": …, "data": …} and name their subprotocol when connecting.
| Path | For |
|---|---|
/v1/events | Every finalized cut, in order, resumable. For indexers and payment systems. Subprotocol tenrai.node-events.v1 |
/v1/ws | Subscriptions by topic, and read requests. For apps and wallets. Subprotocol tenrai.node-api.v1 |
/v1/transactions/{id}/status/stream | One transaction until it is final. Subprotocol tenrai.transaction-status.v1 |
const socket = new WebSocket("wss://api.tenrai-technologies.org/v1/ws", "tenrai.node-api.v1");Finalized cuts
/v1/events starts with a checkpoint, the finalized head, and then sends one cut_finalized per height:
{
"type": "cut_finalized",
"data": {
"api_version": 1,
"cut": { "height": 20139, "cut_id": "80bd765b…", "state_root": "6319bd1d…", "validator_set_id": "cce43cf4…" },
"registered_work": 68,
"executed_work": 71,
"rewards": [
{ "block_id": "0b45968f…", "transaction_id": "c46c371e…", "output_index": 0, "amount": "41118422" }
],
"stake_rewards": "781494251",
"stake_payouts": []
}
}Resume after a reconnect
Store the height of the last cut you handled and reconnect with it:
/v1/events?after_height=20139The server first replays the cuts above that height, then continues live. The replay reaches one hour back. If your height is older, the stream sends resync_required with the reason history_unavailable and closes: read the missed range over HTTP, for example with POST /v1/address/changes, then connect again without the parameter.
resync_required reason | Meaning |
|---|---|
stream_lagged | You read too slowly and events were dropped |
stream_restarted | The source restarted |
history_unavailable | after_height is older than the replay reaches |
Subscriptions
On /v1/ws you send frames with an id of your choice and get the answer under the same id.
{"type":"subscribe","data":{"id":1,"topic":"cuts"}}
{"type":"subscribe","data":{"id":2,"topic":"addresses","addresses":["trai1…"]}}
{"type":"subscribe","data":{"id":3,"topic":"transaction","transaction_id":"…"}}
{"type":"request","data":{"id":4,"path":"/v1/chain/head"}}
{"type":"unsubscribe","data":{"id":5,"subscription":1}}
{"type":"ping","data":{"id":6}}{"type":"subscribed","data":{"id":1,"subscription":1,"topic":"cuts"}}
{"type":"response","data":{"id":4,"status":200,"body":{…}}}
{"type":"unsubscribed","data":{"id":5,"subscription":1}}
{"type":"pong","data":{"id":6}}| Frame | Answer |
|---|---|
subscribe | subscribed with the subscription number, then notification frames |
request | response with status and body, for any GET route |
unsubscribe | unsubscribed |
ping | pong |
A notification names its subscription and carries one event:
{
"type": "notification",
"data": {
"subscription": 1,
"event": {
"type": "cut_finalized",
"data": {
"cut": { "height": 20126, "cut_id": "1764a581…", "state_root": "5876d641…", "validator_set_id": "cce43cf4…" },
"registered_blocks": 53,
"executed_blocks": 68,
"rewards": "781507821",
"stake_rewards": "781494251",
"finalized_at_millis": 1791346785721
}
}
}
}Topics
| Topic | Events | Data |
|---|---|---|
cuts | cut_finalized | The cut, its block counts and rewards |
blocks | block_accepted | Block ID, parents, transactions, reward address, miner tag |
mempool | mempool | The /v1/mempool document |
rates | rates | The /v1/chain/rates document |
sync | sync | The /v1/node/sync document |
transaction | transaction_status | The status document. Takes one transaction_id |
addresses | address_changed, mempool_tx_seen, mempool_tx_removed | Received and spent outputs with the new balance; pending payments. Takes up to 64 addresses |
mempool_transactions | mempool_transaction, mempool_transaction_removed | Every mempool transaction. Not offered by every service |
mempool_tx_seen and mempool_tx_removed describe payments that are still pending. They are what this server sees and never part of a balance. mempool_tx_removed gives the reason: finalized, expired, evicted, conflict, rejected or unknown. The finalized change arrives as address_changed.
When events were missed
{"type":"resync_required","data":{"subscription":1,"reason":"stream_lagged"}} means the subscription lost events. It stays open; read the current state over HTTP and carry on. Do the same after every reconnect, since nothing is replayed on this socket.
One transaction
/v1/transactions/{id}/status/stream sends a snapshot with the current status document and an update on every change, until the status is final.
Limits
| Per socket | Node | Tenrai API |
|---|---|---|
| Subscriptions | 64 | 64 |
| Watched addresses | 512 | 512 |
| Frame you send | 64 KiB | 16 KiB |
| Answer to a request | 256 KiB | 1 MiB |
| Frames per second | 100 | 10 |
| Requests in flight | 8 | 4 |
The Tenrai API column shows its limits. A request path may be 1 KiB long. Frames are text; a binary frame ends the connection. Larger answers need HTTP.
Socket frames are not signed. Read anything you must be able to prove over HTTP with a signed answer.
With the SDK
const live = tenrai.live();
live.subscribe({
topic: "addresses",
addresses: [address],
onEvent: (event) => console.log(event.type, event.data),
onResync: () => reloadBalance(),
});The SDK reconnects with growing delays, restores subscriptions and calls onResync when events may have been missed.
