{
  "openapi": "3.1.0",
  "info": {
    "title": "GGS Results API",
    "version": "1.0.0",
    "summary": "Read access to tournaments run on GGS Play.",
    "description": "Read access to tournaments run on GGS Play: rosters, rounds, pairings and game results, and final standings. Every route is a GET under one base URL, answers in one envelope, and needs an API key.\n\nThe guide to this API, with worked examples, is at https://ggs.club/guides/api, and as Markdown at https://ggs.club/guides/api.md.\n\n## Compatibility\n\nClients must ignore fields they do not know and must accept enum values they do not know (treat an unknown value as \"other\"). Adding a field, an enum value or an `include` is not a breaking change and can happen at any time, so the shapes below are open: an object may carry more than it lists.\n\n## Keys and scopes\n\nSend an API key on every request: `Authorization: Bearer ggs_live_…`. Keys are created in your GGS account settings, under **API keys**, once your account has API access. You can hold several keys, one per integration, and revoke each on its own; the plaintext is shown once, when the key is created.\n\nEach key carries scopes. This document needs `play:read`. A request without a usable key gets `401` (`WWW-Authenticate: Bearer`, or `Bearer error=\"invalid_token\"` when the key sent was refused); a key without the scope gets `403 insufficient_scope`. Call `GET /me` first: it confirms the key, its scopes and its rate limit.\n\n## Base URL\n\nEvery route is under `https://data.ggs.club/play/v1`, which runs each request next to the database in Dublin (`eu-west-1`).\n\n## Envelope\n\nSuccess: `{ \"ok\": true, \"data\": …, \"requestId\": \"…\" }`, plus `\"page\"` on a list. Failure: `{ \"ok\": false, \"error\": { \"code\", \"message\", \"details\" }, \"requestId\": \"…\" }`. Branch on `error.code`, not on the message. Quote the `requestId` (also the `X-Request-Id` header) when reporting a problem.\n\nA request the API cannot parse is `400 invalid_input`, with `details` a list of `{ path, message }`, one per problem. That includes a query parameter the route does not take (every route refuses unknown parameters, so a misspelt one never passes silently) and one sent twice that may be sent only once. Any method other than GET on a route is `405 method_not_allowed`, with an `Allow` header.\n\n## Pagination and incremental sync\n\nLists are cursor-paginated: follow `page.next` until it is `null`.\n\nTo keep a copy in sync, poll `GET /tournaments?updatedSince=<a few minutes before the last updatedAt you saw>` and follow `page.next` to the end. With `updatedSince` the list runs in `updatedAt` order (`sort=-updatedAt` reverses it; any other `sort` is refused), so the last item you process is the newest change. A change is stamped with the moment its transaction started, so one that commits slowly can carry an `updatedAt` just before your last poll: starting each poll a few minutes back (5 is plenty) brings it in. The items that margin repeats are expected, and applying a tournament or a tombstone twice changes nothing.\n\nA tournament's `updatedAt` moves when its record changes, when its results snapshot is recomputed, and, once it is completed, when anything in its roster, teams, rounds, pairings or games changes or its `links` do (its records on other platforms, its stats publication), so every correction after completion comes through. Joins and pairings during an event do not move it: to follow a live event, poll its roster and rounds with their ETag. Nor does a change outside the event, such as an organiser's or player's GGS profile or the venue's details.\n\nWith `updatedSince`, the page also carries a tombstone, `{ \"id\", \"slug\", \"deleted\": true, \"deletedAt\", \"updatedAt\" }`, for every tournament that stopped being visible since then: deleted, made private, unpublished, or turned into a demo, template or listing. Drop it from your copy. Tombstones ignore every other filter (a gone tournament has no state or game system left to match), sit in the same `updatedAt` order as the tournaments (`updatedAt` equals `deletedAt`), and are kept for 30 days: if the last `updatedAt` you saw is older than that, sync again from scratch, without `updatedSince`. A tournament that becomes visible again simply reappears with a newer `updatedAt`. A completed event the organiser reopens to correct is no longer `completed` until they complete it again: it drops out of a `state=completed` feed without a tombstone, and comes back with a newer `updatedAt` once completed again.\n\n## Identifiers\n\nStore a tournament's `id`. Its `slug` follows the event's name and changes when the organiser renames it, so only the id is stable. `GET /tournaments/{idOrSlug}` accepts either (a slug in any case); the routes under it (`/roster`, `/rounds`, `/standings`, `/links`) take the id only. `webUrl` links the public page by id, so it keeps working after a rename.\n\n## Conditional requests\n\nEvery response carries `Cache-Control: private, no-cache`: a shared cache must not store it, and your own cache may keep it only to revalidate. Every `200` carries an `ETag`, a weak tag such as `W/\"3f9a…\"`. Send it back as `If-None-Match` as you received it, and an unchanged resource answers `304` with no body. Tags compare weakly (RFC 9110), so the same tag without its `W/` matches too; the quotes are part of the tag. For a completed tournament that has its results snapshot, the `304` from its detail, roster, rounds, standings and links routes is answered without reading the resource and does not count against your rate limit, so re-checking finished events is cheap. Every other `304` counts: one for an event that is not completed or has no snapshot, any from `GET /tournaments`, and one answered to `If-None-Match: *`. A key already over its limit gets `429` before any `304`.\n\n## Rate limits\n\nPer key, per minute: 30 unless your key carries its own limit (`GET /me` shows the limit that applies to your key). Every response to a key carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`; a `429` carries `Retry-After`.\n\n## What is visible\n\nExactly what the public tournament page shows. Published, public tournaments run on GGS Play; demos, templates and listings copied from other platforms' calendars never appear, and a private tournament answers `404` as if it did not exist. Roster entries appear once active, dropped or withdrawn; pending and rejected registrations never do. Rounds appear once their pairings are published. Army lists and factions follow the event's own list visibility (`lists.visibility` on the tournament). Standings come only from the results snapshot the organiser's completion writes; before that they are `null`.\n\n## Deduplicating against BCP and other platforms\n\nEvery tournament here is an event on GGS Play; `resultsAuthority` says whose record is official when it exists elsewhere too. `links` lists the same thing on other platforms, each with its `relations` to this record and a `confidence`. A tournament's links name the event; a roster entry's name the entry in that event (`kind: entry`) and the player's account on that platform (`kind: player`, the same id in every event they play). `GET /tournaments?platform=bcp&externalId=…` answers \"do you have these BCP events?\" for up to 50 ids in one call.\n\n## Players and names\n\nA roster entry carries `displayName` (what the tournament page shows), `firstName` and `lastName` as the player typed them (null on older entries), and `fullName`, always set: use it to show a player, never to match one. Match on `playerId` or on the entry's `links`. `playerId` is the GGS account behind the entry, when one has claimed it: the same id in every event they play.\n\nThe roster is the only place a name appears. Pairings and standings name entries by `rosterId` alone: join them to the roster. `GET /tournaments/{id}?include=roster,standings` (or `roster,rounds`) returns both in one call. An entry that leaves the public roster keeps its place in the standings and its games in the rounds, and joins nothing.\n\n## Rounds and results\n\nA round lists its pairings; a pairing has two `sides` (one on a bye), each with its `rosterId`, its `score` and its `result` (`win`, `draw`, `loss`, or null until the game is completed). The result is the one the organiser's rules decided and is authoritative: equal scores can be a win and unequal ones a draw, so never derive a result from the scores. A bye (`isBye`) has one side and no opponent. Its result and score count toward the standings as the event's rules say: an automatic bye is a win with the event's default bye score, a bye the organiser scored by hand counts what they entered instead, and a bye the event does not count as a game has no score and no result. Because nobody played it, leave a bye out of game-level and faction statistics. When the event scores games on a points table (20-0 battle points, say), each side also carries its `battlePoints`: the points that game earned it, exactly as the standings count them (a bye at least the smallest win), and null when the event ranks without one. Team and doubles events add `matchups`, each with its teams' game wins, battle points and results, decided by the event's own matchup rules. While the event runs they are computed from its games and settings. Once it is completed, every game's battle points and every matchup are the ones frozen with its standings, by the same computation, so the rounds and the standings always agree and a later settings or roster change cannot rewrite them.\n\n`include=breakdown` adds each side's entered score components; `include=setup` adds the round's name, times, scenario setup, the per-board scenarios of a team event, and table numbers. On `GET /tournaments/{idOrSlug}` they take effect only alongside `rounds` (`include=rounds,breakdown`). Ask for them only when you need them.\n\n## Standings\n\n`players.columns` describes the table: each column's `key`, `label`, `format` and whether the table is `rank`ed on it, in display order. Each row carries its `record` (played, won, drawn, lost) and its `figures`, one `{ key, value }` per column. A spare who never played a game has no row, nor does a player who dropped before playing; a row with `record.played` of 0 is an entry that never played but stayed on the roster (a no-show the organiser did not drop). A `record` or `pair` column joins several figures: its `parts` lists their keys, in order. Today only the team table (`teams`) has such columns; the player table shows every figure in a column of its own. Columns differ from event to event with the organiser's scoring: render the columns you are given rather than expecting a fixed set. On a doubles event each row is a team, identified by its captain's `rosterId` (null only when the team has no roster entry left) and named in `team`; on a team event `teams` holds the team table the same way.\n\n## Keeping copies current\n\nWhen data you copied changes or disappears here (a name corrected, an entry removed), re-sync and apply the change to your copy. Keeping copies current is a condition of using this API.",
    "contact": {
      "name": "Good Games Club",
      "url": "https://ggs.club"
    }
  },
  "servers": [
    {
      "url": "https://data.ggs.club/play/v1"
    }
  ],
  "tags": [
    {
      "name": "Play",
      "description": "Tournaments run on GGS Play and everything that hangs off one."
    },
    {
      "name": "Account",
      "description": "The key you are calling with."
    },
    {
      "name": "Meta",
      "description": "This document."
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "operationId": "me",
        "summary": "The calling key",
        "description": "The first call to make: proves the key works end to end and shows its scopes and rate limit.",
        "tags": [
          "Account"
        ],
        "security": [
          {
            "apiKey": [
              "play:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "The calling key",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "requestId"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Me"
                    },
                    "requestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified: the `If-None-Match` you sent still matches. No body.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidInput"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/tournaments": {
      "get": {
        "operationId": "listTournaments",
        "summary": "List tournaments",
        "description": "Lean summaries for discovery, deduplication and incremental sync, newest first by default. Every summary carries the event's tournament-level `links`, so a batch can be deduplicated against another platform without a detail call; `platform` + `externalId` turns the list into a reverse lookup. To sync, poll with `updatedSince=<a few minutes (5 is plenty) before the last updatedAt you saw>` and follow `page.next` until it is null: a change that commits slowly can carry an `updatedAt` just before your last poll, and the margin brings it in (the items it repeats are safe to apply again). With `updatedSince` the list is in `updatedAt` order (`sort` may only reverse it), and the page also carries a `TournamentTombstone` (`deleted: true`) for each tournament that stopped being visible since then, whatever the other filters.",
        "tags": [
          "Play"
        ],
        "security": [
          {
            "apiKey": [
              "play:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "gameSystem",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/GameSystem"
            }
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/TournamentFormat"
            }
          },
          {
            "name": "state",
            "in": "query",
            "description": "Repeat the key for several: `state=completed&state=in_progress`.",
            "schema": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/TournamentState"
                },
                {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/TournamentState"
                  }
                }
              ]
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Start date on or after (inclusive), `YYYY-MM-DD`, in the event's own timezone.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Start date on or before (inclusive), `YYYY-MM-DD`, in the event's own timezone.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            }
          },
          {
            "name": "country",
            "in": "query",
            "description": "ISO 3166-1 alpha-2, any case.",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 2
            }
          },
          {
            "name": "organiser",
            "in": "query",
            "description": "A GGS handle, case-insensitive; matches any of the tournament's organisers.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 64
            }
          },
          {
            "name": "minRounds",
            "in": "query",
            "description": "At least this many rounds (see `roundCount`).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000
            }
          },
          {
            "name": "maxRounds",
            "in": "query",
            "description": "At most this many rounds (see `roundCount`).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000
            }
          },
          {
            "name": "hasResults",
            "in": "query",
            "description": "`true`: only tournaments with a results snapshot; `false`: only those without.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "description": "Tournaments that changed after this instant, at the millisecond resolution `updatedAt` renders: pass a few minutes (5 is plenty) before the last `updatedAt` you saw. A change is stamped when its transaction starts, so a slow one can land just behind your last poll; the margin brings it in, and the items it repeats are safe to apply again. Sorts by `updatedAt` unless `sort` says `-updatedAt`; any other `sort` is `invalid_input`. The page also carries a `TournamentTombstone` for every tournament that stopped being visible since then (deleted, made private, unpublished, or turned into a demo, template or listing), whatever the other filters say; tombstones are kept for 30 days.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "platform",
            "in": "query",
            "description": "Reverse lookup: the platform `externalId` belongs to. Needs `externalId`.",
            "schema": {
              "$ref": "#/components/schemas/Platform"
            }
          },
          {
            "name": "externalId",
            "in": "query",
            "description": "Reverse lookup: tournaments linked to these ids on `platform`. Repeat the key, up to 50 per request; an unknown id matches nothing.",
            "schema": {
              "anyOf": [
                {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 128
                },
                {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 128
                  }
                }
              ]
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Newest first (`-startDate`) by default. With `updatedSince`, `updatedAt` by default and only `updatedAt` or `-updatedAt` accepted, so the last item of a page is the newest change and tombstones sit in order.",
            "schema": {
              "type": "string",
              "enum": [
                "startDate",
                "-startDate",
                "updatedAt",
                "-updatedAt"
              ]
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque, and carried by `page.next`: follow that URL rather than setting it yourself.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 512
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size.",
            "schema": {
              "default": 50,
              "type": "integer",
              "minimum": 1,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List tournaments",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "page",
                    "requestId"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "anyOf": [
                          {
                            "$ref": "#/components/schemas/TournamentSummary"
                          },
                          {
                            "$ref": "#/components/schemas/TournamentTombstone"
                          }
                        ]
                      }
                    },
                    "page": {
                      "$ref": "#/components/schemas/Page"
                    },
                    "requestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified: the `If-None-Match` you sent still matches. No body.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidInput"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/tournaments/{idOrSlug}": {
      "get": {
        "operationId": "getTournament",
        "summary": "Get a tournament",
        "description": "The summary plus the descriptive fields (description, full location, images, list visibility), and any extras named in `include`. A large event reads better collection by collection, from the `/roster`, `/rounds` and `/standings` routes under the tournament's id.",
        "tags": [
          "Play"
        ],
        "security": [
          {
            "apiKey": [
              "play:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "idOrSlug",
            "in": "path",
            "description": "The tournament's id, or its current URL slug in any case (a slug changes when the event is renamed; store the id).",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 256
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Extras to embed, comma-separated (`include=roster,standings`) or with the key repeated (`include=roster&include=standings`): `roster`, `rounds`, `standings`, `links`, and `resultsCard` (the results share image, `images.resultsCard`). With `rounds`, `breakdown` and `setup` add what they add on the rounds route (ignored without it). `roster,standings` gives the standings and the names they join to in one call. For a large event, read the collections from their own routes instead.",
            "style": "form",
            "explode": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "roster",
                  "rounds",
                  "standings",
                  "links",
                  "resultsCard",
                  "breakdown",
                  "setup"
                ]
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Get a tournament",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "requestId"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/TournamentDetail"
                    },
                    "requestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified: the `If-None-Match` you sent still matches. No body.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidInput"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/tournaments/{id}/roster": {
      "get": {
        "operationId": "getTournamentRoster",
        "summary": "Roster",
        "description": "Active, dropped and withdrawn entries, in the order they joined. Factions and army lists appear once the event has revealed them, as on the public page (`lists.visibility` on the tournament says which).",
        "tags": [
          "Play"
        ],
        "security": [
          {
            "apiKey": [
              "play:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The tournament's id.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Roster",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "requestId"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RosterEntry"
                      }
                    },
                    "requestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified: the `If-None-Match` you sent still matches. No body.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidInput"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/tournaments/{id}/rounds": {
      "get": {
        "operationId": "getTournamentRounds",
        "summary": "Rounds, pairings and games",
        "description": "Every published round with its pairings: each pairing's sides, their scores and their results (`result` is authoritative; never compare the scores). Team and doubles events add `matchups`, each with its sides' game wins and results, and each pairing then carries `matchupId` and `slot`; a doubles matchup is one game on its one pairing. `include=breakdown` adds each side's entered score components; `include=setup` the round's name, times, scenario setup, per-board scenarios on team events, and table numbers.",
        "tags": [
          "Play"
        ],
        "security": [
          {
            "apiKey": [
              "play:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The tournament's id.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Extras, comma-separated (`include=breakdown,setup`) or with the key repeated: `breakdown` adds each side's entered score components, `setup` the round's name, times, scenario setup, boards and table numbers.",
            "style": "form",
            "explode": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "breakdown",
                  "setup"
                ]
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rounds, pairings and games",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "requestId"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Round"
                      }
                    },
                    "requestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified: the `If-None-Match` you sent still matches. No body.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidInput"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/tournaments/{id}/links": {
      "get": {
        "operationId": "getTournamentLinks",
        "summary": "Links",
        "description": "The same event's records on other platforms, including the GGS stats site. The same list the summary carries.",
        "tags": [
          "Play"
        ],
        "security": [
          {
            "apiKey": [
              "play:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The tournament's id.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Links",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "requestId"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Link"
                      }
                    },
                    "requestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified: the `If-None-Match` you sent still matches. No body.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidInput"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/tournaments/{id}/standings": {
      "get": {
        "operationId": "getTournamentStandings",
        "summary": "Standings",
        "description": "The final standings as frozen at completion: each row's record and its figures keyed by the columns that describe the table; `data` is null while the event has no results snapshot. Rows carry no names: join to the roster by `rosterId` (`include=roster,standings` on the tournament gives both in one call). Dropped players keep their place, withdrawn entries never rank. On doubles each row is a team (`team`), identified by its captain's roster entry, as in the pairings (null only when the team has no entry left).",
        "tags": [
          "Play"
        ],
        "security": [
          {
            "apiKey": [
              "play:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The tournament's id.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Standings",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data",
                    "requestId"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "data": {
                      "anyOf": [
                        {
                          "$ref": "#/components/schemas/Standings"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "requestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "description": "Not modified: the `If-None-Match` you sent still matches. No body.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidInput"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "openApiDocument",
        "summary": "This document",
        "description": "The OpenAPI 3.1 document for the routes this base URL serves, generated from the same schemas the routes render through. No key needed.",
        "tags": [
          "Meta"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ggs_live_…",
        "description": "An API key from your GGS account settings (API keys). Send it as `Authorization: Bearer ggs_live_…`. Each key carries its own scopes."
      }
    },
    "schemas": {
      "ArmyList": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string"
          }
        },
        "required": [
          "text"
        ],
        "description": "The list as submitted, as text."
      },
      "ErrorEnvelope": {
        "type": "object",
        "description": "Every failure: a machine-readable `code`, a human-readable `message`, and `details` where the code has any.",
        "required": [
          "ok",
          "error",
          "requestId"
        ],
        "properties": {
          "ok": {
            "const": false
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "details"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "unauthenticated",
                  "insufficient_scope",
                  "not_found",
                  "invalid_input",
                  "method_not_allowed",
                  "rate_limited",
                  "unavailable",
                  "internal"
                ]
              },
              "message": {
                "type": "string"
              },
              "details": {
                "description": "Per code: the problems (`InputIssue`) for `invalid_input`, `{ required }` for `insufficient_scope`, `{ limit, window }` for `rate_limited`, `{ allow }` for `method_not_allowed` (also the `Allow` header), otherwise null.",
                "anyOf": [
                  {
                    "type": "null"
                  },
                  {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/InputIssue"
                    }
                  },
                  {
                    "type": "object"
                  }
                ]
              }
            }
          },
          "requestId": {
            "type": "string"
          }
        }
      },
      "Faction": {
        "type": "object",
        "properties": {
          "id": {
            "description": "The canonical faction slug for the event's game system; null for a name the catalogue does not carry.",
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": "string",
            "description": "The faction as the player entered it."
          },
          "kind": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/FactionKind"
              },
              {
                "type": "null"
              }
            ],
            "description": "Null when `id` is (a name the catalogue does not carry)."
          },
          "parent": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/FactionParent"
              },
              {
                "type": "null"
              }
            ],
            "description": "The main army this one is a variant of: an Age of Sigmar Army of Renown's battletome, a The Old World Army of Infamy's army book. Null for a main army, a legacy army, a Warhammer 40,000 faction, and a name the catalogue does not carry."
          }
        },
        "required": [
          "id",
          "name",
          "kind",
          "parent"
        ],
        "description": "The faction as entered, with its canonical id, kind and parent where the name matches the GGS playable-faction catalogue."
      },
      "FactionKind": {
        "type": "string",
        "enum": [
          "faction",
          "army_of_renown",
          "army_of_infamy",
          "legacy"
        ],
        "description": "What kind of army the faction is: `faction` a main army (a battletome, codex or army book); `army_of_renown` an Age of Sigmar variant of its `parent`; `army_of_infamy` a The Old World Arcane Journal list; `legacy` a The Old World Legacy army."
      },
      "FactionParent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The parent's canonical faction slug."
          },
          "name": {
            "type": "string",
            "description": "The parent's catalogue name."
          }
        },
        "required": [
          "id",
          "name"
        ],
        "description": "The main army a variant list belongs to."
      },
      "GameMode": {
        "type": "string",
        "enum": [
          "matched_play",
          "battle_march",
          "spearhead",
          "combat_patrol",
          "standard"
        ],
        "description": "The game played, from the GGS game-mode catalogue: `matched_play` (the default everywhere it exists), `battle_march` (The Old World), `spearhead` (Age of Sigmar), `combat_patrol` (Warhammer 40,000), `standard` (Kill Team, its only mode)."
      },
      "GameSystem": {
        "type": "string",
        "enum": [
          "age_of_sigmar",
          "warhammer_40k",
          "the_old_world",
          "horus_heresy",
          "kill_team"
        ]
      },
      "InputIssue": {
        "type": "object",
        "description": "One problem with a request's path or query string.",
        "required": [
          "path",
          "message"
        ],
        "properties": {
          "path": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "integer"
                }
              ]
            },
            "description": "Where: the parameter's name, then the position within it for a list (`[\"include\", 1]`)."
          },
          "message": {
            "type": "string",
            "description": "What is wrong, for a person to read."
          }
        }
      },
      "Link": {
        "type": "object",
        "properties": {
          "platform": {
            "$ref": "#/components/schemas/Platform"
          },
          "kind": {
            "$ref": "#/components/schemas/LinkKind"
          },
          "externalId": {
            "type": "string",
            "description": "The other platform's own identifier."
          },
          "url": {
            "anyOf": [
              {
                "type": "string",
                "format": "uri"
              },
              {
                "type": "null"
              }
            ],
            "description": "The record's page on the other platform where we know it. A BCP event's is its id URL, or null when GGS holds only another BCP page for it. Null for entry and player links."
          },
          "relations": {
            "minItems": 1,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LinkRelation"
            }
          },
          "confidence": {
            "$ref": "#/components/schemas/LinkConfidence"
          }
        },
        "required": [
          "platform",
          "kind",
          "externalId",
          "url",
          "relations",
          "confidence"
        ],
        "description": "The same thing on another platform. Match on `platform` + `kind` + `externalId`."
      },
      "LinkConfidence": {
        "type": "string",
        "enum": [
          "verified",
          "platform",
          "claimed",
          "heuristic"
        ],
        "description": "How the link was established: `verified` proven (by email or an invite), `platform` the id came from the other platform's own data, `claimed` asserted by the player, `heuristic` matched by name; treat that one with care."
      },
      "LinkKind": {
        "type": "string",
        "enum": [
          "tournament",
          "entry",
          "player"
        ],
        "description": "What the link identifies: `tournament` the event, `entry` one roster entry in it, `player` the person behind an entry (their account on the other platform, the same across events)."
      },
      "LinkRelation": {
        "type": "string",
        "enum": [
          "listed_on",
          "imported_from",
          "mirrored_to",
          "results_published",
          "same_entry",
          "same_person"
        ],
        "description": "How the linked record relates to this one. For a `bcp` tournament link it follows `resultsAuthority`: `mirrored_to` when GGS holds the official record and the BCP one is the copy, `imported_from` the other way round; a link carries one of the two, never both. `listed_on` is a listing on a platform that ran nothing here; `results_published` is the GGS stats site's record. `same_entry` is this entry on the other platform; `same_person` is the other platform's account of the player behind it."
      },
      "ListVisibility": {
        "type": "string",
        "enum": [
          "disabled",
          "hidden",
          "scheduled",
          "visible"
        ],
        "description": "Why army lists and factions may be absent. `disabled`: the event never collected lists; `hidden` and `scheduled`: not revealed yet; `visible`: shown, as on the public page."
      },
      "LocationDetail": {
        "type": "object",
        "properties": {
          "city": {
            "description": "From the event's own location; null when it has none.",
            "type": [
              "string",
              "null"
            ]
          },
          "countryCode": {
            "description": "ISO 3166-1 alpha-2. From the event's own location; for an event without one, the country of the organisation's venues when they are all in one country, else the country most of the organisation's located events are in (`countryInferred` says so). Null when none of these is known. The `country` filter matches this value.",
            "type": [
              "string",
              "null"
            ]
          },
          "countryInferred": {
            "type": "boolean",
            "description": "True when `countryCode` comes from the organisation rather than the event's own location; false when it is the event's own or null."
          },
          "venueName": {
            "type": [
              "string",
              "null"
            ]
          },
          "formattedAddress": {
            "type": [
              "string",
              "null"
            ]
          },
          "postcode": {
            "type": [
              "string",
              "null"
            ]
          },
          "lat": {
            "type": [
              "number",
              "null"
            ]
          },
          "lng": {
            "type": [
              "number",
              "null"
            ]
          }
        },
        "required": [
          "city",
          "countryCode",
          "countryInferred",
          "venueName",
          "formattedAddress",
          "postcode",
          "lat",
          "lng"
        ]
      },
      "LocationSummary": {
        "type": "object",
        "properties": {
          "city": {
            "description": "From the event's own location; null when it has none.",
            "type": [
              "string",
              "null"
            ]
          },
          "countryCode": {
            "description": "ISO 3166-1 alpha-2. From the event's own location; for an event without one, the country of the organisation's venues when they are all in one country, else the country most of the organisation's located events are in (`countryInferred` says so). Null when none of these is known. The `country` filter matches this value.",
            "type": [
              "string",
              "null"
            ]
          },
          "countryInferred": {
            "type": "boolean",
            "description": "True when `countryCode` comes from the organisation rather than the event's own location; false when it is the event's own or null."
          }
        },
        "required": [
          "city",
          "countryCode",
          "countryInferred"
        ]
      },
      "Matchup": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "isBye": {
            "type": "boolean"
          },
          "sides": {
            "minItems": 1,
            "maxItems": 2,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MatchupSide"
            },
            "description": "Two sides, or one on a bye."
          },
          "tableLabel": {
            "description": "Only with `include=setup`: the organiser's name for the table block (\"Tables 1-4\"); null for byes, when tables are not numbered, and on doubles.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "isBye",
          "sides"
        ],
        "description": "Two teams meeting in a round; their games are the pairings that carry its `matchupId`."
      },
      "MatchupSide": {
        "type": "object",
        "properties": {
          "teamId": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "description": "Null once the organiser removed the team after the draw."
          },
          "gameWins": {
            "type": "integer",
            "description": "Games this team won in the matchup, including wins awarded for boards the other team could not fill."
          },
          "result": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/SideResult"
              },
              {
                "type": "null"
              }
            ],
            "description": "The matchup result as the event's rules decide it (provisional while the round is active). A bye is a win once its round has started. Null while nothing is decided."
          },
          "battlePoints": {
            "description": "The team's battle points in the matchup, as the team standings count them: its games' points under the event's differential table, plus the table maximum for each board the other team could not fill (and for every board of a bye). On doubles, the matchup's one game's points. Null while nothing is decided, and when the event has no such table (a points table that counts results only, 3 for a win say, has points on each game and none on the matchup).",
            "type": [
              "number",
              "null"
            ]
          }
        },
        "required": [
          "teamId",
          "gameWins",
          "result",
          "battlePoints"
        ],
        "description": "One team's side of a matchup. On a completed event the matchup is the one frozen with the standings, so the two always agree."
      },
      "Me": {
        "type": "object",
        "properties": {
          "user": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "handle": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "displayName": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "id",
              "handle",
              "displayName"
            ],
            "description": "The GGS account that owns the key."
          },
          "key": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string",
                "description": "The name you gave the key."
              },
              "prefix": {
                "type": "string",
                "description": "The key's visible prefix, as the keys page lists it."
              },
              "scopes": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "rateLimitPerMinute": {
                "type": "integer",
                "description": "The requests per minute this key may make: its own limit, or the default when it has none."
              },
              "createdAt": {
                "type": "string",
                "format": "date-time"
              },
              "expiresAt": {
                "anyOf": [
                  {
                    "type": "string",
                    "format": "date-time"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            },
            "required": [
              "id",
              "name",
              "prefix",
              "scopes",
              "rateLimitPerMinute",
              "createdAt",
              "expiresAt"
            ],
            "description": "The key this request was made with."
          }
        },
        "required": [
          "user",
          "key"
        ]
      },
      "Organiser": {
        "type": "object",
        "properties": {
          "handle": {
            "type": "string"
          },
          "displayName": {
            "type": "string"
          }
        },
        "required": [
          "handle",
          "displayName"
        ]
      },
      "Page": {
        "type": "object",
        "properties": {
          "next": {
            "anyOf": [
              {
                "type": "string",
                "format": "uri"
              },
              {
                "type": "null"
              }
            ],
            "description": "The next page as an absolute URL, with every filter of this request and the cursor; null on the last page."
          }
        },
        "required": [
          "next"
        ],
        "description": "Beside `data` on a list: follow `next` until it is null."
      },
      "Pairing": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "isBye": {
            "type": "boolean",
            "description": "The one side sat out this round. A bye has one side, and its result and score count toward the standings as the event's rules say. It has no opponent, so leave it out of game-level and faction statistics."
          },
          "matchupId": {
            "description": "Team and doubles events only: the matchup this game belongs to.",
            "type": "string",
            "format": "uuid"
          },
          "slot": {
            "description": "Team and doubles events only: the 1-based board within the matchup. Always 1 on doubles, where the matchup is one game between the two captains' entries.",
            "type": "integer",
            "minimum": 1
          },
          "status": {
            "$ref": "#/components/schemas/PairingStatus"
          },
          "table": {
            "description": "Only with `include=setup`: the table number; null for byes or when tables are not numbered.",
            "type": [
              "integer",
              "null"
            ]
          },
          "sides": {
            "minItems": 1,
            "maxItems": 2,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PairingSide"
            },
            "description": "Two sides, or one on a bye. The order is the pairing's own and carries no meaning."
          }
        },
        "required": [
          "id",
          "isBye",
          "status",
          "sides"
        ],
        "description": "One game of a round (or one bye). On a team event each slot of a matchup is its own pairing."
      },
      "PairingSide": {
        "type": "object",
        "properties": {
          "rosterId": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "description": "The roster entry. Null when the organiser removed the entry after pairing; the result stands as recorded."
          },
          "score": {
            "description": "The primary score in the system's own unit (victory points). Null while nothing is entered. On a bye, the score the standings count for it (an automatic bye's minimal win, or what the organiser entered), and null when the bye counts as no game.",
            "type": [
              "number",
              "null"
            ]
          },
          "result": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/SideResult"
              },
              {
                "type": "null"
              }
            ],
            "description": "Null until the game is completed, and on a completed game with no recorded winner. A bye's result is the one the standings count (a win once its game is completed), and null when the bye counts as no game."
          },
          "battlePoints": {
            "description": "The points this game earned the side under the event's points table, exactly as the standings count them (the standings column that table feeds, e.g. 16 for a 2766 to 1076 win on a 20-0 table whose band reads 16-4). A bye counts at least the smallest win. On a completed event this is the figure frozen with the standings. On a team event whose points table counts margins, a matchup's games plus its forfeited boards add up to the matchup's `battlePoints`; one that counts results only (3 for a win, say) has points on its games and none on its matchups. Null until the game is completed, and when the event ranks without a points table.",
            "type": [
              "number",
              "null"
            ]
          },
          "breakdown": {
            "description": "Only with `include=breakdown`. Null until the game is completed, and when the side entered no breakdown. A bye carries the breakdown the standings count for it.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/ScoreBreakdown"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "rosterId",
          "score",
          "result",
          "battlePoints"
        ],
        "description": "One side of a pairing: who played, what they scored, how it ended for them."
      },
      "PairingStatus": {
        "type": "string",
        "enum": [
          "pending",
          "in_progress",
          "completed",
          "cancelled"
        ],
        "description": "The game's progress. A bye the organiser does not count as a game is `pending` until its round starts, then `completed`."
      },
      "Platform": {
        "type": "string",
        "enum": [
          "ggs",
          "ggs_stats",
          "bcp",
          "milarki",
          "champions_hub",
          "tabletop_herald",
          "ecksen",
          "dankhold",
          "miniheadquarters",
          "longshanks",
          "other"
        ],
        "description": "One enum for every cross-platform reference, GGS included. `ggs_stats` is the GGS stats site."
      },
      "ResultsAuthority": {
        "type": "string",
        "enum": [
          "ggs",
          "external"
        ],
        "description": "Which record of this event is official when it also exists elsewhere: `ggs` when GGS Play ran it, `external` when the linked platform did and GGS mirrors it. Trust the record on the authority platform; a follower's standings are a copy."
      },
      "RosterEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The roster entry: this player in this event. Pairings and standings refer to it as `rosterId`."
          },
          "displayName": {
            "type": "string",
            "description": "The name the player chose to be shown by on this entry (a handle or nickname is fine)."
          },
          "firstName": {
            "description": "The player's first name as typed, never split from another name. Null on older entries recorded before names were collected.",
            "type": [
              "string",
              "null"
            ]
          },
          "lastName": {
            "description": "As `firstName`.",
            "type": [
              "string",
              "null"
            ]
          },
          "fullName": {
            "type": "string",
            "description": "\"First Last\" when both are known, otherwise the display name. Always set: use this to show a player, never to match one (match on `playerId` or `links`)."
          },
          "playerId": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "description": "The GGS account behind the entry, stable across events: the same id on every entry the player claims. Null for an entry no account has claimed."
          },
          "faction": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Faction"
              },
              {
                "type": "null"
              }
            ],
            "description": "Null while the event has not revealed lists (see `lists.visibility`) or when none was entered."
          },
          "status": {
            "$ref": "#/components/schemas/RosterStatus"
          },
          "team": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/TeamRef"
              },
              {
                "type": "null"
              }
            ],
            "description": "Team and doubles events only."
          },
          "armyList": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ArmyList"
              },
              {
                "type": "null"
              }
            ],
            "description": "Null while the event has not revealed lists, or when the player submitted no list text."
          },
          "links": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Link"
            },
            "description": "The entry on other platforms (`kind: entry`) and the player's account there (`kind: player`, the same id across every event they play)."
          }
        },
        "required": [
          "id",
          "displayName",
          "firstName",
          "lastName",
          "fullName",
          "playerId",
          "faction",
          "status",
          "team",
          "armyList",
          "links"
        ]
      },
      "RosterStatus": {
        "type": "string",
        "enum": [
          "active",
          "dropped",
          "withdrawn"
        ],
        "description": "Pending and rejected registrations never appear."
      },
      "Round": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "roundNumber": {
            "type": "integer",
            "minimum": 1
          },
          "status": {
            "$ref": "#/components/schemas/RoundStatus"
          },
          "battleplan": {
            "description": "Scenario or battleplan name, e.g. `Close Quarters`; null when the organiser set none (team events: see `setup.boards`).",
            "type": [
              "string",
              "null"
            ]
          },
          "matchups": {
            "description": "Team and doubles events only. A doubles matchup is one game, carried by its single pairing (captain against captain).",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Matchup"
            }
          },
          "pairings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Pairing"
            }
          },
          "setup": {
            "$ref": "#/components/schemas/RoundSetup"
          }
        },
        "required": [
          "id",
          "roundNumber",
          "status",
          "battleplan",
          "pairings"
        ]
      },
      "RoundBoard": {
        "type": "object",
        "properties": {
          "slot": {
            "type": "integer",
            "minimum": 1,
            "description": "1-based: the `slot` of the pairings played on this board."
          },
          "battleplan": {
            "type": [
              "string",
              "null"
            ]
          },
          "battleplanId": {
            "type": [
              "string",
              "null"
            ]
          },
          "secondaries": {
            "anyOf": [
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "layout": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "slot",
          "battleplan",
          "battleplanId",
          "secondaries",
          "layout"
        ],
        "description": "A team event's setup is per board: each matchup's boards play the slot's scenario."
      },
      "RoundSetup": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The organiser's name for the round, or `Round N` as the page labels an unnamed one."
          },
          "startsAt": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          },
          "endsAt": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          },
          "battleplanId": {
            "type": [
              "string",
              "null"
            ]
          },
          "secondaries": {
            "anyOf": [
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "layout": {
            "type": [
              "string",
              "null"
            ]
          },
          "boards": {
            "anyOf": [
              {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/RoundBoard"
                }
              },
              {
                "type": "null"
              }
            ],
            "description": "Team events only: the scenario per board; null for singles and doubles."
          }
        },
        "required": [
          "name",
          "startsAt",
          "endsAt",
          "battleplanId",
          "secondaries",
          "layout",
          "boards"
        ],
        "description": "Only with `include=setup`. The scenario setup is ids from the GGS game data: the Old World carries the scenario id and its secondary objectives, Warhammer 40,000 its deployment layout. Only what the public round page shows."
      },
      "RoundStatus": {
        "type": "string",
        "enum": [
          "upcoming",
          "active",
          "completed"
        ],
        "description": "A round appears once its pairings are published; `upcoming` is published and not yet started."
      },
      "ScoreBreakdown": {
        "type": "object",
        "properties": {
          "victoryPoints": {
            "type": [
              "number",
              "null"
            ]
          },
          "components": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScoreComponent"
            },
            "description": "The scorecard's components, sorted by id. Pairs rather than a map, so every key is named."
          }
        },
        "required": [
          "victoryPoints",
          "components"
        ],
        "description": "The scoring breakdown of one side as entered."
      },
      "ScoreComponent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "value": {
            "type": "number"
          }
        },
        "required": [
          "id",
          "value"
        ],
        "description": "One scoring component: its id in the event's scorecard and its value."
      },
      "SideResult": {
        "type": "string",
        "enum": [
          "win",
          "draw",
          "loss"
        ],
        "description": "The result as the organiser's rules decided it, authoritative over the scores: equal scores can be a win and unequal ones a draw (a margin rule, say). Read `result`, never compare the scores."
      },
      "Standings": {
        "type": "object",
        "properties": {
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "revision": {
            "type": "integer",
            "description": "Increments on every recomputation; the ETag follows it."
          },
          "scoringMethod": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  }
                },
                "required": [
                  "name"
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "The scoring preset the event ranked under, by its label (`Custom` when the organiser edited it), for display. Null when nothing was configured (the plain record) or the preset is one GGS no longer names."
          },
          "players": {
            "type": "object",
            "properties": {
              "columns": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/StandingsColumn"
                }
              },
              "rows": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/StandingsPlayerRow"
                }
              }
            },
            "required": [
              "columns",
              "rows"
            ]
          },
          "teams": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "columns": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/StandingsColumn"
                    }
                  },
                  "rows": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/StandingsTeamRow"
                    }
                  }
                },
                "required": [
                  "columns",
                  "rows"
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "Team events only; null for singles and doubles (a doubles table ranks teams in `players`, one row per team)."
          }
        },
        "required": [
          "computedAt",
          "revision",
          "scoringMethod",
          "players",
          "teams"
        ],
        "description": "The results snapshot of a completed event: the final placings and the figures behind them, with the column set that describes the table. Read `rows` in order; render with `columns`, reading each column's figure (or its `parts`) from the row's `figures`. Recomputed on every post-completion correction (`revision`, `computedAt`)."
      },
      "StandingsColumn": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "Stable and unique within the table: the key of the row's figure for this column."
          },
          "label": {
            "type": "string"
          },
          "description": {
            "description": "The rule behind the column where the label cannot carry it.",
            "type": "string"
          },
          "format": {
            "$ref": "#/components/schemas/StandingsColumnFormat"
          },
          "rank": {
            "type": "boolean",
            "description": "True for the columns the table is ranked on, listed in rank order; false for a reference figure."
          },
          "parts": {
            "description": "`record` and `pair` columns only (today only in the team table): the figure keys the column joins, in order. Such a column has no figure of its own.",
            "minItems": 2,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "key",
          "label",
          "format",
          "rank"
        ],
        "description": "One figure column of a standings table, as the snapshot stored it: the table describes itself, so a client renders it from the rows alone. The identity columns (placing, player or team) are not listed."
      },
      "StandingsColumnFormat": {
        "type": "string",
        "enum": [
          "number",
          "diff",
          "pct",
          "decimal",
          "record",
          "pair"
        ],
        "description": "How a figure prints: `number` as is; `diff` signed and rounded (`+12`); `pct` a 0..1 rate as a whole percentage (`67%`); `decimal` one decimal place; `record` the `parts` joined with hyphens (`3-1-0`); `pair` the two `parts` joined with an en dash (`40–20`). A null figure prints as a dash."
      },
      "StandingsFigure": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "A column `key`, or one of a composite column's `parts`."
          },
          "value": {
            "description": "Null when the event's scoring never produced it for this row (strength of schedule for a player who faced nobody, say).",
            "type": [
              "number",
              "null"
            ]
          }
        },
        "required": [
          "key",
          "value"
        ],
        "description": "One figure of a standings row. Pairs rather than a map, so every key is named."
      },
      "StandingsPlayerRow": {
        "type": "object",
        "properties": {
          "placing": {
            "type": "integer",
            "minimum": 1,
            "description": "1-based; the rows arrive in this order."
          },
          "rosterId": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "description": "The roster entry: join to the roster for the name, faction and links. On doubles, the team captain's entry, the id the pairings carry for the team (the team is the row, see `team`); null only when no entry of that team is left, never the team's own id."
          },
          "team": {
            "description": "Doubles events only (absent otherwise): the team this row ranks.",
            "$ref": "#/components/schemas/TeamRef"
          },
          "status": {
            "description": "Dropped players keep their place; withdrawn entries never rank.",
            "$ref": "#/components/schemas/RosterStatus"
          },
          "record": {
            "$ref": "#/components/schemas/StandingsRecord"
          },
          "figures": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StandingsFigure"
            },
            "description": "One per column key and per composite part, in column order."
          }
        },
        "required": [
          "placing",
          "rosterId",
          "status",
          "record",
          "figures"
        ],
        "description": "One player's frozen standing: the record, and every figure the columns show keyed by column. No names: join to the roster by `rosterId` (`include=roster` on the tournament gives both in one call). An entry no longer on the public roster keeps its place and its figures and joins nothing."
      },
      "StandingsRecord": {
        "type": "object",
        "properties": {
          "played": {
            "type": "integer"
          },
          "wins": {
            "type": "integer"
          },
          "draws": {
            "type": "integer"
          },
          "losses": {
            "type": "integer"
          }
        },
        "required": [
          "played",
          "wins",
          "draws",
          "losses"
        ],
        "description": "Games (or, in a team table, matchups) played, won, drawn and lost, whatever the event ranks on."
      },
      "StandingsTeamRow": {
        "type": "object",
        "properties": {
          "placing": {
            "type": "integer",
            "minimum": 1
          },
          "teamId": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "withdrawn"
            ],
            "description": "A withdrawn team ranks only if it played."
          },
          "record": {
            "description": "Matchups played, won, drawn and lost; whole-team byes count as won.",
            "$ref": "#/components/schemas/StandingsRecord"
          },
          "figures": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StandingsFigure"
            },
            "description": "One per column key and per composite part, in column order."
          }
        },
        "required": [
          "placing",
          "teamId",
          "name",
          "status",
          "record",
          "figures"
        ],
        "description": "One team's frozen standing on a team event."
      },
      "TeamRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "TournamentDetail": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string",
            "description": "The URL slug on play.ggs.club. `GET /tournaments/{idOrSlug}` accepts it in place of the `id`; the routes under it take the id only. It changes when the organiser renames the event, so store the `id`."
          },
          "name": {
            "type": "string"
          },
          "resultsAuthority": {
            "$ref": "#/components/schemas/ResultsAuthority"
          },
          "state": {
            "$ref": "#/components/schemas/TournamentState"
          },
          "gameSystem": {
            "$ref": "#/components/schemas/GameSystem"
          },
          "gameMode": {
            "description": "The game played. An event that never chose one plays its system's default, `matched_play` (`standard` on Kill Team).",
            "$ref": "#/components/schemas/GameMode"
          },
          "comp": {
            "description": "The Old World only: the composition pack in force, e.g. `square_based_comp`. The comp list the organiser applied, whatever scoring preset they chose; for an event configured before comp lists were stored, the pack its scoring preset implies. Null when none.",
            "type": [
              "string",
              "null"
            ]
          },
          "format": {
            "$ref": "#/components/schemas/TournamentFormat"
          },
          "pointsLimit": {
            "type": [
              "integer",
              "null"
            ]
          },
          "ruleset": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  }
                },
                "required": [
                  "id",
                  "name"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "startDate": {
            "description": "`YYYY-MM-DD` in the event's own timezone.",
            "type": [
              "string",
              "null"
            ]
          },
          "endDate": {
            "description": "`YYYY-MM-DD` in the event's own timezone.",
            "type": [
              "string",
              "null"
            ]
          },
          "timezone": {
            "description": "IANA name, e.g. `Europe/London`.",
            "type": [
              "string",
              "null"
            ]
          },
          "roundCount": {
            "type": "integer",
            "description": "Every round the organiser has created, including ones not yet published (so it can exceed the rounds the rounds route returns)."
          },
          "playerCount": {
            "type": "integer",
            "description": "Once the event has a results snapshot (`resultsFrozenAt` is set), the entrants it ranks, players who dropped part-way included and a spare who never played left out; on doubles, the teams. Before that, the active roster entries, spares included: a spare may never have played a game."
          },
          "location": {
            "$ref": "#/components/schemas/LocationDetail"
          },
          "organisers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Organiser"
            }
          },
          "resultsFrozenAt": {
            "description": "When the current results snapshot was computed: null until the organiser completes the event, and moved by every post-completion correction.",
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          },
          "links": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Link"
            },
            "description": "Tournament-level links only (`kind: tournament`)."
          },
          "webUrl": {
            "type": "string",
            "format": "uri",
            "description": "The public page on play.ggs.club, by id: it keeps working when the event is renamed."
          },
          "updatedAt": {
            "description": "The latest of the record's own update, `resultsFrozenAt`, and, once the event is completed, any change to its roster, teams, rounds, pairings, games or `links` (its records on other platforms and its stats publication). Joins and pairings before completion do not move it; poll those collections with their ETag. A change outside the event (an organiser's or player's GGS profile, the venue) does not move it either.",
            "type": "string",
            "format": "date-time"
          },
          "description": {
            "description": "The organiser's event content as plain text (all sections, in order).",
            "type": [
              "string",
              "null"
            ]
          },
          "images": {
            "type": "object",
            "properties": {
              "banner": {
                "anyOf": [
                  {
                    "type": "string",
                    "format": "uri"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "The wide (3:1) header image of the public page."
              },
              "thumbnail": {
                "anyOf": [
                  {
                    "type": "string",
                    "format": "uri"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "The small image event lists show. Uploaded separately from the banner, so either may be set alone."
              },
              "resultsCard": {
                "description": "Only with `include=resultsCard`: the 1200x630 results share image made when the organiser completes the event; null until then, or when it was not made.",
                "anyOf": [
                  {
                    "type": "string",
                    "format": "uri"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            },
            "required": [
              "banner",
              "thumbnail"
            ],
            "description": "The organiser's images; null where none was uploaded."
          },
          "lists": {
            "type": "object",
            "properties": {
              "visibility": {
                "$ref": "#/components/schemas/ListVisibility"
              }
            },
            "required": [
              "visibility"
            ],
            "description": "Explains why `faction` and `armyList` may be absent on this event."
          },
          "roster": {
            "description": "Only with `include=roster`.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RosterEntry"
            }
          },
          "rounds": {
            "description": "Only with `include=rounds` (plus `breakdown` and `setup` when included).",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Round"
            }
          },
          "standings": {
            "description": "Only with `include=standings`: the results snapshot, or null while the event has none (in progress, or the moment after completion before it is frozen).",
            "anyOf": [
              {
                "$ref": "#/components/schemas/Standings"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "id",
          "slug",
          "name",
          "resultsAuthority",
          "state",
          "gameSystem",
          "gameMode",
          "comp",
          "format",
          "pointsLimit",
          "ruleset",
          "startDate",
          "endDate",
          "timezone",
          "roundCount",
          "playerCount",
          "location",
          "organisers",
          "resultsFrozenAt",
          "links",
          "webUrl",
          "updatedAt",
          "description",
          "images",
          "lists"
        ]
      },
      "TournamentFormat": {
        "type": "string",
        "enum": [
          "singles",
          "teams",
          "doubles"
        ],
        "description": "How players enter and are paired. Spearhead is a game mode (`gameMode`), never a format."
      },
      "TournamentState": {
        "type": "string",
        "enum": [
          "upcoming",
          "registration_open",
          "registration_closed",
          "in_progress",
          "completed",
          "cancelled"
        ],
        "description": "`upcoming` covers a published event before registration opens, or one with no dates yet."
      },
      "TournamentSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string",
            "description": "The URL slug on play.ggs.club. `GET /tournaments/{idOrSlug}` accepts it in place of the `id`; the routes under it take the id only. It changes when the organiser renames the event, so store the `id`."
          },
          "name": {
            "type": "string"
          },
          "resultsAuthority": {
            "$ref": "#/components/schemas/ResultsAuthority"
          },
          "state": {
            "$ref": "#/components/schemas/TournamentState"
          },
          "gameSystem": {
            "$ref": "#/components/schemas/GameSystem"
          },
          "gameMode": {
            "description": "The game played. An event that never chose one plays its system's default, `matched_play` (`standard` on Kill Team).",
            "$ref": "#/components/schemas/GameMode"
          },
          "comp": {
            "description": "The Old World only: the composition pack in force, e.g. `square_based_comp`. The comp list the organiser applied, whatever scoring preset they chose; for an event configured before comp lists were stored, the pack its scoring preset implies. Null when none.",
            "type": [
              "string",
              "null"
            ]
          },
          "format": {
            "$ref": "#/components/schemas/TournamentFormat"
          },
          "pointsLimit": {
            "type": [
              "integer",
              "null"
            ]
          },
          "ruleset": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  }
                },
                "required": [
                  "id",
                  "name"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "startDate": {
            "description": "`YYYY-MM-DD` in the event's own timezone.",
            "type": [
              "string",
              "null"
            ]
          },
          "endDate": {
            "description": "`YYYY-MM-DD` in the event's own timezone.",
            "type": [
              "string",
              "null"
            ]
          },
          "timezone": {
            "description": "IANA name, e.g. `Europe/London`.",
            "type": [
              "string",
              "null"
            ]
          },
          "roundCount": {
            "type": "integer",
            "description": "Every round the organiser has created, including ones not yet published (so it can exceed the rounds the rounds route returns)."
          },
          "playerCount": {
            "type": "integer",
            "description": "Once the event has a results snapshot (`resultsFrozenAt` is set), the entrants it ranks, players who dropped part-way included and a spare who never played left out; on doubles, the teams. Before that, the active roster entries, spares included: a spare may never have played a game."
          },
          "location": {
            "$ref": "#/components/schemas/LocationSummary"
          },
          "organisers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Organiser"
            }
          },
          "resultsFrozenAt": {
            "description": "When the current results snapshot was computed: null until the organiser completes the event, and moved by every post-completion correction.",
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          },
          "links": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Link"
            },
            "description": "Tournament-level links only (`kind: tournament`)."
          },
          "webUrl": {
            "type": "string",
            "format": "uri",
            "description": "The public page on play.ggs.club, by id: it keeps working when the event is renamed."
          },
          "updatedAt": {
            "description": "The latest of the record's own update, `resultsFrozenAt`, and, once the event is completed, any change to its roster, teams, rounds, pairings, games or `links` (its records on other platforms and its stats publication). Joins and pairings before completion do not move it; poll those collections with their ETag. A change outside the event (an organiser's or player's GGS profile, the venue) does not move it either.",
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "slug",
          "name",
          "resultsAuthority",
          "state",
          "gameSystem",
          "gameMode",
          "comp",
          "format",
          "pointsLimit",
          "ruleset",
          "startDate",
          "endDate",
          "timezone",
          "roundCount",
          "playerCount",
          "location",
          "organisers",
          "resultsFrozenAt",
          "links",
          "webUrl",
          "updatedAt"
        ]
      },
      "TournamentTombstone": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string",
            "description": "The slug it had when it left."
          },
          "deleted": {
            "type": "boolean",
            "const": true,
            "description": "Always `true`: tells a tombstone from a `TournamentSummary`, which has no `deleted`."
          },
          "deletedAt": {
            "description": "When it stopped being visible.",
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "description": "Equal to `deletedAt`: its place in the `updatedAt` order, so the last `updatedAt` you saw is the next `updatedSince` whatever the item.",
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "slug",
          "deleted",
          "deletedAt",
          "updatedAt"
        ],
        "description": "A tournament that is no longer visible, listed only with `updatedSince`: drop it from your copy. Kept for 30 days; if your last sync is older than that, sync again from scratch."
      }
    },
    "responses": {
      "InvalidInput": {
        "description": "A path parameter or the query string did not parse, or the query string names a parameter the route does not take. `details` lists the problems, each with the `path` of the offending parameter and a `message`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "ok": false,
              "error": {
                "code": "invalid_input",
                "message": "Invalid input",
                "details": [
                  {
                    "path": [
                      "limit"
                    ],
                    "message": "Too big: expected number to be <=200"
                  }
                ]
              },
              "requestId": "req_2f6c1a9e4b7d"
            }
          }
        }
      },
      "Unauthenticated": {
        "description": "No key, or one that is unknown, revoked or expired, or whose owner no longer holds API access.",
        "headers": {
          "WWW-Authenticate": {
            "description": "`Bearer` when no key was sent; `Bearer error=\"invalid_token\"` when the key sent was refused (RFC 6750).",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "ok": false,
              "error": {
                "code": "unauthenticated",
                "message": "A valid API key is required",
                "details": null
              },
              "requestId": "req_2f6c1a9e4b7d"
            }
          }
        }
      },
      "InsufficientScope": {
        "description": "The key is valid but does not carry the scope this route needs.",
        "headers": {
          "WWW-Authenticate": {
            "description": "`Bearer error=\"insufficient_scope\", scope=\"<scope>\"` (RFC 6750).",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "ok": false,
              "error": {
                "code": "insufficient_scope",
                "message": "This key does not carry the play:read scope",
                "details": {
                  "required": "play:read"
                }
              },
              "requestId": "req_2f6c1a9e4b7d"
            }
          }
        }
      },
      "NotFound": {
        "description": "No such resource, or one this key cannot see. A private tournament is indistinguishable from a missing one.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "ok": false,
              "error": {
                "code": "not_found",
                "message": "Tournament not found",
                "details": null
              },
              "requestId": "req_2f6c1a9e4b7d"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Over this key's per-minute limit.",
        "headers": {
          "Retry-After": {
            "description": "Seconds until the window resets.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "ok": false,
              "error": {
                "code": "rate_limited",
                "message": "Rate limit exceeded; try again shortly",
                "details": {
                  "limit": 30,
                  "window": "minute"
                }
              },
              "requestId": "req_2f6c1a9e4b7d"
            }
          }
        }
      },
      "Internal": {
        "description": "Something failed on our side. Quote the `requestId` when reporting it.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "ok": false,
              "error": {
                "code": "internal",
                "message": "Something went wrong",
                "details": null
              },
              "requestId": "req_2f6c1a9e4b7d"
            }
          }
        }
      },
      "Unavailable": {
        "description": "A service the request depends on (the database, key verification) did not answer. Nothing was changed: wait a moment and retry.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            },
            "example": {
              "ok": false,
              "error": {
                "code": "unavailable",
                "message": "The server could not complete that just now. Please try again.",
                "details": null
              },
              "requestId": "req_2f6c1a9e4b7d"
            }
          }
        }
      }
    },
    "headers": {
      "ETag": {
        "description": "A weak validator of this response. Send it back as `If-None-Match` to get a `304` while nothing changed.",
        "schema": {
          "type": "string"
        }
      },
      "X-RateLimit-Limit": {
        "description": "Requests this key may make per minute.",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Remaining": {
        "description": "Requests left in the current minute.",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Reset": {
        "description": "When the current window ends, as Unix time in seconds.",
        "schema": {
          "type": "integer"
        }
      },
      "X-Request-Id": {
        "description": "The request id, also `requestId` in the body. A well-formed `X-Request-Id` you send is echoed instead of a new one.",
        "schema": {
          "type": "string"
        }
      }
    }
  }
}
