asyncapi: 3.0.0
info:
  title: Sport feed stream
  version: '0.4'
  description: |
    The WebSocket stream of the sport feed: one connection, subscriptions with
    filters, a snapshot of every matching event, then every change. The REST
    endpoints are described in `openapi.yaml`; both share the same objects.

    Connecting:

    - Open `wss://<host>/v1/stream` with `Authorization: Bearer <api_key>`. A browser, which cannot set that header, first gets a ticket from `POST /v1/stream/ticket` and opens `/v1/stream?ticket=<ticket>`; a ticket is valid for one connection within 30 s.
    - A missing or invalid key or ticket is refused with HTTP 401, a disabled or expired key with 403, a blocked address with 429, and maintenance or a full service with 503; no connection is opened.
    - The server does not offer `permessage-deflate` in this version.

    Subscribing:

    - Send `subscribe` with an `id` you choose (at most 64 bytes of UTF-8, unique among your subscriptions) and a filter. Filter fields combine with AND, values within a field with OR; an absent or empty field matches everything, except `periods`, which defaults to `["match"]`. Unknown filter fields are refused.
    - The server answers `subscribed`, then one `snapshot` per matching event that the connection does not already follow (the event, its matching markets with their prices and results), then `snapshot_end`. Every accepted `subscribe` gets its `subscribed` and its `snapshot_end`, even when an `unsubscribe` for it comes first; `unsubscribed` follows them. Your state is complete after `snapshot_end`. An event that starts matching later arrives as its own `snapshot`.
    - A connection follows an event from its snapshot until the event's status is final and every market of it that was not removed has a result, or until no subscription matches it after an `unsubscribe`. While followed, every message about the event is delivered once, even after its status leaves your `statuses` filter, so you receive its end, its markets' final states and its results. A result correction is delivered to every connection with a subscription that matches the event apart from `statuses` and selects the market's type and period. While a snapshot of the event is still to come and you do not follow the event with that market's type and period, the correction waits, and is dropped if the snapshot carries it. A subscription selects messages from its `subscribed` on: nothing it selects comes before its `subscribed`.
    - A later subscription that adds markets of an event the connection already follows sends that event's snapshot again under the new subscription's `id`; replace your state for the event with it.
    - Up to 50 subscriptions per connection; at most 10 `subscribe` messages per minute, a refused one answered with an `error` of code `rate_limited`, and more than 3 refused in a minute close the connection with 4029; at most 10 client messages per second and 4 KiB per message. A `ping` `id` is at most 64 bytes of UTF-8, and a `pong` is not held behind `subscribed` or `unsubscribed`: it queues with live messages. An `error` reply waits behind an unsent `subscribed` or `unsubscribed` sent before it, and behind an earlier `error` waiting so; when more than 64 wait, the connection is closed with 4029.

    Messages:

    - Every server message has `type`, `seq` and `sent_at`. `seq` starts at 1 on each connection and increases by exactly 1 per message, so a gap can only mean a loss: reconnect.
    - `event` is sent when any event field changes, `market` when a market is added, removed or settled, `price` when probabilities or uncertainty change (or the reason changes while `caution`) and the state does not, `state` when the state changes (or the reason changes while `suspended`), `result` when a result is published or corrected. `price` and `state` carry the full price.
    - Update frequency: `state` at once on every change; `price` when an outcome probability has changed by at least 0.001, the uncertainty by at least 0.002, or the reason while `caution`, at most 10 per second per market from the start of the event and 2 per second before it, a change that comes sooner being sent as soon as that limit allows, merged with any later change; before the start, prices are refreshed at most every 5 minutes for events more than 24 h away and at most every minute until 1 hour before the start. An unchanged price is never repeated.
    - Football is priced before and during the match; the other sports are priced before the start only: from the start of play (or its scheduled start, if earlier) every market of such an event is `suspended` with reason `match_event` until the event ends, while its `event` and `result` messages continue.
    - Messages about one market arrive in the order they were produced. A `state` replaces the market's unsent `price` messages and goes before the queued messages of other markets.
    - `heartbeat` is sent every second. Treat every price as suspended and reconnect after 3 s without any message.

    Slow clients: when a connection's unsent messages pass 4 MiB or 2 s, its queued `price` messages are reduced to the latest per market; if it is still over, the connection is closed with 4008. Snapshots do not count towards these limits. While the oldest unsent live (non-snapshot) message is more than 1 s old (half of `max_backlog_age`), `snapshot` messages wait; if they wait so for more than 2 s (`max_backlog_age`), the connection is closed with 4008. `subscribed`, `snapshot_end` and `unsubscribed` messages wait only for queued `state` messages and for the `snapshot` messages before them.

    Reconnecting: reconnect with exponential backoff from 1 s to 30 s with jitter, subscribe again, and replace your state with the new snapshot.

    Change log:

    - 0.1: first release.
    - 0.2: tennis, basketball (NBA) and ice hockey (NHL) listed, with their settlement rules; every sport but football is priced before the start only.
    - 0.3: a match interrupted and resumed later stays `live` and keeps `started_at`; the 48 h rule applies before the start, and to resumption.
    - 0.4: `version` on every event object, in `event` and `snapshot` messages (the same as in REST); `price`, `state`, `market` and `result` messages carry none, since `seq` and snapshots order them.
