Getting started
Requests & Responses
The rules every endpoint follows: the formats you can ask for, the shapes responses come back in, how paging works, and how the API tells you a request was wrong. Getting Started covers the basics; this page is the detail.
Response Format
All endpoints return JSON by default. You can request alternative formats using the format query parameter:
| Format | Content-Type | Description |
|---|---|---|
json |
application/json |
Default. Standard JSON. |
csv |
text/csv |
Comma-separated values (RFC 4180). |
toon |
text/toon |
TOON — compact, token-efficient encoding ideal for LLM consumers. |
# CSV
curl "https://api.bytetree.io/api/rates?format=csv" \
-H "Authorization: Bearer YOUR_API_KEY"
# TOON
curl "https://api.bytetree.io/api/rates?format=toon" \
-H "Authorization: Bearer YOUR_API_KEY"
Unsupported format values return 400 Bad Request.
Where csv is available
json and toon work on every endpoint. csv is offered only where the response is one table. An endpoint whose response is several tables at once refuses csv: /api/summary, /api/monitor, and the grouped maps /api/macro and /api/universes/:universe, which hold one table per dimension. The single-dimension and single-group reads under them do offer csv. The refusal is a 400 that names the formats the endpoint does support:
{ "error": "format=csv is not available for this endpoint; use json or toon", "formats": ["json", "toon"] }
The refusal itself is JSON, not CSV, and it is decided by the endpoint’s shape, never by its data — an empty result does not change the answer.
How CSV is shaped
| Response shape | CSV rendering |
|---|---|
| Paginated list, entity series | One row per record in data; the envelope fields (count, total, …) are not included |
| Empty list | An empty document — no header row |
Single-record read (e.g. /api/assets/:symbol) |
One row; nested objects flatten to dotted column names (metadata.exchange) |
Keyed map (/api/metrics/latest) |
One row per symbol, with a leading key column carrying the symbol |
Inside a row, an array of primitives is joined into one quoted cell, and an array of objects is written as JSON text in the cell.
Response Conventions
Paginated list envelope
List endpoints that support consumer-driven pagination return:
{
"count": 10,
"total": 4287,
"limit": 10,
"offset": 0,
"data": [ /* records on this page */ ]
}
| Field | Description |
|---|---|
count |
Length of the returned data array (records on this page). |
total |
Full match count for the same filter, ignoring limit and offset. Computed in parallel with the paged query. |
limit / offset |
Echoed input. Consumers paginate by advancing offset until offset + count >= total. |
data |
Array of records for this page. |
This is the shape for top-level list endpoints — /api/assets, /api/rates, /api/prices, /api/universes, /api/fx among them.
Compatibility: the envelope is additive. Callers that read only count and data are unaffected. See the Changelog for the cutover note.
Which shape to expect
Rather than memorising a list, read it from the request. Pagination fields appear exactly where you can meaningfully page, and data is an array wherever the response is a list or a series:
| Shape | Returns | Where |
|---|---|---|
| Paginated list | {count, total, limit, offset, data[]} |
Top-level list endpoints — the envelope above |
| Entity series | {symbol, count, data[]} (or {pair, …}) |
One entity’s series, e.g. /api/prices/:symbol, /api/rates/:pair, /api/fx/:symbol/series. Not paginated — it’s one thing’s history, not a list |
| Single record | {data: {…}}, sometimes with the lookup key beside it |
One thing’s current state, e.g. /api/assets/:symbol, /api/metrics/:symbol/latest, /api/fx/:symbol |
| Keyed map | {count, data} where data is an object keyed by symbol |
/api/metrics/latest. count is the number of keys |
| Keyed series | {data} where data maps each symbol to an array of numbers |
/api/prices/sparklines, /api/rates/sparklines |
| Grouped map | {universe, count, smallOmitted, data} where data is an object keyed by dimension, each holding an array of group rows |
/api/universes/:universe, /api/macro |
| Snapshot | One structured document with several sections | /api/monitor, /api/summary |
The keyed shapes are the ones to watch: data is an object, not an array, so iterating it as a list fails rather than returning nothing.
Single-record reads
Single-record endpoints — e.g. GET /api/assets/:symbol, GET /api/fx/:symbol — return { data: ... } and may include the lookup key as a sibling field. They are not paginated.
Unknown query parameters are rejected
Passing a parameter an endpoint doesn’t accept returns 400, rather than being silently ignored:
{ "error": "Unknown query parameter 'typ'. Did you mean 'type'? Accepted parameters: type, region, sector, limit, offset, format." }
Close misspellings get a suggestion, and the accepted list is always echoed. An endpoint that takes no parameters says so explicitly.
This is deliberate. Silently ignoring an unrecognised parameter means a filter you thought you applied simply didn’t apply, and the response looks plausible — a filtered query returning unfiltered data with a 200. Failing loudly turns a wrong answer into an obvious error.
format is accepted everywhere and never counts as unknown. Authentication is checked first, so an invalid key returns 401 rather than 400, even if the request also carries a bad parameter.
Malformed limit and offset values are rejected
The same rule applies to values. On every paginated list endpoint, limit must be a positive integer and offset a non-negative integer. Anything else returns 400 naming the parameter and the value received:
{ "error": "Invalid value for 'limit': expected a positive integer, received 'abc'." }
| Request | Result |
|---|---|
?limit=abc, ?limit=1.5, ?limit=1e3, ?limit=12abc |
400 — not an integer |
?limit=0, ?limit=-3 |
400 — limit must be 1 or more |
?offset=-1, ?offset=1.5, ?offset=abc |
400 — offset must be a whole number, 0 or more |
?limit=5000 |
200 — clamped to the endpoint’s maximum, which the envelope echoes in limit |
limit / offset absent or empty |
200 — the endpoint’s default applies |
Only malformed values are rejected. An over-large limit is still clamped rather than refused, and the echoed limit tells you what was applied. limit=0 is an error, not “no limit”.
Value validation
Other parameter values are checked the same way. A malformed value returns 400, and the message names the parameter and what it expected:
| Value | Rule | Example error |
|---|---|---|
Dates (from, to) |
A real calendar date, YYYY-MM-DD |
Invalid value for 'from': expected an ISO calendar date (YYYY-MM-DD), received '2026-02-30'. |
Field names (fields) |
Letters, digits and underscores, starting with a letter | Invalid field name 'a.b' in 'fields': expected letters, digits and underscores, starting with a letter. |
Fixed choices (type, status, regime, variant, and so on) |
One of the values the endpoint lists | Invalid value for 'type': expected one of stock, crypto, etf, index, commodity, received 'bogus'. |
An empty fixed-choice value, such as ?type=, is the same as leaving the parameter out. A well-formed field name that the endpoint doesn’t have is not an error. Each endpoint’s page says whether it is left out of the response or returned as null.
The same rule covers a dimension in a path. On /api/macro and the
/api/universes/:universe group routes, a dimension that isn’t one of the five
returns 400:
{ "error": "Invalid value for 'dimension': expected one of region, country, sector, industry, type, received 'foo'." }
Unknown paths
A path that matches no endpoint returns 404 with the same JSON error body as every other refusal:
{ "error": "Not found: GET /api/nope" }
Available Endpoints
Every endpoint is a read-only GET.
| Path | Description |
|---|---|
/api/health |
Health check, and the one endpoint that needs no key. Returns {"status":"ok","mongo":"connected","timestamp":"…"} with Cache-Control: no-store, and 503 when the service can’t reach its database |
/api/currencies |
Currency code to name mapping |
/api/rates |
Latest rates for all FX pairs |
/api/rates/sparklines |
12-month weekly closes per currency pair |
/api/rates/:pair |
Daily time series for one pair |
/api/assets |
List assets with optional filters |
/api/assets/:symbol |
Single asset by symbol |
/api/prices |
Latest price per symbol |
/api/prices/sparklines |
12-month weekly CAPR per asset (weekly closes for currency pairs) |
/api/prices/:symbol |
Daily price time series for one symbol |
/api/fx |
Tracked FX pairs with latest scores and trend regime |
/api/fx/:symbol |
Latest computed metrics for one FX pair |
/api/fx/:symbol/series |
Daily metric time series for one FX pair |
/api/fx/:symbol/prices |
Close-only price series for one FX pair |
/api/metrics/latest |
Latest metrics across all symbols |
/api/metrics/:symbol/latest |
Latest computed metrics snapshot |
/api/metrics/:symbol |
Computed metrics time series |
/api/metrics/distribution |
Cross-sectional metric distributions |
/api/metrics/regime-transitions |
Trend-regime transition event log |
/api/summary |
Single-screen dataset overview |
/api/universes |
The universe roster with membership counts |
/api/universes/:universe |
Latest group metrics across all dimensions for a universe’s members |
/api/universes/:universe/:dimension |
Groups within a dimension |
/api/universes/:universe/:dimension/:group |
Time series for one group |
/api/macro |
Group metrics across every tracked asset (also /:dimension and /:dimension/:group) |
/api/dimensions |
The five global grouping dimensions (also /:dimension) |
/api/monitor |
The latest ByteTrend Monitor snapshot for one asset type |
/api/monitor/history |
Daily Monitor figures for one group of one asset type |
/api/portfolios |
ByteTree’s model portfolios (also /:slug, /:slug/series, /:slug/eligible-symbols, /:slug/rebalances, /:slug/constituents) |
:universe takes a universe slug — bytetrend, bytefolio and the rest of the roster on Universes. Every API key reaches every universe. macro is not a universe: the whole-registry aggregation is at /api/macro.