> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kairos.trade/llms.txt
> Use this file to discover all available pages before exploring further.

# Get a single OHLCV candle series

> Returns OHLCV candles for one `(provider, contract_id, timeframe_seconds, outcome)`
series over `[start, end)`.

**Alignment.** `start` is floored and `end` is ceiled to the nearest
`timeframe_seconds` bucket boundary; `end` is additionally clamped to
`now + 1 bucket`. Degenerate windows (`end <= start` after alignment)
are extended by exactly one bucket. The requested window is silently
clamped (not rejected) to the per-timeframe retention ceiling: 1s=1
day, 1m=30 days, 5m=90 days, 15m=180 days, 1h=365 days, 4h=365 days,
1d=730 days. For `timeframe_seconds=1`, if the aligned window falls
entirely before `now - 24h` (the retention window for 1-second
candles) the endpoint short-circuits to `{"candles":[]}` without
querying upstream.

**Timeframes.** Only 1s and 1m candles are stored directly; every
other timeframe (5m, 15m, 1h, 4h, 1d) is rolled up server-side from
the 1m base. When the first read comes back empty, `60`, `3600`,
`14400` and `86400` retry as a rollup of the 1s base (brand-new
contracts whose only history is 1-second candles); `300` and `900`
have no such fallback and return an empty series instead. Prices are
on a 0-100 scale. For providers that are not natively per-token
(i.e. not `kalshi`, `polymarket`, `dome`, `opinion`, `predictfun`,
`hyperliquid`), requesting `outcome > 0` inverts prices
(`price = 100 - price`, high/low swapped) rather than resolving a
distinct token. The check is on the provider string exactly as
submitted, so the `kalshi_offchain` alias takes the inverting path
even though `kalshi` does not — send `kalshi` for Kalshi candles.

**Rate limiting / cost.** This route is in the `light` rate-limit
bucket. Larger requests — wider windows, finer timeframes — consume
more of your quota; a typical chart paint (a few hundred bars) costs
1 unit.

**Caching.** `Cache-Control` depends on how the aligned window
relates to `now`: a window still in progress (`end >= now`) is
`no-store`; a window entirely in the recent past is cached briefly;
anything older is cached for longer. Cacheable responses carry a
strong ETag and honor `If-None-Match`, returning `304` on a match.
Responses ≥1KB are gzip-encoded when the client sends
`Accept-Encoding: gzip`.

**Binary format.** Pass `?fmt=binary` or send
`Accept: application/x-kairos-candles` to receive a compact
columnar binary frame instead of JSON. Layout: `u8 magic=0xCA,
u8 version=1, u16 num_results`, then per result `u32 index,
u32 count`, followed by the columnar arrays `u32[count] t` (epoch
seconds), `u16[count] o,h,l,c` (price ×100), `i64[count] vol`
(×100). A single-series `GET` response always has `num_results=1`.




## OpenAPI

