Bitcoin Data in Python: Four Public APIs Tested with requests
We called four public Bitcoin APIs from Python on 12 August 2026 and compared exactly what came back. One endpoint returns HTTP 200 and still crashes response.json(), two providers reported different block counts and both were right, and the two big explorers answer the fee question in shapes that are not interchangeable. The findings, the twenty-line client that survives them, and why no Bitcoin package is required.
By the BitcoinDatabase team
August 2026 · 8 min read
Hit Run to query the fully-indexed Bitcoin blockchain.
BTC
30-day trend
informational on-chain data · not financial advice
The short answer
Getting Bitcoin data into Python takes one HTTP call with the requests library, not a Bitcoin package. We called four public Bitcoin APIs from Python on 12 August 2026 and compared exactly what came back. Three findings are worth knowing before you write the client: response.json() crashes on some endpoints that return HTTP 200, two providers disagreed about the block count for a reason that is not a bug, and the two big explorers returned fee estimates in shapes that are not interchangeable. Only BlockCypher told us how much quota we had left.
Almost every Python guide to Bitcoin data starts by installing a Bitcoin package. That is usually the wrong first move. The libraries that dominate a search for a Bitcoin Python library are built to handle keys, derive addresses and construct transactions. They are good at that. None of them holds the chain, so the moment you want a balance or a transaction history, something has to go and ask an index over the network anyway.
So the real question is not which library to install. It is which data source to point requests at, and what its responses do to your code. We called four of them from Python on 12 August 2026 at 15:51 UTC, from one US server, using ordinary single requests rather than a burst. We did not try to force a 429 out of anyone, because that is abusive and the headers told us what we needed.
What we called and what came back
Four providers, cheap endpoints, one request each. The column that matters most is the last one, because it is the one that decides whether your code runs or raises.
| Provider | Endpoint | Elapsed | Content type | response.json() |
|---|---|---|---|---|
| mempool.space | /api/blocks/tip/height |
0.09s | text/plain | Returns 962164 as an int |
| mempool.space | /api/blocks/tip/hash |
0.10s | text/plain | Raises JSONDecodeError |
| Blockstream Esplora | /api/blocks/tip/height |
0.59s | text/plain | Returns 962164 as an int |
| Blockstream Esplora | /api/address/{addr} |
0.57s | application/json | Returns a dict |
| BlockCypher | /v1/btc/main |
0.16s | application/json | Returns a dict |
| Blockchair | /bitcoin/stats |
0.21s | application/json | Returns a dict, data nested |
Why response.json() fails on an endpoint that returned 200
This is the one that costs people an afternoon, because the request succeeded. Status 200, no timeout, no rate limit. The explorer endpoints for the chain tip return a bare value with a text/plain content type, and requests will happily try to parse it as JSON if you ask it to. Sometimes that works. Sometimes it raises, and the difference is invisible from the endpoint name.
import requests
r = requests.get("https://mempool.space/api/blocks/tip/height")
r.status_code # 200
r.headers["content-type"] # text/plain
r.json() # 962164 <- works, an int
r = requests.get("https://mempool.space/api/blocks/tip/hash")
r.status_code # 200
r.json() # JSONDecodeError: Extra data: line 1 column 2 (char 1)
A bare number is valid JSON, so the height parses cleanly into a Python int. A block hash is a bare hex string with no quotes around it, which is not valid JSON, so the parser fails. The error message is the confusing part. Every Bitcoin block hash begins with a run of zeroes, so the JSON parser reads the first 0 as a complete number, then finds another character where the document should have ended, and reports Extra data: line 1 column 2. It sounds like a truncation problem. It is not.
The same class of bug produces a completely different message on a transaction ID, because a txid usually starts with a letter:
import json
json.loads("962164")
# 962164
json.loads("000000000000000000008d64936439fec9c438cf23328758534ac9db5fc7bc17")
# JSONDecodeError: Extra data: line 1 column 2 (char 1)
json.loads("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d")
# JSONDecodeError: Expecting value: line 1 column 1 (char 0)
Two messages, one cause. If you are searching an error string to work out what broke, you will find nothing useful for either. The rule is simple: use response.text for endpoints that return a single scalar, and keep response.json() for endpoints that return an object. Checking the content type does not save you here, because both of those endpoints reported text/plain and one of them parsed fine.
Two providers, two different block counts, both correct
We polled all four tip endpoints in parallel at 15:52:17 UTC on 12 August 2026 so nobody had time to fall behind:
mempool height = 962164 esplora height = 962164 blockcypher height = 962164 blockchair blocks = 962165
The obvious reading is that Blockchair is a block ahead, or that three providers are lagging. Neither is true, and an alert built on that assumption will page someone at three in the morning for nothing. Blockchair's field is named blocks and it is a count. The other three return a height. Because the genesis block is height 0, a chain whose tip is at height 962,164 contains 962,165 blocks. Both numbers are right, they answer different questions, and the difference is exactly one forever.
This matters in Python specifically because the two values look identical once they are in a variable. If you are reconciling providers, compare like with like and normalize on ingest rather than at the comparison, or subtract one from any field named for a count. We first measured this on 11 August and it reproduced exactly on 12 August, so it is structural rather than a moment of drift. There is more on how providers differ under load in our comparison of Bitcoin API rate limits.
Fee estimates come back in two shapes that are not interchangeable
If your Python code attaches a fee to a transaction, this one has a cost attached. Both major explorers answer the question "what should I pay" and neither answers it in the same format.
# mempool.space /api/v1/fees/recommended
{"fastestFee": 1, "halfHourFee": 1, "hourFee": 1, "economyFee": 1, "minimumFee": 1}
# Blockstream /api/fee-estimates (keys are target block counts)
{"1": 2.113, "2": 2.113, "3": 1.222, "6": 1.0030000000000001,
"12": 0.756, "144": 0.346, "1008": 0.104}
Named tiers against numeric string keys. Whole integers against floats. At the same moment on 12 August 2026, mempool.space said the fastest fee was 1 sat per vByte and Blockstream said the next block wanted 2.113. Swapping providers behind the same function silently doubles or halves what your wallet pays, and a KeyError on fastestFee is the friendlier of the two failure modes, because at least it is loud.
Notice 1.0030000000000001 in the raw response. That is binary floating point noise, and it arrives that way over the wire. Round fee rates before you use them, and never assume a fee estimate is an integer: Blockstream returns values below 1 for distant targets, which mempool.space cannot express because its tiers floor at 1.
Only one provider tells you how much quota is left
We read every response header on all four calls. Only BlockCypher returned a quota header, and it was not comfortable reading:
r = requests.get("https://api.blockcypher.com/v1/btc/main")
r.headers.get("x-ratelimit-remaining") # '1'
One request left on the hour, on a shared server IP, from a single polite call. That is the free tier working as documented at 3 requests per second and 100 per hour, and it is a fair illustration of why free explorer tiers do not survive a production workload. The credit where it is due is real though: BlockCypher is the only one of the four that lets your code see the ceiling coming. mempool.space, Blockstream and Blockchair returned no quota header at all, so on those three your first signal is a failed request.
A client that survives all of the above
None of this needs a framework. It needs about twenty lines that assume the network is hostile, retry on the status codes worth retrying, and never call .json() on a scalar endpoint.
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
retry = Retry(
total=5,
backoff_factor=0.5, # 0.5s, 1s, 2s, 4s, 8s
status_forcelist=(429, 500, 502, 503, 504),
allowed_methods=("GET",),
respect_retry_after_header=True,
)
session.mount("https://", HTTPAdapter(max_retries=retry))
session.headers.update({"User-Agent": "my-app/1.0"})
def get_json(url, **kw):
r = session.get(url, timeout=10, **kw)
r.raise_for_status()
return r.json()
def get_scalar(url, cast=str):
r = session.get(url, timeout=10)
r.raise_for_status()
return cast(r.text.strip()) # never .json() here
Two functions, one rule each. backoff_factor matters more than total: retrying a 429 immediately just burns the next slot of a quota you have already exhausted. If writing that wrapper for the fourth time this year is not how you want to spend the morning, an AI coding agent will scaffold the boilerplate and let you spend the time on the parts that are actually Bitcoin specific.
Satoshis, floats and the one thing Python gets right
Every serious Bitcoin API returns amounts as integer satoshis, and this is where Python is genuinely easier to get right than JavaScript. Python integers have arbitrary precision, so you can total tens of thousands of outputs without a rounding error creeping in. The mistake is converting to BTC too early:
from decimal import Decimal
sats = 5734295147
sats / 1e8 # 57.34295147 float, fine to print, unsafe to sum
Decimal(sats) / Decimal(100_000_000) # Decimal('57.34295147') exact
That number is real. It is the total ever funded to the genesis address 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa, read from Blockstream on 12 August 2026: 5,734,295,147 satoshis across 78,091 funded outputs and 65,230 transactions. When we read the same address on 4 August it was 5,732,830,722 satoshis over 64,897 transactions, so in eight days people sent Satoshi's address another 1,464,425 satoshis in 333 transactions. Nobody can spend any of it. It is also a useful reminder that these figures move, which is why every number on this page carries the date we read it.
Keep amounts as int for the whole pipeline, convert with Decimal at the display layer, and never let a float into a total.
How do I get Bitcoin blockchain data in Python?
Call an indexed Bitcoin API over HTTP with requests. Install requests, send a GET with your API key in an Authorization header, and parse the JSON into a dict. No Bitcoin-specific package is needed, because the indexing work happens server side. Our Bitcoin API for Python page walks through the endpoints and shows how the same data is queryable with SQL.
Which Bitcoin Python library should I use?
For cryptography and transaction building, python-bitcoinlib and bitcoinlib are the established options, at 0.12.2 and 0.7.9 on PyPI as of 12 August 2026. For reading chain data, use no Bitcoin library at all. A library cannot answer what an address held last March without querying an index over every block, so it will make the same HTTP call you would have made yourself, with a package release cycle in between.
How do I avoid Bitcoin API rate limits in Python?
Assume the limit is invisible and design for the 429. Mount a Retry with a backoff factor, cache anything you poll more than once per second, and batch address lookups into one request where the provider supports it. Blockstream sets a 10 second CDN cache on its responses, so polling faster than that spends quota on a value that cannot have changed. Fixing the polling interval usually removes the problem entirely.
What this means for picking a source
The public explorers are genuinely good and cost nothing, and for a prototype they are the right answer. What they will not do is commit to a limit, an uptime figure or a stable response shape, and two of the three findings above are the direct consequence of that. The moment a customer is waiting on the answer, you want one integration whose responses are documented and whose quota you can plan against, covering current state and history from the same base URL. That is what the Bitcoin API is for, and you can look up any Bitcoin address or pull UTXO data from the same key.
Everything in this article was measured on 12 August 2026 and the exact values are printed above so you can rerun them. Providers change their responses without announcing it, so if you are reading this much later, run the four calls again before trusting the numbers. This is informational on-chain data only, not financial or investment advice.
Query the Bitcoin blockchain yourself
Pull balances, UTXOs, transactions, on-chain metrics and fund flows from the fully indexed Bitcoin blockchain by REST API, SQL and dashboards. Indexed since 2009, new blocks within seconds, no node to run.