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.

Ask for the current state
curl https://maketo.com/api/live/state
What comes back (cut down, made-up numbers)
{
  "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

The endpoints
Method and pathWhat it does
GET /api/live/stateEvery current reading in one reply.
GET /api/live/state/<key>One reading. An unknown key answers 404.
GET /api/live/tapeThe big-payment tape, newest first, or only what changed since a counter.
GET /api/live/healthWhether each reading is fresh, and how old each is in seconds.
POST /api/live/webhooksRegister a webhook. Answers with an id and a secret, once.
GET /api/live/webhooks/<id>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 /api/live/webhooks/<id>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.

The readings in the state reply
KeyWhat it holdsChanges about
priceThe Bitcoin price from three large exchanges, and its change over 5 minutes, 1 hour and 24 hours.every 5 seconds
volThe 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
windowThe 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
blockclockThe tip block, seconds since it, the odds of the next block, fee rates, the next blocks and the last blocks.every 5 seconds
tapemetaTotals 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
receiptsFor 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.

Tape parameters
ParameterMeaning
sinceA 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.
limitHow many rows to return, up to 500.
min_btcOnly payments of at least this many bitcoin.
Follow the tape
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);
One row (cut down, made-up numbers)
{
  "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
    }
  ]
}
Tape fields
FieldMeaning
id, updThe row's number, and a counter that goes up each time the row changes. Keep the highest upd you have seen.
txidThe Bitcoin transaction id. Addresses are never sent.
first_seenWhen the payment first joined the queue, in epoch seconds, UTC.
in_btc, moved_btcBitcoin going in, and the part that really changed hands. The rest came back to the sender as change.
n_in, n_out, feerate, vsizeInputs, outputs, the fee rate in sat/vB and the size in virtual bytes.
age_wavg_daysHow long the spent coins had sat still, in days, averaged by size.
old155_btc, old1y_btc, old5y_btcHow many bitcoin of the payment had sat still for at least 155 days, a year and five years.
mvrv_spentToday'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_btcHow many bitcoin were last bought below, and above, today's price.
kinds, flagsTags 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.
statuswaiting, confirmed or dropped.
conf_height, conf_ts, lead_sFor a confirmed payment: the block, the block's time, and how many seconds before it we first saw the payment.
proj_blockWhile waiting: which block should take it. 1 is the next block.
price_seenThe Bitcoin price when the payment was first seen.
sender_label, dest_labelNull 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.

A health reply (made-up numbers)
{
  "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.

What a webhook address must be
RuleDetail
https onlyThe address must start with https and use port 443.
A name, not a numberNo IP addresses, no user name or password in the address, and no names ending in .local, .internal or .localhost.
Topicstape, vol, window and blockclock. A tape webhook may add min_btc.
No redirectsA reply that redirects counts as a failure.
Registration checkBefore 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.
SpeedAnswer within 5 seconds with a 2xx status.
How oftenvol and blockclock about every 15 seconds, window about every 10. tape as payments arrive and change.
RetriesA 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 offA 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.
Caps5 webhooks for each visitor and 100 in all.
Register a webhook
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}'
The check your address must answer (made-up challenge)
{ "topic": "ping", "sent": 1761820020, "challenge": "9f2c0d3a77b14e6a8c51d0e2b4a6f813" }
What your address answers
{ "challenge": "9f2c0d3a77b14e6a8c51d0e2b4a6f813" }
The reply to the registration
{ "id": "wh_2f9c1e", "secret": "shown once, keep it" }
A tape delivery (cut down, made-up numbers)
{
  "topic": "tape",
  "sent": 1761820020,
  "events": [
    { "id": 4999, "txid": "6b4cb2424a23d5962217beaddbc496cb8e81973e0becd7b03898d190f9ebdacc", "in_btc": 105.8768, "status": "waiting", "upd": 8999 }
  ]
}
A delivery for another topic (cut down, made-up numbers)
{
  "topic": "blockclock",
  "sent": 1761820020,
  "data": { "ts": 1761820020, "tip": { "height": 973833 }, "secs_since": 339 }
}
Remove it
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.

Python
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)
Node
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.

Licence and citation

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.

THE WEEKLY READ

A little context.
A clearer week.

Get the week’s Bitcoin moves, important charts and news in one short email. The first email has not gone out yet.

We’ll send one short email a week once it starts. No spam, and you can leave at any time. How we use your details.

You’re on the list. The Weekly Read will reach you once it starts.

That did not go through. Please check your details and try again.

AS THE MARKET MOVES

Follow the story
throughout the day.

New charts, important moves and live updates from Maketo.

Follow on X@Maketocom