Overview
One request, one already-aggregated response. Every route shares the same shape, the same envelope and the same cross-cutting parameters: what is true of one family is true of the others.
#The shape of a request
GET https://api.bytnode.com/v1/<family>/<metric>
?symbol=BTCUSDT # required on per-asset routes
&timeframe=1h # on bucketed routes
&limit=100 # on /history routes
&fields=time,rate_pct
X-API-KEY: your_keyThe path says the family and the metric. The parameters say the asset, the timeframe and the depth. The header carries the key. There is nothing else to know to call any of the routes.
One endpoint alone escapes this shape: POST /v1/indicators, which takes a JSON body because it describes a computation, not a read.
#Latest value or history
Most metrics come in two versions. The bare route returns the latest value: an object, or null. The /history suffix returns a list of closed windows only, from the newest to the oldest.
# The latest value
GET /v1/basis?symbol=BTCUSDT
# The last 24, from newest to oldest
GET /v1/basis/history?symbol=BTCUSDT&timeframe=1h&limit=24
# A dated window, in epoch milliseconds (here the last 12 hours, computed by the shell)
NOW=$(( $(date +%s) * 1000 ))
curl -H "X-API-KEY: $BYTNODE_API_KEY" \
"https://api.bytnode.com/v1/basis/history?symbol=BTCUSDT&timeframe=1h&since_ms=$(( NOW - 43200000 ))&until_ms=$NOW"#Common parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
symbol | string | required | The asset, in catalogue format (BTCUSDT), in any case. Missing: 422. Unknown: 422 with the list. Whole-market routes do not accept it. |
timeframe | string | required | On bucketed routes, required: 1m, 5m, 15m, 30m, 1h, 4h, 1d (and 1w on candles). Missing or off the list: 422. One row per minute is timeframe=1m. |
limit | integer | — | Number of rows, newest first, on /history and series routes: 100 by default and 1000 at most on every route; 10,000 at most on individual events (spot and futures ticks, large trades, liquidations). Beyond a page, follow next. |
since_ms / until_ms | integer | — | Time window in Unix milliseconds, where the route accepts it. The filter applies before limit. |
live | boolean | false | On bucketed latest-value routes: exposes the period in progress, marked is_closed: false. |
fields | string | — | CSV projection of data, dotted notation for nested fields. Unknown field: 422 with a suggestion. |
What does not exist
The exchange parameter exists on no route: responses are aggregated and do not name their venue, with the single exception of the cross-venue spreads. Passing it returns 422, as does any parameter a route does not declare, or a parameter repeated; the error suggests the closest name.
#Paginating a history
Every /history route pages with next: each response carries the URL of the following page, or null once the range is read. Rows come newest first by default; with order=asc (and since_ms), oldest first. Every row carries time and is_closed.
- Ask for a first page:
limitrows, with no bound for the most recent ones, orsince_msandorder=ascto read forward from a date. - Call
nextas is: it is the relative URL of the following page (/v1/klines/history?symbol=BTCUSDT&timeframe=1h&limit=500&before_ms=…), your parameters included — prefix it with the API host. Backwards, it usesbefore_ms, an EXCLUSIVE bound: no row comes back twice, nothing to subtract. - Stop when
nextisnull: the range is read, or the following page would leave the history window of your plan (the last page then asks only for the rows that still fit). A403history_depthonly comes back if you write a bound beyond that window yourself;GET /v1/limitsgives the windows of your plan. On individual trades (/v1/trades/*) a page covers at most 15 minutes and can be empty or partial whilenextis notnull: onlynext: nullends the range. The same holds on every paged route: a history read that reaches a dense period returns, within its time budget, the rows it has read — a short page with anext, never a timeout. The official SDKs follownextfor you.
import os
import requests # pip install requests
def history(path: str, params: dict, max_pages: int = 20) -> list[dict]:
"""Every row of a /history route, following `next` until the range is read."""
rows: list[dict] = []
url, query = f"https://api.bytnode.com{path}", params
for _ in range(max_pages):
body = requests.get(
url,
params=query,
headers={"X-API-KEY": os.environ["BYTNODE_API_KEY"]},
timeout=30,
).json()
if body["status"] == "error":
# history_depth: the range asked goes further back than your plan's window;
# error.message gives the largest limit that fits. Never swallow it.
raise RuntimeError(f'{body["error"]["code"]}: {body["error"]["message"]}')
page = body["data"] or []
rows += page
if body.get("next") is None:
break # the range is read
# `next` is the relative URL of the following page, your parameters
# included: call it as is. It never repeats a row.
url, query = f"https://api.bytnode.com{body['next']}", None
return rows
# Without `limit`, each page stops at your plan's history window: no 403.
candles = history("/v1/klines/history", {"symbol": "BTCUSDT", "timeframe": "1h"})#The families
Every route fits on one page in the endpoint catalogue; every field of every route, with its unit and a real response, is in the interactive reference.