v3.2.1 Source
API reference

fullnode HTTP API

Two surfaces. /api/v1/ is the request/response REST API and has been stable since 2023. /api/v3/ is the streaming protocol, which is on its third revision because the framing changed twice.

Base URL

BASEhttps://full-node.online/api/

Self-hosted installs serve the same paths under whatever prefix you mount fullnode.urls at. Every example below uses the public gateway; substitute your own host and the responses are identical.

Versioning

The two surfaces are versioned independently, which surprises people often enough that it is worth stating plainly:

PrefixSurfaceStatusNotes
/api/v1/RESTStableAdditive changes only. No breaking change since 1.0.
/api/v2/StreamRemoved 3.0Length-prefixed JSON. Gone; do not build against it.
/api/v3/StreamCurrentBinary framing, per-node cursors, resumable.

The project version (3.2.1) tracks the stream protocol, not the REST API. A gateway on 3.x still serves /api/v1/.

Authentication

Reads on /api/v1/ are open and need no credential — the data is public chain state and rate limiting is by address. Two things do need a key:

Keys are issued per project in the dashboard of a self-hosted install (manage.py issuekey on the command line). On REST they go in a header; on streams they are part of the path — see endpoint shape for why.

header auth
curl -H "X-Api-Key: 7f2b91c4…" \
     -d '{"method":"getblockcount","params":[]}' \
     https://full-node.online/api/v1/bitcoin/rpc

Conventions

Errors

Errors are Django REST Framework's own shape — a detail string, plus a code the gateway adds.

404
{ "detail": "Not found." }
StatuscodeMeans
400invalid_chainUnknown chain slug.
401not_authenticatedKey missing on an endpoint that needs one.
404not_foundNo such block, transaction or route.
429throttledRate limit; Retry-After is set.
502upstream_unavailableThe adapter could not reach its node.
503not_syncedNode is behind; lag gives the gap in blocks.

Rate limits

60 requests/minute per IP unauthenticated, 1,200/minute with a key, and four concurrent streams per key. Every response carries X-RateLimit-Remaining.

Chains

GET/api/v1/chains

Every chain this gateway is configured for, with its adapter state.

200
{
  "count": 5,
  "results": [
    {
      "chain": "bitcoin",
      "name": "Bitcoin",
      "network": "mainnet",
      "decimals": 8,
      "symbol": "BTC",
      "block_time": 600,
      "features": ["blocks", "mempool", "utxo", "streams"]
    }
  ]
}

Nodes

GET/api/v1/nodes

The gateway's own fleet. The id here is what goes into a stream URL, and what status reports as node.

200
{
  "count": 4,
  "results": [
    { "id": "s1", "region": "eu-central", "city": "Frankfurt",
      "chains": ["solana", "ton"], "streams": false },
    { "id": "s2", "region": "eu-west", "city": "Strasbourg",
      "chains": ["bitcoin", "bsc", "tron"], "streams": true },
    { "id": "s3", "region": "eu-north", "city": "Stockholm",
      "chains": ["solana"], "streams": false },
    { "id": "s4", "region": "eu-west", "city": "Gravelines",
      "chains": ["bitcoin", "bsc", "ton", "tron"], "streams": true }
  ]
}

Chain status

GET/api/v1/<chain>/status

Sync state of the node currently serving that chain. Returns 503 not_synced when height trails header_height by more than the chain's tolerance.

GET /api/v1/solana/status
{
  "chain": "solana",
  "network": "mainnet-beta",
  "synced": true,
  "height": 428914300,
  "header_height": 428914300,
  "epoch": 993,
  "peers": 2841,
  "client": "Agave 2.3.4",
  "node": "s1"
}

Blocks

GET/api/v1/<chain>/blocks/latest
GET/api/v1/<chain>/blocks/<height|hash>

confirmations is depth below the tip and is the field to branch on — it goes to 0 on a block that has been reverted, rather than the block disappearing.

200
{
  "chain": "bitcoin",
  "height": 912480,
  "hash": "00000000000000000001b4f8c0a37e2d9155c8e3f60b7a248d91e0c4f7a2b3d6",
  "parent": "000000000000000000021c7ea5f39b0d84c6e17b2f9a05d3c8e14b60a97f2e85",
  "time": "2026-08-12T09:41:22Z",
  "confirmations": 1,
  "tx_count": 3184,
  "size": 1483920,
  "raw": { "…" }
}

Transactions

GET/api/v1/<chain>/tx/<hash>
POST/api/v1/<chain>/tx

GET normalises into transfers[] as shown on the overview. POST broadcasts a signed raw transaction — the gateway never signs and never holds a key, so the body is the already-signed hex or base64 blob the chain expects.

Addresses

GET/api/v1/<chain>/address/<addr>/balance
GET/api/v1/<chain>/address/<addr>/txs
GET/api/v1/<chain>/address/<addr>/utxos

Balance returns the native asset plus every token the address holds, in the same {amount, decimals, symbol} shape. utxos is Bitcoin-only and 404s elsewhere — check features on /chains first.

Mempool

GET/api/v1/<chain>/mempool
GET/api/v1/<chain>/mempool/fees

