Skip to main content
Historical candles, venue trade proxies, and trade history/metrics. Use these endpoints to chart a market, replay its tape, or read aggregate volume for a contract. Full parameter reference and a live tester: API Reference.

Base URL

All paths on this page are relative to that host.

Authentication

Every endpoint accepts an API key (X-Client-Id + X-Api-Key + X-Api-Secret), a first-party session JWT, or an admin secret. Session-JWT callers additionally pass the invite gate; API-key and admin callers bypass it. Examples below read credentials from KAIROS_CLIENT_ID, KAIROS_API_KEY, and KAIROS_API_SECRET in your shell. Candle routes (GET /candles, POST /candles/batch) need no scope and are not platform-gated. They share one candles rate-limit group at 200 requests/minute, so the two endpoints draw on a single budget; an operator tier can raise or lower it at runtime. /trades/* routes need the trade:read scope on API-key credentials (admin and JWT callers bypass the scope check). They also enforce platform access for API-key callers: the fixed proxy routes check their fixed provider, while history and metrics check the provider query value. The /trades/* routes carry no explicit limit decorator, so they fall back to the service-wide default of 100 requests/minute per route, keyed on the authenticated user (or the client IP when unauthenticated).

Get candles

Fetch OHLCV candles for a single (provider, contract_id, outcome) series over [start, end), bucketed at timeframe_seconds. Auth: API key, session JWT, or admin secret — no scope required. 200 requests/minute (shared candles group).

Request

Unless rebuild=true, the handler first checks for a byte-identical cached response and streams it straight back — a miss (or rebuild=true) falls through to reconstructing the candles from the underlying trade data and repopulates the cache. There is no “wait for ingestion” mode.
Gotcha: start is silently clamped, never rejected. Each timeframe has a maximum lookback measured back from end; asking for more returns the clamped window rather than an error. Compare candles[].bucket_start against the start you sent if the exact window matters.
end is likewise clamped forward to the current bucket. A 1-second request whose window ends more than 24 hours ago is past retention and returns "candles": [] — not an error.

Response

Batch candles

Fetch up to 200 candle series in one call. Each item accepts the same fields as GET /candles. Auth: same as GET /candles. 200 requests/minute — the same candles limit group, so the two endpoints share one budget.

Request

Every item is checked against the cache in parallel first; only the uncached tail is rebuilt from the underlying trade data. There is also a whole-batch size budget. After clamping, the estimated bar count across every item must not exceed 400,000; over that the request fails with 400 batch implies <n> candles; maximum is 400000. The request body itself is capped at 8 MiB (413 beyond that). A single bad or failed item never fails the whole batch. An empty window comes back as "candles": [], not an error. If the rebuild step fails after some items were already cached, those still return normally and every uncached item instead gets "candles": [], "error": "<message>". Only when no item could be served at all does the request fail with 500. Check results[].error per item rather than relying on the HTTP status.

Response

Kalshi trade proxy

Thin proxy over Kalshi’s own GET /trade-api/v2/markets/trades, with Kairos-computed volume metrics appended. Auth: API key or session JWT; API-key credentials need trade:read and Kalshi must be enabled globally for them. 100 requests/minute.

Request

Cached briefly (250 ms — burst de-duplication, not a real cache window) before falling through to Kalshi’s live API; a non-2xx or network failure returns 502. The rest of Kalshi’s response body is passed through verbatim with metrics merged in. The metrics object is computed locally over the time-filtered trades, since Kalshi’s API doesn’t filter server-side beyond min_ts/max_ts passthrough, and its window_seconds is hardcoded to 86400 regardless of the requested span.
Gotcha: The trades array is Kalshi’s unfiltered list. The local time filter narrows only what metrics is derived from, so metrics.trade_count can be smaller than trades.length. Re-apply min_ts/max_ts client-side if you need the two to agree.

Response

Polymarket trade proxy

Thin proxy over Polymarket’s GET https://data-api.polymarket.com/trades, with the same metrics shape as /trades/kalshi. Auth: same model as /trades/kalshi, gated on Polymarket API access instead of Kalshi’s. 100 requests/minute.

Request

Non-0x values are resolved to a condition ID before querying upstream — via the market-metadata cache, with a provider-API lookup as a last resort. If resolution never yields a 0x... value, the endpoint short-circuits and returns {"trades": [], "metrics": {}} without ever calling Polymarket. Cached briefly; a non-2xx/network error from Polymarket returns 502. metrics approximates outcome_0/outcome_1 as the top two outcomes by traded volume in the window — an approximation, not a guaranteed yes/no mapping.

Response

{ "trades": [...], "metrics": {...} } — the metrics object has the same shape as /trades/kalshi’s, and trades is Polymarket’s raw trade array (fields like proxyWallet, side, asset, price). There is no cursor field on this endpoint. As on /trades/kalshi, the returned trades are unfiltered while metrics is derived from the after-filtered subset.
Gotcha: price on these raw Polymarket trades is on a 0–1 scale — not the 0–100 cents scale used by candles and /trades/history elsewhere in this API.

Trade history

Normalized trade history for one (provider, contract_id), served from Kairos’s own trade store. Unlike the two proxy endpoints above, this is not a live upstream call — coverage depends on how much history has been backfilled/streamed for that contract. Auth: API key or session JWT; API-key credentials need trade:read and the requested provider must be enabled globally for them. 100 requests/minute.

Request

Gotcha: window_seconds and before are independent bounds, not a sliding window. The lower bound is always now − window_seconds; before only moves the upper bound. Walking backwards by feeding the oldest returned timestamp into before therefore narrows the window rather than shifting it.
If that window matches nothing, the endpoint retries once with the lower bound removed and returns the newest trades at or before before instead — so an empty trades array means the contract has no ingested trades at all, not merely none in the window. trigger_ingest is inert. It is validated and ignored: no ingestion job is started, and indexing is always false on this endpoint.

Response

Gotcha: trades[].timestamp and oldest_available_ts are unix seconds (fractional), not milliseconds. Multiplying by 1000 before feeding a JavaScript Date is the usual fix.

Volume metrics

Aggregate volume and outcome-pressure metrics for one (provider, contract_id) over a lookback window — the same backing store as /trades/history, not a live upstream proxy. There’s no buy/sell breakdown: metrics split by outcome (outcome_0/outcome_1), since trade direction can’t be reliably derived from every provider’s exchange data. Auth: same as /trades/history. 100 requests/minute.

Request

Responses are cached per (provider, contract_id, window_seconds), with a shorter TTL when coverage_pct came back low.

Response

An empty window is not an error. It returns zeroed volumes, trade_count: 0, coverage_pct: 0.0, and outcome_0_pressure_pct: 50.0 — the neutral midpoint, not a measurement. Read trade_count before you trust a pressure value.

Errors

Errors are returned as { "detail": "<message>" }. Two shapes deviate: 422s use FastAPI’s standard validation envelope, and 429s use { "error": "Rate limit exceeded: <limit>" } alongside X-RateLimit-* and Retry-After headers. The Code column holds the exact detail string the service returns; — means the body has no fixed message. A batch whose ClickHouse tail fails but whose cache answered part of the request does not return 500 — see Batch candles.