---
title: "Get up to 200 candle series in one request"
method: POST
path: "/v1/candles/batch"
tags: ["Candles"]
---

# Get up to 200 candle series in one request

`POST /v1/candles/batch`

## Request

- Base URL: `https://md.kairos.trade`
- URL: `https://md.kairos.trade/v1/candles/batch`
- Auth: one of:
  - none
  - API key in header `X-Client-Id` and API key in header `X-Api-Key` and API key in header `X-Api-Secret`

Batched form of `GET /v1/candles`: fetches up to 200 independent
`(provider, contract_id, timeframe_seconds, start, end, outcome)`
series concurrently and returns them in request order, with
**per-index partial failure** — one item's validation or fetch
error does not fail the others.

Each item is validated and aligned exactly as in `GET /v1/candles`
(same clamping, rollup, and inversion rules). Body accepts either a
`requests` array or a legacy `items` alias; if `requests` is
non-empty it is used, otherwise `items` is used.
`timeframe_seconds` and `outcome` in each item may be a JSON number
or a numeric string. The request body is capped at 4 MiB.

**Rate limiting / cost.** Priced like the equivalent run of
single `GET /v1/candles` calls: 1 `light`-bucket unit per item plus
1 per 5,000 requested bars, so a 200-item batch costs at least 200
units. The first unit is admitted up front; the rest is charged as
a deferred extra charge against your next request.

**Caching.** Batch responses are always `Cache-Control: no-store`
and never carry an ETag.

**Binary format.** Pass `?fmt=binary` or
`Accept: application/x-kairos-candles` to receive a single
multi-result binary frame (`num_results = len(requests)`) — one
columnar result section per request index, in input order. An item
that failed sets the high bit (`0x80000000`) of its section's
`count`, so it stays distinguishable from a genuinely empty window;
mask the bit off to read the (always zero) count. Per-item error
messages are only available in the JSON response.

**Failure semantics.** Returns `500` only when *every* item in the
batch failed. Otherwise `200` with a mix of populated and
empty/errored entries.

## Request body

- CandleBatchRequest: Request body of `POST /v1/candles/batch`. Provide either `requests` or the legacy `items` alias — if `requests` is non-empty it takes precedence, otherwise `items` is used. At least one entry is required across the two fields, and the effective array may not exceed 200 entries.
  - `requests` CandleBatchItem[]
    - `provider` 'kalshi' | 'polymarket' | 'opinion' | 'predictfun' | 'hyperliquid' | 'dome' | 'kalshi_offchain', required
    - `contract_id` string, required
    - `timeframe_seconds` union, required: Bucket width in seconds. Accepted as a JSON number or a numeric string. Valid values 1, 60, 300, 900, 3600, 14400, 86400.
      - integer
      - string
    - `start` string, required: Same accepted formats as `GET /v1/candles`'s `start` parameter (RFC 3339, bare datetime, or bare date).
    - `end` string, required: Same accepted formats as `GET /v1/candles`'s `end` parameter.
    - `outcome` union: Zero-based outcome index. Accepted as a JSON number or a numeric string. Defaults to 0.
      - integer
      - string
  - `items` CandleBatchItem[]: Legacy alias for `requests`, used only when `requests` is absent or empty.
    - `provider` 'kalshi' | 'polymarket' | 'opinion' | 'predictfun' | 'hyperliquid' | 'dome' | 'kalshi_offchain', required
    - `contract_id` string, required
    - `timeframe_seconds` union, required: Bucket width in seconds. Accepted as a JSON number or a numeric string. Valid values 1, 60, 300, 900, 3600, 14400, 86400.
      - integer
      - string
    - `start` string, required: Same accepted formats as `GET /v1/candles`'s `start` parameter (RFC 3339, bare datetime, or bare date).
    - `end` string, required: Same accepted formats as `GET /v1/candles`'s `end` parameter.
    - `outcome` union: Zero-based outcome index. Accepted as a JSON number or a numeric string. Defaults to 0.
      - integer
      - string

## Response `200`

Per-index results (populated, empty, or errored). Order matches the request array.

- CandleBatchResponse: Response body of `POST /v1/candles/batch` in JSON mode.
  - `results` CandleBatchResult[], required
    - `index` integer, required: Zero-based position in the input `requests`/`items` array.
    - `candles` CandleObject[], required: Empty when the item's window was authoritatively empty OR when the item errored (check `error`).
      - `contract_id` string, required: Echo of the request's `contract_id` (the caller-supplied id, not the resolved canonical market id).
      - `timeframe_seconds` 1 | 60 | 300 | 900 | 3600 | 14400 | 86400, required: Bucket width in seconds, echoing the request.
      - `bucket_start` string, required: Bucket start timestamp, UTC, always rendered with an explicit `+00:00` offset (not `Z`).
      - `open` number, double, required: Opening price (0-100 scale).
      - `high` number, double, required: High price in the bucket (0-100 scale).
      - `low` number, double, required: Low price in the bucket (0-100 scale).
      - `close` number, double, required: Closing price (0-100 scale).
      - `volume` integer, required: Traded volume in the bucket, provider-native units.
      - `token_id` string: Resolved per-outcome CLOB token id. Omitted entirely from the JSON object when empty.
    - `error` string: Present only when this item failed validation or fetch. When present, `candles` is always `[]`.

## Other responses

- `400`: Malformed body, empty/oversized `requests` array.
- `401`: No valid credential presented. Every path returns `code: unauthorized`; the triggers differ: - An `Authorization` header on a deployment where bearer auth is not enabled (`bearer authentication is not enabled`). - A bearer token that fails verification (`invalid or expired session`), or whose token/session has been revoked (`session has been revoked`). - An incomplete or wrong API-key header set. Sending *any* of `X-Client-Id`, `X-Api-Key`, `X-Api-Secret` commits the request to the API-key path, so a partial set is rejected rather than silently degrading to anonymous limits (`valid X-Client-Id, X-Api-Key, and X-Api-Secret headers are required`). - No credential material at all while the anonymous free tier is unavailable — switched off for the deployment, forced off through the runtime kill switch, or not yet resolvable on a replica that has never read the flag (it fails closed).
- `403`: API key presented from a source IP not in that credential's whitelist.
- `429`: Two different gates return this status, both with `code: rate_limited`: - **Per-credential (or per-IP, when anonymous) weighted budget exhausted.** The message is `rate limit exceeded`, or, for anonymous callers, `anonymous rate limit exceeded — request an API key for higher limits (…)`. `Retry-After` is the whole number of seconds until the current one-minute window rolls over, floored at 1. All four `X-RateLimit-*` headers are present. - **Per-IP admission gate.** An in-process, pre-authentication guard that keeps a flood off the shared credential cache and database. The message is `too many requests from this address` and `Retry-After` is a flat `60`. Because this fires *before* authentication, the response carries **no** `X-RateLimit-*` headers at all.
- `500`: Every item in the batch failed (validation or fetch).
- `503`: The rate limiter is unreachable. Fails CLOSED — the request is rejected rather than let through unmetered.

---

[API](https://skmtc.dev/kairos/apis/kairos-market-data-api.md) - [All operations](https://skmtc.dev/kairos/apis/kairos-market-data-api/llms.txt) - [OpenAPI document](https://skmtc.dev/kairos/apis/kairos-market-data-api/revisions/01e987f59a04?raw)