defaultContentType: application/json
servers:
  production:
    host: '{host}'
    protocol: wss
    description: The API host name given with your key.
    variables:
      host:
        default: api.example.com
    security:
      - $ref: '#/components/securitySchemes/bearer'
      - $ref: '#/components/securitySchemes/ticket'
channels:
  stream:
    address: /v1/stream
    title: Stream
    description: |
      The only channel. Close codes:

      - `1000`: normal close. No action.
      - `1012`: service restart. Reconnect with backoff.
      - `1013`: server overloaded. Reconnect with backoff.
      - `4003`: key disabled or expired during the connection. Contact support.
      - `4008`: slow consumer: unsent messages over 4 MiB or 2 s, or `snapshot` messages that waited more than 2 s behind live messages more than 1 s old. Reconnect; read faster or subscribe to less.
      - `4009`: too many connections for this key. Close another connection.
      - `4029`: too many client messages: over 10 a second, more than 3 refused `subscribe` messages in a minute, or more than 64 `error` replies waiting behind an unsent `subscribed`, `unsubscribed` or earlier `error`. Reconnect with backoff.
      - `4400`: invalid client message. Fix the message.
    servers:
      - $ref: '#/servers/production'
    messages:
      subscribe:
        $ref: '#/components/messages/subscribe'
      unsubscribe:
        $ref: '#/components/messages/unsubscribe'
      ping:
        $ref: '#/components/messages/ping'
      subscribed:
        $ref: '#/components/messages/subscribed'
      unsubscribed:
        $ref: '#/components/messages/unsubscribed'
      snapshot:
        $ref: '#/components/messages/snapshot'
      snapshot_end:
        $ref: '#/components/messages/snapshot_end'
      event:
        $ref: '#/components/messages/event'
      market:
        $ref: '#/components/messages/market'
      price:
        $ref: '#/components/messages/price'
      state:
        $ref: '#/components/messages/state'
      result:
        $ref: '#/components/messages/result'
      heartbeat:
        $ref: '#/components/messages/heartbeat'
      pong:
        $ref: '#/components/messages/pong'
      error:
        $ref: '#/components/messages/error'
    bindings:
      ws:
        method: GET
        headers:
          type: object
          properties:
            Authorization:
              type: string
              description: '`Bearer <api_key>`; or use `ticket` instead.'
        query:
          type: object
          properties:
            ticket:
              type: string
              description: A ticket from `POST /v1/stream/ticket`, instead of the header.
        bindingVersion: 0.1.0
    x-close-codes:
      - code: 1000
        meaning: Normal close
        action: None
      - code: 1012
        meaning: Service restart
        action: Reconnect with backoff
      - code: 1013
        meaning: Server overloaded
        action: Reconnect with backoff
      - code: 4003
        meaning: Key disabled or expired during the connection
        action: Contact support
      - code: 4008
        meaning: Slow consumer
        action: Reconnect; read faster or subscribe to less
      - code: 4009
        meaning: Too many connections for this key
        action: Close another connection
      - code: 4029
        meaning: Too many client messages
        action: Reconnect with backoff
      - code: 4400
        meaning: Invalid client message
        action: Fix the message
