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"
# TOON
curl "https://api.bytetree.io/api/rates?format=toon"
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 — /api/summary is the documented case — refuses csv with a 400 that names the formats it 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/:id) | 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 everywhere except one endpoint:
| 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/fx/:symbol/series. Not paginated — it’s one thing’s history, not a list |
| Keyed map | {count, data} where data is an object keyed by symbol | /api/metrics/latest only. count is the number of keys |
The keyed map is the one 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/:id, GET /api/rates/:pair — 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." }
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”.
Available Endpoints
Every endpoint is a read-only GET.
| Path | Description |
|---|---|
/api/health | Health check — the one endpoint that needs no key |
/api/currencies | Currency code to name mapping |
/api/rates | Latest rates for all FX pairs |
/api/rates/sparklines | 12-month weekly closes per pair |
/api/rates/:pair | Daily time series for one pair |
/api/assets | List assets with optional filters |
/api/assets/:id | Single asset by ID or symbol |
/api/prices | Latest price per symbol |
/api/prices/sparklines | 12-month weekly closes per symbol |
/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/:category | Groups within a dimension |
/api/universes/:universe/:category/: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) |
: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.