1What JSON-RPC is, and which node to ask
When you type divi-cli getblockcount, the command line tool wraps your words in a small piece of JSON, sends it to the node over HTTP, and prints the answer. That wire format is JSON-RPC, and anything that can send an HTTP request can use it directly: a Python script, a Node.js service, a shell with curl. Every command you know from divi-cli is an RPC method with the same name and the same arguments.
A request is one JSON object with four keys. The id is anything you like; the node echoes it back so you can match answers to questions.
{"jsonrpc": "1.0", "id": "tutorial", "method": "getblockhash", "params": [360218]}
There are two places you can send it:
- Your own node answers every method, including wallet commands, and needs a username and password. Section 2 shows how to turn it on.
- The public endpoint at
https://services.divi.domains/api/testnet/rpc/needs no password and no node of your own. It only answers read-only chain questions on testnet, and it has limits (section 6). Every example below runs against it unless you point it elsewhere, so you can follow along before you have a node.
Divi has the original C++ node (divid) and IronDivi, a Rust rewrite. Both speak JSON-RPC and both take the same method names, but the small print of their replies differs. The scripts below work with both; section 8 lists the differences so you are not surprised.
2Turn on RPC on your own node
If you don't have a testnet node yet, Get Testnet DIVI walks through installing either one. Both nodes protect RPC with HTTP Basic authentication: a username and password you put in the config file. Neither uses the cookie file that newer Bitcoin nodes use, so a password is required.
The Divi node (divid)
Add these lines to divi.conf in the data folder (~/.divi/divi.conf on Linux, ~/Library/Application Support/DIVI/divi.conf on a Mac, %APPDATA%\DIVI\divi.conf on Windows), then restart the node:
# divi.conf testnet=1 server=1 rpcuser=pick-a-name rpcpassword=pick-a-long-random-password # rpcport=51475 optional: the testnet default, mainnet is 51473 # rpcallowip=192.168.1.0/24 optional: only if another machine needs in
server=1 starts the RPC server; divid refuses to start one without an rpcpassword. By default it listens only on the machine it runs on. rpcallowip opens it to other addresses; leave it out unless you need it, because anyone who can reach the port and knows the password controls your wallet.
IronDivi (irondivid)
IronDivi reads irondivi.conf from its data folder (~/.irondivi/testnet/irondivi.conf on Linux, ~/Library/Application Support/IronDivi/testnet/irondivi.conf on a Mac when you run with --testnet). The file is TOML, not the key=value style of divi.conf, so text values must be in double quotes:
# irondivi.conf (TOML: strings in quotes) rpcuser = "pick-a-name" rpcpassword = "pick-a-long-random-password" # rpcport = 51473 optional: the testnet default
The same three settings also work as command line flags (--rpcuser, --rpcpassword, --rpcport), which is handy for a quick test but puts the password in your shell history, so prefer the file. irondivi-cli reads the same file, so it keeps working after you add the password.
divid listens on 51475 for testnet RPC; IronDivi listens on 51473, which is divid's mainnet port. If a call hangs or is refused, this is the first thing to check. Both nodes talk to peers on 51474.
3Your first call with curl
Start with the public endpoint, which needs nothing from you:
curl -s https://services.divi.domains/api/testnet/rpc/ \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"1.0","id":"tutorial","method":"getblockcount","params":[]}'
# reply
{"error":null,"id":"tutorial","result":359956}
The reply has three keys. result holds the answer. error is null when all went well and an object with a code and a message when it did not. id is what you sent.
Against your own node, add the username and password with -u and point at the right port. Never type the password into the command itself; curl prompts for it when you give only the username:
# divid on testnet (port 51475); IronDivi uses 51473 curl -s -u pick-a-name http://127.0.0.1:51475/ \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"1.0","id":"tutorial","method":"getblockcount","params":[]}'
Methods take their arguments in params, in order, with JSON types: block heights are numbers, hashes and addresses are strings.
-d '{"jsonrpc":"1.0","id":"tutorial","method":"getblockhash","params":[360218]}'
-d '{"jsonrpc":"1.0","id":"tutorial","method":"getblock","params":["7d87795e23bbaac012b9686f64cd793d62476817d4cea3e1fcdccfa2853493bf"]}'
divid does not care whether the jsonrpc key is there. IronDivi does: a request without it is answered with a parse error and never reaches the method. Put "jsonrpc":"1.0" in every request and both nodes are happy.
4From Python
Python's standard library has everything needed: urllib for HTTP, json for the body, base64 for the password header. Save this as divi_rpc.py. It reads where to connect from three environment variables and falls back to the public endpoint when none is set.
"""Talk to a Divi node over JSON-RPC with nothing but the standard library.
Set DIVI_RPC_URL to your node (http://127.0.0.1:51473/ for IronDivi testnet,
http://127.0.0.1:51475/ for divid testnet) and DIVI_RPC_USER / DIVI_RPC_PASSWORD
to the rpcuser / rpcpassword from its config file. With nothing set it talks to
the public read-only testnet endpoint, which needs no password.
"""
import base64
import json
import os
import urllib.error
import urllib.request
URL = os.environ.get("DIVI_RPC_URL", "https://services.divi.domains/api/testnet/rpc/")
USER = os.environ.get("DIVI_RPC_USER")
PASSWORD = os.environ.get("DIVI_RPC_PASSWORD")
class RpcError(Exception):
"""The node answered, but with an error object."""
def rpc(method, *params):
body = json.dumps({"jsonrpc": "1.0", "id": "tutorial", "method": method, "params": list(params)})
req = urllib.request.Request(URL, data=body.encode(), headers={"Content-Type": "application/json"})
if USER:
token = base64.b64encode(f"{USER}:{PASSWORD}".encode()).decode()
req.add_header("Authorization", "Basic " + token)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
raw = resp.read()
except urllib.error.HTTPError as e:
raw = e.read() # divid answers RPC errors with HTTP 500 and a JSON body
if not raw.lstrip().startswith(b"{"):
raise RpcError(f"HTTP {e.code}: {raw[:120]!r}") from None
reply = json.loads(raw)
if reply.get("error"):
raise RpcError(reply["error"])
return reply["result"]
if __name__ == "__main__":
height = rpc("getblockcount")
best = rpc("getblockhash", height)
block = rpc("getblock", best)
print("height", height)
print("hash ", best)
print("time ", block["time"], "transactions", len(block["tx"]))
print("peers ", rpc("getconnectioncount"))
try:
rpc("getblockhash", "not-a-number")
except RpcError as e:
print("error ", e)
Run it with no settings and it asks the public endpoint:
python3 divi_rpc.py
height 359971
hash 82adcf780feeb3f4540cdf2227013d904e35efdc451a11f47a09dda3059fb5d5
time 1792614177 transactions 3
peers 52
error {'code': -1, 'message': 'value is type str, expected int'}
The last line is on purpose: the script asks for block "not-a-number" to show what an error looks like. The node answers with code -1 and a message, and the rpc() helper turns that into a Python exception you can catch.
To talk to your own node, set the three variables in the shell first. Export the password from a prompt or a secrets tool rather than typing it on the command line, so it stays out of your shell history:
export DIVI_RPC_URL=http://127.0.0.1:51475/ # 51473 for IronDivi export DIVI_RPC_USER=pick-a-name read -s DIVI_RPC_PASSWORD && export DIVI_RPC_PASSWORD # typed, not echoed python3 divi_rpc.py
The same run against a synced IronDivi testnet node a few minutes later:
height 360218
hash 7d87795e23bbaac012b9686f64cd793d62476817d4cea3e1fcdccfa2853493bf
time 1792629046 transactions 2
peers 9
error {'code': -1, 'message': 'Missing height parameter'}
Same height and hash as divid on the same chain; a different wording for the same mistake. Your code should look at the code or simply catch the exception, not match on the message text.
When divid reports an RPC error it also sets the HTTP status to 500 (or 404 for an unknown method), and urllib raises on any status above 399. The body is still perfectly good JSON with the error inside, so the script reads it from the exception. IronDivi and the public endpoint answer every RPC error with HTTP 200, which needs no special handling. A reply that is not JSON at all, such as the HTML page divid sends for a wrong password, becomes RpcError: HTTP 401: ….
5From JavaScript
Node.js 18 and newer have fetch built in, so this needs no packages either. Save it as divi_rpc.mjs (the .mjs ending lets it use import and top-level await). It reads the same three environment variables as the Python version.
// Talk to a Divi node over JSON-RPC from Node.js 18+ (built-in fetch), no packages. // Same environment variables as the Python version. const URL = process.env.DIVI_RPC_URL ?? "https://services.divi.domains/api/testnet/rpc/"; const headers = { "Content-Type": "application/json" }; if (process.env.DIVI_RPC_USER) { const token = Buffer.from(`${process.env.DIVI_RPC_USER}:${process.env.DIVI_RPC_PASSWORD}`).toString("base64"); headers.Authorization = `Basic ${token}`; } export async function rpc(method, ...params) { const res = await fetch(URL, { method: "POST", headers, body: JSON.stringify({ jsonrpc: "1.0", id: "tutorial", method, params }), }); const text = await res.text(); let reply; try { reply = JSON.parse(text); } catch { throw new Error(`HTTP ${res.status}: ${text.slice(0, 120)}`); } if (reply.error) throw new Error(`${reply.error.code}: ${reply.error.message}`); return reply.result; } const height = await rpc("getblockcount"); const best = await rpc("getblockhash", height); const block = await rpc("getblock", best); console.log("height", height); console.log("hash ", best); console.log("time ", block.time, "transactions", block.tx.length); console.log("peers ", await rpc("getconnectioncount")); try { await rpc("getblockhash", "not-a-number"); } catch (e) { console.log("error ", e.message); }
node divi_rpc.mjs
height 359971 hash 82adcf780feeb3f4540cdf2227013d904e35efdc451a11f47a09dda3059fb5d5 time 1792614177 transactions 3 peers 52 error -1: value is type str, expected int
Unlike urllib, fetch does not throw on a 500, so the script reads the body first and only complains if it is not JSON. Errors come back as ordinary exceptions with the node's code and message.
Your own node does not send the CORS headers a browser needs, so a web page cannot call it directly; and you should never put an RPC password in a page anyway. The public endpoint does send CORS headers, but only to pages served from https://services.divi.domains. For your own site, put a small server (this script, for example) between the browser and the node. If you are building a browser dApp, the extension API is the right tool for wallet actions.
6The public testnet endpoint and its rules
The endpoint is a filtering proxy in front of a Divi testnet node. Open it in a browser (a plain GET) and it describes itself in JSON: its name, the list of methods it allows, and the batch limit. The rules, as of 2026-10-21:
| Rule | Value |
|---|---|
| Address | https://services.divi.domains/api/testnet/rpc/ |
| Authentication | none |
| Network | Divi testnet only |
| Methods | 34 methods (list below); anything else is refused |
| Calls per batch | 1 to 20 |
| Request body | at most 512 KiB |
| CORS | only for pages on https://services.divi.domains |
| Reply shape | the node's result, error and id, always HTTP 200 |
Allowed methods: createmultisig, createrawtransaction, decoderawtransaction, decodescript, getaddressbalance, getaddressdeltas, getaddresstxids, getaddressutxos, getbestblockhash, getblock, getblockchaininfo, getblockcount, getblockhash, getblockheader, getchaintips, getconnectioncount, getdifficulty, getlotteryblockwinners, getmasternodecount, getmasternodestatus, getmasternodewinners, getmempoolinfo, getmininginfo, getnetworkinfo, getrawmempool, getrawtransaction, getspentinfo, gettxout, listmasternodes, listmnbroadcasts, ping, sendrawtransaction, validateaddress, verifymessage.
Notice what is on the list: everything that reads the chain, the address index (getaddress…), and sendrawtransaction, so you can broadcast a transaction you signed yourself (see raw transactions and multisig). What is not on it: anything that touches a wallet. getnewaddress, sendtoaddress, signmessage and friends need your own node.
Ask for a method that is not allowed and you get a normal JSON-RPC error, HTTP 200, code -32601:
curl -s https://services.divi.domains/api/testnet/rpc/ -H 'Content-Type: application/json' \
-d '{"jsonrpc":"1.0","id":1,"method":"getnewaddress","params":[]}'
{"result":null,"error":{"code":-32601,"message":"Method not allowed: getnewaddress"},"id":null}
Errors the proxy raises itself come back with id null, because it refuses the request before reading the id. Errors from the node keep your id.
| Error from the public endpoint | Code | What it means |
|---|---|---|
Method not allowed: … | -32601 | Not on the list above, or misspelt. Unknown methods get this too. |
Invalid request | -32600 | The JSON is valid but is not a request object (missing method, for example). |
Batch must contain 1 to 20 calls | -32600 | An empty array, or more than 20 calls in one request. |
Parse error | -32700 | The body is not JSON. |
Backend unavailable | -32603 | The node behind the proxy did not answer. Try again in a minute. |
Errors from the node itself, such as a wrong parameter type or a block hash that does not exist, pass through unchanged with the node's own code and message.
7Several calls in one request
JSON-RPC lets you send an array of request objects and get an array of replies back, one HTTP round trip instead of many. It matters most over the internet: fetching the last five block hashes from the public endpoint is one request instead of five. Save this next to divi_rpc.py as batch.py:
"""One HTTP request, several calls: a JSON-RPC batch. The public endpoint allows up to 20."""
import base64
import json
import urllib.request
from divi_rpc import PASSWORD, URL, USER, rpc
tip = rpc("getblockcount")
calls = [{"jsonrpc": "1.0", "id": h, "method": "getblockhash", "params": [h]} for h in range(tip - 4, tip + 1)]
req = urllib.request.Request(URL, data=json.dumps(calls).encode(), headers={"Content-Type": "application/json"})
if USER:
req.add_header("Authorization", "Basic " + base64.b64encode(f"{USER}:{PASSWORD}".encode()).decode())
for reply in json.load(urllib.request.urlopen(req)):
print(reply["id"], reply["result"])
python3 batch.py
360214 33dd61ee63ae97a7b6473137aba70d65a896c2b9c551dcdc87768e19722f2a62 360215 f29fe2506dcb2866d73c2e34d045476282f237fe33e3441fa7211a3039879e6c 360216 f902057bee85c7de34f6db2c2a197fffe246814b24112e9fe26eb251d40b08e9 360217 3aa2d46dbec1be0b57148e76930d5c08df96714fc5b4eddf2bb1065986e0a803 360218 7d87795e23bbaac012b9686f64cd793d62476817d4cea3e1fcdccfa2853493bf
We ran this against the public endpoint, a local divid and a local IronDivi on 2026-10-21 and all three printed the same five lines. The replies come back in the same order as the calls, but the safe habit is to match on id, which is why each call uses its block height as the id.
- The public endpoint accepts 1 to 20 calls per batch. Your own node has no fixed limit.
- Each call in the array needs its own
jsonrpc,id,methodandparams. IronDivi rejects the whole batch with a parse error if any entry lacksjsonrpc. - One bad call does not spoil the others: its reply carries an
errorand the rest carry theirresult.
8Where divid and IronDivi differ
Both nodes take the same methods and give the same answers for the same chain. The differences are all in the envelope around the answer. The scripts above handle every row of this table; it is here for when you write your own client or debug a reply that looks odd.
| divid (C++) | IronDivi (Rust) | Public endpoint | |
|---|---|---|---|
| Testnet RPC port | 51475 | 51473 | 443 (HTTPS) |
| Config file | divi.conf, key=value | irondivi.conf, TOML with quoted strings | none |
| Successful reply | {"result":…,"error":null,"id":…} | {"jsonrpc":"2.0","id":…,"result":…} | {"error":null,"id":…,"result":…} |
| HTTP status on an RPC error | 500 (404 for an unknown method, 400 for a malformed request) | 200 | 200 |
| Error reply | "result":null plus an error object | an error object, no result key | "result":null plus an error object |
| Unknown method | HTTP 404, -32601 Method not found | -32601 Method not found: name | -32601 Method not allowed: name |
Request without jsonrpc | accepted | -32700 Parse error, id null | accepted |
| Wrong or missing password | HTTP 401 with an HTML body | HTTP 401, empty body | no password needed |
| Plain GET | 401 without a password; with one, HTTP 500 and -32700 Parse error | 401 without a password; with one, HTTP 405 | HTTP 200, JSON description of the service |
| Wrong parameter type (string for a height) | -1 value is type str, expected int | -1 Missing height parameter | whatever the node behind it says |
Three habits make a client work with all three: always send jsonrpc, read the body before looking at the HTTP status, and decide on error being present rather than on the message text.
If something goes wrong
| You see | What it means |
|---|---|
Connection refused | Nothing is listening on that port. Check the node is running, that server=1 is in divi.conf, and that you used 51475 for divid or 51473 for IronDivi on testnet. |
HTTP 401, RpcError: HTTP 401: … | Wrong username or password, or none sent. Compare with the rpcuser and rpcpassword lines in the config file; on IronDivi make sure the values are in quotes. |
-32700 Parse error from IronDivi on a request that works on divid | The request has no jsonrpc field. Add "jsonrpc":"1.0". |
-32601 Method not found / Method not allowed | Misspelt method, or (on the public endpoint) a method that is not on the allowed list, such as any wallet command. |
-1 value is type str, expected int / Missing height parameter | A parameter has the wrong JSON type. Heights are numbers, hashes are strings. |
-32600 Batch must contain 1 to 20 calls | Split the batch into pieces of 20 or fewer. |
| Browser console says CORS blocked the request | Only services.divi.domains pages may call the endpoint from a browser. Call it from a server instead. |
divid exits at start with a message about rpcpassword | server=1 is set but no rpcpassword. Add one. |
What next? RPC is pull: you ask, the node answers. To be told the moment a block or transaction arrives, see ZMQ for developers. To put this to work, see accepting Divi payments and sign and verify a message.
Still stuck? Ask the Divi chatbot, or see all tutorials.