Totalis Markets API

Cached Kalshi and Polymarket market data.

OpenAPI Specification

totalis-markets-api-openapi.yml Raw ↑
openapi: 3.0.3
info:
  title: Totalis RFQ API Keys Markets API
  version: 2.1.0
  description: 'Public REST surface for the Totalis parlay RFQ platform — a decentralized request-for-quote marketplace for parlay bets across Kalshi and Polymarket, with on-chain Solana vault settlement.


    **Wire format**: snake_case JSON. All successful responses are wrapped in `{ "data": ... }`; list endpoints add `meta` with cursor-based pagination. Errors use a `{ "error": { code, message, details? } }` envelope.


    **Authentication**: programmatic clients send `X-API-Key`; the web dashboard uses Privy JWTs (`Authorization: Bearer ...`). Any authenticated user can both place parlays and quote as a market maker — there is no separate MM role. Admin endpoints are out of scope for this public reference.


    **Rate limiting**: 100 req/min anonymous, 300 req/min authenticated. Responses include `X-RateLimit-*` headers. Request body limit: 256KB.


    **Pagination**: list endpoints use opaque cursor pagination. Pass `meta.cursor` from the previous response as `?cursor=...` to fetch the next page; do not parse or construct cursors client-side.'
servers:
- url: https://api.totalis.trade
  description: Production
security:
- ApiKey: []
tags:
- name: Markets
  description: Cached Kalshi and Polymarket market data.
