{
  "openapi": "3.1.0",
  "info": {
    "title": "Chizumulu API",
    "version": "1.0.0",
    "description": "Read-only public API over public NRFA football updates. No authentication, no API key. Raw observations are preserved; structured data (schema version 2) is derived from them and unknown values stay `null`. Also a small, separate pass-through of one public YouTube channel feed (`/v1/videos`). Public GET responses send `Access-Control-Allow-Origin: *` (read-only; preflight allows only GET and OPTIONS; no credentials). Human documentation: https://api.chizumulu.net/docs/"
  },
  "servers": [
    {
      "url": "https://api.chizumulu.net",
      "description": "Canonical public origin"
    }
  ],
  "tags": [
    {
      "name": "Raw",
      "description": "Messages and observations exactly as received."
    },
    {
      "name": "Structured",
      "description": "Derived schema-v2 data next to the untouched original text."
    },
    {
      "name": "Status",
      "description": "Health."
    },
    {
      "name": "Videos",
      "description": "A cached pass-through of one public YouTube channel feed."
    }
  ],
  "paths": {
    "/v1/latest": {
      "get": {
        "operationId": "getLatest",
        "summary": "Newest stored message",
        "description": "The newest logical message in the same view order as /v1/updates. `content.text` is the text of the message's first observation.",
        "tags": [
          "Raw"
        ],
        "responses": {
          "200": {
            "description": "The newest message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RawUpdate"
                }
              }
            }
          },
          "404": {
            "description": "No message has been stored yet (`no updates yet`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/updates": {
      "get": {
        "operationId": "listUpdates",
        "summary": "Raw messages, newest first",
        "description": "One item per logical message, newest first, ordered by the earlier of `sourceTimestamp` and `receivedAt`. `cursor` pages a view; it is not a sync primitive: a message stored later with a newer position lands ahead of your cursor. Use /v1/feed to consume everything incrementally.",
        "tags": [
          "Raw"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Default 20. Values below 1 become 1 and values above 100 become 100; a non-numeric value is ignored (default).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "ISO-8601 timestamp WITH an explicit offset (`Z` or `+HH:MM`). Only messages positioned strictly after it. Invalid values are a 400.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque cursor from a previous response's `nextCursor`. Anything else is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "chizumulu",
            "in": "query",
            "required": false,
            "description": "`true` keeps messages whose text mentions \"chizumulu\", `false` keeps those that do not. Any other value is ignored.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of messages.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpdatesPage"
                }
              }
            }
          },
          "400": {
            "description": "`since is not a valid, parseable timestamp` or `cursor is invalid`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/feed": {
      "get": {
        "operationId": "getFeed",
        "summary": "Every observation, in ingestion order",
        "description": "The incremental primitive. Observations (including edits, each as its own item) come back oldest first in ingestion order. Store `nextAfter` and send it as `after` next time. Source timestamps play no part, so a late or edited message is never missed.",
        "tags": [
          "Raw"
        ],
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "Return observations with `id` greater than this. Default 0. Must be a non-negative integer of at most 15 digits, else 400.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{1,15}$",
              "default": "0"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Default 20. Values below 1 become 1 and values above 100 become 100; a non-numeric value is ignored (default).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of observations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedPage"
                }
              }
            }
          },
          "400": {
            "description": "`after must be a non-negative integer id`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/structured": {
      "get": {
        "operationId": "listStructured",
        "summary": "Messages with structured data, newest first",
        "description": "One item per logical message, in the same view order as /v1/updates, carrying the LATEST observation and its derived, schema-v2 structured result. Structured data appears only when the latest observation is `done`. Optional exact-match filters (`competition`, `teamId`, `teamAlias`, `team`, `postType`) select messages by that same latest, `done` result; a matching item is returned whole, and paging is unchanged.",
        "tags": [
          "Structured"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Default 20. Values below 1 become 1 and values above 100 become 100; a non-numeric value is ignored (default).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque cursor from a previous response's `nextCursor`. Anything else is a 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "competition",
            "in": "query",
            "required": false,
            "description": "A competition code (for example `nrfa_league_one`). Matches items whose post-level `competition.code` or any match's `competitionCode` equals it. An unknown code matches nothing (empty page). An empty value is a 400.",
            "schema": {
              "type": "string",
              "examples": [
                "nrfa_league_one"
              ]
            }
          },
          {
            "name": "teamId",
            "in": "query",
            "required": false,
            "description": "Recommended stable team filter. A canonical team id (for example `chizumulu_united`): matches any match whose `homeTeam` or `awayTeam` is one of the source spellings listed for that id, e.g. `Chizumulu United` and `Chizumulu United FC`. The table is explicit (exact equality, no fuzzy matching). Ambiguous names are not mapped. Source spellings in the response are never changed. Known ids: `chibavi_real_stars`, `jenda_united`, `chizumulu_united`, `chintheche_united`, `chilumba_barracks`, `mafu_stars`, `mmbelwa_warriors`, `chipolopolo_boys`, `ekwendeni`, `lube_masters`, `chihame_all_stars`, `raiply`, `euthini_veterans`, `vision_s_academy`, `luviri`. An unknown id matches nothing (empty page). An empty value is a 400.",
            "schema": {
              "type": "string",
              "examples": [
                "chizumulu_united"
              ]
            }
          },
          {
            "name": "teamAlias",
            "in": "query",
            "required": false,
            "description": "Human-friendly team shorthand from an explicit reviewed table (not fuzzy): the value must equal an alias exactly, ignoring only letter case (`Chizumulu`, `chizumulu` and `CHIZUMULU` are the same; no trimming, no partial match), and resolves to a `teamId`, then behaves like it. Aliases: `Chibavi`, `Jenda`, `Chizumulu`, `Chintheche`, `Chilumba`, `Mafu`, `M'mbelwa`, `M’mbelwa`, `M'mberwa`, `Chipolopolo`, `Lube`, `Euthini`, `Chihame`, `Luviri` (for example `Chizumulu` = `chizumulu_united`). An unknown or ambiguous shorthand (such as `Vision`) matches nothing (empty page). An empty value is a 400. Aliases never appear in a response. Integrations should prefer `teamId`.",
            "schema": {
              "type": "string",
              "examples": [
                "Chizumulu"
              ]
            }
          },
          {
            "name": "team",
            "in": "query",
            "required": false,
            "description": "A team name, matched exactly and case-sensitively against any match's `homeTeam` or `awayTeam`, as written in the source (no fuzzy matching, no aliases). Use it to find one specific spelling; use `teamId` for a stable filter. An empty value is a 400.",
            "schema": {
              "type": "string",
              "examples": [
                "Mafu Stars FC"
              ]
            }
          },
          {
            "name": "postType",
            "in": "query",
            "required": false,
            "description": "One `postType` value, matched exactly. An unknown value matches nothing (empty page). An empty value is a 400.",
            "schema": {
              "type": "string",
              "examples": [
                "result"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of items.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StructuredPage"
                }
              }
            }
          },
          "400": {
            "description": "`cursor is invalid`, or `competition must not be empty` / `teamId must not be empty` / `teamAlias must not be empty` / `team must not be empty` / `postType must not be empty`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/structured/{rawUpdateId}/history": {
      "get": {
        "operationId": "getStructuredHistory",
        "summary": "All observations of one message",
        "description": "Every observation of one logical message (an edit is another observation), oldest first, each with its own processing state and structured result.",
        "tags": [
          "Structured"
        ],
        "parameters": [
          {
            "name": "rawUpdateId",
            "in": "path",
            "required": true,
            "description": "The `rawUpdateId` of a message.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{1,15}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The message and its observations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/History"
                }
              }
            }
          },
          "400": {
            "description": "`rawUpdateId must be a positive integer`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/status": {
      "get": {
        "operationId": "getStatus",
        "summary": "Service and collector health",
        "description": "Cached for a few seconds (`cache-control`, `x-status-cache: HIT|MISS`). Separates a quiet source (`data.freshness`) from an unhealthy collector (`collector`, `delivery`).",
        "tags": [
          "Status"
        ],
        "responses": {
          "200": {
            "description": "Current status.",
            "headers": {
              "cache-control": {
                "schema": {
                  "type": "string"
                },
                "description": "`public, max-age=<seconds>`"
              },
              "x-status-cache": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "HIT",
                    "MISS"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Status"
                }
              }
            }
          }
        }
      }
    },
    "/v1/videos": {
      "get": {
        "operationId": "listVideos",
        "summary": "Recent videos of the channel feed",
        "description": "The entries of one public YouTube channel feed (`https://www.youtube.com/feeds/videos.xml?channel_id=UC5JtIP2gExbPDMbNMFr3aHA`, the Korean fan channel 창박골 Changbakgol), normalized, in the feed's own order (newest first, at most 15). Cached for about 10 minutes; if YouTube cannot be reached the last cached list is served (`x-videos-cache: STALE`). Not connected to the NRFA data or to the structured model. Query parameters are ignored.",
        "tags": [
          "Videos"
        ],
        "responses": {
          "200": {
            "description": "The current feed entries (possibly an empty list).",
            "headers": {
              "x-videos-cache": {
                "description": "`HIT` (served from the cache), `MISS` (just fetched from YouTube) or `STALE` (YouTube could not be reached; the last cached list is served).",
                "schema": {
                  "type": "string",
                  "enum": [
                    "HIT",
                    "MISS",
                    "STALE"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VideosPage"
                }
              }
            }
          },
          "502": {
            "description": "YouTube could not be reached and nothing is cached (`video feed unavailable`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/videos/latest": {
      "get": {
        "operationId": "getLatestVideo",
        "summary": "Newest video of the channel feed",
        "description": "The entry with the newest `publishedAt` of the same cached feed as /v1/videos, as a single video object.",
        "tags": [
          "Videos"
        ],
        "responses": {
          "200": {
            "description": "The newest video.",
            "headers": {
              "x-videos-cache": {
                "description": "`HIT` (served from the cache), `MISS` (just fetched from YouTube) or `STALE` (YouTube could not be reached; the last cached list is served).",
                "schema": {
                  "type": "string",
                  "enum": [
                    "HIT",
                    "MISS",
                    "STALE"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Video"
                }
              }
            }
          },
          "404": {
            "description": "The feed has no entries (`no videos yet`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "YouTube could not be reached and nothing is cached (`video feed unavailable`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/live": {
      "get": {
        "summary": "Matches that are credibly live right now",
        "description": "Derived matches whose latest observation is live or half-time AND fresh: the last update at most 45 minutes ago (for live and for half-time), the estimated minute still plausible, less than 4 hours since the first live observation, no conflicting or unresolved newer observation. If it is uncertain a match is omitted. Stale historical live matches are not returned here but are kept and remain available at /v1/matches/{matchId}.",
        "operationId": "getLive",
        "responses": {
          "200": {
            "description": "Credibly live matches (possibly none).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LivePage"
                }
              }
            }
          }
        }
      }
    },
    "/v1/matches/{matchId}": {
      "get": {
        "summary": "One derived match with its provenance",
        "operationId": "getMatch",
        "parameters": [
          {
            "name": "matchId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The match and every observation it was built from.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchDetail"
                }
              }
            }
          },
          "400": {
            "description": "Malformed matchId.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown matchId.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Short human-readable message. Not a stable machine code."
          }
        },
        "required": [
          "error"
        ],
        "description": "Every error response is a JSON object with a single `error` string."
      },
      "RawUpdate": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Id of the logical source message (`rawUpdateId` elsewhere)."
          },
          "source": {
            "type": "string",
            "description": "Source system. Currently `whatsapp`."
          },
          "sourceChannelId": {
            "type": "string"
          },
          "sourceMessageId": {
            "type": "string",
            "description": "Source-side message key. With `source` it identifies the logical message."
          },
          "sourceServerId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Alias id assigned by the source server, when known. Never used for identity."
          },
          "sourceTimestamp": {
            "type": "string",
            "description": "Timestamp of the source message, exactly as sent, normalized to UTC ISO-8601. Not guaranteed to be plausible or monotonic."
          },
          "receivedAt": {
            "type": "string",
            "description": "When this API first received the message."
          },
          "content": {
            "type": "object",
            "properties": {
              "text": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Text or caption of the FIRST observation of this message. `null` for a message without text (for example image-only). Later edits do not change it here; see /v1/feed and /v1/structured."
              }
            },
            "required": [
              "text"
            ]
          },
          "chizumuluRelated": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Cheap ingest-time heuristic: the raw text mentions \"chizumulu\" (case-insensitive). `null` when there is no text. Not a classification and not derived from structured data."
          }
        },
        "required": [
          "id",
          "source",
          "sourceChannelId",
          "sourceMessageId",
          "sourceServerId",
          "sourceTimestamp",
          "receivedAt",
          "content",
          "chizumuluRelated"
        ]
      },
      "UpdatesPage": {
        "type": "object",
        "properties": {
          "updates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RawUpdate"
            }
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque. Present when the page was full (there MAY be more; the next page can be empty). `null` when the page was short."
          }
        },
        "required": [
          "updates",
          "nextCursor"
        ]
      },
      "FeedItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Observation sequence number in ingestion order: the checkpoint key for /v1/feed `after`."
          },
          "observationId": {
            "type": "string",
            "description": "Stable identifier of this observation."
          },
          "rawUpdateId": {
            "type": "integer",
            "description": "The logical source message this observation belongs to."
          },
          "source": {
            "type": "string"
          },
          "sourceChannelId": {
            "type": "string"
          },
          "sourceMessageId": {
            "type": "string"
          },
          "sourceServerId": {
            "type": [
              "string",
              "null"
            ]
          },
          "sourceTimestamp": {
            "type": "string"
          },
          "receivedAt": {
            "type": "string",
            "description": "When this API received this observation."
          },
          "collectorObservedAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the collector observed it, when reported."
          },
          "content": {
            "type": "object",
            "properties": {
              "text": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Text of THIS observation."
              }
            },
            "required": [
              "text"
            ]
          }
        },
        "required": [
          "id",
          "observationId",
          "rawUpdateId",
          "source",
          "sourceChannelId",
          "sourceMessageId",
          "sourceServerId",
          "sourceTimestamp",
          "receivedAt",
          "collectorObservedAt",
          "content"
        ]
      },
      "FeedPage": {
        "type": "object",
        "properties": {
          "updates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FeedItem"
            }
          },
          "nextAfter": {
            "type": "integer",
            "description": "The last observation `id` returned, or the `after` you sent when nothing was returned. Always safe to store."
          },
          "hasMore": {
            "type": "boolean",
            "description": "`true` when the page was full (there MAY be more)."
          }
        },
        "required": [
          "updates",
          "nextAfter",
          "hasMore"
        ]
      },
      "Evidence": {
        "type": "object",
        "properties": {
          "text": {
            "type": "boolean",
            "description": "The item was read from the post text."
          },
          "attachments": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "Zero-based indexes of the attachments (images) the item was read from. Currently only `standings` facts read from a retained image cite an attachment (with `text` false); everything else is read from the text."
          }
        },
        "required": [
          "text",
          "attachments"
        ]
      },
      "Player": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "As written in the source."
          },
          "role": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "scorer",
              "assist",
              "build_up",
              "player_in",
              "player_out",
              "carded",
              "taker",
              "keeper",
              "involved",
              "coach",
              "referee",
              "official",
              "other",
              null
            ]
          }
        },
        "required": [
          "name",
          "role"
        ]
      },
      "MatchEvent": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer",
            "description": "Position of the event inside its match."
          },
          "type": {
            "type": "string",
            "enum": [
              "kickoff",
              "goal",
              "own_goal",
              "penalty",
              "half_time",
              "second_half_kickoff",
              "full_time",
              "substitution",
              "yellow_card",
              "red_card",
              "chance",
              "save",
              "free_kick",
              "corner",
              "other"
            ]
          },
          "minuteText": {
            "type": [
              "string",
              "null"
            ],
            "description": "Minute as written (`23'`, `45+2'`). Never normalized."
          },
          "team": {
            "type": [
              "string",
              "null"
            ],
            "description": "Team as written in the source."
          },
          "homeScore": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Score stated at that moment, only when the source states one."
          },
          "awayScore": {
            "type": [
              "integer",
              "null"
            ]
          },
          "players": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Player"
            }
          }
        },
        "required": [
          "index",
          "type",
          "minuteText",
          "team",
          "homeScore",
          "awayScore",
          "players"
        ]
      },
      "Match": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer",
            "description": "Observation-local reference (0, 1, ...). Not a global match identity."
          },
          "homeTeam": {
            "type": [
              "string",
              "null"
            ],
            "description": "Left-hand team as written. `null` when the source does not establish home/away."
          },
          "awayTeam": {
            "type": [
              "string",
              "null"
            ]
          },
          "homeScore": {
            "type": [
              "integer",
              "null"
            ]
          },
          "awayScore": {
            "type": [
              "integer",
              "null"
            ]
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "scheduled",
              "live",
              "half_time",
              "full_time",
              "postponed",
              "cancelled",
              null
            ],
            "description": "Stated or clearly implied state of the match in this post, or `null`."
          },
          "competition": {
            "type": [
              "string",
              "null"
            ],
            "description": "Competition name only when it is written as words in the post. Never expanded from a hashtag."
          },
          "competitionCode": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "nrfa_league_one",
              "nrfa_league_two",
              null
            ],
            "description": "Deterministic classification from known wording/hashtags in the post. `null` when there is no unambiguous signal."
          },
          "competitionBasis": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "match-text",
              "post-signal",
              null
            ],
            "description": "`match-text`: decided by this match's own `competition` text. `post-signal`: inherited from a single unambiguous signal elsewhere in the post."
          },
          "venue": {
            "type": [
              "string",
              "null"
            ]
          },
          "dateText": {
            "type": [
              "string",
              "null"
            ],
            "description": "As written (`SATURDAY 19-09-2026`). Never normalized."
          },
          "timeText": {
            "type": [
              "string",
              "null"
            ]
          },
          "roundText": {
            "type": [
              "string",
              "null"
            ]
          },
          "minuteText": {
            "type": [
              "string",
              "null"
            ],
            "description": "Match clock the post states for this snapshot, literally. `null` when none or when several different clocks are stated."
          },
          "competitionGroupText": {
            "type": [
              "string",
              "null"
            ],
            "description": "Cluster/group heading the match sits under, as written, without a leading `#`."
          },
          "evidence": {
            "$ref": "#/components/schemas/Evidence"
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MatchEvent"
            }
          }
        },
        "required": [
          "index",
          "homeTeam",
          "awayTeam",
          "homeScore",
          "awayScore",
          "status",
          "competition",
          "competitionCode",
          "competitionBasis",
          "venue",
          "dateText",
          "timeText",
          "roundText",
          "minuteText",
          "competitionGroupText",
          "evidence",
          "events"
        ]
      },
      "CallUpFact": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer"
          },
          "type": {
            "const": "call_up"
          },
          "data": {
            "$ref": "#/components/schemas/CallUpData"
          },
          "evidence": {
            "$ref": "#/components/schemas/Evidence"
          }
        },
        "required": [
          "index",
          "type",
          "data",
          "evidence"
        ]
      },
      "PlayerStatsData": {
        "type": "object",
        "properties": {
          "player": {
            "type": [
              "string",
              "null"
            ]
          },
          "club": {
            "type": [
              "string",
              "null"
            ]
          },
          "competition": {
            "type": [
              "string",
              "null"
            ]
          },
          "stats": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "description": "The exact word from the source that labels the number."
                },
                "value": {
                  "type": "integer"
                }
              },
              "required": [
                "name",
                "value"
              ]
            }
          }
        },
        "required": [
          "player",
          "club",
          "competition",
          "stats"
        ]
      },
      "PlayerStatsFact": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer"
          },
          "type": {
            "const": "player_stats"
          },
          "data": {
            "$ref": "#/components/schemas/PlayerStatsData"
          },
          "evidence": {
            "$ref": "#/components/schemas/Evidence"
          }
        },
        "required": [
          "index",
          "type",
          "data",
          "evidence"
        ]
      },
      "StandingsData": {
        "type": "object",
        "properties": {
          "competition": {
            "type": [
              "string",
              "null"
            ]
          },
          "round_text": {
            "type": [
              "string",
              "null"
            ]
          },
          "date_text": {
            "type": [
              "string",
              "null"
            ]
          },
          "season_text": {
            "type": [
              "string",
              "null"
            ]
          },
          "rows": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "position": {
                  "type": "integer"
                },
                "team": {
                  "type": "string"
                },
                "played": {
                  "type": "integer"
                },
                "won": {
                  "type": "integer"
                },
                "drawn": {
                  "type": "integer"
                },
                "lost": {
                  "type": "integer"
                },
                "goal_difference": {
                  "type": "integer"
                },
                "points": {
                  "type": "integer"
                }
              },
              "required": [
                "position",
                "team",
                "played",
                "won",
                "drawn",
                "lost",
                "goal_difference",
                "points"
              ]
            }
          }
        },
        "required": [
          "competition",
          "round_text",
          "date_text",
          "season_text",
          "rows"
        ]
      },
      "StandingsFact": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer"
          },
          "type": {
            "const": "standings"
          },
          "data": {
            "$ref": "#/components/schemas/StandingsData"
          },
          "evidence": {
            "$ref": "#/components/schemas/Evidence"
          }
        },
        "required": [
          "index",
          "type",
          "data",
          "evidence"
        ]
      },
      "NoticeData": {
        "type": "object",
        "properties": {
          "notice_type": {
            "type": "string",
            "enum": [
              "club_rename",
              "registration",
              "disciplinary",
              "schedule_change",
              "venue_change",
              "event_notice",
              "other"
            ]
          },
          "subject": {
            "type": [
              "string",
              "null"
            ]
          },
          "old_value": {
            "type": [
              "string",
              "null"
            ]
          },
          "new_value": {
            "type": [
              "string",
              "null"
            ]
          },
          "date_text": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "notice_type",
          "subject",
          "old_value",
          "new_value",
          "date_text"
        ]
      },
      "NoticeFact": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer"
          },
          "type": {
            "const": "notice"
          },
          "data": {
            "$ref": "#/components/schemas/NoticeData"
          },
          "evidence": {
            "$ref": "#/components/schemas/Evidence"
          }
        },
        "required": [
          "index",
          "type",
          "data",
          "evidence"
        ]
      },
      "TransferData": {
        "type": "object",
        "properties": {
          "player": {
            "type": [
              "string",
              "null"
            ]
          },
          "from_club": {
            "type": [
              "string",
              "null"
            ]
          },
          "to_club": {
            "type": [
              "string",
              "null"
            ]
          },
          "position": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "player",
          "from_club",
          "to_club",
          "position"
        ]
      },
      "TransferFact": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer"
          },
          "type": {
            "const": "transfer"
          },
          "data": {
            "$ref": "#/components/schemas/TransferData"
          },
          "evidence": {
            "$ref": "#/components/schemas/Evidence"
          }
        },
        "required": [
          "index",
          "type",
          "data",
          "evidence"
        ]
      },
      "CommunityData": {
        "type": "object",
        "properties": {
          "occasion": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "birthday",
              "congratulations",
              "condolence",
              "thanks",
              "event_promo",
              "other",
              null
            ]
          },
          "recipients": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "occasion",
          "recipients"
        ]
      },
      "CommunityFact": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer"
          },
          "type": {
            "const": "community"
          },
          "data": {
            "$ref": "#/components/schemas/CommunityData"
          },
          "evidence": {
            "$ref": "#/components/schemas/Evidence"
          }
        },
        "required": [
          "index",
          "type",
          "data",
          "evidence"
        ]
      },
      "Fact": {
        "description": "A non-match fact. The `data` keys are snake_case (they are stored exactly as extracted), unlike the camelCase match fields.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/CallUpFact"
          },
          {
            "$ref": "#/components/schemas/PlayerStatsFact"
          },
          {
            "$ref": "#/components/schemas/StandingsFact"
          },
          {
            "$ref": "#/components/schemas/NoticeFact"
          },
          {
            "$ref": "#/components/schemas/TransferFact"
          },
          {
            "$ref": "#/components/schemas/CommunityFact"
          }
        ]
      },
      "Competition": {
        "type": "object",
        "properties": {
          "code": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "nrfa_league_one",
              "nrfa_league_two",
              null
            ]
          },
          "conflict": {
            "type": "boolean",
            "description": "`true` when the post carried signals for more than one competition; then `code` is `null`."
          },
          "evidence": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "string",
                  "enum": [
                    "nrfa_league_one",
                    "nrfa_league_two"
                  ]
                },
                "signal": {
                  "type": "string",
                  "description": "The text or hashtag that produced the code."
                },
                "kind": {
                  "type": "string",
                  "enum": [
                    "hashtag",
                    "text"
                  ]
                }
              },
              "required": [
                "code",
                "signal",
                "kind"
              ]
            }
          }
        },
        "required": [
          "code",
          "conflict",
          "evidence"
        ]
      },
      "Structured": {
        "type": "object",
        "properties": {
          "schemaVersion": {
            "type": "integer",
            "const": 2
          },
          "postType": {
            "type": "string",
            "enum": [
              "result",
              "fixtures",
              "live_update",
              "match_update",
              "standings",
              "player_news",
              "administrative",
              "promotional",
              "community",
              "other"
            ]
          },
          "competition": {
            "$ref": "#/components/schemas/Competition"
          },
          "matches": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Match"
            }
          },
          "facts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Fact"
            }
          }
        },
        "required": [
          "schemaVersion",
          "postType",
          "competition",
          "matches",
          "facts"
        ],
        "description": "Derived from the observation's text. A finished post with zero matches and zero facts (a birthday greeting) is a valid result."
      },
      "Processing": {
        "description": "Processing state of the latest observation.",
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "state": {
                "const": "unregistered",
                "description": "Not yet picked up by the processor. Only `state` is present."
              }
            },
            "required": [
              "state"
            ]
          },
          {
            "type": "object",
            "properties": {
              "state": {
                "type": "string",
                "enum": [
                  "pending",
                  "awaiting_ai",
                  "done",
                  "failed"
                ]
              },
              "method": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "deterministic",
                  "ai",
                  "reused",
                  "none",
                  null
                ],
                "description": "How a `done` result was produced. `null` until `done`."
              },
              "model": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Model that produced an `ai` result. `null` otherwise."
              },
              "postType": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "result",
                  "fixtures",
                  "live_update",
                  "match_update",
                  "standings",
                  "player_news",
                  "administrative",
                  "promotional",
                  "community",
                  "other",
                  null
                ],
                "description": "Post type once `done` with a structured result, else `null`."
              },
              "translationState": {
                "type": "string",
                "enum": [
                  "none",
                  "done",
                  "failed"
                ]
              },
              "reusedFromObservationId": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Set when `method` is `reused`."
              },
              "error": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Short diagnostic for a `failed` observation. Not a stable contract."
              }
            },
            "required": [
              "state",
              "method",
              "model",
              "postType",
              "translationState",
              "reusedFromObservationId",
              "error"
            ]
          }
        ]
      },
      "Korean": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "description": "Korean translation of the whole post. Derived; the original text is never replaced."
          },
          "model": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "text",
          "model"
        ]
      },
      "StructuredItem": {
        "type": "object",
        "properties": {
          "rawUpdateId": {
            "type": "integer",
            "description": "The logical source message."
          },
          "source": {
            "type": "string"
          },
          "sourceMessageId": {
            "type": "string"
          },
          "sourceServerId": {
            "type": [
              "string",
              "null"
            ]
          },
          "sourceTimestamp": {
            "type": "string",
            "description": "Timestamp of the LATEST observation."
          },
          "observationCount": {
            "type": "integer",
            "description": "Observations of this message seen so far (an edit adds one)."
          },
          "latestObservation": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "observationId": {
                    "type": "string"
                  },
                  "receivedAt": {
                    "type": "string"
                  },
                  "text": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "required": [
                  "id",
                  "observationId",
                  "receivedAt",
                  "text"
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "The latest observation of the message, in ingestion order."
          },
          "processing": {
            "$ref": "#/components/schemas/Processing"
          },
          "structured": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Structured"
              },
              {
                "type": "null"
              }
            ],
            "description": "`null` unless the latest observation is `done` and has a structured result."
          },
          "attachmentFacts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Fact"
            },
            "description": "Present only when non-empty. Facts read from an attached image (their `evidence.attachments` cites it) of an observation that has no `structured` result: an image-only post, or a post whose text processing failed. Never merged into a text result; when `structured` exists, image facts are inside `structured.facts` instead."
          },
          "korean": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Korean"
              },
              {
                "type": "null"
              }
            ],
            "description": "`null` unless `done` with a translation."
          }
        },
        "required": [
          "rawUpdateId",
          "source",
          "sourceMessageId",
          "sourceServerId",
          "sourceTimestamp",
          "observationCount",
          "latestObservation",
          "processing",
          "structured",
          "korean"
        ]
      },
      "StructuredPage": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StructuredItem"
            }
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque. Present when the page was full (there MAY be more). `null` when the page was short."
          }
        },
        "required": [
          "items",
          "nextCursor"
        ]
      },
      "HistoryObservation": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "observationId": {
            "type": "string"
          },
          "sourceTimestamp": {
            "type": "string"
          },
          "receivedAt": {
            "type": "string"
          },
          "text": {
            "type": [
              "string",
              "null"
            ]
          },
          "processing": {
            "$ref": "#/components/schemas/Processing"
          },
          "structured": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Structured"
              },
              {
                "type": "null"
              }
            ]
          },
          "attachmentFacts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Fact"
            },
            "description": "Present only when non-empty. Facts read from an attached image (their `evidence.attachments` cites it) of an observation that has no `structured` result: an image-only post, or a post whose text processing failed. Never merged into a text result; when `structured` exists, image facts are inside `structured.facts` instead."
          },
          "korean": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Korean"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "id",
          "observationId",
          "sourceTimestamp",
          "receivedAt",
          "text",
          "processing",
          "structured",
          "korean"
        ]
      },
      "History": {
        "type": "object",
        "properties": {
          "rawUpdateId": {
            "type": "integer"
          },
          "source": {
            "type": "string"
          },
          "sourceMessageId": {
            "type": "string"
          },
          "sourceServerId": {
            "type": [
              "string",
              "null"
            ]
          },
          "observations": {
            "type": "array",
            "maxItems": 200,
            "items": {
              "$ref": "#/components/schemas/HistoryObservation"
            },
            "description": "Oldest first, in ingestion order; at most 200. Observation history (what the collector saw), not a guaranteed-complete source revision history."
          }
        },
        "required": [
          "rawUpdateId",
          "source",
          "sourceMessageId",
          "sourceServerId",
          "observations"
        ]
      },
      "Status": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "operational",
              "degraded"
            ],
            "description": "`operational` when the collector is `connected` and delivery is `healthy`. A quiet feed alone never makes it `degraded`."
          },
          "collector": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "connected",
                  "degraded",
                  "unavailable"
                ]
              },
              "heartbeatAt": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "whatsapp": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "connected",
                      "disconnected",
                      "unknown"
                    ]
                  },
                  "lastConnectedAt": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "lastDisconnectedAt": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "lastDisconnectReason": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "logged_out",
                      "forbidden",
                      "bad_session",
                      "conflict",
                      "restart_required",
                      "timeout",
                      "transient",
                      "stream_error",
                      "unknown",
                      null
                    ]
                  },
                  "streamRetryCount": {
                    "type": "integer"
                  }
                },
                "required": [
                  "status",
                  "lastConnectedAt",
                  "lastDisconnectedAt",
                  "lastDisconnectReason",
                  "streamRetryCount"
                ]
              },
              "version": {
                "type": "object",
                "properties": {
                  "collector": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "baileys": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "required": [
                  "collector",
                  "baileys"
                ]
              },
              "processStartedAt": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "storageFailureActive": {
                "type": "boolean"
              },
              "lastStorageFailureAt": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "status",
              "heartbeatAt",
              "whatsapp",
              "version",
              "processStartedAt",
              "storageFailureActive",
              "lastStorageFailureAt"
            ]
          },
          "delivery": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "healthy",
                  "degraded"
                ]
              },
              "queueDepth": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "oldestPendingAgeSeconds": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "quarantinedCount": {
                "type": "integer"
              },
              "lastSuccessfulIngestAt": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "status",
              "queueDepth",
              "oldestPendingAgeSeconds",
              "quarantinedCount",
              "lastSuccessfulIngestAt"
            ]
          },
          "data": {
            "type": "object",
            "properties": {
              "lastUpdateAt": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Source timestamp of the newest stored message."
              },
              "freshness": {
                "type": "string",
                "enum": [
                  "fresh",
                  "quiet"
                ],
                "description": "`fresh`: a message within the last 15 minutes. `quiet` is a normal state, not an outage."
              }
            },
            "required": [
              "lastUpdateAt",
              "freshness"
            ]
          },
          "processing": {
            "type": "object",
            "properties": {
              "registeredThrough": {
                "type": "integer"
              },
              "latestObservation": {
                "type": "integer"
              },
              "lag": {
                "type": "integer"
              },
              "pending": {
                "type": "integer"
              },
              "awaitingAi": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              }
            },
            "required": [
              "registeredThrough",
              "latestObservation",
              "lag",
              "pending",
              "awaitingAi",
              "failed"
            ],
            "description": "Informational. Structured processing never changes `status`."
          },
          "media": {
            "type": "object",
            "description": "Informational raw-media retention health (images are retained, not served; a retained image may be read for standings tables only). `{\"status\":\"unavailable\"}` alone if it could not be computed.",
            "required": [
              "status"
            ],
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "idle",
                  "ok",
                  "degraded",
                  "unavailable"
                ]
              },
              "reasons": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "retention-failures-in-last-24h",
                    "attachment-pending-over-15m"
                  ]
                }
              },
              "stored": {
                "type": "integer"
              },
              "pending": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "skipped": {
                "type": "integer"
              },
              "oldestPendingAt": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "oldestPendingAgeSeconds": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          }
        },
        "required": [
          "status",
          "collector",
          "delivery",
          "data",
          "processing",
          "media"
        ]
      },
      "CallUpData": {
        "type": "object",
        "properties": {
          "player": {
            "type": [
              "string",
              "null"
            ]
          },
          "club": {
            "type": [
              "string",
              "null"
            ]
          },
          "position": {
            "type": [
              "string",
              "null"
            ]
          },
          "called_to": {
            "type": [
              "string",
              "null"
            ]
          },
          "squad_text": {
            "type": [
              "string",
              "null"
            ]
          },
          "competition": {
            "type": [
              "string",
              "null"
            ]
          },
          "opponent": {
            "type": [
              "string",
              "null"
            ]
          },
          "first_leg_date_text": {
            "type": [
              "string",
              "null"
            ]
          },
          "camp_date_text": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "player",
          "club",
          "position",
          "called_to",
          "squad_text",
          "competition",
          "opponent",
          "first_leg_date_text",
          "camp_date_text"
        ]
      },
      "Video": {
        "type": "object",
        "description": "One entry of the YouTube channel feed. Values are copied from the feed; a value the feed does not provide is `null`.",
        "properties": {
          "videoId": {
            "type": "string",
            "description": "The YouTube video id."
          },
          "title": {
            "type": "string"
          },
          "publishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the video was first published (UTC, ISO 8601)."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The watch URL."
          },
          "thumbnailUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          }
        },
        "required": [
          "videoId",
          "title",
          "publishedAt",
          "url",
          "thumbnailUrl"
        ]
      },
      "VideosPage": {
        "type": "object",
        "properties": {
          "videos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Video"
            }
          },
          "fetchedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When this list was fetched from YouTube. Older than about 10 minutes means the list is being served from the cache because YouTube could not be reached."
          }
        },
        "required": [
          "videos",
          "fetchedAt"
        ]
      },
      "DerivedMatch": {
        "type": "object",
        "description": "A real match stitched from structured observations by deterministic rules (never fuzzy, never an LLM). Derived data: it can be missing or wrong, and a match that could not be merged with confidence stays separate.",
        "properties": {
          "matchId": {
            "type": "string",
            "format": "uuid",
            "description": "Opaque and stable: it does not change as more observations attach."
          },
          "competition": {
            "type": "object",
            "properties": {
              "code": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "nrfa_league_one",
                  "nrfa_league_two",
                  null
                ]
              }
            },
            "required": [
              "code"
            ]
          },
          "homeTeam": {
            "$ref": "#/components/schemas/DerivedTeam"
          },
          "awayTeam": {
            "$ref": "#/components/schemas/DerivedTeam"
          },
          "status": {
            "type": "string",
            "enum": [
              "scheduled",
              "live",
              "half_time",
              "full_time",
              "postponed",
              "cancelled"
            ]
          },
          "score": {
            "type": "object",
            "properties": {
              "home": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "away": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            },
            "required": [
              "home",
              "away"
            ]
          },
          "minute": {
            "type": [
              "string",
              "null"
            ],
            "description": "The literal minute of the latest observation, as written (for example \"48'\")."
          },
          "date": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO date, only when the posts stated an explicit, consistent date."
          },
          "round": {
            "type": [
              "string",
              "null"
            ]
          },
          "firstSeenAt": {
            "type": "string"
          },
          "lastSeenAt": {
            "type": "string"
          },
          "completedAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "Source timestamp of the first full-time observation."
          },
          "updateCount": {
            "type": "integer",
            "description": "Observations attached to this match."
          },
          "quality": {
            "type": "string",
            "enum": [
              "ok",
              "conflict"
            ],
            "description": "conflict: a same-pair observation inside this match's activity window contradicted it (for example a score went down) and was NOT merged."
          }
        },
        "required": [
          "matchId",
          "competition",
          "homeTeam",
          "awayTeam",
          "status",
          "score",
          "minute",
          "date",
          "round",
          "firstSeenAt",
          "lastSeenAt",
          "completedAt",
          "updateCount",
          "quality"
        ]
      },
      "DerivedTeam": {
        "type": "object",
        "properties": {
          "id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Canonical team id (see teamId), null when the source spelling has no reviewed id."
          },
          "name": {
            "type": "string",
            "description": "The source spelling as first seen."
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "LivePage": {
        "type": "object",
        "properties": {
          "matches": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DerivedMatch"
            }
          },
          "asOf": {
            "type": "string"
          }
        },
        "required": [
          "matches",
          "asOf"
        ]
      },
      "MatchDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DerivedMatch"
          },
          {
            "type": "object",
            "properties": {
              "updates": {
                "type": "array",
                "description": "Every observation attached to the match, oldest first, with what it said.",
                "items": {
                  "type": "object",
                  "properties": {
                    "rawUpdateId": {
                      "type": "integer"
                    },
                    "observationId": {
                      "type": "integer"
                    },
                    "matchIndex": {
                      "type": "integer"
                    },
                    "sourceTimestamp": {
                      "type": "string"
                    },
                    "outcome": {
                      "type": "string",
                      "enum": [
                        "created",
                        "attached"
                      ]
                    },
                    "status": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "homeScore": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "awayScore": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "minute": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "rawUpdateId",
                    "observationId",
                    "matchIndex",
                    "sourceTimestamp",
                    "outcome"
                  ]
                }
              },
              "unresolved": {
                "type": "array",
                "description": "Observations of this team pair that fitted several matches and were therefore NOT attached to any (never guessed).",
                "items": {
                  "type": "object",
                  "properties": {
                    "rawUpdateId": {
                      "type": "integer"
                    },
                    "observationId": {
                      "type": "integer"
                    },
                    "matchIndex": {
                      "type": "integer"
                    },
                    "sourceTimestamp": {
                      "type": "string"
                    },
                    "reason": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "rawUpdateId",
                    "observationId",
                    "matchIndex",
                    "sourceTimestamp"
                  ]
                }
              }
            },
            "required": [
              "updates",
              "unresolved"
            ]
          }
        ]
      }
    }
  }
}
