Public API

Block events

A block event says that a new block appeared on a network. You can receive it in two ways, and they carry different data. The HTTP webhook block delivers the block itself, exactly as the node returned it. The WebSocket event block on the channel private-blocks delivers a short descriptor (network, height, hash, time) and you fetch the block yourself when you need it. Block events are off by default, are paid per delivery (webhook:block), and arrive every few seconds per network. This article is about block events only; the webhook transport is described in Webhooks and the socket in WebSockets. Base URL: see Overview; all routes require signed headers (see Authentication).

blockswebhookWebSocketcreditsrealtime

← Public API

Flow diagram

One new block, two independent ways to hear about it. The webhook carries the whole block; the WebSocket carries a short descriptor, and you fetch the block yourself when you need it.

Network node (or yourRPC endpoint)RpcNodeYour server (webhookURL)Your WebSocket client1. A new block appears onthe network (every fewseconds)Each transport has itsown switch and networklist (events.block). Ablock wanted by bothis delivered andcharged twice(webhook:block).2. POST: the raw block asthe node returned it;headersX-RpcNode-Network,X-RpcNode-Block-Height,X-RpcNode-Signature3. 2xx; any other answeris retried (10 s, 30 s, 2min, 10 min, 1 h, 6 h)4. event block on private-blocks: { network,block_height, block_hash, block_time }5. If you need the contents: read the block by block_height or block_hash
Solid arrows are requests, dashed arrows are answers, green arrows are what RpcNode pushes to you (webhook or WebSocket). Steps are numbered in time order.

Two ways to receive a block

HTTP webhook, kind block: a signed POST to your webhook URL whose body is the whole block as the node returned it, with no wrapper and no header fields. Which block it is, is told by the headers X-RpcNode-Network and X-RpcNode-Block-Height. Use it when you want the block contents without making an RPC call of your own.

WebSocket, event block on the channel private-blocks: one small JSON message per block, the same for every network. It has no transactions in it. Use it as a trigger: when a message arrives, read the block from your RPC endpoint or node if you need its contents.

The two transports are independent. Each has its own switch and its own list of networks. A block that both want is delivered twice and charged twice.

Why two forms: a block can weigh hundreds of KB up to tens of MB depending on the network. The webhook is a single POST that is retried and logged, so it carries the full block. The socket only carries the descriptor.

Subscribe

Both transports use the same block setting: false (off, the default), true (every network that supports block events) or an array of network slugs. A bare slug such as "tron" means the mainnet of that network; a testnet is named by its full environment slug, for example "tron-nile". Slugs are lowercased and deduplicated and are not checked against the catalogue, so a misspelt slug is saved and never matches.

Webhook: PUT /v1/webhooks with url, enabled and events.block. Socket: PUT /v1/realtime/subscription with enabled and events.block. Both routes are described in full in Webhooks and WebSockets.

Important: when you send events, it replaces all three lists (payment, payout, block) and a key that is missing counts as off. To add blocks without losing your other subscriptions, send all keys. If you omit events, the saved lists stay as they are. A save that only enables the transport, with no subscriptions saved yet, turns payment and payout on for every network and leaves block off.

true on block includes the testnets of every network that supports block events, which can be a lot of deliveries. List the networks you need.

Which networks support block events: GET /v1/networks lists their environments in scan_environment_kinds (an empty array means none). Listing a network that does not support them is accepted but nothing arrives.

Enabling either transport needs a plan with webhook slots. Without them the enabling save fails with 403 watch.webhook_unavailable.

Subscribe a webhook and a socket to blocks of Tron and the Tron Nile testnet

// PUT /v1/webhooks   (keep payment and payout, add blocks)
{
  "url": "https://example.com/hooks/rpcnode",
  "enabled": true,
  "events": { "payment": true, "payout": true, "block": ["tron", "tron-nile"] }
}

// PUT /v1/realtime/subscription   (the socket only wants blocks)
{
  "enabled": true,
  "events": { "block": ["tron", "tron-nile"] }
}

// Both answer with the saved lists, for example:
//   "events": { "payment": [], "payout": [], "block": ["tron", "tron-nile"],
//               "payment_all": true, "payout_all": true, "block_all": false }

Block over the webhook

