Bitcoin live data API and webhooks
How do I use this live data in my own tools?
The same feed these pages read, as plain JSON. Every read is a GET, so a browser, a script or a spreadsheet can make it.
Quick start
Every read is a plain GET that answers JSON, with cross-origin access open to any site. There is nothing to sign up for to read.
The whole current picture is one request. The reply below is cut down to two readings; the real one carries all six.
curl https://maketo.com/api/live/state{
"now": 1761820020,
"keys": {
"price": {
"updated": 1761820020,
"data": { "ts": 1761820020, "px": 110234.5, "change_5m_bps": -3.1, "change_1h_bps": 12.4, "change_24h_bps": -85.0 }
},
"blockclock": {
"updated": 1761820020,
"data": { "ts": 1761820020, "tip": { "height": 973833 }, "secs_since": 339, "p_next_within": { "60s": 0.1, "300s": 0.41, "600s": 0.63 } }
}
}
}Endpoints
| Method and path | What it does |
|---|---|
GET / | Every current reading in one reply. |
GET / | One reading. An unknown key answers 404. |
GET / | The big-payment tape, newest first, or only what changed since a counter. |
GET / | Whether each reading is fresh, and how old each is in seconds. |
POST / | Register a webhook. Answers with an id and a secret, once. |
GET / | Check a webhook: whether it is still on and how many deliveries have failed in a row. Send its secret in the x-secret header. |
DELETE / | Remove a webhook. Send its secret in the x-secret header. |
The state reply
The reply has the time on our clock, now, and one entry for each reading. An entry holds the time the reading was made, updated, and the reading itself, data. A reading that has not been made yet is left out rather than sent empty.
GET /api/live/state/<key> answers one reading, with the same updated and data. Check updated before you trust a number: if it is old, the feed behind it has paused.
| Key | What it holds | Changes about |
|---|---|---|
price | The Bitcoin price from three large exchanges, and its change over 5 minutes, 1 hour and 24 hours. | every 5 seconds |
vol | The move-size forecast: for 5 minutes, 15 minutes, 1 hour, 4 hours and 24 hours, the forecast move, its range, its 0 to 100 gauge and its live record. | every minute |
window | The 5 and 15 minute windows now running: time left, move since the open, big payments inside, the lean with its live record. | every 3 seconds |
blockclock | The tip block, seconds since it, the odds of the next block, fee rates, the next blocks and the last blocks. | every 5 seconds |
tapemeta | Totals for the waiting room: payments waiting, the last hour and day, and how long before a block we usually see a payment. | every 5 seconds |
receipts | For each kind of big payment, the average move that followed the last 100, beside the average move from random moments. | every 10 minutes |
The tape
The tape lists every Bitcoin payment of 1 coin or more that has joined the queue for a block, with how old its coins are and what block should take it. A row changes when the payment gets a block or is dropped, and each change gets a higher upd.
To follow it, ask once without since, then keep asking with the highest upd you have. The pages here ask every 4 seconds.
| Parameter | Meaning |
|---|---|
since | A counter. Leave it out for the newest 300 payments. Give the highest upd you have and you get the rows that changed after it, oldest change first. |
limit | How many rows to return, up to 500. |
min_btc | Only payments of at least this many bitcoin. |
let since = 0;
async function poll() {
const url = since ? `https://maketo.com/api/live/tape?since=${since}&limit=500` : `https://maketo.com/api/live/tape?limit=300`;
const res = await fetch(url);
if (!res.ok) return; // wait and try again
const body = await res.json();
for (const row of body.events) handle(row); // a row can come again, with a higher upd
since = Math.max(since, body.upd);
}
setInterval(poll, 4000);{
"now": 1761820020,
"upd": 9000,
"events": [
{
"id": 4999,
"txid": "6b4cb2424a23d5962217beaddbc496cb8e81973e0becd7b03898d190f9ebdacc",
"first_seen": 1761819971,
"in_btc": 105.8768,
"moved_btc": 66.9722,
"n_in": 37,
"n_out": 3,
"age_wavg_days": 412.3,
"mvrv_spent": 1.87,
"kinds": ["big100"],
"status": "waiting",
"proj_block": 1,
"price_seen": 110234.5,
"upd": 8999
}
]
}| Field | Meaning |
|---|---|
id, upd | The row's number, and a counter that goes up each time the row changes. Keep the highest upd you have seen. |
txid | The Bitcoin transaction id. Addresses are never sent. |
first_seen | When the payment first joined the queue, in epoch seconds, UTC. |
in_btc, moved_btc | Bitcoin going in, and the part that really changed hands. The rest came back to the sender as change. |
n_in, n_out, feerate, vsize | Inputs, outputs, the fee rate in sat/vB and the size in virtual bytes. |
age_wavg_days | How long the spent coins had sat still, in days, averaged by size. |
old155_btc, old1y_btc, old5y_btc | How many bitcoin of the payment had sat still for at least 155 days, a year and five years. |
mvrv_spent | Today's price over the average price the spent coins were last bought at. Above 1 means they are worth more than was paid. |
profit_btc, loss_btc | How many bitcoin were last bought below, and above, today's price. |
kinds, flags | Tags for what sort of payment it is, such as big100 (100 bitcoin or more) and big1000 (1,000 or more). New tags can be added, so treat one you do not know as unknown. |
status | waiting, confirmed or dropped. |
conf_height, conf_ts, lead_s | For a confirmed payment: the block, the block's time, and how many seconds before it we first saw the payment. |
proj_block | While waiting: which block should take it. 1 is the next block. |
price_seen | The Bitcoin price when the payment was first seen. |
sender_label, dest_label | Null for now. |
Health
GET /api/live/health says whether the feed is healthy and how old each reading is, in seconds. It is the cheapest way to tell a paused feed from a quiet market.
{
"ok": true,
"now": 1761820020,
"ages": { "price": 3, "vol": 12, "window": 2, "blockclock": 4, "tapemeta": 4, "receipts": 211 }
}Webhooks
A webhook sends you the feed instead of waiting to be asked. Register an address and the topics you want. The reply holds an id and a secret. The secret is shown once and cannot be looked up again, so keep it.
Registering is checked: Maketo first sends one signed delivery with the topic ping to your address and saves the webhook only if you answer it with the challenge it carried, as plain text or as JSON.
A delivery is a POST of JSON to your address. Its body names the topic and the time it was sent, then carries what the read endpoints return: for tape, an events list of rows; for the other topics, a data object holding the same reading the state reply holds.
| Rule | Detail |
|---|---|
https only | The address must start with https and use port 443. |
A name, not a number | No IP addresses, no user name or password in the address, and no names ending in .local, .internal or .localhost. |
Topics | tape, vol, window and blockclock. A tape webhook may add min_btc. |
No redirects | A reply that redirects counts as a failure. |
Registration check | Before a webhook is saved, Maketo sends one signed delivery with the topic ping and a random challenge, and wants the challenge back in a 2xx answer. If it does not come back, the call answers 422 and nothing is saved. |
Speed | Answer within 5 seconds with a 2xx status. |
How often | vol and blockclock about every 15 seconds, window about every 10. tape as payments arrive and change. |
Retries | A tape delivery that fails is tried again after 1, 5 and 15 minutes. A vol, window or blockclock delivery is not tried again, because the next one replaces it. |
Switched off | A webhook that does not answer is left alone for a few seconds, then for longer each time, up to 5 minutes. After 20 failures in a row it is switched off, and removed a day later. |
Caps | 5 webhooks for each visitor and 100 in all. |
curl -X POST https://maketo.com/api/live/webhooks \
-H "content-type: application/json" \
-d '{"url":"https://example.com/hooks/maketo","topics":["tape","blockclock"],"min_btc":100}'{ "topic": "ping", "sent": 1761820020, "challenge": "9f2c0d3a77b14e6a8c51d0e2b4a6f813" }{ "challenge": "9f2c0d3a77b14e6a8c51d0e2b4a6f813" }{ "id": "wh_2f9c1e", "secret": "shown once, keep it" }{
"topic": "tape",
"sent": 1761820020,
"events": [
{ "id": 4999, "txid": "6b4cb2424a23d5962217beaddbc496cb8e81973e0becd7b03898d190f9ebdacc", "in_btc": 105.8768, "status": "waiting", "upd": 8999 }
]
}{
"topic": "blockclock",
"sent": 1761820020,
"data": { "ts": 1761820020, "tip": { "height": 973833 }, "secs_since": 339 }
}curl -X DELETE https://maketo.com/api/live/webhooks/wh_2f9c1e \
-H "x-secret: <the secret you were given>"Checking a delivery is from Maketo
Every delivery carries two headers that matter here: x-maketo-timestamp, the time we sent it in epoch seconds, and x-maketo-signature, which reads sha256= followed by a hex code. It also carries x-maketo-topic, x-maketo-delivery (an id, the same again if the delivery is retried) and x-maketo-attempt.
The code is an HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with your secret. Work it out yourself from the raw bytes you received and compare. Refuse a delivery whose timestamp is more than a few minutes off, and compare in constant time.
import hashlib, hmac, time
def is_from_maketo(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
if abs(time.time() - int(timestamp)) > 300: # too old or too new
return False
signed = timestamp.encode() + b"." + body # the timestamp, a dot, the raw body
expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)import { createHmac, timingSafeEqual } from "node:crypto";
export function isFromMaketo(secret, timestamp, rawBody, signature) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = "sha256=" + createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && timingSafeEqual(a, b);
}Limits and good manners
- Reads are cached for a few seconds in front of the feed (Cache-Control: public, max-age=2, s-maxage=3), so asking more than once every 2 seconds returns the same answer.
- The pages themselves ask for the state every 5 seconds and the tape every 4 seconds. Staying at that pace or slower is fine.
- Each visitor may make 120 reads a minute that reach the feed (the cached ones do not count). The pages themselves use about 27. Going over answers 429 with a retry-after header that says how many seconds to wait. Wait that long, then go back to your normal pace.
- Registering webhooks is limited to 10 an hour for each visitor, and checking or removing one to 60 an hour. These answer 429 the same way.
- A webhook body and every reply are small. Nothing here needs a key, and nothing here can change anything but your own webhooks.
- Times are UTC epoch seconds. Bitcoin amounts have 4 decimals, prices 2, and a missing figure is null, never NaN.
Using the data
The numbers are measured values with the size of the sample behind them. They are not advice. For how to reuse them and the credit we ask for, see the licence page.
How to read this
Start with the quick start. Everything else is a reference you can come back to.
Reading
All reads are GET requests that answer JSON. Try one in a browser's address bar.
Freshness
Each reading carries the time it was made. If that is old, the feed has paused and the numbers are the last ones sent.
Webhooks
Register an address to be sent the feed. Check every delivery's signature before you act on it.
Understanding
Bitcoin live data API and webhooks
What is this API?
The live pages on Maketo read a small JSON feed, and the feed is open to anyone. It carries the price, the move-size forecast, the 5 and 15 minute windows, the block clock, the waiting-room totals and the tape of big payments. It is the same data the pages show, in the same units.
What is a webhook?
Normally your program asks for data. A webhook turns that round: you give Maketo an address, and Maketo sends the data to it as it changes. Because anyone could send a request to your address and claim to be Maketo, each delivery is signed with a secret only you and Maketo hold.
Why might the feed pause?
The feed is made by several programs, and any of them can stop for a while. When one does, the readings it makes stop updating. The updated time on each reading, and the health reply, say how old each one is. A reply that comes back quickly is not proof the data in it is fresh.
Source: Maketo’s own live feed.