Fee estimates come back as targets in blocks, with the unit the chain actually charges in — sat/vB for Bitcoin, gwei for BSC, energy and bandwidth for TRON.

Raw RPC passthrough

POST/api/v1/<chain>/rpc

The escape hatch. The body is forwarded to the node as-is and its reply is returned untouched — no normalisation, no error mapping beyond transport failures. Requires a key, and a per-chain method allowlist applies (FULLNODE_RPC_ALLOW in settings). State-changing and wallet methods are rejected by default.

Stream protocol · /api/v3

Polling /blocks/latest in a loop is how most people start and it is wrong on every chain with sub-second blocks. The stream protocol pushes instead: you open one connection, it stays open, and events arrive as they happen.

Streams are node-affine. The resume cursor lives on the node that issued it, so the node id is chosen when the connection is opened and does not move for the life of that stream. Reconnect to the same node id and the cursor resumes; reconnect elsewhere and you get a fresh one.

Endpoint shape

WS/api/v3/ws-<node>-<key>
GET/api/v3/stream-<node>-<key>
Why the key is in the path. The browser WebSocket constructor cannot set request headers — there is no argument for it and there never has been. Every browser-side stream API therefore puts its credential in the URL, and doing it only for browsers would mean two code paths for one protocol. So the node id and key are path segments on both transports, and the whole exchange is inside TLS.

<node> is an id from /api/v1/nodes with "streams": true. <key> is your project key. A request with a good shape and a bad key is 401 not_authenticated; a request to a node that does not carry streams is 404.

javascript
const ws = new WebSocket(
  "wss://full-node.online/api/v3/ws-s2-7f2b91c4"
);

ws.onopen = () => ws.send(JSON.stringify({
  op: "subscribe",
  chain: "bsc",
  topics: ["block", "mempool"],
  cursor: localStorage.getItem("fn.cursor")
}));

ws.onmessage = (e) => {
  const ev = JSON.parse(e.data);
  if (ev.type === "block") console.log(ev.height);
  localStorage.setItem("fn.cursor", ev.cursor);
};

Events

typeFired whenCarries
readySubscription acceptednode, chains, cursor
blockNew block at the tipThe block object
block.revertedA block left the main chainheight, hash — always before its replacement
txA watched address movedThe transaction object
mempoolEntry admitted or droppedhash, fee, action
heartbeatEvery 20s of silenceserver time, so idle proxies do not reap the socket

HTTP fallback

Some corporate proxies strip the Upgrade header and a WebSocket simply never completes. stream-<node>-<key> is the same protocol over plain HTTP with chunked transfer encoding: identical events, one JSON document per chunk, and the subscription is sent as the request body instead of the first frame. It is slower to start and cannot be used from a browser, but it goes through anything that passes ordinary HTTPS.

curl
curl -N https://full-node.online/api/v3/stream-s4-7f2b91c4 \
     -d '{"op":"subscribe","chain":"bitcoin","topics":["block"]}'
Both stream endpoints hold a connection open for as long as you keep it. If you are behind a gateway of your own, raise its idle timeout above the 20-second heartbeat, or it will close the stream out from under you.

Header archive · /archive

Streaming gives you the tip. The archive gives you the recent chain behind it: a rolling window of Bitcoin headers in fixed segments of 400, as flat binary. It is what an SPV client or a fresh indexer should bootstrap from — pulling fifty thousand headers one REST call at a time is unkind to everybody, and it is the single most common way people get rate limited here. Read index.json for the range the window currently covers; older segments are pruned as it advances.

No key is needed and no rate limit applies. Segments are addressed by the block range they contain and can never change, so they are served immutable with a one-year lifetime and cache anywhere — your CDN, your proxy, your disk. Fetch a segment once and never ask for it again.

Manifest

GET/archive/index.json

Read this first; it tells you how far the archive goes and how to build a segment URL. It is the only part that changes — one new segment roughly every 66 hours, which is what 400 blocks of Bitcoin comes to — so it is cached for five minutes and everything under it is cached for a year.

200
{
  "chain": "bitcoin",
  "format": "fnha/1",
  "record_size": 96,
  "records_per_segment": 400,
  "segment_bytes": 38432,
  "first_height": 861200,
  "last_height": 912399,
  "segments": 128,
  "path": "/archive/bitcoin/headers-{start:09d}-{end:09d}.bin"
}

Segment format

GET/archive/bitcoin/headers-<start>-<end>.bin

A 32-byte little-endian file header identifying the range, then the packed record block. Read the header to know what you have; decode the body with the client library rather than by hand — the record encoding is an implementation detail and has changed once already.

OffsetSizefieldNotes
04magicFNHA
42versionFormat revision, currently 1.
62chain1 = Bitcoin mainnet.
88first_heightHeight of record 0.
164countRecords that follow.
202record_sizeBytes per record.
32restrecordsPacked record block.
curl
curl -sO https://full-node.online/archive/bitcoin/headers-000912000-000912399.bin
Only whole segments are published, so the archive tip trails the chain tip by up to 400 blocks. Use /api/v3 for anything newer than that — the archive is for history, not for the head.