The POST body is the block exactly as it was read from the node, without parsing or wrapping. Its shape is the block of the network, so it is different for each family: for Tron it is the answer of wallet/getblockbynum, for EVM networks the block object of eth_getBlockByNumber with full transactions, for Bitcoin-style networks the result of getblock, for Solana the result of getBlock. For a few families whose block has several parts (for example Cosmos, NEAR, Sui, Stellar, Stacks) the body is one JSON object that holds the parts under named keys.

Headers: X-RpcNode-Event: block. X-RpcNode-Network: the network slug (tron, tron-nile, ...). X-RpcNode-Block-Height: the block height. X-RpcNode-Delivery: the delivery id (the idempotency key). X-RpcNode-Signature: t=<unix seconds>,v1=<hex> where v1 is HMAC-SHA256 with your webhook secret over "<t>.<raw body>". The signature covers this body like any other; verify it before you parse anything. Content-Type is application/json.

Because the body has no envelope, there is no delivery_id, type or schema_version inside it. Take the id from X-RpcNode-Delivery and the network and height from the headers. The block hash and time are inside the block in the form of that network.

Size: allow large bodies. A block of a busy network can be tens of MB, so raise the body limit of your HTTP server and answer 2xx as soon as the body is read and verified. RpcNode waits 10 seconds for the answer. Process the block after you answered.

The rest of the delivery rules are the same as for every webhook: a non-2xx answer or a timeout is retried after 10 s, 30 s, 2 min, 10 min, 1 h and 6 h (7 attempts in total), then the delivery is dead. At most 4 deliveries are in flight to your webhook at once, so arrival order is not guaranteed.

What a Tron block POST looks like (body shortened)

POST /rpcnode/webhook HTTP/1.1
Content-Type: application/json
User-Agent: RpcNode-Webhooks/1
X-RpcNode-Delivery: 9a0b4c2e7d1f4a63b8e5c0d2f6a19b37c4e8d051
X-RpcNode-Event: block
X-RpcNode-Network: tron
X-RpcNode-Block-Height: 85558636
X-RpcNode-Signature: t=1787347206,v1=5d0c…e91a

{
  "blockID": "000000000519856c255ef4a490eae5ee6ff6e210bd572edcb9d49e7dafef8500",
  "block_header": { "raw_data": { "number": 85558636, "timestamp": 1787347203000, "…": "…" }, "witness_signature": "…" },
  "transactions": [ { "txID": "…", "…": "…" } ]
}

Block over the WebSocket

The socket is a Pusher-protocol connection. Subscribe to the private channel private-blocks and bind the event block (the Pusher event name is the kind). To open the connection, sign the channel on your server and keep the secret away from the browser: see WebSockets for the host, the authorisation endpoint and the connection limits. This article does not repeat them.

Every block event is the same short message for every network: schema_version "2", delivery_id, type "block", event "new", network, block_height, block_hash and block_time. There are no transactions and no block body in it, so it is a few hundred bytes and is never truncated.

network is the network slug of the block (tron, or tron-nile for a testnet). block_height is a number. block_hash is a string. block_time is Unix seconds, and is 0 when the node does not report a time. delivery_id is the idempotency key of this socket delivery; it is not the same value as the delivery id of the webhook delivery of the same block.

In pusher-js the data of the event arrives already parsed. If you read the raw frame, data is a JSON text.

Event block on private-blocks

{
  "schema_version": "2",
  "delivery_id": "c7e19a40b5d24f8e91a3d6b02f58c4e7a1b9d630",
  "type": "block",
  "event": "new",
  "network": "tron-nile",
  "block_height": 71734001,
  "block_hash": "0000000004…",
  "block_time": 1760270400
}

Fetch the block yourself

With the socket descriptor you decide whether you need the block. Fetch it by block_height (or block_hash, where the node supports it) from your RPC endpoint (see Endpoints) or from your own node, using the get-block method of the network.

The get-block method of each network is: EVM networks (and any network not listed below): eth_getBlockByNumber with full transactions. Solana: getBlock. Bitcoin, Litecoin, Bitcoin Cash, Dogecoin, Zcash, Dash: getblock. Tron: wallet/getblockbynum. NEAR: block. Sui: sui_getCheckpoint. XRPL: ledger. Starknet: starknet_getBlockWithTxs. Polkadot: chain_getBlock. Cosmos-family networks: /block. Aptos: /v1/blocks/by_height/{block_height}. Stacks: /extended/v1/block/{hash_or_height}. Stellar: /ledgers/{sequence}. TON: getBlockTransactions. Aleo: getblock. Fuel: block. Flow: access.AccessAPI/GetBlockByHeight.

