BitcoinDatabase.com
All posts
Data guides

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

Query Console
btc
try:

Hit Run to query the fully-indexed Bitcoin blockchain.

BTC

30-day trend

informational on-chain data · not financial advice

REST API · SQL · dashboards, one indexed dataset. Querying the indexed Bitcoin blockchain ...

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.

Keep reading

More from the BitcoinDatabase blog

Data guides

BigQuery Bitcoin Dataset: How to Query It and What It Costs

Google publishes the Bitcoin chain as four raw tables in BigQuery, free to access and billed per terabyte scanned. What is in the dataset, why balances and entity labels are not, the SQL that reconstructs a balance, and the four reasons a Bitcoin query gets expensive.

Read
Buying guides

Bitcoin API Rate Limits Compared: Four Providers Tested

We called four public Bitcoin endpoints on 11 August 2026 and read every response header. Only one returned a quota header, two publish no limit at all, and one reported the chain a block ahead of the others for a reason that is not a bug. What each provider tells you before it throttles you, and the four things worth building into any client.

Read
Data guides

How to Check a Bitcoin Address Balance on a Specific Date

The blockchain stores no balances, only unspent outputs, and explorers report the present tense. We tested four public endpoints on one address: two ignored a height parameter, one returned zeroes, one is gone. What actually works, the arithmetic behind a point-in-time balance, and the five errors that quietly corrupt one.

Read
Data guides

How to Check Multiple Bitcoin Address Balances at Once

Free bulk checkers cap out at 20 to 50 addresses per request, which is fine for a few paper wallets and useless for a treasury. The four methods compared, why the caps exist, how xpub scanning and its gap limit work, and why no balance check ever needs a private key.

Read

Query the whole Bitcoin blockchain

BitcoinDatabase indexes the public Bitcoin blockchain block by block and returns balances, UTXOs, transactions, on-chain metrics and fund flows by REST API, SQL and dashboards, with no node to run.

REST + SQL + dashboards · indexed since 2009 · new blocks within seconds

Informational on-chain data only · not financial, investment or legal advice · AML features are compliance tooling to support your own review.