> ## 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.

# Connect your own Kalshi account by storing its API credentials

> Kalshi is the one venue where Kairos does NOT custody or provision an identity — you trade against your own Kalshi account, so you supply your own Kalshi API key and RSA private key here. The server validates the PEM, proves the credentials work by calling Kalshi's balance endpoint, then encrypts them (AES-256-GCM) and stores them.
**You are handing over a live private key.** It is stored encrypted and used only to sign Kalshi requests on your behalf. Rotate it at Kalshi and re-run this endpoint to replace it.
Both PKCS#1 and PKCS#8 PEM encodings are accepted, and a key pasted as a single line with literal `\n` escapes is normalized before parsing.
`POST /exchanges/kalshi_offchain/enable-trading` is a deprecated alias of this route, kept for clients that are not deployed in lockstep.
> **Error shape.** Minimal `{"error": "...", "code": "..."}` (`OrderSimpleErrorResponse`), not the structured envelope.
**Auth & scope.** Requires `trade:execute` for `kalshi` and the mutation gate.



## OpenAPI

````yaml /openapi/execution.yaml post /exchanges/kalshi/enable-trading
openapi: 3.1.0
info:
  title: Kairos Order Execution API
  version: 1.0.0
  summary: >-
    Order entry, cancellation, fee quotes, and live position exposure across
    venues.
  description: >
    The Order Execution API at `execution.kairos.trade` places and manages

    orders across every venue Kairos integrates. Two lanes:


    - **Custodial** (`POST /orders`) — Kairos signs and routes on your behalf
      (custodial signing). Async ack: a 200 means validated + enqueued; track via
      `GET /orders/{order_id}` or the WebSocket order/fill stream.
    - **Self-custody / external signing** (`POST /v2/orders/intent` +
      `POST /v2/orders/submit`) — for allow-listed institutional accounts on
      Polymarket and Predict.fun: Kairos builds the EIP-712 payload, you sign
      with your own key, submission is synchronous with the venue's verbatim
      result. `POST /v2/onchain/intent` + `POST /v2/onchain/submit` are the
      same pattern for on-chain operations (approvals, redeem, split/merge,
      unwrap) on Polygon, where you also pay the gas.

    ## Authentication


    All endpoints require a Kairos API key (`X-Client-Id` + `X-Api-Key` +

    `X-Api-Secret`) or a first-party session JWT, plus the per-operation

    scope (`trade:execute`, `trade:read`, `position:read`) where the operation

    documents one. Synthetic Books accepts any otherwise-valid account

    credential without an extra scope or allowlist. There is

    no anonymous tier. Scope checks apply to API-key credentials only; a

    session JWT carries no scope list and is treated as holding every scope.


    The custodial mutation endpoints (`POST /orders`, the three cancel

    endpoints) and Synthetic Books create/refresh/release carry an additional

    `RequireServiceToken` gate satisfied by **any** of: the full API-key triple,

    an internal `X-Service-Token`, or a valid `X-Csrf-Token`. API-key consumers

    need no extra header — but a bare `Authorization: Bearer <jwt>` alone is NOT

    sufficient on those routes. The `/v2/orders/*` and `/v2/onchain/*`

    external-signing routes have no such gate: a session JWT alone works there.


    ## Rate limits


    Order submission is capped per user (default 5 orders per **second**,

    sliding window, `ORDER_RATE_LIMIT_PER_SEC`); some credentials carry an

    `orders` override, which is enforced on its own per-credential window of

    the same duration *in addition to* the per-user window — either one

    denying is a `429`. Idempotent replays (same `client_order_id`) return the

    existing order before the limiter runs and never consume a slot. The

    limiter is Redis-backed and **fails closed**: if Redis is unreachable the

    submission is denied with `429`.


    Separately, repeated *authentication failures* from one client IP are

    throttled at 10 failures per 60 s (also fail-closed). That limiter returns

    the minimal `{"error": "Too many authentication attempts"}` body.


    **No rate-limit headers.** This service does not emit `Retry-After`,

    `X-RateLimit-*`, or any other backoff hint on a `429` — back off on your

    own schedule.


    ## Error responses


    **Six different error body shapes are in use across this service and they

    are not interchangeable.** Check the shape documented on the specific

    operation before writing a parser; a client that assumes one shape will

    read `undefined` for the reason on the others.


    1. `OrderErrorResponse` — the structured envelope (`error`, `code`,
       `error_details{code,message,details?,metadata?,actions}`) returned by
       every handler that surfaces an `ExecutionError`/`ApiError`: order
       submission, cancel-all, the CTF endpoints, the Polymarket/Opinion
       onboarding endpoints, Hyperliquid withdraw/transfer, and most of the
       deposit-wallet family. Note the two `code` fields differ in case:
       top-level `code` is PascalCase (`"AuthInsufficientScope"`), while
       `error_details.code` is the SCREAMING_SNAKE_CASE wire code
       (`"AUTH_INSUFFICIENT_SCOPE"`). Match on `error_details.code`.
    2. `OrderSimpleErrorResponse` — the minimal `{"error": "..."}` (sometimes
       with `code`) used by the auth middleware (any endpoint's
       `401`/`403`/`429`), the whole `/v2/*` external-signing and on-chain
       lane, the Kalshi and Predict.fun endpoints.
    3. `OrderMarketLinkErrorResponse` — `{"message": "..."}`, with no `error`
       and no `code`. Used by `POST /orders/market-links` **only**.
    4. **Empty or plain-text, not JSON at all.** Several deposit-wallet
       endpoints — most importantly
       the RPC signed-batch submission and
       `POST /exchanges/polymarket/imported/relay-info` — return most 4xx/5xx
       responses with a **completely empty body** and `content-type:
       text/plain`. A handful of cases carry a bare plain-text sentence
       (`batch is not an allowed withdrawal or collateral conversion`,
       `relayer rejected batch: …`). Do not attempt to JSON-parse these.
    5. Handlers whose Rust signature returns a bare `StatusCode` likewise send
       **no body at all**; those responses are marked "empty body (status code
       only)".
    6. `SyntheticBookErrorResponse` — `{"error": "stable_snake_case_code",
       "message": "...", "details"?: {...}}` on `/v1/synthetics*`. Match the
       top-level `error`; upstream definition validation may be nested under
       `details`.

    A further wrinkle inside shape 1: some provider-access checks discard the

    specific reason and return a generic `"Request failed with status 403"` /

    `AUTH_CREDENTIALS_INVALID` body, while others preserve

    `AUTH_INSUFFICIENT_SCOPE` and the real message. Do not rely on the message

    text of an access denial being stable.


    The central `ExecutionError` → HTTP mapping (`ApiError::from`) is:


    | `ExecutionError` | Status | `error_details.code` |

    |---|---|---|

    | `InsufficientBalance` | 400 | `FUNDS_INSUFFICIENT_USDC` |

    | `CollateralLocation` | 400 | `FUNDS_COLLATERAL_LOCATION` |

    | `InvalidOrder` | 400 | `VALIDATION_INVALID_ORDER` |

    | `PositionShortfall` | 400 | `FUNDS_INSUFFICIENT_BALANCE` |

    | `MarketClosed` | 400 | `EXCHANGE_POLYMARKET_MARKET_CLOSED` |

    | `OrderAlreadyCancelled` | 400 | `VALIDATION_INVALID_ORDER` |

    | `UnsupportedExchange` | 400 | `EXCHANGE_UNSUPPORTED` |

    | `SlippageExceeded` | 400 | `MARKET_FOK_NOT_FILLED` |

    | `FokNotFilled` | 400 | `MARKET_FOK_NOT_FILLED` |

    | `AuthenticationError` | 401 | `AUTH_CREDENTIALS_INVALID` |

    | `CredentialError` | 401 | `AUTH_CREDENTIALS_NOT_FOUND` |

    | `MarketNotFound` | 404 | `VALIDATION_MARKET_NOT_FOUND` |

    | `OrderNotFound` | 404 | `VALIDATION_INVALID_ORDER` |

    | `MarketNotSettledOnChain` | 409 | `VALIDATION_MARKET_NOT_SETTLED_ON_CHAIN`
    |

    | `OrderbookUnavailable` | 422 | `ORDERBOOK_UNAVAILABLE` |

    | `ExchangeError` (code `429`/`RATE_LIMITED`) | 429 |
    `EXCHANGE_POLYMARKET_RATE_LIMITED` |

    | `ExchangeError` (code `401`/`UNAUTHORIZED`) | 401 |
    `AUTH_POLYMARKET_API_KEY_INVALID` |

    | `SigningError` | 500 | `SIGNATURE_ERROR` |

    | `DatabaseError` | 500 | `DATABASE_ERROR` |

    | `InternalError` / `LedgerReconciliationRequired` /
    `SponsoredRequestWedged` / `PreTradeError` | 500 | `INTERNAL_ERROR` |

    | `NetworkError`, `ExchangeError` (any other code) | 502 | `NETWORK_ERROR` /
    venue-classified |

    | `LockError` | 503 | `INTERNAL_ERROR` |

    | `Timeout` | 504 | `NETWORK_TIMEOUT` |


    A venue `ExchangeError` is further classified from the venue's own message

    text before it is mapped, so an "allowance is not enough" rejection becomes

    `ALLOWANCE_CTF_NOT_SET`, a "not enough balance" rejection becomes

    `FUNDS_INSUFFICIENT_BALANCE`, "post-only mode" becomes `MARKET_NOT_READY`,

    a "no liquidity" rejection becomes `MARKET_INSUFFICIENT_LIQUIDITY`, and so

    on. Match on `error_details.code`, never on `error`.


    ## Server-level guards


    Every request is subject to a 120 s timeout (`OE_REQUEST_TIMEOUT_SECS`), a

    2 MiB request-body cap (`OE_MAX_BODY_BYTES`, over-size bodies get `413`),

    and a 1024-request global concurrency ceiling

    (`OE_MAX_CONCURRENT_REQUESTS`). Every response carries
    `X-Content-Type-Options:

    nosniff`, `X-Frame-Options: DENY`, HSTS, and a `default-src 'none'` CSP.


    ## Conventions


    - Prices are decimal strings on the 0–1 scale; `*_bps` fields are the
      same value in basis points (× 10000).
    - Quantities/sizes are decimal strings.

    - Structured errors carry `error_details.code`
      (SCREAMING_SNAKE_CASE) plus actionable recovery `actions`.

    ## Changelog


    **2026-09-14 — collateral routing fields on `POST /orders`.** Four optional

    request fields and one optional response field. `collateral`

    (`skip` | `check` | `fund`) **defaults to** `skip`, which is exactly today's

    path: no affordability check, no hold, no funding, zero added latency — so

    no existing caller changes behaviour without opting in. `shard_funding`

    **defaults to** `true`, which is also exactly today's behaviour: the Kalshi

    shard move is same-venue, zero-fee and already runs for every account; send

    `false` to opt out. `collateral=fund` requires both `max_bridge_fee_usdc`

    and `max_funding_wait_ms` and is rejected `400` naming the missing cap;

    either cap without `fund` is likewise a `400`. The response gains

    `funding`, `null` whenever no funding work ran. FIX sessions carry the same

    fields on `NewOrderSingle(D)` as optional tags `5701`–`5704`; a session that

    sends none of them is unchanged.
  contact:
    name: Kairos
    url: https://app.kairos.trade/docs/api-reference
  termsOfService: https://kairos.trade/terms