GET /v1/networks/{slug}/methods lists the RPC methods of a network with their credit prices, so you can see which of them your endpoint serves. A call made through your endpoint is billed as an ordinary RPC call; it is not part of the block event price.

Node.js: socket trigger, then read the block from an EVM endpoint

// 'channel' is the private-blocks channel from the WebSockets article
const RPC_URL = process.env.RPC_URL // your endpoint URL, see Endpoints
let lastHeight = null

channel.bind('block', async (msg) => {
  if (msg.network !== 'ethereum') return

  // A gap means we missed events (the socket was closed): events are not replayed.
  if (lastHeight !== null && msg.block_height > lastHeight + 1) {
    for (let h = lastHeight + 1; h < msg.block_height; h++) await loadBlock(h)
  }
  if (lastHeight === null || msg.block_height > lastHeight) lastHeight = msg.block_height
  await loadBlock(msg.block_height)
})

async function loadBlock(height) {
  const res = await fetch(RPC_URL, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      jsonrpc: '2.0',
      id: 1,
      method: 'eth_getBlockByNumber',
      params: ['0x' + height.toString(16), true],
    }),
  })
  const { result } = await res.json()
  return result // the block with its transactions
}

Handle block webhooks in Node.js

The handler below verifies the signature (the verify function is the one from Webhooks), answers at once, deduplicates by delivery id and keeps the network and height from the headers. The body is the block, so it is not parsed here.

Node.js (Express): block handler

import express from 'express'
// verify(rawBody, header, secret) is the function from the Webhooks article

const app = express()
const handled = new Set() // use a store with expiry in production

app.post('/rpcnode/webhook', express.raw({ type: '*/*', limit: '256mb' }), (req, res) => {
  if (!verify(req.body, req.get('X-RpcNode-Signature'), process.env.RPCNODE_WEBHOOK_SECRET)) {
    return res.status(401).end()
  }
  res.status(200).end()

  if (req.get('X-RpcNode-Event') !== 'block') return // other kinds: see Webhooks

  const deliveryId = req.get('X-RpcNode-Delivery')
  if (handled.has(deliveryId)) return
  handled.add(deliveryId)

  const network = req.get('X-RpcNode-Network')
  const height = Number(req.get('X-RpcNode-Block-Height'))
  indexBlock(network, height, req.body) // req.body is a Buffer: the block as the node returned it
})

app.listen(3000)

Credits

Each block delivery is charged as webhook:block, once per delivery. It does not depend on whether your server answers, and a retry is not charged again. The socket is charged separately, once per event and subscription, also when no socket is open at that moment.

The price is the price of the full get-block call of the network, and it is the same for every environment of the network. Read it from block_credits in GET /v1/networks/{slug}/credits (see Networks). A price of 0 makes the delivery free. The spend appears as webhook:block in GET /v1/usage.

Because a block arrives every few seconds on busy networks (Tron about every 3 seconds, Solana 2 to 3 slots per second), block events are the fastest-growing kind of spend. Subscribe only to the networks you need.

Delivery guarantees

Webhook: retried on any non-2xx answer or timeout (10 s, 30 s, 2 min, 10 min, 1 h, 6 h). Each (network, height) is one delivery per webhook, with one delivery id. If your server is down for long, some blocks may not be delivered later, so after an outage, load the missing heights yourself. GET /v1/webhooks/deliveries shows kind block with network and block_height.

WebSocket: there is no replay. A message is pushed to the sockets open at that moment. While your socket is closed, the blocks are missed. After every reconnect compare block_height with the last one you handled and fetch the gap from your RPC endpoint (see the example above).

Order is not guaranteed on either transport. Webhook deliveries run up to 4 at a time and are retried; use block_height, not arrival order.

Tips

  • The webhook block is the whole raw block; the socket block is a descriptor only. Pick the transport that matches what you do with a block.
  • PUT /v1/webhooks and PUT /v1/realtime/subscription replace all three lists when events is present. Send payment and payout too if you want to keep them.
  • Turn block on for the networks you need, not with true. true includes every testnet.
  • Raise the body limit of the server that receives block webhooks; Express needs express.raw with a large limit, and the signature is computed over the raw bytes.
  • After an outage or a reconnect, fetch the missing heights yourself: sockets never replay, and a long outage can leave gaps in webhook block deliveries too.
  • The price of a block event equals the get-block call of the network (block_credits). Check it in GET /v1/networks/{slug}/credits before you subscribe.