````yaml /openapi/market-data-api.yaml get /v1/candles
openapi: 3.1.0
info:
  title: Kairos Market Data API
  version: 1.2.0
  summary: Read-only prediction and perpetual market data across venues.
  description: >
    The Kairos Market Data API provides read-only access

    to normalized prediction-market data across every venue Kairos integrates:

    OHLCV candles, the trade tape, market metadata, resolution lifecycles, and

    latest mark prices.


    ## No signup required


    Every endpoint works **without credentials** on the anonymous free tier,

    rate-limited per source IP (120 light units/min, 20 heavy units/min). Try
    any

    request from the interactive docs at

    [app.kairos.trade/docs/market-data](https://app.kairos.trade/docs/market-data)

    or straight from curl. For production budgets (1,200 light and 150 heavy

    units/min by default, raisable per credential), request an API key from the

    Kairos team.


    ## Authentication


    Three modes, resolved per request:


    - **Anonymous** — no credential headers at all. Free tier, IP-keyed limits.

    - **API key** — `X-Client-Id` + `X-Api-Key` + `X-Api-Secret` headers
      (all three; partial header sets are rejected with 401 rather than
      degrading to anonymous).
    - **First-party session** — `Authorization: Bearer` JWT (Kairos web app
      sessions only; not offered to API consumers).

    Resolution order is fixed: an `Authorization` header takes the bearer

    path, any of the three API-key headers takes the API-key path, and only

    a request carrying neither is eligible for the anonymous tier.


    On the API-key path, an unreachable credential store surfaces on **any**

    endpoint as `500` with `code: internal` and the message

    `authentication backend unavailable`. A source IP outside a credential's

    whitelist is `403 ip_not_whitelisted`; that is the only 403 this service

    emits.


    ## Rate limiting


    Weighted sliding-window budgets over one minute, in **units**, across two

    independent buckets: light (candles, metadata, resolutions, trade

    metrics) and heavy (trade history, marks, perpetual snapshots). Most

    requests cost 1 unit; larger requests may consume more of your quota —

    each operation's `x-kairos-rate-limit` states its exact cost model.

    Every admitted response carries `X-RateLimit-Bucket`, `X-RateLimit-Tier`,

    `X-RateLimit-Remaining`, and `X-RateLimit-Reset`; 429s add `Retry-After`.


    A separate per-IP admission gate runs *before* authentication to keep a

    flood off shared infrastructure. It also answers 429 `rate_limited`, but

    with `Retry-After: 60` and no `X-RateLimit-*` headers.


    Redis backs the weighted limiter and it fails **closed**: if it is

    unreachable the request is rejected with 503 `rate_limiter_unavailable`

    rather than let through unmetered.


    ## Conventions


    - **Prediction-market prices** are on the 0–100 scale (implied probability
      × 100) in the prediction candle, trade, and mark endpoints. Prediction
      USD notional = `size × price / 100`.
    - **Perpetual prices are direct venue prices**, not probabilities and not
      0–100 values. Perpetual quantities retain their declared native unit
      (`base_asset` or `contracts`). Never divide a perpetual price by 100 or
      compute contract notional without the instrument's authoritative contract
      multiplier; the snapshot does not synthesize a cross-venue notional.
    - **Hyperliquid is two products on one venue.** Its HIP-4 outcome markets
      are prediction markets, read with `provider=hyperliquid` on the
      prediction endpoints; its perpetuals are bare coins read under
      `/v1/perpetuals/hyperliquid/{instrument}/snapshot`, where the instrument
      is the canonical id `hl-mainnet-<symbol>-<quote>` (e.g.
      `hl-mainnet-btc-usdt`). Different id schemes, different price semantics;
      they are never interchangeable.
    - **Errors** all use one envelope:
      `{"error": {"code": "...", "message": "..."}}`.
    - **Providers** are case-insensitive; `kalshi_offchain` aliases `kalshi`,
      `dome` aliases `polymarket`, and `opinion` is resolvable for historic
      reads only.
  contact:
    name: Kairos
    url: https://app.kairos.trade/docs/market-data
  termsOfService: https://kairos.trade/terms
servers:
  - url: https://md.kairos.trade
    description: Production
  - url: https://staging-md.kairos.trade
    description: Staging
security:
  - {}
  - apiKeyClientId: []
    apiKeyKey: []
    apiKeySecret: []
tags:
  - name: Candles
    description: OHLCV candle series (1s → 1d), JSON or compact columnar binary.
  - name: Trades
    description: Trade tape and aggregate volume metrics.
  - name: Markets
    description: Market metadata, batch lookup, identifier resolution, and enumeration.
  - name: Resolutions
    description: Resolution outcomes and UMA-style lifecycle state/timelines.
  - name: Marks
    description: Latest mark (last trade price) per outcome token.
  - name: Perpetuals
    description: Live perpetual books, trades, candles, funding, and market state. Beta.
  - name: Status
    description: Health and readiness probes.
paths:
  /v1/candles:
    get:
      tags:
        - Candles
      summary: Get a single OHLCV candle series
      description: >
        Returns OHLCV candles for one `(provider, contract_id,
        timeframe_seconds, outcome)`

        series over `[start, end)`.


        **Alignment.** `start` is floored and `end` is ceiled to the nearest

        `timeframe_seconds` bucket boundary; `end` is additionally clamped to

        `now + 1 bucket`. Degenerate windows (`end <= start` after alignment)

        are extended by exactly one bucket. The requested window is silently

        clamped (not rejected) to the per-timeframe retention ceiling: 1s=1

        day, 1m=30 days, 5m=90 days, 15m=180 days, 1h=365 days, 4h=365 days,

        1d=730 days. For `timeframe_seconds=1`, if the aligned window falls

        entirely before `now - 24h` (the retention window for 1-second

        candles) the endpoint short-circuits to `{"candles":[]}` without

        querying upstream.


        **Timeframes.** Only 1s and 1m candles are stored directly; every

        other timeframe (5m, 15m, 1h, 4h, 1d) is rolled up server-side from

        the 1m base. When the first read comes back empty, `60`, `3600`,

        `14400` and `86400` retry as a rollup of the 1s base (brand-new

        contracts whose only history is 1-second candles); `300` and `900`

        have no such fallback and return an empty series instead. Prices are

        on a 0-100 scale. For providers that are not natively per-token

        (i.e. not `kalshi`, `polymarket`, `dome`, `opinion`, `predictfun`,

        `hyperliquid`), requesting `outcome > 0` inverts prices

        (`price = 100 - price`, high/low swapped) rather than resolving a

        distinct token. The check is on the provider string exactly as

        submitted, so the `kalshi_offchain` alias takes the inverting path

        even though `kalshi` does not — send `kalshi` for Kalshi candles.


        **Rate limiting / cost.** This route is in the `light` rate-limit

        bucket. Larger requests — wider windows, finer timeframes — consume

        more of your quota; a typical chart paint (a few hundred bars) costs

        1 unit.


        **Caching.** `Cache-Control` depends on how the aligned window

        relates to `now`: a window still in progress (`end >= now`) is

        `no-store`; a window entirely in the recent past is cached briefly;

        anything older is cached for longer. Cacheable responses carry a

        strong ETag and honor `If-None-Match`, returning `304` on a match.

        Responses ≥1KB are gzip-encoded when the client sends

        `Accept-Encoding: gzip`.


        **Binary format.** Pass `?fmt=binary` or send

        `Accept: application/x-kairos-candles` to receive a compact

        columnar binary frame instead of JSON. Layout: `u8 magic=0xCA,

        u8 version=1, u16 num_results`, then per result `u32 index,

        u32 count`, followed by the columnar arrays `u32[count] t` (epoch

        seconds), `u16[count] o,h,l,c` (price ×100), `i64[count] vol`

        (×100). A single-series `GET` response always has `num_results=1`.
      operationId: getCandles
      parameters:
        - name: provider
          in: query
          required: true
          schema:
            type: string
            enum:
              - kalshi
              - polymarket
              - opinion
              - predictfun
              - hyperliquid
              - dome
              - kalshi_offchain
          description: >
            Market-data provider. Case-insensitive. `dome` and `kalshi_offchain`
            are aliases resolved to `polymarket` and `kalshi` respectively.
            `opinion` is a disabled provider kept resolvable for historic reads
            only. Unknown values return `400 invalid_request`.
          example: polymarket
        - name: contract_id
          in: query
          required: true
          schema:
            type: string
            maxLength: 128
          description: >
            Provider-native contract/market/ticker id. For polymarket this may
            be either a numeric market id or a `0x`-prefixed condition_id (both
            resolve to the same series). Empty or >128 chars is rejected with
            `400 invalid_request`.
          example: >-
            21742633143463906290569050155826241533067272736897614950488156847949938836455
        - name: timeframe_seconds
          in: query
          required: true
          schema:
            type: integer
            enum:
              - 1
              - 60
              - 300
              - 900
              - 3600
              - 14400
              - 86400
          description: >
            Candle bucket width in seconds. Any value outside this set is
            rejected with `400 invalid_request`. Only 1 and 60 are stored
            directly; 300/900/3600/14400/86400 are always served as rollups.
          example: 3600
        - name: start
          in: query
          required: true
          schema:
            type: string
          description: >
            Window start, inclusive (before alignment). Accepts RFC 3339
            (`2024-01-15T10:00:00Z` or with a numeric offset), a bare datetime
            (`2024-01-15T10:00:00`, treated as UTC), or a bare date
            (`2024-01-15`, treated as UTC midnight). Unparseable values return
            `400 invalid_request`.
          example: '2026-07-15T00:00:00Z'
        - name: end
          in: query
          required: true
          schema:
            type: string
          description: >
            Window end, exclusive (before alignment). Same accepted formats as
            `start`. Must be strictly after `start` or the request is rejected
            with `400 invalid_request: end must be after start`.
          example: '2026-07-16T00:00:00Z'
        - name: outcome
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            default: 0
          description: >
            Zero-based outcome index (e.g. 0=Yes, 1=No for a binary market, or
            an index into a multi-outcome market's token list). Defaults to 0.
            Negative or non-integer values return `400 invalid_request`.
          example: 0
        - name: fmt
          in: query
          required: false
          schema:
            type: string
            enum:
              - binary
          description: >-
            Set to `binary` to receive the columnar binary frame instead of
            JSON. Equivalent to sending `Accept: application/x-kairos-candles`.
      responses:
        '200':
          description: >
            Candle series for the requested window. `{"candles":[]}` (empty
            array, `no-store`) is a valid 200 response, not an error — it means
            the window is authoritatively empty.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CandleListResponse'
              examples:
                hourlyCandles:
                  value:
                    candles:
                      - contract_id: >-
                          21742633143463906290569050155826241533067272736897614950488156847949938836455
                        timeframe_seconds: 3600
                        bucket_start: '2026-07-15T00:00:00+00:00'
                        open: 61.5
                        high: 63
                        low: 60.8
                        close: 62.4
                        volume: 184230
                        token_id: '704721957297303853272349184'
            application/x-kairos-candles:
              schema:
                $ref: '#/components/schemas/CandleBinaryFrame'
        '304':
          description: Not Modified — `If-None-Match` matched the current ETag. Empty body.
        '400':
          description: Malformed or invalid query parameters.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                unknownProvider:
                  value:
                    error:
                      code: invalid_request
                      message: unknown provider "foo"
                badTimeframe:
                  value:
                    error:
                      code: invalid_request
                      message: >-
                        invalid timeframe_seconds (valid: 1, 60, 300, 900, 3600,
                        14400, 86400)
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/IPNotWhitelisted'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: Upstream candle fetch failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          $ref: '#/components/responses/RateLimiterUnavailable'
components:
  schemas:
    CandleListResponse:
      type: object
      description: Response body of `GET /v1/candles` in JSON mode.
      required:
        - candles
      properties:
        candles:
          type: array
          items:
            $ref: '#/components/schemas/CandleObject'
          description: >-
            Candles in ascending `bucket_start` order. Empty when the aligned
            window is authoritatively empty.
    CandleBinaryFrame:
      type: string
      format: binary
      description: >
        Columnar binary candle frame (`Content-Type:
        application/x-kairos-candles`), returned by both candle endpoints when
        negotiated via `?fmt=binary` or an `Accept:
        application/x-kairos-candles` header. Byte layout (little-endian): `u8
        magic=0xCA, u8 version=1, u16 num_results`, then for each section `u32
        index, u32 count`, followed by six columnar arrays of length `count`:
        `u32[] t` (bucket start, epoch seconds), `u16[] o,h,l,c` (prices scaled
        ×100 of the 0-100 float, clamped to [0, 10000]), and `i64[] vol` (volume
        ×100). In a batch frame, a failed item sets the high bit (`0x80000000`)
        of its section's `count` — mask it off before using the count, and treat
        a set bit as "this series errored" rather than "no data". The error
        message itself is only available in the JSON response.
    ErrorResponse:
      type: object
      description: Canonical error envelope emitted by every Market Data API endpoint.
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Machine-readable error code.
              example: invalid_request
            message:
              type: string
              description: Human-readable detail.
              example: market_ids exceeds maximum 200
    CandleObject:
      type: object
      description: One OHLCV bucket. Prices are on a 0-100 scale (percentage probability).
      required:
        - contract_id
        - timeframe_seconds
        - bucket_start
        - open
        - high
        - low
        - close
        - volume
      properties:
        contract_id:
          type: string
          description: >-
            Echo of the request's `contract_id` (the caller-supplied id, not the
            resolved canonical market id).
          example: >-
            21742633143463906290569050155826241533067272736897614950488156847949938836455
        timeframe_seconds:
          type: integer
          enum:
            - 1
            - 60
            - 300
            - 900
            - 3600
            - 14400
            - 86400
          description: Bucket width in seconds, echoing the request.
          example: 3600
        bucket_start:
          type: string
          description: >-
            Bucket start timestamp, UTC, always rendered with an explicit
            `+00:00` offset (not `Z`).
          example: '2026-07-15T00:00:00+00:00'
        open:
          type: number
          format: double
          minimum: 0
          maximum: 100
          description: Opening price (0-100 scale).
          example: 61.5
        high:
          type: number
          format: double
          minimum: 0
          maximum: 100
          description: High price in the bucket (0-100 scale).
          example: 63
        low:
          type: number
          format: double
          minimum: 0
          maximum: 100
          description: Low price in the bucket (0-100 scale).
          example: 60.8
        close:
          type: number
          format: double
          minimum: 0
          maximum: 100
          description: Closing price (0-100 scale).
          example: 62.4
        volume:
          type: integer
          format: int64
          description: Traded volume in the bucket, provider-native units.
          example: 184230
        token_id:
          type: string
          description: >-
            Resolved per-outcome CLOB token id. Omitted entirely from the JSON
            object when empty.
          example: '704721957297303853272349184'
  responses:
    Unauthorized:
      description: >
        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).
      headers:
        WWW-Authenticate:
          schema:
            type: string
          description: >-
            `Bearer realm="market-data-api"` on bearer-verification failure,
            `APIKey realm="market-data-api", header="X-Client-Id, X-Api-Key,
            X-Api-Secret"` on missing/invalid API-key credentials. Absent on the
            revoked-session and bearer-disabled paths.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: unauthorized
              message: >-
                valid X-Client-Id, X-Api-Key, and X-Api-Secret headers are
                required
    IPNotWhitelisted:
      description: API key presented from a source IP not in that credential's whitelist.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: ip_not_whitelisted
              message: source IP not in credential whitelist
    RateLimited:
      description: >
        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.
      headers:
        Retry-After:
          schema:
            type: integer
          description: >-
            Seconds until the caller may retry — window-rollover seconds for the
            weighted limiter, a flat 60 for the IP admission gate.
        X-RateLimit-Bucket:
          schema:
            type: string
            enum:
              - light
              - heavy
        X-RateLimit-Tier:
          schema:
            type: string
            enum:
              - anonymous
              - api-key
              - user
        X-RateLimit-Remaining:
          schema:
            type: integer
        X-RateLimit-Reset:
          schema:
            type: integer
            description: Unix timestamp (seconds) when the current window resets.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: rate_limited
              message: rate limit exceeded
    RateLimiterUnavailable:
      description: >
        The rate limiter is unreachable. Fails CLOSED — the request is rejected
        rather than let through unmetered.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: rate_limiter_unavailable
              message: rate limiter temporarily unavailable
  securitySchemes:
    apiKeyClientId:
      type: apiKey
      in: header
      name: X-Client-Id
      description: >-
        Credential client id (`kairos_ck_...`). Must be sent together with
        X-Api-Key and X-Api-Secret.
    apiKeyKey:
      type: apiKey
      in: header
      name: X-Api-Key
      description: >-
        64-char hex API key. Must be sent together with X-Client-Id and
        X-Api-Secret.
    apiKeySecret:
      type: apiKey
      in: header
      name: X-Api-Secret
      description: >-
        64-char hex API secret. Must be sent together with X-Client-Id and
        X-Api-Key.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.