Tutorial · Developers

Talk to a Divi Node from Python and JavaScript

Every Divi node answers questions over JSON-RPC. Turn it on, make your first call with curl, then do the same from Python and Node.js with no packages to install.

About 25 minutes · Checked 2026-10-21

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
localhost
Public testnet endpoint
services.divi.domains/api/testnet/rpc/
Two node programs, one protocol

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.

divid testnet RPC
port 51475
divid mainnet RPC
port 51473

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.

IronDivi testnet RPC
port 51473
IronDivi mainnet RPC
port 51471
The two nodes use different testnet ports

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"]}'
Always send the jsonrpc field

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.

Why the script catches HTTPError

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.

In a browser, use the public endpoint only

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:

RuleValue
Addresshttps://services.divi.domains/api/testnet/rpc/
Authenticationnone
NetworkDivi testnet only
Methods34 methods (list below); anything else is refused
Calls per batch1 to 20
Request bodyat most 512 KiB
CORSonly for pages on https://services.divi.domains
Reply shapethe 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 endpointCodeWhat it means
Method not allowed: …-32601Not on the list above, or misspelt. Unknown methods get this too.
Invalid request-32600The JSON is valid but is not a request object (missing method, for example).
Batch must contain 1 to 20 calls-32600An empty array, or more than 20 calls in one request.
Parse error-32700The body is not JSON.
Backend unavailable-32603The 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.

Batch rules
  • 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, method and params. IronDivi rejects the whole batch with a parse error if any entry lacks jsonrpc.
  • One bad call does not spoil the others: its reply carries an error and the rest carry their result.

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 port5147551473443 (HTTPS)
Config filedivi.conf, key=valueirondivi.conf, TOML with quoted stringsnone
Successful reply{"result":…,"error":null,"id":…}{"jsonrpc":"2.0","id":…,"result":…}{"error":null,"id":…,"result":…}
HTTP status on an RPC error500 (404 for an unknown method, 400 for a malformed request)200200
Error reply"result":null plus an error objectan error object, no result key"result":null plus an error object
Unknown methodHTTP 404, -32601 Method not found-32601 Method not found: name-32601 Method not allowed: name
Request without jsonrpcaccepted-32700 Parse error, id nullaccepted
Wrong or missing passwordHTTP 401 with an HTML bodyHTTP 401, empty bodyno password needed
Plain GET401 without a password; with one, HTTP 500 and -32700 Parse error401 without a password; with one, HTTP 405HTTP 200, JSON description of the service
Wrong parameter type (string for a height)-1 value is type str, expected int-1 Missing height parameterwhatever 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 seeWhat it means
Connection refusedNothing 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 dividThe request has no jsonrpc field. Add "jsonrpc":"1.0".
-32601 Method not found / Method not allowedMisspelt 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 parameterA parameter has the wrong JSON type. Heights are numbers, hashes are strings.
-32600 Batch must contain 1 to 20 callsSplit the batch into pieces of 20 or fewer.
Browser console says CORS blocked the requestOnly 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 rpcpasswordserver=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.