{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://raw.githubusercontent.com/api-evangelist/sharpapi/main/json-schema/sharpapi-event-game-state-schema.json",
  "title": "EventGameState",
  "description": "Aggregated live game state for a single event, merged across the sportsbooks covering it. Scores are consensus-selected; temporal fields use sport-aware clock direction; situational fields priority-fill from a fixed book ranking. Every field is optional except the always-present core set — sport coverage varies, so e.g. tennis rows carry sets_home/server while hockey rows carry power_play. Rows also come in a terminal flavour: a fixture that has finished is carried here for 15 minutes after completion with status \"final\", completed_at, score_type and the final scores. A terminal row is projected from the completion record, not from a live cross-book merge, so it carries no primary_book/book_count/game_period/game_clock and no situational fields — branch on status == \"final\" before reading those.",
  "x-generated": "2026-10-14",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/sharpapi-openapi.json#/components/schemas/EventGameState",
  "type": "object",
  "required": [
    "home_team",
    "away_team",
    "sport",
    "league",
    "is_live"
  ],
  "properties": {
    "home_team": {
      "type": "string",
      "description": "Normalized home team name."
    },
    "away_team": {
      "type": "string",
      "description": "Normalized away team name."
    },
    "sport": {
      "type": "string",
      "description": "Atlas sport id (e.g. `baseball`, `soccer`)."
    },
    "league": {
      "type": "string",
      "description": "Atlas league id (e.g. `mlb`, `england_-_premier_league`)."
    },
    "home_score": {
      "type": "integer",
      "description": "Current home score in the natural unit for the sport (runs, points, goals, sets — tennis uses summed set points). Present on live rows and on terminal rows whose `score_type` is `points`; a terminal row with `score_type: \"sets\"` carries `sets_home`/`sets_away` instead and omits this."
    },
    "away_score": {
      "type": "integer",
      "description": "Current away score. Present on live rows and on terminal rows whose `score_type` is `points`; a terminal row with `score_type: \"sets\"` carries `sets_home`/`sets_away` instead and omits this."
    },
    "is_live": {
      "type": "boolean",
      "description": "`true` for a live event; `false` on a carried terminal (finished) row."
    },
    "primary_book": {
      "type": "string",
      "description": "The sportsbook whose scores were selected by the merge. Consensus-respecting — if ≥2 books agree on a score the highest-priority book in that group wins. Live rows only — absent on a terminal row."
    },
    "book_count": {
      "type": "integer",
      "description": "Number of sportsbooks that contributed live state for this event at merge time. Live rows only — absent on a terminal row."
    },
    "game_period": {
      "type": "string",
      "description": "Current period. Sport-specific format: `T5` (MLB top 5), `Q3` (NBA Q3), `2H` (soccer 2nd half), `P2` (NHL P2), `S2` (tennis set 2), `FT` (full time)."
    },
    "game_clock": {
      "type": "string",
      "description": "Game clock. Countup format (`49:06`) for soccer; countdown format (`5:42`) for basketball/hockey/football."
    },
    "possession": {
      "type": "string",
      "enum": [
        "home",
        "away"
      ],
      "description": "Which team has possession (team sports) or is receiving (tennis)."
    },
    "server": {
      "type": "string",
      "enum": [
        "home",
        "away"
      ],
      "description": "Tennis only — current server."
    },
    "last_play": {
      "type": "string",
      "description": "Sport-specific last-play description."
    },
    "is_timeout": {
      "type": "boolean",
      "description": "`true` during a timeout."
    },
    "fouls_home": {
      "type": "integer",
      "description": "Team fouls (basketball, soccer)."
    },
    "fouls_away": {
      "type": "integer"
    },
    "corners_home": {
      "type": "integer",
      "description": "Soccer only."
    },
    "corners_away": {
      "type": "integer"
    },
    "yellow_cards_home": {
      "type": "integer",
      "description": "Soccer only."
    },
    "yellow_cards_away": {
      "type": "integer"
    },
    "red_cards_home": {
      "type": "integer",
      "description": "Soccer only."
    },
    "red_cards_away": {
      "type": "integer"
    },
    "power_play": {
      "type": "string",
      "enum": [
        "home",
        "away"
      ],
      "description": "Hockey only — which team is on the power play."
    },
    "sets_home": {
      "type": "integer",
      "description": "Tennis only — sets won."
    },
    "sets_away": {
      "type": "integer"
    },
    "hits_home": {
      "type": "integer",
      "description": "Baseball only."
    },
    "hits_away": {
      "type": "integer"
    },
    "home_pitcher": {
      "type": "string",
      "description": "Baseball only — starting pitcher display name."
    },
    "away_pitcher": {
      "type": "string"
    },
    "wickets_home": {
      "type": "integer",
      "description": "Cricket only — dismissals."
    },
    "wickets_away": {
      "type": "integer"
    },
    "overs": {
      "type": "number",
      "description": "Cricket only."
    },
    "batting_team": {
      "type": "string",
      "enum": [
        "home",
        "away"
      ],
      "description": "Cricket only."
    },
    "stale": {
      "type": "boolean",
      "description": "Only emitted when `true`. Indicates the `primary_book`'s livestate key TTL fell below the aggregator's freshness threshold (~10s) at merge time — scores reflect the last known values; the book has gone quiet. Absence of the field means fresh. On live rows it is written only when `true` (absence = fresh); a terminal row always carries it explicitly as `false`."
    },
    "aggregator_stale": {
      "type": "boolean",
      "description": "Only emitted when `true`. Indicates the SharpAPI aggregator itself hasn't advanced the gen counter for this event's sport in more than ~30s. Broader signal than `stale` — suggests a pipeline hiccup, not a single-book issue."
    },
    "status": {
      "type": "string",
      "enum": [
        "final"
      ],
      "description": "Present only on a carried terminal row, always `final`. Live rows carry no `status` key, so `status == \"final\"` is the reliable terminal test."
    },
    "completed_at": {
      "type": "string",
      "format": "date-time",
      "description": "Terminal rows only — when SharpAPI detected the completion (not a league-official final whistle). The 15-minute carry window is measured from this timestamp."
    },
    "score_type": {
      "type": "string",
      "enum": [
        "points",
        "sets"
      ],
      "description": "Terminal rows only — which fields carry the final result. `sets` for the set-scored sports (`tennis`, `table_tennis`, `volleyball`, `badminton`), whose result is in `sets_home`/`sets_away`; `points` everywhere else, whose result is in `home_score`/`away_score`."
    },
    "winner": {
      "type": "string",
      "enum": [
        "home",
        "away",
        "draw"
      ],
      "description": "Terminal rows only, and optional even there — omitted when the recorded result does not settle the match (a level score in a sport that cannot draw, a sub-terminal set count, or a snapshot whose scores never arrived and is recorded as 0-0). A missing `winner` means \"not settled\", not a tie. `draw` is produced only for sports where a level score is a real outcome: `soccer`, `hockey`, `football`, `rugby_union`, `rugby_league`."
    }
  }
}