servers:
  - url: https://execution.kairos.trade
    description: Production (central primary, us-east-1)
  - url: https://eu-west-1-polymarket.executor.kairos.trade
    description: >-
      Production regional execution node — Ireland, colocated with Polymarket.
      Same API surface; assigned at onboarding.
  - url: https://ap-northeast-1-predictfun.executor.kairos.trade
    description: >-
      Production regional execution node — Tokyo, colocated with Predict.fun.
      Same API surface; assigned at onboarding.
  - url: https://staging-execution.kairos.trade
    description: Staging
security:
  - apiKeyClientId: []
    apiKeyKey: []
    apiKeySecret: []
paths:
  /exchanges/kalshi/enable-trading:
    post:
      tags:
        - Onboarding
      summary: Connect your own Kalshi account by storing its API credentials
      description: >-
        Kalshi is the one venue where Kairos does NOT custody or provision an
        identity — you trade against your own Kalshi account, so you supply your
        own Kalshi API key and RSA private key here. The server validates the
        PEM, proves the credentials work by calling Kalshi's balance endpoint,
        then encrypts them (AES-256-GCM) and stores them.

        **You are handing over a live private key.** It is stored encrypted and
        used only to sign Kalshi requests on your behalf. Rotate it at Kalshi
        and re-run this endpoint to replace it.

        Both PKCS#1 and PKCS#8 PEM encodings are accepted, and a key pasted as a
        single line with literal `\n` escapes is normalized before parsing.

        `POST /exchanges/kalshi_offchain/enable-trading` is a deprecated alias
        of this route, kept for clients that are not deployed in lockstep.

        > **Error shape.** Minimal `{"error": "...", "code": "..."}`
        (`OrderSimpleErrorResponse`), not the structured envelope.

        **Auth & scope.** Requires `trade:execute` for `kalshi` and the mutation
        gate.
      operationId: enableKalshiTrading
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderKalshiEnableTradingRequest'
      responses:
        '200':
          description: Credentials validated against Kalshi and stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderKalshiEnableTradingResponse'
        '400':
          description: >-
            `INVALID_USER_ID`; `MISSING_API_KEY_ID` ("API Key ID is required");
            `INVALID_PRIVATE_KEY` ("Invalid RSA private key. Must be a valid
            PEM-encoded RSA key."); or `INVALID_CREDENTIALS` — the key parsed
            but Kalshi rejected it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '401':
          $ref: '#/components/responses/ExecUnauthorized'
        '403':
          description: '`INSUFFICIENT_SCOPE` or `PLATFORM_API_ACCESS_DISABLED`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
        '500':
          description: >-
            `CONFIG_ERROR` ("Internal configuration error" / "Encryption
            configuration error"), `ENCRYPTION_ERROR` ("Failed to encrypt
            credentials"), or `DB_ERROR` ("Failed to save credentials").
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderSimpleErrorResponse'
components:
  schemas:
    OrderKalshiEnableTradingRequest:
      type: object
      description: >-
        Body for `POST /exchanges/kalshi/enable-trading`. Kalshi trades against
        the user's OWN Kalshi account, so the caller supplies their own Kalshi
        API credentials — Kairos does not provision them.
      required:
        - api_key_id
        - private_key_pem
      properties:
        api_key_id:
          type: string
          description: Kalshi API Key ID (the public identifier). Must be non-blank.
        private_key_pem:
          type: string
          description: >-
            The matching RSA private key, PEM-encoded. Both PKCS#1 (`BEGIN RSA
            PRIVATE KEY`) and PKCS#8 (`BEGIN PRIVATE KEY`) are accepted, and a
            key pasted as a single line with literal `\n` escape sequences is
            normalized server-side.
    OrderKalshiEnableTradingResponse:
      type: object
      required:
        - success
        - message
      properties:
        success:
          type: boolean
        message:
          type: string
    OrderSimpleErrorResponse:
      type: object
      description: >-
        Minimal error body (`{"error": "..."}`, optionally `{"code": "..."}`)
        used by the auth middleware (401/403/429 on any endpoint), the
        external-signing `/v2/orders/*` lane, and the Kalshi-offchain balance
        endpoint. Distinct from `OrderErrorResponse` — these endpoints do not
        emit the full structured `error_details` shape.
      required:
        - error
      properties:
        error:
          type: string
          description: Human-readable error message.
          example: Insufficient scope
        code:
          type: string
          nullable: true
          description: >-
            Present only on the Kalshi-offchain endpoints (omitted entirely, not
            null, when absent). Observed values: `PLATFORM_API_ACCESS_DENIED`,
            `PLATFORM_API_ACCESS_DISABLED`, `INSUFFICIENT_SCOPE`,
            `NOT_CONFIGURED`, `NO_CREDENTIALS`, `INVALID_USER_ID`,
            `MISSING_API_KEY_ID`, `INVALID_PRIVATE_KEY`, `INVALID_CREDENTIALS`,
            `CONFIG_ERROR`, `ENCRYPTION_ERROR`, `DB_ERROR`, `INTERNAL_ERROR`,
            `KALSHI_API_ERROR`.
  responses:
    ExecUnauthorized:
      description: >
        No valid credential presented — missing/invalid API-key headers or an
        invalid/expired session token.
      content:
        application/json:
          schema:
            type: object
            required:
              - error
            properties:
              error:
                type: string
                example: Unauthorized
  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.
    apiKeySecret:
      type: apiKey
      in: header
      name: X-Api-Secret
      description: 64-char hex API secret.

````

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