bitcoin-core-rpc
A standalone JSON-RPC client against Bitcoin Core. As used by the btclib library.
Nothing but the standard library behind it, fully annotated and shipping
py.typed. BitcoinCoreRpcClient invokes any one rpc method a node has,
with positional or named parameters: one HTTP POST per call, basic
authentication, the result or an exception.
pip install bitcoin-core-rpc
Talking to a node
from_chain is the local node of one of Core's chains: the loopback url,
the port and the cookie file all come from Core's own defaults, so a node
started with none of them overridden needs no arguments at all.
from bitcoin_core_rpc import BitcoinCoreRpcClient
client = BitcoinCoreRpcClient.from_chain("main")
print(client.call("getblockcount"))
print(client.call("getblockchaininfo")["chain"])
The credential is the cookie file bitcoind rewrites at every start, read at
each call rather than held: a client built when the node was up still works
an hour and a restart later. The datadir it is looked for under is Core's
own for the platform running — %APPDATA%\Bitcoin on Windows,
~/Library/Application Support/Bitcoin on macOS, ~/.bitcoin elsewhere —
so from_chain needs no help on any of the three. A node started with
-datadir= somewhere else is what nothing can derive, and there the path
is an argument:
from pathlib import Path
client = BitcoinCoreRpcClient(
"http://127.0.0.1:8332",
cookie_path=Path("/srv/bitcoin/.cookie"),
)
rpcuser and rpcpassword are the other way. They are arguments and never
part of the url: a url with user:password@ in it is refused, because that
string ends up in configuration files, tracebacks and logs.
client = BitcoinCoreRpcClient(
"http://127.0.0.1:8332", user="rpcuser", password="rpcpassword"
)
A cookie or a datadir only authenticates the node that wrote it, not the
chain it is running: -chain=test and a main cookie both exist. main
is from_chain's own default, so a caller who names it and gets test
back either passed the wrong name or reused a cookie carried over from
elsewhere — verify_chain asks getblockchaininfo once and raises
BtcRpcValueError if the two disagree, rather than trusting the port and
the datadir name to have agreed with the node underneath them.
client = BitcoinCoreRpcClient.from_chain("main", verify_chain=True)
Signet is the case a name cannot settle: Core answers signet for the
default signet and for every custom one alike, so the challenge is what
tells two of them apart. Pass the one you mean, hex or bytes, as
-signetchallenge takes it; the comparison is of the p2p magic it derives,
which is what makes the same challenge in upper case the same challenge.
client = BitcoinCoreRpcClient.from_chain(
"signet", verify_chain=True, signet_challenge="512102...ae"
)
assert_chain is that check on its own, for a client built at a url of your
own — a node on another host, or behind a proxy — where there is no
from_chain to ask for it.
client = BitcoinCoreRpcClient("https://node.example/", user="u", password="p")
client.assert_chain("signet", signet_challenge="512102...ae")
Calling
params is one value, shaped as JSON-RPC shapes it: a sequence for the
positional form, a mapping for the named one. Core takes both, and which
one a method wants is the method's business.
block_id = client.call("getblockhash", [700_000])
block = client.call("getblock", {"blockhash": block_id, "verbosity": 2})
Amounts do not travel as binary floating point in either direction: a
number in the reply decodes as a Decimal, and a Decimal parameter is
refused rather than rounded through float.
balance = client.for_wallet("hot").call("getbalance") # Decimal, exact
for_wallet is the /wallet/<name> endpoint of a node with several
wallets loaded, with the name percent-encoded — a wallet is a directory and
may be called anything a filesystem accepts. Every wallet's client is
derived from the client built for the node: calling it on a client that is
already a wallet endpoint is refused rather than composing
/wallet/hot/wallet/cold, which is no path Core serves.
call_raw is call with the envelope handed back instead of read: the
same request, the same params validation, with the protocol marker an
argument rather than always "2.0" — a string is sent verbatim, None
sends no marker at all. The answer is the HTTP status and whatever the
body parses to, with neither id, error nor shape interpreted — an
array or a bare scalar comes back exactly as parsed rather than being
refused the way call's own reply is, which is what a harness testing
a server's own conformance wants in place of a result already extracted.
status, reply = client.call_raw("getblockcount", jsonrpc=None)
Batching
call_batch sends several (method, params) pairs in one HTTP POST,
each built and each member's reply read the way call builds and reads
its own. The result is a list aligned with the input: a member's
RpcError sits at its own position rather than raising and discarding
every answer beside it.
results = client.call_batch([("getblockhash", [700_000]), ("getblockcount", None)])
Only a failure of the whole exchange raises — a non-2xx status, a reply
that is not an array — exactly as call raises for its own request.
COMPARISON.md has where this is worth
reaching for over a loop of call, and where it is not.
Attribute-style calls
RpcChannel wraps a client for a caller who wants rpc.getblockcount()
over client.call("getblockcount") and has weighed the trade in
COMPARISON.md: an unknown method is a
request to the node rather than an AttributeError here.
from bitcoin_core_rpc import BitcoinCoreRpcClient, RpcChannel
rpc = RpcChannel(BitcoinCoreRpcClient.from_chain("main"))
print(rpc.getblockcount())
print(rpc.getblock(blockhash=block_id, verbosity=2))
Positional arguments become the sequence form of params, named ones the
mapping form — never both at once, json-rpc having one shape per call.
request_timeout and max_body_size, call's own keyword-only controls,
reach call rather than travelling to the node as a named parameter; every
other name starting with _, dunders included, is an AttributeError
instead of a request, which is what keeps copy.deepcopy and an
interactive shell's attribute probing from becoming one.
Keeping a connection alive
The default transport opens one connection per call and lets the node
close it — COMPARISON.md has
why. SessionTransport is this module's own alternative: one connection
kept per node, reused across calls, passed the way any other transport
is.
from bitcoin_core_rpc import BitcoinCoreRpcClient, SessionTransport
with SessionTransport() as session:
client = BitcoinCoreRpcClient.from_chain("main", transport=session)
client.call("getblockcount")
client.call("getblockchaininfo") # the same connection, reused
One instance is safe to share between threads: the whole exchange — connect or reuse, send, read — is serialized under one lock, at the cost of never running two calls through it at once. A caller wanting concurrency across several nodes keeps one instance per node instead.
Reading Core's -rest interface
-rest is Core's other interface, read-only, off by default, and — unlike
JSON-RPC — authenticating nobody who reaches it. BitcoinCoreRestClient
speaks it: get_bin for a .bin path, returning the body unread, and
get_json for a .json one, returning what it parses to. It shares the
transport and the chain vocabulary with BitcoinCoreRpcClient and nothing
else — no credentials, -rest taking none.
from bitcoin_core_rpc import BitcoinCoreRestClient
rest = BitcoinCoreRestClient.from_chain("main")
info = rest.get_json("/chaininfo.json")
raw_tx = rest.get_bin(f"/tx/{tx_id}.bin")
There is no get_tx, no get_block, no get_utxos: path is built from
Core's own documentation of -rest and appended after /rest unread.
/getutxos is why a per-resource method is refused rather than merely
undone here — it reads the UTXO set, so an output already spent and one
never created answer the same way, and a wrapper turning that answer into
None would read as telling more than /getutxos does.
When it goes wrong
Each exception below is a different thing to do about a failure. Every
one of them is a FetchError, so one except FetchError covers the lot.
| exception | what happened | what it carries |
|---|---|---|
RpcError |
the node computed an error | code, data |
HttpError |
the exchange failed | status |
FetchError |
there was no answer to read | — |
from bitcoin_core_rpc import FetchError, HttpError, RpcError
try:
raw = client.call("getrawtransaction", [tx_id])
except RpcError as e:
if e.code == -5: # no such transaction; a node without -txindex
... # answers this for anything outside its wallet
except HttpError as e:
if e.status == 503: # the rpc work queue is full: try again later
... # a 401 never works again, so tell the two apart
except FetchError:
... # refused connection, expired timeout, no answer
There is no retry, and it is deliberate: call carries any method, so this
client cannot know whether re-sending one is safe, and a timeout is not a
deadline — a node that stopped answering may still be executing the call.
HttpError.status is what makes a caller's own policy three lines rather
than a match on the text of a message.
What it does not do
- notifications, a request sent with no
id, which a node does not answer. - retries, per above.
- redirects. A 30x arrives as an
HttpErrorrather than as a second request: the first one already carries theAuthorizationfor the host it names, and following the redirect would send that credential wherever theLocationpoints. - proxies from the environment.
HTTP_PROXYis set for a browser or a package manager and inherited by everything in the shell, which is the wrong source for the decision of where a wallet command is sent. A caller who does want one passes atransport.
Migrating from AuthServiceProxy
It is not python-bitcoinrpc's AuthServiceProxy and not a port of it.
That class, and the copy of it Core's test framework maintains, carry the
LGPL-2.1 of their python-jsonrpc ancestry, where this is MIT: this is an
implementation of the protocol and shares no line with either.
# AuthServiceProxy
rpc = AuthServiceProxy(f"http://{user}:{password}@127.0.0.1:8332")
# this client
client = BitcoinCoreRpcClient("http://127.0.0.1:8332", user=user, password=password)
docs/source/migrating.md has the rest, an
AuthServiceProxy line at a time with the line that replaces it, for a
caller carrying either Core's copy or python-bitcoinrpc's own.
COMPARISON.md has the case for the switch beyond any
one rewrite, and why the features this client does not have — and the
one, dynamic dispatch, offered differently instead — are decisions rather
than omissions.
Testing code that calls a node
transport is the seam, and it is public for this: a callable taking the
request and a timeout, answering with the HTTP status and the body. The
suite of this project opens no socket, and neither has yours to.
import json
def transport(request, timeout):
request_id = json.loads(request.data)["id"]
body = {"jsonrpc": "2.0", "id": request_id, "result": 481824}
return 200, json.dumps(body).encode()
client = BitcoinCoreRpcClient(
"http://127.0.0.1:8332", user="u", password="p", transport=transport
)
assert client.call("getblockcount") == 481824
What a transport of your own owes, none of which this module can check for
it: its own bound on what it holds in memory while reading, its own bound
on how long it holds the call — most client libraries spend timeout per
socket operation, which a peer dripping a body resets forever — no
redirect followed, and its own thread safety.
Type checking
The source is annotated throughout, mypy --strict runs over it here, and
the distribution ships py.typed — so your own checker reads those
annotations with no configuration of any kind:
$ mypy --strict your_code.py
your_code.py:4: error: Argument 1 to "call" of "BitcoinCoreRpcClient" has
incompatible type "int"; expected "str"
That marker is why py.typed sits inside src/bitcoin_core_rpc/: PEP 561
puts it inside a package directory and nowhere else. pyproject.toml
records what the alternatives were measured to do.
Security
Basic authentication is cleartext over plain HTTP, that being what Core's rpc speaks. On loopback that cleartext is between one process and the node beside it; for a node anywhere else it is on the wire, and rpc credentials authorise every wallet command that node has. SECURITY.md carries the rest, and how to report a vulnerability.
Contributing
CONTRIBUTING.md has the commands each CI job runs,
verbatim. uv sync creates the environment; uv is the only tool that has
to be installed. REVIEWING.md is what a pull request is
answered against.
Links
- Documentation: https://bitcoin-core-rpc.readthedocs.io/
- Source: https://github.com/btclib-org/bitcoin-core-rpc
- Releases: https://github.com/btclib-org/bitcoin-core-rpc/releases
- CHANGELOG.md, and RELEASE_NOTES.md for what a release asks a user to act on
The btclib organization and its projects are actively supported by DGI and CheckSig.
Release files for bitcoin-core-rpc 2026.9.24
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| bitcoin_core_rpc-2026.9.24.tar.gz | 576.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bitcoin_core_rpc-2026.9.24-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 641.8 kB
Release files / bitcoin_core_rpc-2026.9.24.tar.gz
| Download URL | bitcoin_core_rpc-2026.9.24.tar.gz |
|---|---|
| Size | 576.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3e4f7f01a892753f150d07fccfc100b04567bf75ba4e3435e829bee42ddecb7a
|
|
BLAKE2b-256 checksum How to use checksums |
669fb925bfa8b1b3b57185f6d6bb4b6bf0b7fef0412145ebe86b4d385903dae6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency logRelease files / bitcoin_core_rpc-2026.9.24-py3-none-any.whl
| Download URL | bitcoin_core_rpc-2026.9.24-py3-none-any.whl |
|---|---|
| Size | 65.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8aebfffedeb60d6e34c6b07b5f31a9174ea5b8552d16e0d0166bf9422a0fd99a
|
|
BLAKE2b-256 checksum How to use checksums |
39ab7643e929c676ec2c0097423557d789bd9a563139ae6a9e442468004a40f8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.
Transparency log