operations:
  receiveClientMessages:
    action: receive
    title: Client messages
    summary: What a client sends.
    channel:
      $ref: '#/channels/stream'
    messages:
      - $ref: '#/channels/stream/messages/subscribe'
      - $ref: '#/channels/stream/messages/unsubscribe'
      - $ref: '#/channels/stream/messages/ping'
  sendServerMessages:
    action: send
    title: Server messages
    summary: What the server sends.
    channel:
      $ref: '#/channels/stream'
    messages:
      - $ref: '#/channels/stream/messages/subscribed'
      - $ref: '#/channels/stream/messages/unsubscribed'
      - $ref: '#/channels/stream/messages/snapshot'
      - $ref: '#/channels/stream/messages/snapshot_end'
      - $ref: '#/channels/stream/messages/event'
      - $ref: '#/channels/stream/messages/market'
      - $ref: '#/channels/stream/messages/price'
      - $ref: '#/channels/stream/messages/state'
      - $ref: '#/channels/stream/messages/result'
      - $ref: '#/channels/stream/messages/heartbeat'
      - $ref: '#/channels/stream/messages/pong'
      - $ref: '#/channels/stream/messages/error'
components:
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: >-
        An API key, `sf_live_` followed by 43 characters, as
        `Authorization: Bearer <api_key>`.
    ticket:
      type: httpApiKey
      in: query
      name: ticket
      description: A single-use ticket from `POST /v1/stream/ticket`.
  messages:
    subscribe:
      name: subscribe
      title: subscribe
      summary: Add a subscription.
      payload:
        $ref: '#/components/schemas/SubscribeMessage'
    unsubscribe:
      name: unsubscribe
      title: unsubscribe
      summary: Remove a subscription.
      payload:
        $ref: '#/components/schemas/UnsubscribeMessage'
    ping:
      name: ping
      title: ping
      summary: Request a `pong`.
      payload:
        $ref: '#/components/schemas/PingMessage'
    subscribed:
      name: subscribed
      title: subscribed
      summary: A subscription is active; its snapshots follow.
      payload:
        $ref: '#/components/schemas/SubscribedMessage'
    unsubscribed:
      name: unsubscribed
      title: unsubscribed
      summary: A subscription was removed.
      payload:
        $ref: '#/components/schemas/UnsubscribedMessage'
    snapshot:
      name: snapshot
      title: snapshot
      summary: One event with its matching markets, their prices and results.
      payload:
        $ref: '#/components/schemas/SnapshotMessage'
    snapshot_end:
      name: snapshot_end
      title: snapshot_end
      summary: The subscription's snapshot is complete.
      payload:
        $ref: '#/components/schemas/SnapshotEndMessage'
    event:
      name: event
      title: event
      summary: An event field changed.
      payload:
        $ref: '#/components/schemas/EventMessage'
    market:
      name: market
      title: market
      summary: A market was added, removed or settled.
      payload:
        $ref: '#/components/schemas/MarketMessage'
    price:
      name: price
      title: price
      summary: Probabilities or uncertainty changed and the state did not.
      payload:
        $ref: '#/components/schemas/PriceMessage'
    state:
      name: state
      title: state
      summary: The state changed, or the reason while suspended.
      payload:
        $ref: '#/components/schemas/StateMessage'
    result:
      name: result
      title: result
      summary: A result was published or corrected.
      payload:
        $ref: '#/components/schemas/ResultMessage'
    heartbeat:
      name: heartbeat
      title: heartbeat
      summary: Sent every second.
      payload:
        $ref: '#/components/schemas/HeartbeatMessage'
    pong:
      name: pong
      title: pong
      summary: The answer to a `ping`.
      payload:
        $ref: '#/components/schemas/PongMessage'
    error:
      name: error
      title: error
      summary: A client message was refused.
      payload:
        $ref: '#/components/schemas/ErrorMessage'
  schemas:
    ClientMessage:
      description: Any message a client sends.
      oneOf:
        - $ref: '#/components/schemas/SubscribeMessage'
        - $ref: '#/components/schemas/UnsubscribeMessage'
        - $ref: '#/components/schemas/PingMessage'
    SubscribeMessage:
      type: object
      required: [op, id]
      additionalProperties: false
      properties:
        op:
          const: subscribe
        id:
          type: string
          description: Chosen by the client, unique among its subscriptions; at most 64 bytes of UTF-8.
        filter:
          $ref: '#/components/schemas/Filter'
    UnsubscribeMessage:
      type: object
      required: [op, id]
      additionalProperties: false
      properties:
        op:
          const: unsubscribe
        id:
          type: string
          description: The subscription's `id`.
    PingMessage:
      type: object
      required: [op, id]
      additionalProperties: false
      properties:
        op:
          const: ping
        id:
          type: string
          description: At most 64 bytes of UTF-8.
    Filter:
      type: object
      description: Fields combine with AND, values within a field with OR; an absent or empty field matches everything, except `periods`, which defaults to `["match"]`. Unknown fields are refused.
      additionalProperties: false
      properties:
        sports:
          type: array
          items:
            $ref: '#/components/schemas/SportId'
        competitions:
          type: array
          items:
            $ref: '#/components/schemas/CompetitionId'
        events:
          type: array
          items:
            $ref: '#/components/schemas/EventId'
        market_types:
          type: array
          items:
            $ref: '#/components/schemas/MarketType'
        periods:
          type: array
          items:
            $ref: '#/components/schemas/Period'
        statuses:
          type: array
          items:
            $ref: '#/components/schemas/EventStatus'
    ServerMessage:
      description: Any message the server sends; `type` tells which.
      oneOf:
        - $ref: '#/components/schemas/SubscribedMessage'
        - $ref: '#/components/schemas/UnsubscribedMessage'
        - $ref: '#/components/schemas/SnapshotMessage'
        - $ref: '#/components/schemas/SnapshotEndMessage'
        - $ref: '#/components/schemas/EventMessage'
        - $ref: '#/components/schemas/MarketMessage'
        - $ref: '#/components/schemas/PriceMessage'
        - $ref: '#/components/schemas/StateMessage'
        - $ref: '#/components/schemas/ResultMessage'
        - $ref: '#/components/schemas/HeartbeatMessage'
        - $ref: '#/components/schemas/PongMessage'
        - $ref: '#/components/schemas/ErrorMessage'
    Seq:
      type: integer
      minimum: 1
      description: Starts at 1 on each connection and increases by exactly 1 per message.
    SubscribedMessage:
      type: object
      required: [seq, sent_at, type, id]
      properties:
        seq:
          $ref: '#/components/schemas/Seq'
        sent_at:
          $ref: '#/components/schemas/Timestamp'
        type:
          const: subscribed
        id:
          type: string
    UnsubscribedMessage:
      type: object
      required: [seq, sent_at, type, id]
      properties:
        seq:
          $ref: '#/components/schemas/Seq'
        sent_at:
          $ref: '#/components/schemas/Timestamp'
        type:
          const: unsubscribed
        id:
          type: string
    SnapshotMessage:
      type: object
      required: [seq, sent_at, type, sub, event, markets]
      properties:
        seq:
          $ref: '#/components/schemas/Seq'
        sent_at:
          $ref: '#/components/schemas/Timestamp'
        type:
          const: snapshot
        sub:
          type: string
          description: The `id` of the subscription the snapshot is for.
        event:
          $ref: '#/components/schemas/Event'
        markets:
          type: array
          items:
            $ref: '#/components/schemas/Market'
    SnapshotEndMessage:
      type: object
      required: [seq, sent_at, type, id]
      properties:
        seq:
          $ref: '#/components/schemas/Seq'
        sent_at:
          $ref: '#/components/schemas/Timestamp'
        type:
          const: snapshot_end
        id:
          type: string
    EventMessage:
      type: object
      required: [seq, sent_at, type, event]
      properties:
        seq:
          $ref: '#/components/schemas/Seq'
        sent_at:
          $ref: '#/components/schemas/Timestamp'
        type:
          const: event
        event:
          $ref: '#/components/schemas/Event'
    MarketMessage:
      type: object
      required: [seq, sent_at, type, market]
      properties:
        seq:
          $ref: '#/components/schemas/Seq'
        sent_at:
          $ref: '#/components/schemas/Timestamp'
        type:
          const: market
        market:
          $ref: '#/components/schemas/MarketInfo'
    PriceMessage:
      type: object
      required: [seq, sent_at, type, event_id, market_id, price]
      properties:
        seq:
          $ref: '#/components/schemas/Seq'
        sent_at:
          $ref: '#/components/schemas/Timestamp'
        type:
          const: price
        event_id:
          $ref: '#/components/schemas/EventId'
        market_id:
          $ref: '#/components/schemas/MarketId'
        price:
          $ref: '#/components/schemas/Price'
    StateMessage:
      type: object
      required: [seq, sent_at, type, event_id, market_id, price]
      properties:
        seq:
          $ref: '#/components/schemas/Seq'
        sent_at:
          $ref: '#/components/schemas/Timestamp'
        type:
          const: state
        event_id:
          $ref: '#/components/schemas/EventId'
        market_id:
          $ref: '#/components/schemas/MarketId'
        price:
          $ref: '#/components/schemas/Price'
    ResultMessage:
      type: object
      required: [seq, sent_at, type, event_id, market_id, result]
      properties:
        seq:
          $ref: '#/components/schemas/Seq'
        sent_at:
          $ref: '#/components/schemas/Timestamp'
        type:
          const: result
        event_id:
          $ref: '#/components/schemas/EventId'
        market_id:
          $ref: '#/components/schemas/MarketId'
        result:
          $ref: '#/components/schemas/MarketResult'
    HeartbeatMessage:
      type: object
      required: [seq, sent_at, type]
      properties:
        seq:
          $ref: '#/components/schemas/Seq'
        sent_at:
          $ref: '#/components/schemas/Timestamp'
        type:
          const: heartbeat
    PongMessage:
      type: object
      required: [seq, sent_at, type, id]
      properties:
        seq:
          $ref: '#/components/schemas/Seq'
        sent_at:
          $ref: '#/components/schemas/Timestamp'
        type:
          const: pong
        id:
          type: string
          description: The `id` of the `ping`.
    ErrorMessage:
      type: object
      required: [seq, sent_at, type, code, message]
      properties:
        seq:
          $ref: '#/components/schemas/Seq'
        sent_at:
          $ref: '#/components/schemas/Timestamp'
        type:
          const: error
        code:
          $ref: '#/components/schemas/ErrorCode'
        message:
          type: string
        id:
          type: string
          description: The `id` of the refused message, when it has a short one.
    MarketInfo:
      type: object
      description: A market without its price, as `market` messages carry it.
      required: [id, event_id, type, period, line, status, outcomes, result]
      properties:
        id:
          $ref: '#/components/schemas/MarketId'
        event_id:
          $ref: '#/components/schemas/EventId'
        type:
          $ref: '#/components/schemas/MarketType'
        period:
          $ref: '#/components/schemas/Period'
        line:
          $ref: '#/components/schemas/Line'
        status:
          $ref: '#/components/schemas/MarketStatus'
        outcomes:
          type: array
          items:
            $ref: '#/components/schemas/Outcome'
        result:
          oneOf:
            - $ref: '#/components/schemas/MarketResult'
            - type: 'null'
    Timestamp:
      type: string
      format: date-time
      description: RFC 3339 in UTC with milliseconds and `Z`.
      pattern: '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$'
      examples: ['2026-10-24T14:23:52.004Z']
    Version:
      type: integer
      minimum: 0
      maximum: 9007199254740991
      description: Tells which of two copies of one market's row, or of one event, is newer; a greater `version` is a newer state, also across restarts of the feed, and one state has the same `version` everywhere it is shown. A row's `version` grows whenever something the row shows changes, an event's whenever any of its fields does. It is close to the time of the change in microseconds since the Unix epoch, but compare versions only; never read them as times. Below 2^53, so it is exact in every JSON parser.
      examples: [1792851832004001]
    CompetitionId:
      type: string
      description: A competition's opaque ID; do not parse it.
      pattern: '^cp_[A-Za-z0-9]+$'
    EventId:
      type: string
      description: An event's opaque ID; do not parse it.
      pattern: '^ev_[A-Za-z0-9]+$'
    MarketId:
      type: string
      description: A market's opaque ID; do not parse it.
      pattern: '^mk_[A-Za-z0-9]+$'
    SportId:
      type: string
      description: A sport's fixed slug.
      enum: [football, basketball, tennis, baseball, ice_hockey, american_football, mma, cricket, rugby, table_tennis, cs2, lol, dota2, valorant, r6, overwatch]
    CompetitionRef:
      type: object
      required: [id, name]
      properties:
        id:
          $ref: '#/components/schemas/CompetitionId'
        name:
          type: string
    Role:
      type: string
      description: A participant's side. For sports without a home side (tennis, MMA, esports) `home` is a fixed one of the two participants, the same in every message and every market.
      enum: [home, away]
    Participant:
      type: object
      required: [name, role]
      properties:
        name:
          type: string
        role:
          $ref: '#/components/schemas/Role'
    EventStatus:
      type: string
      description: |
        - `scheduled`: not started.
        - `live`: in play. For sports with fixed start times the event is `live` from about 6 minutes after its start time (or earlier, once play is confirmed; up to 10 minutes later after a feed restart); until play is confirmed it can still return to `scheduled` or become `postponed` or `cancelled`.
        - `ended`: finished; results not all known yet.
        - `resolved`: finished; every market has a result. Final.
        - `postponed`: will not start at the scheduled time; a new time may follow.
        - `cancelled`: will not take place; markets are void. Final.
        - `abandoned`: stopped after the start; markets are settled under the settlement rules. Final.
      enum: [scheduled, live, ended, resolved, postponed, cancelled, abandoned]
    EndReason:
      type: string
      description: Why the event finished; set when it reaches `ended`, `resolved`, `cancelled` or `abandoned`.
      enum: [completed, retired, walkover, abandoned, postponed, cancelled]
    FootballPeriod:
      type: string
      enum: ['1H', HT, '2H', ET, PEN, FT]
    Score:
      type: object
      required: [home, away]
      properties:
        home:
          type: integer
          minimum: 0
        away:
          type: integer
          minimum: 0
    Clock:
      type: object
      description: The match clock. Compute the current clock from `seconds`, `running` and `as_of`.
      required: [seconds, running, as_of]
      properties:
        seconds:
          type: integer
          minimum: 0
        running:
          type: boolean
        as_of:
          $ref: '#/components/schemas/Timestamp'
    Live:
      type: object
      description: Live match data, football only in v1. Any field may be null. An `event` message is sent only when the score, the period or `running` changes.
      required: [score, period, clock]
      properties:
        score:
          oneOf:
            - $ref: '#/components/schemas/Score'
            - type: 'null'
        period:
          oneOf:
            - $ref: '#/components/schemas/FootballPeriod'
            - type: 'null'
        clock:
          oneOf:
            - $ref: '#/components/schemas/Clock'
            - type: 'null'
    Event:
      type: object
      description: An event. `started_at` is null again if a `live` event returns to `scheduled` or `postponed` or is cancelled before play was confirmed. A match interrupted and resumed later stays `live` and keeps its `started_at`, while its `scheduled_start` may move to the resumption. `live` is null outside live play, for sports other than football, and when no live data is available.
      required: [id, sport, competition, name, participants, scheduled_start, status, started_at, ended_at, end_reason, resolved_at, live, updated_at, version]
      properties:
        id:
          $ref: '#/components/schemas/EventId'
        sport:
          $ref: '#/components/schemas/SportId'
        competition:
          $ref: '#/components/schemas/CompetitionRef'
        name:
          type: string
          examples: [Arsenal vs Chelsea]
        participants:
          type: array
          items:
            $ref: '#/components/schemas/Participant'
        scheduled_start:
          $ref: '#/components/schemas/Timestamp'
        status:
          $ref: '#/components/schemas/EventStatus'
        started_at:
          $ref: '#/components/schemas/NullableTimestamp'
        ended_at:
          $ref: '#/components/schemas/NullableTimestamp'
        end_reason:
          oneOf:
            - $ref: '#/components/schemas/EndReason'
            - type: 'null'
        resolved_at:
          $ref: '#/components/schemas/NullableTimestamp'
        live:
          oneOf:
            - $ref: '#/components/schemas/Live'
            - type: 'null'
        updated_at:
          $ref: '#/components/schemas/Timestamp'
        version:
          $ref: '#/components/schemas/Version'
          description: The version of this state of the event; grows whenever any of its fields changes.
    NullableTimestamp:
      oneOf:
        - $ref: '#/components/schemas/Timestamp'
        - type: 'null'
    MarketType:
      type: string
      description: |
        - `1x2`: result of the match, three-way; outcomes `home`, `draw`, `away`; no line.
        - `moneyline`: winner of the match, two-way; outcomes `home`, `away`; no line.
        - `total`: total goals, points or games against the line; outcomes `over`, `under`.
        - `handicap`: `home` wins with the line added to its score; the line is from the home side, so `away` holds the opposite line; outcomes `home`, `away`.
        v1 lines are always a whole number plus 0.5, so no market can push.
      enum: ['1x2', moneyline, total, handicap]
    Period:
      type: string
      description: The part of the event a market settles on; v1 has only the full match.
      enum: [match]
    OutcomeKey:
      type: string
      enum: [home, draw, away, over, under]
    MarketStatus:
      type: string
      description: '`active` while the market is listed; `removed` when it is withdrawn before the start of the event, after which no price follows; `settled` once it has a result.'
      enum: [active, removed, settled]
    Outcome:
      type: object
      required: [key, name]
      properties:
        key:
          $ref: '#/components/schemas/OutcomeKey'
        name:
          type: string
          examples: [Over 2.5]
    MarketState:
      type: string
      description: |
        - `open`: the price is reliable.
        - `caution`: the price is usable, but uncertainty is raised; widen margins or reduce limits.
        - `suspended`: do not use the price; `prob`, `fair_odds` and `uncertainty` are null.
      enum: [open, caution, suspended]
    Reason:
      type: string
      description: |
        Why the market is not `open`; null when `open`.
        - `match_event`: something happened in the match (goal, card, break, end), or the match has started in a sport priced before the start only; `suspended`.
        - `unstable`: the price is moving fast or is not yet settled; `caution` or `suspended`.
        - `low_confidence`: the price is less certain than usual; `caution` or `suspended`.
        - `insufficient_data`: not enough data to price the market; `suspended`.
        - `starting`: the market was just listed or the feed restarted; `suspended`.
        - `event_not_active`: the event is not scheduled or live; `suspended`.
        - `maintenance`: planned maintenance; `suspended`.
      enum: [match_event, unstable, low_confidence, insufficient_data, starting, event_not_active, maintenance]
    OutcomePrice:
      type: object
      required: [key, prob, fair_odds]
      properties:
        key:
          $ref: '#/components/schemas/OutcomeKey'
        prob:
          type: [number, 'null']
          minimum: 0.0001
          maximum: 0.9999
          description: The fair probability, 4 decimals; null while suspended.
        fair_odds:
          type: [number, 'null']
          minimum: 1
          maximum: 10000
          description: '`1/prob`, 3 decimals; null while suspended.'
    Price:
      type: object
      description: A market's price. The 4-decimal `prob` values of a market sum to exactly 1.
      required: [state, reason, outcomes, uncertainty, ts]
      properties:
        state:
          $ref: '#/components/schemas/MarketState'
        reason:
          oneOf:
            - $ref: '#/components/schemas/Reason'
            - type: 'null'
        outcomes:
          type: array
          items:
            $ref: '#/components/schemas/OutcomePrice'
        uncertainty:
          type: [number, 'null']
          minimum: 0
          description: How far the probability may be from the truth, in probability units, 3 decimals; null while suspended.
        ts:
          $ref: '#/components/schemas/Timestamp'
          description: The time of the newest input behind the price. A `suspended` price with reason `starting` shown before any input for the market (a new listing, or after a restart) carries the time it was made, so the next price's `ts` can be earlier.
    MarketResult:
      type: object
      description: >-
        Who won a market. A void market has `"winner": null, "void": true`.
        `revision` starts at 1 and increases with each correction.
      required: [winner, void, revision, resolved_at]
      properties:
        winner:
          oneOf:
            - $ref: '#/components/schemas/OutcomeKey'
            - type: 'null'
        void:
          type: boolean
        revision:
          type: integer
          minimum: 1
        resolved_at:
          $ref: '#/components/schemas/Timestamp'
    Line:
      type: [number, 'null']
      multipleOf: 0.5
      description: The line, such as `2.5` or `-1.5`; null for types without a line.
    Market:
      type: object
      description: A market with its current price and result.
      required: [id, event_id, type, period, line, status, outcomes, price, result]
      properties:
        id:
          $ref: '#/components/schemas/MarketId'
        event_id:
          $ref: '#/components/schemas/EventId'
        type:
          $ref: '#/components/schemas/MarketType'
        period:
          $ref: '#/components/schemas/Period'
        line:
          $ref: '#/components/schemas/Line'
        status:
          $ref: '#/components/schemas/MarketStatus'
        outcomes:
          type: array
          items:
            $ref: '#/components/schemas/Outcome'
        price:
          oneOf:
            - $ref: '#/components/schemas/Price'
            - type: 'null'
        result:
          oneOf:
            - $ref: '#/components/schemas/MarketResult'
            - type: 'null'
    ErrorCode:
      type: string
      description: |
        - `invalid_request`: 400.
        - `unauthorized`: 401.
        - `forbidden`: 403.
        - `not_found`: 404.
        - `rate_limited`: 429.
        - `internal`: 500.
        - `unavailable`: 503.
      enum: [invalid_request, unauthorized, forbidden, not_found, rate_limited, internal, unavailable]