paths:
  /markets:
    get:
      operationId: listMarkets
      summary: List Markets
      description: 'List active markets grouped by event across supported venues (Kalshi and Polymarket). Filters: `category`, `venue`, `subcategory` (validated against the selected category), `frequency` (`daily`/`weekly`/`monthly`/`hourly`), `date_filter` (time bucket), and `search`. Pagination is cursor-based at the event level. The response includes `available_subcategories` and `available_frequencies` so clients can drive filter chips from the same result set.'
      tags:
      - Markets
      security: []
      parameters:
      - name: category
        in: query
        description: Filter by category. Pass `all` to disable the default category filter.
        schema:
          type: string
          enum:
          - politics
          - sports
          - crypto
          - finance
          - economics
          - entertainment
          - weather
          - tech
          - all
      - name: venue
        in: query
        description: Filter by venue. Omit to include all venues.
        schema:
          $ref: '#/components/schemas/Venue'
      - name: subcategory
        in: query
        description: Filter events by subcategory. Valid values depend on the selected `category` (requires `category` to also be set).
        schema:
          $ref: '#/components/schemas/SubcategorySlug'
      - name: frequency
        in: query
        description: Filter events by the cadence of the underlying series. `other` is intentionally rejected — it's a residual response bucket, not a filter target.
        schema:
          type: string
          enum:
          - daily
          - weekly
          - monthly
          - hourly
      - name: date_filter
        in: query
        description: Time bucket filter. If omitted, a default 7-day horizon is applied. Pass `all` to retrieve all active markets regardless of close date.
        schema:
          type: string
          enum:
          - 1h
          - 1h-1d
          - 1d-7d
          - today
          - this_week
          - this_month
          - all
      - name: search
        in: query
        description: Full-text search across event/market titles.
        schema:
          type: string
      - $ref: '#/components/parameters/CursorParam'
      - name: limit
        in: query
        description: Events per page. Default 21, max 100. The product `limit × markets_per_event` must not exceed 2000.
        schema:
          type: integer
          default: 21
          minimum: 1
          maximum: 100
      - name: markets_per_event
        in: query
        description: Max markets returned per event group. Default 10, max 50.
        schema:
          type: integer
          default: 10
          minimum: 1
          maximum: 50
      responses:
        '200':
          description: Events grouped by event_ticker, plus subcategory/frequency facets for the current result set
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      events:
                        type: array
                        items:
                          $ref: '#/components/schemas/EventGroup'
                  meta:
                    allOf:
                    - $ref: '#/components/schemas/PaginationMeta'
                    - type: object
                      properties:
                        total_events:
                          type: integer
                        available_subcategories:
                          type: array
                          items:
                            $ref: '#/components/schemas/SubcategorySlug'
                        available_frequencies:
                          type: array
                          items:
                            type: string
                            enum:
                            - daily
                            - weekly
                            - monthly
                            - hourly
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/markets/list:
    get:
      operationId: listMarketsFlat
      security: []
      tags:
      - Markets
      summary: List markets (flat)
      description: 'Returns a paginated list of individual markets, most-traded first. Unlike the event-grouped `GET /markets`, results aren''t grouped by event and are never capped per event, so every market is reachable. Filter by venue, category, subcategory, or one or more series.


        Paginate with `cursor` until it returns `null`; `meta.total` is the total number of matches. For a complete sync, de-duplicate by `ticker` — markets with equal volume aren''t guaranteed a stable order between requests. Scoping to a `subcategory` or `series_ticker` set avoids this entirely.'
      parameters:
      - name: category
        in: query
        description: Filter by category, e.g. `sports` or `crypto`.
        schema:
          type: string
          enum:
          - crypto
          - economics
          - finance
          - sports
          - politics
          - tech
          - entertainment
          - weather
          - all
      - name: venue
        in: query
        description: 'Filter by venue: `kalshi` or `polymarket`.'
        schema:
          $ref: '#/components/schemas/Venue'
      - name: subcategory
        in: query
        description: Filter by subcategory, e.g. `world_cup`. Requires `category`.
        schema:
          $ref: '#/components/schemas/SubcategorySlug'
      - name: frequency
        in: query
        description: 'Filter by series cadence: `daily`, `weekly`, `monthly`, or `hourly`.'
        schema:
          type: string
          enum:
          - daily
          - weekly
          - monthly
          - hourly
      - name: series_ticker
        in: query
        description: Filter by one or more series, comma-separated and case-sensitive, e.g. `KXWCAST,KXWCTCORNERS`. Up to 100.
        schema:
          type: string
      - name: status
        in: query
        description: Filter by status. Defaults to tradeable markets (`active,open`); pass a comma-separated list of `active`, `open`, `closed`, `settled`, `finalized`, or `all`. An empty or unrecognised value returns 400.
        schema:
          type: string
          default: active,open
      - $ref: '#/components/parameters/CursorParam'
      - name: limit
        in: query
        description: Number of markets per page, from 1 to 500. Defaults to 100. Only the first 10,000 matches are reachable — narrow the filters to page further.
        schema:
          type: integer
          default: 100
          minimum: 1
          maximum: 500
      responses:
        '200':
          description: OK
          headers:
            Cache-Control:
              schema:
                type: string
                example: public, max-age=15, stale-while-revalidate=60
          content:
            application/json:
              schema:
                type: object
                required:
                - data
                properties:
                  data:
                    type: object
                    required:
                    - markets
                    properties:
                      markets:
                        type: array
                        items:
                          $ref: '#/components/schemas/Market'
                  meta:
                    allOf:
                    - $ref: '#/components/schemas/PaginationMeta'
                    - type: object
                      properties:
                        total:
                          type: integer
                          description: Total markets matching the filters across all pages.
        '400':
          $ref: '#/components/responses/BadRequest'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          description: Market index unavailable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /markets/{ticker}:
    get:
      operationId: getMarket
      summary: Get Market
      description: Fetch a single Kalshi market by ticker or a single Polymarket market by condition ID. When `venue` is omitted, the API infers Polymarket for condition IDs that look like `0x...`; otherwise it looks up the market as Kalshi.
      tags:
      - Markets
      security: []
      parameters:
      - name: ticker
        in: path
        required: true
        description: Kalshi market ticker or Polymarket `condition_id`.
        schema:
          type: string
          maxLength: 160
          pattern: ^[A-Za-z0-9_.:-]+$
      - name: venue
        in: query
        required: false
        description: Optional venue hint. Use this when the caller already knows whether the ticker belongs to Kalshi or Polymarket.
        schema:
          $ref: '#/components/schemas/Venue'
      responses:
        '200':
          description: Market details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Market'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  schemas:
    Venue:
      type: string
      enum:
      - kalshi
      - polymarket
      description: Source venue for a market.
    PaginationMeta:
      type: object
      properties:
        cursor:
          type: string
          nullable: true
        has_more:
          type: boolean
        limit:
          type: integer
          description: Maximum number of items requested for this page.
        offset:
          type: integer
          description: Number of items skipped before this page.
        total:
          type: integer
          description: Total number of items available across all pages, when returned by the endpoint.
    SubcategorySlug:
      type: string
      description: Subcategory wire format (lowercase snake_case). Valid values depend on the selected `category` and are enforced server-side.
      enum:
      - nba
      - ncaa
      - nfl
      - nhl
      - mlb
      - ufc
      - lol
      - epl
      - efl
      - la_liga
      - world_cup
      - bundesliga
      - liga_portugal
      - ligue_1
      - f1
      - ipl
      - btc
      - eth
      - sol
      - stocks
      - indices
      - etfs
      - earnings
      - rates
      - inflation
      - commodities
      - fx
      - jobs
      - ai
      - companies
      - other
    ErrorEnvelope:
      type: object
      required:
      - error
      properties:
        error:
          $ref: '#/components/schemas/ErrorBody'
    EventGroup:
      type: object
      required:
      - event_ticker
      - series_ticker
      - category
      - total_markets
      - markets
      properties:
        event_ticker:
          type: string
          description: 'Event identifier. **Kalshi**: event ticker (e.g. `KXBTC-25FEB07`). **Polymarket**: Gamma `event_slug` (falls back to `market_slug` when no parent event exists).'
        series_ticker:
          type: string
          description: 'Series identifier. **Kalshi**: series ticker (e.g. `KXBTC`). **Polymarket**: Gamma `market_slug`.'
        series_title:
          type: string
          nullable: true
        event_title:
          type: string
          nullable: true
        event_sub_title:
          type: string
          nullable: true
        category:
          type: string
        image_url:
          type: string
          nullable: true
        total_markets:
          type: integer
        markets:
          type: array
          items:
            $ref: '#/components/schemas/Market'
    Market:
      type: object
      properties:
        ticker:
          type: string
          description: 'Unique market identifier on its venue. **Kalshi**: market ticker (e.g. `KXBTC-25FEB07-T100000`). **Polymarket**: `condition_id`, the 0x-prefixed hex string from Polymarket''s CTF (e.g. `0x4d2…`); not the gamma id, question id, or market slug. Yes/no CLOB token ids are not returned by this API.'
        event_ticker:
          type: string
          description: 'Event identifier the market belongs to. **Kalshi**: event ticker (e.g. `KXBTC-25FEB07`). **Polymarket**: Gamma `event_slug` (falls back to `market_slug` when no parent event exists).'
        series_ticker:
          type: string
          description: 'Series identifier the event belongs to. **Kalshi**: series ticker (e.g. `KXBTC`). **Polymarket**: Gamma `market_slug` — Polymarket has no native series concept, so we key whitelisting on the market slug.'
        title:
          type: string
        subtitle:
          type: string
          nullable: true
        yes_sub_title:
          type: string
          nullable: true
        no_sub_title:
          type: string
          nullable: true
        bet_group:
          type: string
          nullable: true
          description: 'Polymarket-only: section label for the row (e.g. "Match O/U 21.5"). Always null on Kalshi rows.'
        category:
          type: string
        subcategory:
          type: string
          nullable: true
        frequency:
          type: string
          nullable: true
          description: 'Cadence of the underlying series: daily/weekly/monthly/hourly/other. Null for categories without timeframe pills.'
        venue:
          $ref: '#/components/schemas/Venue'
        status:
          type: string
        yes_bid:
          type: number
        yes_ask:
          type: number
        no_bid:
          type: number
        no_ask:
          type: number
        last_price:
          type: number
        price_to_beat:
          type: number
          nullable: true
          description: Reference price for Polymarket up/down markets. Null on Kalshi and on non-up/down Polymarket markets.
        volume:
          type: integer
        volume_24h:
          type: integer
        open_interest:
          type: integer
        venue_url:
          type: string
          nullable: true
          description: Canonical market URL on its source venue.
        open_time:
          type: string
          format: date-time
        close_time:
          type: string
          format: date-time
        expiration_time:
          type: string
          format: date-time
        exclusion_keys:
          type: array
          items:
            type: string
          description: Parlay-correlation keys. Two markets **cannot co-exist in the same parlay if (and only if) their `exclusion_keys` arrays intersect** — i.e. share at least one string. Apply a pure set-intersection; there is no rule table to maintain client-side. The keys encode every correlation rule (cross-series exclusion groups, threshold ladders, same-game sports family/kind/participant correlation, Polymarket sub-events). Keys are opaque and namespaced by reason and scope (e.g. `kp:<gameKey>:moneyline|spread`, `grp:<idx>:<eventKey>`, `fam:<gameKey>:<series>`), and are derived solely from the market itself, so a selected leg can snapshot its keys and compare them later. The plain one-leg-per-event rule (same `event_ticker`) is intentionally not encoded here. Always present; an empty array means the market participates in no correlation rule.
    ErrorBody:
      type: object
      required:
      - code
      - message
      properties:
        code:
          type: string
          enum:
          - VALIDATION_ERROR
          - UNAUTHORIZED
          - FORBIDDEN
          - NOT_FOUND
          - CONFLICT
          - RATE_LIMITED
          - PAYLOAD_TOO_LARGE
          - INTERNAL_ERROR
          - SERVICE_UNAVAILABLE
          description: Machine-readable error code.
        message:
          type: string
          description: Human-readable error message.
        details:
          type: object
          description: Additional error context.
        retry_after:
          type: integer
          nullable: true
          description: Seconds until retry; set on 429 responses.
  responses:
    InternalError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: NOT_FOUND
              message: Resource not found
    BadRequest:
      description: Validation error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
  parameters:
    CursorParam:
      name: cursor
      in: query
      description: Opaque pagination cursor from a previous response's `meta.cursor`. Do not construct manually.
      schema:
        type: string
  securitySchemes:
    PrivyJWT:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: 'Privy JWT issued to the web dashboard. Sent as `Authorization: Bearer <jwt>`. The Privy session signer underpins all wallet-signed actions.'
    ApiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: 'Programmatic API key. Sent as `X-API-Key: <key>`. Generate one from the Totalis dashboard. The same header is accepted on the WebSocket auth message.'