{"openapi":"3.1.0","info":{"title":"ClubBattles API","version":"1.0.0","summary":"Ranked ladders for clubs, across multiple games.","description":"Every response is the same envelope: `{ success, message, data }`, plus an `error` key on a failure.\nBranch on the HTTP status and `error.code`; `message` is copy and may be reworded.\n\n**Identifiers.** Numbers address users and clubs — `users.user_number` and `clubs.club_number` are the public identifiers and the only things that route. Slugs address games, where the slug IS the subdomain. A user or club slug in a URL 404s by design.\n\n**Times** are ISO 8601 in UTC.\n\n**Rate limit.** 600 requests per minute per key, reported in the `RateLimit-*` response headers. Anonymous traffic is not limited in the application — see docs/API.md.","contact":{"name":"ClubBattles","url":"https://clubbattles.com"}},"servers":[{"url":"https://api.clubbattles.com","description":"Production"},{"url":"http://localhost:4000","description":"Local development"}],"tags":[{"name":"Meta","description":"Liveness, build info and this document."},{"name":"Games","description":"The titles ClubBattles runs. Adding one is a row plus an adapter — never a new process."},{"name":"Ladder","description":"Standings and rating history. Everything here is derived from `rating_history` and match results."},{"name":"Clubs","description":"Club identity, per-title rosters and staff."},{"name":"Players","description":"Public player profiles and lookup."},{"name":"Matches","description":"Results, confirmation and disputes."},{"name":"Queue","description":"The live queue: searching, fixtures and the match room."},{"name":"Account","description":"The caller’s own profile. Session only."},{"name":"API keys","description":"Minting and revoking keys. Session only — a key can never mint a key."},{"name":"Roadmap","description":"What is being built, what shipped and when, and the ideas people have suggested."},{"name":"Calendar","description":"Events on the season calendar. Seasons themselves are the UTC calendar month and are not listed."}],"security":[{}],"paths":{"/health":{"get":{"summary":"Liveness probe","description":"Checks the database as well as the process, and answers 503 with `status: degraded` when it cannot be reached. It did NOT probe the database before 21-09-2026, which made a Postgres outage invisible — a green `/health` while every viewer appeared signed out.","tags":["Meta"],"security":[{}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{"status":{"type":"string","description":"`ok`, or `degraded` on the 503."},"database":{"type":"string","description":"`up`, or `down` on the 503."}}}}}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/version":{"get":{"summary":"What commit this process is running","description":"Read ONCE at process start, on purpose. The webapp reads the same file per request, so the two disagree for the whole of every deploy — a disagreement that persists after the deploy finishes is a build that never restarted the API.","tags":["Meta"],"security":[{}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{"sha":{"type":"string","description":"Commit sha, or `dev` outside a deployed build."},"subject":{"type":"string","description":"Commit subject."},"branch":{"type":"string","description":"Branch deployed."},"builtAt":{"type":"string","description":"When the build ran."}}}}}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/openapi.json":{"get":{"summary":"This document","description":"The OpenAPI 3.1 description of everything above. There is no checked-in `openapi.json` — this endpoint is the file.","tags":["Meta"],"security":[{}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{}}}}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games":{"get":{"summary":"Every title","description":"The catalogue every other call needs, so it is reachable by ANY valid key regardless of scope, and anonymously. Listed titles only: sandbox titles and titles staff have hidden are left out, which is the one filter that takes a title off every list.","tags":["Games"],"security":[{}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/Game"}}}}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games/{slug}":{"get":{"summary":"One title","description":"Deliberately resolves sandbox and hidden titles, unlike the list above — otherwise their own subdomains would 404.","tags":["Games"],"security":[{}],"parameters":[{"name":"slug","in":"path","required":true,"description":"The title’s slug.","schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Game"}}}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/ladder":{"get":{"summary":"Club standings across every title","description":"Derived, never stored: a club’s score is the sum over the titles it has a rated match in this season of (rating − 1000), with archived clubs and archived rosters excluded. Ties break on rated matches this season, then name.","tags":["Ladder"],"security":[{"apiKey":["ladder.read"]},{}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/OverallLadderEntry"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games/{gameSlug}/ladder":{"get":{"summary":"One title’s ladder","description":"Clubs with a rated match this season. Ratings reset to 1000 at every monthly UTC season boundary.","tags":["Ladder"],"security":[{"apiKey":["ladder.read"]},{}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/LadderEntry"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games/{gameSlug}/clubs/{clubNumber}/rating-history":{"get":{"summary":"A club’s rating movements","description":"Every entry `rating_history` holds for this club in this title. The ladder is rebuildable from this plus match results, which is why nothing but the rating module may write it.","tags":["Ladder"],"security":[{"apiKey":["ladder.read"]},{}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}},{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/RatingHistoryEntry"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/clubs":{"get":{"summary":"Every club that exists","description":"The apex directory. NOT a filter on the per-title list: a club that has retired from every title it entered still has an identity and appears here with no titles. Archived clubs are excluded from both.","tags":["Clubs"],"security":[{"apiKey":["clubs.read"]},{}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/Club"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}}},"post":{"summary":"Found a club","description":"Makes you staff and puts you on no roster. A founder who wants to play adds themselves afterwards, which is also where the one-club-per-game seat is taken.","tags":["Clubs"],"security":[{"sessionToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Display name."},"tag":{"type":"string","description":"Short badge."}},"required":["name","tag"]}}}},"responses":{"201":{"description":"Club created","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Club"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/clubs/{clubNumber}":{"get":{"summary":"One club’s identity","tags":["Clubs"],"security":[{"apiKey":["clubs.read"]},{}],"parameters":[{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Club"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}}},"patch":{"summary":"Rename or re-tag a club","description":"The slug deliberately does NOT move on rename — a rename that 404s every shared link is the worse bug.","tags":["Clubs"],"security":[{"sessionToken":[]}],"parameters":[{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"New name."},"tag":{"type":"string","description":"New tag."}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Club"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/clubs/{clubNumber}/rosters":{"get":{"summary":"Every title this club has entered","description":"Includes titles the club has RETIRED from, which the club’s own `games` array excludes. That is the difference between the two and the reason this exists.","tags":["Clubs"],"security":[{"apiKey":["clubs.read"]},{}],"parameters":[{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"type":"object","properties":{"gameSlug":{"type":"string","description":"The title’s slug, which is also its subdomain."},"gameName":{"type":"string","description":"The title’s display name."},"isArchived":{"type":"boolean","description":"True when the club has retired from this title. Its roster and history are kept."}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/clubs/{clubNumber}/archive":{"patch":{"summary":"Archive or restore a club everywhere","description":"Archive, never delete — rating history and match lineups reference a club forever. The route deliberately does not refuse an already-archived club, or archiving would be one-way and a mis-click permanent.","tags":["Clubs"],"security":[{"sessionToken":[]}],"parameters":[{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"archived":{"type":"boolean","description":"True to archive, false to restore."}},"required":["archived"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Club"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/clubs/{clubNumber}/merge":{"post":{"summary":"Merge another club into this one","tags":["Clubs"],"security":[{"sessionToken":[]}],"parameters":[{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"fromClubNumber":{"type":"integer","description":"The club being merged away."}},"required":["fromClubNumber"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Club"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/clubs/{clubNumber}/staff":{"get":{"summary":"Who administers this club","description":"Running a club and playing for it are separate. Staff is club-wide and grants administration; membership is per game and grants play. Neither implies the other.","tags":["Clubs"],"security":[{"sessionToken":[]}],"parameters":[{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/Member"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}},"post":{"summary":"Add a staffer","description":"Only the owner manages staff, so a staffer cannot lock the owner out.","tags":["Clubs"],"security":[{"sessionToken":[]}],"parameters":[{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"player":{"type":"string","description":"A player tag like `Name#10042`, or the bare player number."}},"required":["player"]}}}},"responses":{"201":{"description":"Staffer added","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/Member"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/clubs/{clubNumber}/staff/{userNumber}":{"delete":{"summary":"Remove a staffer","description":"The owner’s own staff row cannot be removed, because ownership transfer does not exist.","tags":["Clubs"],"security":[{"sessionToken":[]}],"parameters":[{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}},{"name":"userNumber","in":"path","required":true,"description":"The player’s public number — the half of a player tag after the `#`.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/Member"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games/{gameSlug}/clubs":{"get":{"summary":"Clubs fielding a roster in this title","tags":["Clubs"],"security":[{"apiKey":["clubs.read"]},{}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/Club"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}}},"post":{"summary":"Found a club and enter it into this title","tags":["Clubs"],"security":[{"sessionToken":[]}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Display name."},"tag":{"type":"string","description":"Short badge."}},"required":["name","tag"]}}}},"responses":{"201":{"description":"Club created","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Club"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games/{gameSlug}/clubs/{clubNumber}":{"get":{"summary":"A club as it stands in one title","tags":["Clubs"],"security":[{"apiKey":["clubs.read"]},{}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}},{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Club"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}}},"patch":{"summary":"Rename or re-tag from inside a title","description":"Two routes rename a club — this and the apex one. They agree today; a change to one is a change to half the product.","tags":["Clubs"],"security":[{"sessionToken":[]}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}},{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"New name."},"tag":{"type":"string","description":"New tag."}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Club"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games/{gameSlug}/clubs/{clubNumber}/archive":{"patch":{"summary":"Retire or restore this title’s roster","description":"Scoped to one title, unlike the apex archive route which archives the club everywhere.","tags":["Clubs"],"security":[{"sessionToken":[]}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}},{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"archived":{"type":"boolean","description":"True to retire the roster, false to restore it."}},"required":["archived"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Club"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games/{gameSlug}/clubs/{clubNumber}/members":{"get":{"summary":"A club’s roster in this title","description":"Active members only. A player is on ONE club per game, enforced by a partial unique index rather than by a guard.","tags":["Clubs"],"security":[{"apiKey":["clubs.read"]},{}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}},{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/Member"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games/{gameSlug}/clubs/{clubNumber}/members/me":{"post":{"summary":"Add yourself to this roster","description":"Where a founder takes their one-club-per-game seat. Refused if the roster is full or you already hold a seat in this title.","tags":["Clubs"],"security":[{"sessionToken":[]}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}},{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"responses":{"201":{"description":"Joined","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/Member"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games/{gameSlug}/clubs/{clubNumber}/members/{userNumber}":{"delete":{"summary":"Leave, or remove a member","description":"The roster lock binds BOTH sides: once a player has played a rated match this season, neither they nor staff may end that membership until the season changes or the club goes fourteen days without a rated match.","tags":["Clubs"],"security":[{"sessionToken":[]}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}},{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}},{"name":"userNumber","in":"path","required":true,"description":"The player’s public number — the half of a player tag after the `#`.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/Member"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games/{gameSlug}/clubs/{clubNumber}/roster":{"post":{"summary":"Enter this club into the title","description":"Creates an empty roster, or restores one the club previously retired — with the members it was left with. 201 when the roster is new, 200 when it is restored.","tags":["Clubs"],"security":[{"sessionToken":[]}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}},{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"responses":{"201":{"description":"Roster created","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Club"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games/{gameSlug}/clubs/{clubNumber}/invitations":{"get":{"summary":"Invitations this club has outstanding","tags":["Invitations"],"security":[{"sessionToken":[]}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}},{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/Invitation"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}},"post":{"summary":"Invite a player to the roster","description":"Joining a club requires consent: staff invite and the player accepts. There is no direct-add route, because consent staff can bypass is not consent. The cap is checked at acceptance, not here, so staff may invite twelve people for eight places.","tags":["Invitations"],"security":[{"sessionToken":[]}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}},{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"player":{"type":"string","description":"A player tag like `Name#10042`, or the bare player number."}},"required":["player"]}}}},"responses":{"201":{"description":"Invitation sent","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Invitation"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/users/me/invitations":{"get":{"summary":"Invitations addressed to you","tags":["Invitations"],"security":[{"sessionToken":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/Invitation"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/invitations/{invitationId}":{"delete":{"summary":"Withdraw an invitation you sent","tags":["Invitations"],"security":[{"sessionToken":[]}],"parameters":[{"name":"invitationId","in":"path","required":true,"description":"The invitation’s uuid.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{"id":{"type":"string","description":"The withdrawn invitation."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/invitations/{invitationId}/accept":{"post":{"summary":"Accept an invitation","description":"Marks the invitation and creates the membership in ONE transaction: if the roster filled while it sat unanswered, the acceptance rolls back and the invitation stays pending rather than stranding the player.","tags":["Invitations"],"security":[{"sessionToken":[]}],"parameters":[{"name":"invitationId","in":"path","required":true,"description":"The invitation’s uuid.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Club"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/invitations/{invitationId}/decline":{"post":{"summary":"Decline an invitation","tags":["Invitations"],"security":[{"sessionToken":[]}],"parameters":[{"name":"invitationId","in":"path","required":true,"description":"The invitation’s uuid.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{"id":{"type":"string","description":"The declined invitation."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games/{gameSlug}/players/{userNumber}":{"get":{"summary":"A player’s profile in one title","description":"Game-scoped because everything on it is — a rating is per game and so is a roster, so the same person has a different profile in a different title. Contact details are not in it.","tags":["Players"],"security":[{"apiKey":["players.read"]},{}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}},{"name":"userNumber","in":"path","required":true,"description":"The player’s public number — the half of a player tag after the `#`.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/PlayerProfile"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/players":{"get":{"summary":"Search players by name, tag or number","description":"The autocomplete behind every field that takes a player. Signed-in rather than permissioned, because club staff need it. Returns the three fields a picker draws, all already printed on public pages — so it adds a way to LOOK somebody up, not a new fact about them.","tags":["Players"],"security":[{"sessionToken":[]}],"parameters":[{"name":"q","in":"query","required":true,"description":"At least two characters.","schema":{"type":"string","minLength":2}},{"name":"limit","in":"query","required":false,"description":"Capped at 20 on the schema, so a caller cannot turn this into a directory dump.","schema":{"type":"integer","minimum":1,"maximum":20,"default":8}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"type":"object","properties":{"userNumber":{"type":"integer","description":"Public player number."},"displayName":{"type":"string","description":"From Discord."},"avatarUrl":{"type":["string","null"],"description":"Discord avatar."}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games/{gameSlug}/matches":{"get":{"summary":"Recent matches in a title","tags":["Matches"],"security":[{"apiKey":["matches.read"]},{}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/MatchSummary"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/matches/{matchSlug}":{"get":{"summary":"One match in full","description":"Scores are ALWAYS club A then club B, never the reporter’s point of view — a bare \"3-1\" is what gets confirmed backwards, and a reversed confirmation is unrecoverable once rated.","tags":["Matches"],"security":[{"apiKey":["matches.read"]},{}],"parameters":[{"name":"matchSlug","in":"path","required":true,"description":"The match’s slug.","schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Match"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}}},"delete":{"summary":"Withdraw an unanswered result","description":"DELETES the match, so the room reads \"no result at all\" and offers the report form again. Safe only because rating applies on entry to `completed`; never extend this to a rated match.","tags":["Matches"],"security":[{"sessionToken":[]}],"parameters":[{"name":"matchSlug","in":"path","required":true,"description":"The match’s slug.","schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{"slug":{"type":"string","description":"The withdrawn match."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games/{gameSlug}/clubs/{clubNumber}/matches":{"get":{"summary":"A club’s matches in a title","tags":["Matches"],"security":[{"apiKey":["matches.read"]},{}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}},{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/MatchSummary"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}}},"post":{"summary":"Report a result","description":"The gate is enforced from the fixture found by the two club ids, never from a fixture id in the request, so it cannot be skipped by omitting a field.","tags":["Matches"],"security":[{"sessionToken":[]}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}},{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"opponentClubNumber":{"type":"integer","description":"The other club."},"scoreA":{"type":"integer","description":"Club A’s score — club A, not yours."},"scoreB":{"type":"integer","description":"Club B’s score."},"stats":{"type":"array","items":{"$ref":"#/components/schemas/PlayerStats"},"description":"Optional. The reporting side’s own players only, keyed by the title’s `statFields`. Every stat is optional; a result is never refused for a missing one."}},"required":["opponentClubNumber","scoreA","scoreB"]}}}},"responses":{"201":{"description":"Result reported","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Match"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games/{gameSlug}/clubs/{clubNumber}/matches/pending":{"get":{"summary":"Results awaiting this club’s answer","tags":["Matches"],"security":[{"apiKey":["matches.read"]},{}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}},{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/MatchSummary"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/matches/{matchSlug}/confirm":{"post":{"summary":"Confirm a reported result","description":"Confirming is what enters `completed`, and entry to `completed` is what applies rating. The guard reads `match_results.reported_by` so the reporter cannot confirm their own result.","tags":["Matches"],"security":[{"sessionToken":[]}],"parameters":[{"name":"matchSlug","in":"path","required":true,"description":"The match’s slug.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"lineup":{"type":"array","items":{"type":"integer","description":"A player number in the confirming side’s lineup."}},"stats":{"type":"array","items":{"$ref":"#/components/schemas/PlayerStats"},"description":"Optional. The confirming side’s own players only."}},"required":["lineup"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Match"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/matches/{matchSlug}/decline":{"post":{"summary":"Dispute a reported result","description":"Sends it to staff. A resolved dispute rates at resolution time, not retroactively at the original timestamp.","tags":["Matches"],"security":[{"sessionToken":[]}],"parameters":[{"name":"matchSlug","in":"path","required":true,"description":"The match’s slug.","schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Match"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games/{gameSlug}/queue":{"get":{"summary":"Whether the queue is open, and how many clubs are searching","tags":["Queue"],"security":[{"apiKey":["queue.read"]},{}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/QueueSnapshot"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games/{gameSlug}/clubs/{clubNumber}/queue":{"get":{"summary":"Your club’s own queue state","tags":["Queue"],"security":[{"sessionToken":[]}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}},{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}},"post":{"summary":"Join the queue","tags":["Queue"],"security":[{"sessionToken":[]}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}},{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"lineup":{"type":"array","items":{"type":"integer","description":"A player number in the lineup."}}}}}}},"responses":{"201":{"description":"Queued, or matched immediately","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}},"delete":{"summary":"Leave the queue","tags":["Queue"],"security":[{"sessionToken":[]}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}},{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/games/{gameSlug}/clubs/{clubNumber}/fixtures":{"get":{"summary":"A club’s fixtures","tags":["Queue"],"security":[{"apiKey":["queue.read"]},{}],"parameters":[{"name":"gameSlug","in":"path","required":true,"description":"The title’s slug, which is also its subdomain — e.g. `rocketleague`.","schema":{"type":"string"}},{"name":"clubNumber","in":"path","required":true,"description":"The club’s public number. Slugs are not routable and 404 by design.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/Fixture"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/fixtures/{fixtureId}":{"get":{"summary":"One fixture","description":"Lobby credentials are NOT in this response for a caller who is not in the match — they are generated at pairing for the two clubs playing.","tags":["Queue"],"security":[{"apiKey":["queue.read"]},{}],"parameters":[{"name":"fixtureId","in":"path","required":true,"description":"The fixture’s uuid.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Fixture"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/fixtures/{fixtureId}/accept":{"post":{"summary":"Accept a proposed fixture","tags":["Queue"],"security":[{"sessionToken":[]}],"parameters":[{"name":"fixtureId","in":"path","required":true,"description":"The fixture’s uuid.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Fixture"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/fixtures/{fixtureId}/decline":{"post":{"summary":"Decline a proposed fixture","description":"Decline penalties are derived from the fixture row, never stored as a counter — and they are on the club, not on whoever happened to queue it in.","tags":["Queue"],"security":[{"sessionToken":[]}],"parameters":[{"name":"fixtureId","in":"path","required":true,"description":"The fixture’s uuid.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Fixture"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/fixtures/{fixtureId}/messages":{"get":{"summary":"Match room chat","description":"Polled, never socketed — the bearer token stays server-side and the process count is capped.","tags":["Queue"],"security":[{"sessionToken":[]}],"parameters":[{"name":"fixtureId","in":"path","required":true,"description":"The fixture’s uuid.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Message uuid."},"body":{"type":"string","description":"What was said."},"authorName":{"type":"string","description":"Who said it."},"createdAt":{"type":"string","description":"When.","format":"date-time"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}},"post":{"summary":"Post to the match room","tags":["Queue"],"security":[{"sessionToken":[]}],"parameters":[{"name":"fixtureId","in":"path","required":true,"description":"The fixture’s uuid.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","description":"The message."}},"required":["body"]}}}},"responses":{"201":{"description":"Posted","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/fixtures/{fixtureId}/scoresheet":{"get":{"summary":"The match room’s scoresheet","description":"For a title with a scorer. Everything in it is replayed from the visits entered, so it cannot disagree with them. Room members only, like the chat.","tags":["Queue"],"security":[{"sessionToken":[]}],"parameters":[{"name":"fixtureId","in":"path","required":true,"description":"The fixture’s uuid.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Scoresheet"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/fixtures/{fixtureId}/visits":{"post":{"summary":"Enter a visit","description":"For the side whose throw it is. Send the darts — `throws`, one to three `{ ring, number? }` — or a total as `score`, with `darts` on a checkout. Darts are taken one at a time: past zero, leaving 1 on double-out, or reaching zero on anything but a double when the title is double-out, is a bust, and the visit ends on the dart that finished or went bust.","tags":["Queue"],"security":[{"sessionToken":[]}],"parameters":[{"name":"fixtureId","in":"path","required":true,"description":"The fixture’s uuid.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"throws":{"type":"array","items":{"$ref":"#/components/schemas/Dart"}},"score":{"type":"integer","description":"Points scored, 0–180, when entering a total."},"darts":{"type":"integer","description":"Darts used, on a checkout entered as a total."}}}}}},"responses":{"201":{"description":"Entered","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Scoresheet"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/fixtures/{fixtureId}/visits/preview":{"post":{"summary":"What a visit would do","description":"The same checks entering it runs, and nothing written — so the room can ask a player to confirm what a visit scores, whether it goes bust or wins the leg, before it counts. With `edit: true`, what correcting the side’s last visit to this would do.","tags":["Queue"],"security":[{"sessionToken":[]}],"parameters":[{"name":"fixtureId","in":"path","required":true,"description":"The fixture’s uuid.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"throws":{"type":"array","items":{"$ref":"#/components/schemas/Dart"}},"score":{"type":"integer","description":"A total instead of darts."},"darts":{"type":"integer","description":"Darts used, on a checkout entered as a total."},"edit":{"type":"boolean","description":"Preview a correction to the side’s last visit instead of a new one."}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/VisitPreview"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/fixtures/{fixtureId}/visits/draft":{"put":{"summary":"Save the darts picked so far","description":"The visit being thrown, none to three darts, sent as the thrower taps them so the other side sees them land (the scoresheet’s `draft`). Only on your own throw — 409 otherwise, which only means the throw moved on. Not judged, and never a visit: entering one is still `POST …/visits`.","tags":["Queue"],"security":[{"sessionToken":[]}],"parameters":[{"name":"fixtureId","in":"path","required":true,"description":"The fixture’s uuid.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"throws":{"type":"array","items":{"$ref":"#/components/schemas/Dart"}}},"required":["throws"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"null"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/fixtures/{fixtureId}/visits/last":{"put":{"summary":"Correct your last visit","description":"Replaces the most recent visit the caller’s side threw. If the other side has thrown since, the correction may not change whether the visit won the leg. Never once a result is filed.","tags":["Queue"],"security":[{"sessionToken":[]}],"parameters":[{"name":"fixtureId","in":"path","required":true,"description":"The fixture’s uuid.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"throws":{"type":"array","items":{"$ref":"#/components/schemas/Dart"}},"score":{"type":"integer","description":"A total instead of darts."},"darts":{"type":"integer","description":"Darts used, on a checkout entered as a total."}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Scoresheet"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/fixtures/{fixtureId}/scoresheet/file":{"post":{"summary":"Send the result from the scoresheet","description":"Once a side has won and the report gate is open. Files the score, lineup and stats worked out from the visits; the other club then confirms or disputes it as usual.","tags":["Queue"],"security":[{"sessionToken":[]}],"parameters":[{"name":"fixtureId","in":"path","required":true,"description":"The fixture’s uuid.","schema":{"type":"string","format":"uuid"}}],"responses":{"201":{"description":"Sent","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/fixtures/{fixtureId}/signals":{"get":{"summary":"Match camera signalling","description":"The few messages two browsers swap to connect a camera — a hello, one offer, one answer, a bye. Video itself goes browser to browser and never reaches the API. Room members only, on a title with a camera; signals older than a minute are gone.","tags":["Queue"],"security":[{"sessionToken":[]}],"parameters":[{"name":"fixtureId","in":"path","required":true,"description":"The fixture’s uuid.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/Signal"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}},"post":{"summary":"Send a camera signal","tags":["Queue"],"security":[{"sessionToken":[]}],"parameters":[{"name":"fixtureId","in":"path","required":true,"description":"The fixture’s uuid.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"peer":{"type":"string","description":"This page’s peer id.","format":"uuid"},"to":{"type":["string","null"],"description":"The peer it is for, or null for everybody."},"kind":{"type":"string","description":"What it is.","enum":["hello","offer","answer","bye"]},"role":{"type":["string","null"],"enum":["camera","viewer",null]},"payload":{"type":"object","properties":{}}},"required":["peer","kind"]}}}},"responses":{"201":{"description":"Sent","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/fixtures/{fixtureId}/camera-links":{"post":{"summary":"Make a phone camera link","description":"A short-lived link a phone can open, with no sign-in, to stream the caller’s side’s board. The token is shown once and stored hashed.","tags":["Queue"],"security":[{"sessionToken":[]}],"parameters":[{"name":"fixtureId","in":"path","required":true,"description":"The fixture’s uuid.","schema":{"type":"string","format":"uuid"}}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{"token":{"type":"string","description":"Goes in /camera/{token}."},"expiresAt":{"type":"string","description":"When the link stops working.","format":"date-time"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/camera-links/{token}":{"get":{"summary":"What a phone camera link opens","description":"The token is the credential. 404 once it has expired or its match is over.","tags":["Queue"],"security":[{}],"parameters":[{"name":"token","in":"path","required":true,"description":"A phone camera link’s token — the credential itself.","schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{"fixtureId":{"type":"string","description":"The fixture.","format":"uuid"},"side":{"type":"string","description":"The side it streams for.","enum":["a","b"]},"clubName":{"type":"string","description":"That side’s club."},"opponentName":{"type":"string","description":"The other club."},"gameSlug":{"type":"string","description":"The title."},"mode":{"type":"string","description":"The title’s camera setting.","enum":["optional","required"]}}}}}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/camera-links/{token}/signals":{"get":{"summary":"Camera signalling, for a phone","tags":["Queue"],"security":[{}],"parameters":[{"name":"token","in":"path","required":true,"description":"A phone camera link’s token — the credential itself.","schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/Signal"}}}}}}},"default":{"$ref":"#/components/responses/Error"}}},"post":{"summary":"Send a camera signal, as a phone","description":"Always sent as a camera — the role is set by the link, not the request.","tags":["Queue"],"security":[{}],"parameters":[{"name":"token","in":"path","required":true,"description":"A phone camera link’s token — the credential itself.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"peer":{"type":"string","description":"This phone’s peer id.","format":"uuid"},"to":{"type":["string","null"],"description":"The peer it is for, or null for everybody."},"kind":{"type":"string","description":"What it is.","enum":["hello","offer","answer","bye"]},"payload":{"type":"object","properties":{}}},"required":["peer","kind"]}}}},"responses":{"201":{"description":"Sent","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{}}}}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/users/sync":{"post":{"summary":"Create or refresh the caller’s account at sign-in","description":"Guards itself rather than using the usual user guard, because it must answer BEFORE a users row exists. Not a general-purpose route — the webapp calls it once per sign-in.","tags":["Account"],"security":[{}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"discordId":{"type":"string","description":"The Discord id, which must match the bearer token’s subject."},"displayName":{"type":"string","description":"From Discord. Overwrites whatever is stored."},"avatarUrl":{"type":["string","null"],"description":"From Discord."}},"required":["discordId","displayName"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Profile"}}}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/users/me":{"get":{"summary":"Your own profile","description":"Display name and avatar are read-only: `users/sync` overwrites them at every sign-in, so an editable name would be silently reverted.","tags":["Account"],"security":[{"sessionToken":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Profile"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}},"patch":{"summary":"Update your bio, timezone or notification preferences","description":"A body carrying `displayName` is ignored rather than refused, for the reason above.","tags":["Account"],"security":[{"sessionToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"bio":{"type":["string","null"],"description":"Empty clears it; absent leaves it alone."},"timezone":{"type":"string","description":"An IANA zone."},"notificationPrefs":{"type":"object","properties":{}}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/Profile"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/users/me/clubs":{"get":{"summary":"Clubs you play for or administer","tags":["Account"],"security":[{"sessionToken":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/Club"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/users/me/founding":{"get":{"summary":"Whether you may found a club","description":"A ceiling on ACTIVE clubs one person may own. Archived clubs do not count towards it.","tags":["Account"],"security":[{"sessionToken":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{"mayFound":{"type":"boolean","description":"Whether founding another club would be accepted right now."},"activeClubs":{"type":"integer","description":"Active clubs you already own."},"maxActiveClubs":{"type":"integer","description":"The ceiling. Founding is refused at or above it."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/users/me/accounts":{"post":{"summary":"Claim a platform account","description":"CLAIMED, never verified. Nothing checks a handle against Steam or Epic, and no reader may treat one as identity.","tags":["Account"],"security":[{"sessionToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"platform":{"type":"string","description":"The platform."},"handle":{"type":"string","description":"The handle, as typed."}},"required":["platform","handle"]}}}},"responses":{"201":{"description":"Account claimed","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/users/me/accounts/{platform}":{"delete":{"summary":"Drop a claimed platform account","tags":["Account"],"security":[{"sessionToken":[]}],"parameters":[{"name":"platform","in":"path","required":true,"description":"The platform to unclaim.","schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{"platform":{"type":"string","description":"The platform removed."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/users/me/api-keys":{"get":{"summary":"Your API keys","description":"Live keys first, then revoked ones. Revoked keys are shown rather than hidden, because \"did I already revoke that\" is a question this should answer.","tags":["API keys"],"security":[{"sessionToken":[]}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{"keys":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}},"scopes":{"type":"array","items":{"type":"string","description":"A scope that exists.","enum":["ladder.read","clubs.read","players.read","matches.read","queue.read"]}},"maxLiveKeys":{"type":"integer","description":"How many live keys one account may hold."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}},"post":{"summary":"Mint a key","description":"THE ONLY TIME THE TOKEN IS EVER RETURNED. Nothing stores it and no later read can recover it — lose it and you mint a new key. Requires a signed-in session, which an API key cannot provide, so a key can never mint another key.","tags":["API keys"],"security":[{"sessionToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"What to call it. Must be unique among your live keys."},"scopes":{"type":"array","items":{"type":"string","description":"A scope to grant.","enum":["ladder.read","clubs.read","players.read","matches.read","queue.read"]}},"expiresInDays":{"type":["integer","null"],"description":"1 to 730, or null for no expiry."}},"required":["name","scopes"]}}}},"responses":{"201":{"description":"Key created","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{"key":{"$ref":"#/components/schemas/ApiKey"},"token":{"type":"string","description":"The full token, e.g. `cb_a3f9c1d20b7e_…`. Shown once."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/users/me/api-keys/{keyId}":{"delete":{"summary":"Revoke a key","description":"Revoke, never delete: the row is the record that the credential existed, and its prefix stays claimed so a prefix in an old log always resolves to exactly one key. Takes effect on the next request — there is no cache in front of verification.","tags":["API keys"],"security":[{"sessionToken":[]}],"parameters":[{"name":"keyId","in":"path","required":true,"description":"The key’s uuid, from the list above.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{"id":{"type":"string","description":"The revoked key."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/roadmap":{"get":{"summary":"The board and the changelog","description":"Everything planned, being built or shipped, with the date each thing shipped — the board and the changelog in one read. Ideas still under review and declined ones are NOT here, because they grow with the number of people suggesting rather than with the work: page through them at `/v1/roadmap/ideas`. Their counts are here so a page can show them without reading them. Hidden items are excluded. Personalised when a session is present — `viewerVoted` and `viewerIsAuthor` — and identical to a signed-out read otherwise, which is why any API key may call it.","tags":["Roadmap"],"security":[{}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/RoadmapItem"}},"maxOpenIdeas":{"type":"integer","description":"How many ideas one person may have waiting for an answer at once — currently 5."},"openIdeaCount":{"type":"integer","description":"Ideas waiting for an answer, across everybody."},"viewerOpenIdeaCount":{"type":["integer","null"],"description":"How many of those are the signed-in caller’s — what the ceiling above counts. Null without a session."}}}}}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/roadmap/{itemNumber}":{"get":{"summary":"One roadmap item","tags":["Roadmap"],"security":[{}],"parameters":[{"name":"itemNumber","in":"path","required":true,"description":"The roadmap item’s public number.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/RoadmapItem"}}}}}},"default":{"$ref":"#/components/responses/Error"}}},"delete":{"summary":"Withdraw your own idea","description":"Only while nobody has answered it. It is deleted with its votes, so it stops counting towards your waiting ideas. Once answered it is part of the record, and this answers 409.","tags":["Roadmap"],"security":[{"sessionToken":[]}],"parameters":[{"name":"itemNumber","in":"path","required":true,"description":"The roadmap item’s public number.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{"itemNumber":{"type":"integer","description":"The withdrawn idea."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/roadmap/ideas":{"get":{"summary":"What people have suggested, a page at a time","description":"Community ideas only. `top` and `new` are the ones still taking votes (under review, planned or being built) by votes or by age; `answered` is the ones that shipped or were declined, most recently answered first, with the reason in `reply`. Numbered pages: `top` can shift under a reader as votes move, which is what `new` is for. A page past the end answers an empty list with the real `total`, not a 404.","tags":["Roadmap"],"security":[{}],"parameters":[{"name":"view","in":"query","required":false,"description":"Which list. Defaults to `top`.","schema":{"type":"string","enum":["top","new","answered"],"default":"top"}},{"name":"page","in":"query","required":false,"description":"From 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","required":false,"description":"Page size.","schema":{"type":"integer","minimum":1,"maximum":50,"default":20}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/RoadmapItem"}},"view":{"type":"string","description":"The list you asked for.","enum":["top","new","answered"]},"page":{"type":"integer","description":"The page you asked for."},"pageSize":{"type":"integer","description":"Items per page."},"total":{"type":"integer","description":"Items in the whole list."},"pages":{"type":"integer","description":"Pages in the whole list — at least 1, even when it is empty."}}}}}}}},"default":{"$ref":"#/components/responses/Error"}}},"post":{"summary":"Suggest an idea","description":"Posting an idea counts as your vote for it. At most 5 of your ideas may wait for an answer at once; past that this answers 409 with `error.code` `too_many_open_ideas`, and withdrawing one or getting an answer frees a slot. There is no time-based limit.","tags":["Roadmap"],"security":[{"sessionToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","description":"One line, 3–120 characters."},"body":{"type":"string","description":"What it would let you do. Optional, up to 2000 characters."},"area":{"type":"string","description":"Which part of the product. Defaults to `general`.","enum":["ladder","queue","clubs","matches","players","discord","circuits","mobile","developers","general"]}},"required":["title"]}}}},"responses":{"201":{"description":"Idea posted","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/RoadmapItem"}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/roadmap/search":{"get":{"summary":"Find items like a title","description":"Items still taking votes — under review, planned or being built — whose titles share words with `q`, best match first. Words are compared stemmed, with common words ignored; a longer `q` must share at least a third of its words. The suggestion form asks this while somebody types, so they vote for an existing idea instead of posting a copy.","tags":["Roadmap"],"security":[{}],"parameters":[{"name":"q","in":"query","required":true,"description":"At least two characters.","schema":{"type":"string","minLength":2,"maxLength":120}},{"name":"limit","in":"query","required":false,"description":"How many matches.","schema":{"type":"integer","minimum":1,"maximum":10,"default":3}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"array","items":{"$ref":"#/components/schemas/RoadmapItem"}}}}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/roadmap/{itemNumber}/vote":{"put":{"summary":"Vote for an item","description":"Idempotent: voting twice is one vote. Refused with 409 (`voting_closed`) once an item has shipped or been declined.","tags":["Roadmap"],"security":[{"sessionToken":[]}],"parameters":[{"name":"itemNumber","in":"path","required":true,"description":"The roadmap item’s public number.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{"itemNumber":{"type":"integer","description":"The item."},"votes":{"type":"integer","description":"The new total."},"voted":{"type":"boolean","description":"Always true here."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}},"delete":{"summary":"Take your vote back","description":"Idempotent, and closed at the same point voting is.","tags":["Roadmap"],"security":[{"sessionToken":[]}],"parameters":[{"name":"itemNumber","in":"path","required":true,"description":"The roadmap item’s public number.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{"itemNumber":{"type":"integer","description":"The item."},"votes":{"type":"integer","description":"The new total."},"voted":{"type":"boolean","description":"Always false here."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorised"},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/calendar":{"get":{"summary":"Events in one month","description":"Published events touching one UTC calendar month, in day order, for every title or — with `game` — for one — a multi-day event that starts before the month or runs past it is included. Seasons are not events and never appear here: a season is the UTC calendar month, so its start and end are the month itself.","tags":["Calendar"],"security":[{}],"parameters":[{"name":"month","in":"query","required":true,"description":"Which month, `YYYY-MM`.","schema":{"type":"string","pattern":"^\\d{4}-(0[1-9]|1[0-2])$"}},{"name":"game","in":"query","required":false,"description":"A title’s slug. Given, the month is that title’s calendar: its own events plus the ones for every title, and never another title’s. Omitted, it is the overall calendar — every event. An unknown slug is a 404.","schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"type":"object","properties":{"month":{"type":"string","description":"The month you asked for."},"game":{"type":["string","null"],"description":"The slug you asked for, or null for the overall calendar."},"events":{"type":"array","items":{"$ref":"#/components/schemas/CalendarEvent"}}}}}}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/v1/calendar/{eventNumber}":{"get":{"summary":"One event","tags":["Calendar"],"security":[{}],"parameters":[{"name":"eventNumber","in":"path","required":true,"description":"The event’s public number.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","required":["success","message","data"],"properties":{"success":{"type":"boolean","enum":[true]},"message":{"type":"string","description":"Human-readable summary. Safe to show a person; not safe to branch on."},"data":{"$ref":"#/components/schemas/CalendarEvent"}}}}}},"default":{"$ref":"#/components/responses/Error"}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"An API key, presented as `Authorization: Bearer cb_<prefix>_<secret>`.\n\nKeys are read-only and scoped. A key presented to a route outside its scope is refused with 403 rather than treated as anonymous — silently ignoring a credential somebody deliberately presented is how a misconfigured integration looks healthy.\n\nMint one at https://clubbattles.com/developers."},"sessionToken":{"type":"http","scheme":"bearer","description":"A short-lived HS256 JWT the webapp mints server-side, per request, whose subject is the caller’s Discord id.\n\nIt is documented so the routes that need one are not a blank space in this document, NOT because it is available to you: there is no flow that issues one to a third party, and an API key cannot be exchanged for one. Every route marked with it is therefore out of reach of a key, deliberately — that is what keeps the roster lock, consent and the one-rating-writer rule out of a bearer credential’s reach."}},"responses":{"Error":{"description":"Something went wrong. Read `error.code` where present.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"message":{"type":"string","description":"Human-readable. Safe to show a person."},"data":{"type":"null"},"error":{"description":"Machine-readable detail when there is any. Branch on `error.code`, never on `message`.","oneOf":[{"type":"object","properties":{"code":{"type":"string","description":"A stable code, e.g. `invalid_api_key`, `missing_scope`, `rate_limited`."}}},{"type":"string"},{"type":"null"}]}},"required":["success","message","data"]}}}},"Unauthorised":{"description":"No credential, or a credential that could not be verified — a malformed, unknown, revoked or expired key all answer identically.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"message":{"type":"string","description":"Human-readable. Safe to show a person."},"data":{"type":"null"},"error":{"description":"Machine-readable detail when there is any. Branch on `error.code`, never on `message`.","oneOf":[{"type":"object","properties":{"code":{"type":"string","description":"A stable code, e.g. `invalid_api_key`, `missing_scope`, `rate_limited`."}}},{"type":"string"},{"type":"null"}]}},"required":["success","message","data"]}}}},"Forbidden":{"description":"The credential is valid but does not carry the scope this route needs.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"message":{"type":"string","description":"Human-readable. Safe to show a person."},"data":{"type":"null"},"error":{"description":"Machine-readable detail when there is any. Branch on `error.code`, never on `message`.","oneOf":[{"type":"object","properties":{"code":{"type":"string","description":"A stable code, e.g. `invalid_api_key`, `missing_scope`, `rate_limited`."}}},{"type":"string"},{"type":"null"}]}},"required":["success","message","data"]}}}},"RateLimited":{"description":"Too many requests for this key this minute. `Retry-After` and `RateLimit-Reset` both say when to try again.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"message":{"type":"string","description":"Human-readable. Safe to show a person."},"data":{"type":"null"},"error":{"description":"Machine-readable detail when there is any. Branch on `error.code`, never on `message`.","oneOf":[{"type":"object","properties":{"code":{"type":"string","description":"A stable code, e.g. `invalid_api_key`, `missing_scope`, `rate_limited`."}}},{"type":"string"},{"type":"null"}]}},"required":["success","message","data"]}}}}},"schemas":{"Game":{"type":"object","properties":{"id":{"type":"string","description":"Internal uuid. Addresses nothing publicly — use the slug."},"slug":{"type":"string","description":"Also the subdomain. There is no mapping table; the slug IS the host label."},"name":{"type":"string","description":"Display name."},"lineupSize":{"type":"integer","description":"Players per side — 3 for Rocket League."},"maxRosterSize":{"type":"integer","description":"Cap on active roster members. Per title: never quote one number as the platform’s."},"defaultFormat":{"type":"string","description":"The match best-of, e.g. `bo3`. NOT the queue format, which is the lineup bucket like `3v3`."},"isActive":{"type":"boolean","description":"Whether the title is accepting play."},"isHidden":{"type":"boolean","description":"Taken off every list by staff. Always false in `GET /v1/games`, which leaves hidden titles out; true only when a hidden title is asked for by slug."},"queueMode":{"type":"string","description":"`open`, `closed_beta` (open only to clubs granted access) or `closed`.","enum":["open","closed_beta","closed"]},"queueClosedReason":{"type":["string","null"],"description":"Player-facing reason, carried whenever the queue is not open."},"accentHex":{"type":"string","description":"The title’s accent colour."},"accentContrastHex":{"type":"string","description":"A colour that clears 4.5:1 on the accent."},"iconUrl":{"type":["string","null"],"description":"The title’s mark, or null."},"resultFormat":{"$ref":"#/components/schemas/ResultFormat"},"statFields":{"type":"array","items":{"$ref":"#/components/schemas/StatField"}},"scorer":{"type":["object","null"],"description":"Null when results are typed. Otherwise the match room scores the match itself and the result comes from the scoresheet — `{ kind: \"x01\", config: { startScore, doubleOut } }`. While a scorer is on, `statFields` are the scorer’s."},"camera":{"type":"string","description":"Whether the match room offers a camera between the players. `required` holds a side’s visits back until its camera is on.","enum":["off","optional","required"]}},"required":["id","slug","name","lineupSize","maxRosterSize","isActive","queueMode"]},"Signal":{"type":"object","properties":{"id":{"type":"integer","description":"Cursor: pass the highest one seen as `after`."},"side":{"type":"string","description":"Which side the sender speaks for — from who they are, never from what they sent.","enum":["a","b"]},"fromPeer":{"type":"string","description":"The sending page’s peer id.","format":"uuid"},"toPeer":{"type":["string","null"],"description":"The peer it is for, or null for everybody in the room."},"kind":{"type":"string","description":"What it is.","enum":["hello","offer","answer","bye"]},"role":{"type":["string","null"],"enum":["camera","viewer",null]},"payload":{"type":"object","properties":{}}}},"Scoresheet":{"type":"object","properties":{"fixtureId":{"type":"string","description":"The fixture this match is being scored under.","format":"uuid"},"kind":{"type":"string","description":"The scorer’s kind.","enum":["x01"]},"config":{"type":"object","properties":{"startScore":{"type":"integer","description":"What each leg counts down from."},"doubleOut":{"type":"boolean","description":"Whether a leg must finish on a double."}}},"firstTo":{"type":"integer","description":"Legs to win the match."},"firstStarter":{"type":"string","description":"Which side threw first in leg 1 — a coin toss. The throw alternates leg by leg.","enum":["a","b"]},"sides":{"type":"object","properties":{}},"legs":{"type":"array","items":{"type":"object","properties":{"leg":{"type":"integer","description":"The leg number."},"starter":{"type":"string","description":"Who threw first in this leg.","enum":["a","b"]},"winner":{"type":["string","null"],"enum":["a","b",null]},"visits":{"type":"array","items":{"type":"object","properties":{"seq":{"type":"integer","description":"The visit’s place in the leg."},"side":{"type":"string","description":"Who threw it.","enum":["a","b"]},"score":{"type":"integer","description":"What was entered."},"points":{"type":"integer","description":"What came off — 0 for a bust."},"darts":{"type":"integer","description":"Darts thrown: 3 unless it was a checkout."},"bust":{"type":"boolean","description":"Whether it went past the finish."},"checkout":{"type":"boolean","description":"Whether it won the leg."},"remaining":{"type":"integer","description":"What the side needed afterwards."},"throws":{"type":["array","null"],"description":"The darts, when the visit was entered dart by dart: `{ ring, number? }` each. Null for a visit entered as a total.","items":{"$ref":"#/components/schemas/Dart"}}}}}}}},"legsWon":{"type":"object","properties":{"a":{"type":"integer","description":"Legs won by club A."},"b":{"type":"integer","description":"Legs won by club B."}}},"winner":{"type":["string","null"],"enum":["a","b",null]},"current":{"type":["object","null"],"description":"The next visit: leg, seq, whose throw, and what each side needs. Null once the match is won."},"stats":{"type":"object","properties":{}},"filedMatchSlug":{"type":["string","null"],"description":"The result filed from this scoresheet, once there is one."},"reportableAt":{"type":["string","null"],"description":"When the result may be sent."},"draft":{"type":["array","null"],"description":"The darts the side whose throw it is has picked for this visit and not yet entered, as they tap them — for the other side to watch them land. Display only: it never moves a score. Null when nobody is mid-visit.","items":{"$ref":"#/components/schemas/Dart"}}}},"Dart":{"type":"object","properties":{"ring":{"type":"string","description":"Where the dart landed.","enum":["single","double","treble","outer","bull","miss"]},"number":{"type":"integer","description":"1–20, for a single, double or treble. Absent for the outer bull, the bull and a miss."}},"required":["ring"]},"VisitPreview":{"type":"object","properties":{"leg":{"type":"integer","description":"The leg the visit is in."},"score":{"type":"integer","description":"What the darts add up to."},"points":{"type":"integer","description":"What would come off — 0 for a bust."},"darts":{"type":"integer","description":"Darts it counts as: 3, or the dart a checkout finished on."},"bust":{"type":"boolean","description":"Whether it would go bust."},"checkout":{"type":"boolean","description":"Whether it would win the leg."},"remaining":{"type":"integer","description":"What the side would need afterwards."},"winsMatch":{"type":"boolean","description":"Whether it would win the match."}}},"ResultFormat":{"type":"object","properties":{"unit":{"type":["string","null"],"description":"What one point of a score counts, plural — `legs`, `games`. Null means a plain score."},"maxScore":{"type":"integer","description":"The highest score either side may report in this title."},"allowDraws":{"type":"boolean","description":"Whether a level score is a result."},"firstTo":{"type":["integer","null"],"description":"First to N: the winner has exactly N and the loser fewer. Null means any score within bounds."}},"required":["unit","maxScore","allowDraws","firstTo"]},"StatField":{"type":"object","properties":{"key":{"type":"string","description":"How the value is keyed in a player’s `stats`. Never renamed once stored."},"label":{"type":"string","description":"What to call it."},"kind":{"type":"string","description":"`integer` for a count, `decimal` for a measure.","enum":["integer","decimal"]},"min":{"type":"number","description":"Lowest accepted value."},"max":{"type":"number","description":"Highest accepted value."}},"required":["key","label","kind","min","max"]},"PlayerStats":{"type":"object","properties":{"player":{"type":"integer","description":"A player number in the lineup being filed."},"values":{"type":"object","properties":{}}},"required":["player","values"]},"Club":{"type":"object","properties":{"id":{"type":"string","description":"Internal uuid."},"clubNumber":{"type":"integer","description":"The club’s public, routable identifier."},"slug":{"type":"string","description":"Retained for logs and historical audit subjects. NOT routable — a slug URL 404s by design."},"name":{"type":"string","description":"Display name. Changing it does not move the slug."},"tag":{"type":"string","description":"Short badge, e.g. `RSC`."},"ownerUserId":{"type":["string","null"],"description":"The founding account."},"isArchived":{"type":"boolean","description":"Archived clubs are excluded from every list but keep their identity and history."},"createdAt":{"type":"string","description":"ISO 8601, UTC.","format":"date-time"}},"required":["id","clubNumber","name","tag","isArchived"]},"LadderEntry":{"type":"object","properties":{"clubNumber":{"type":"integer","description":"The club."},"name":{"type":"string","description":"Club name."},"tag":{"type":"string","description":"Club tag."},"rating":{"type":"integer","description":"Elo. Every club starts a season at 1000."},"played":{"type":"integer","description":"Rated matches this season."},"won":{"type":"integer","description":"Rated wins this season."},"lost":{"type":"integer","description":"Rated losses this season."},"tier":{"type":["string","null"],"description":"Exposed only for clubs active this season."},"rank":{"type":"integer","description":"Position, 1-based."}},"required":["clubNumber","name","rating","played"]},"OverallLadderEntry":{"type":"object","properties":{"clubNumber":{"type":"integer","description":"The club."},"name":{"type":"string","description":"Club name."},"tag":{"type":"string","description":"Club tag."},"score":{"type":"number","description":"Sum over the titles the club has a rated match in this season of (rating − 1000). Can be negative on purpose — it is derived, never stored."},"played":{"type":"integer","description":"Rated matches this season across every title. The first tie-break."},"games":{"type":"integer","description":"How many titles contributed."},"rank":{"type":"integer","description":"Position, 1-based."}},"required":["clubNumber","name","score","played"]},"RatingHistoryEntry":{"type":"object","properties":{"matchSlug":{"type":["string","null"],"description":"The match that moved it, when there was one."},"ratingBefore":{"type":"integer","description":"Elo before the match."},"ratingAfter":{"type":"integer","description":"Elo after the match."},"delta":{"type":"integer","description":"The movement. `ratingAfter − ratingBefore`."},"createdAt":{"type":"string","description":"When the rating was applied — which for a resolved dispute is resolution time, not the original match time.","format":"date-time"}},"required":["ratingBefore","ratingAfter","createdAt"]},"Member":{"type":"object","properties":{"userNumber":{"type":"integer","description":"The player."},"displayName":{"type":"string","description":"From Discord, overwritten at every sign-in."},"avatarUrl":{"type":["string","null"],"description":"Discord avatar."},"joinedAt":{"type":"string","description":"When they joined this roster.","format":"date-time"}},"required":["userNumber","displayName"]},"PlayerProfile":{"type":"object","properties":{"userNumber":{"type":"integer","description":"The player’s public number."},"displayName":{"type":"string","description":"From Discord."},"avatarUrl":{"type":["string","null"],"description":"Discord avatar."},"club":{"oneOf":[{"$ref":"#/components/schemas/Club"},{"type":"null"}],"description":"The club they field for in this title, or null."},"played":{"type":"integer","description":"Rated matches this season."},"won":{"type":"integer","description":"Rated wins this season."}},"required":["userNumber","displayName"]},"MatchSummary":{"type":"object","properties":{"slug":{"type":"string","description":"The match’s address."},"state":{"type":"string","description":"`pending`, `completed`, `disputed`, `declined` or `voided`."},"clubA":{"$ref":"#/components/schemas/Club"},"clubB":{"$ref":"#/components/schemas/Club"},"scoreA":{"type":["integer","null"],"description":"Club A’s score. ALWAYS club A then club B — never the reporter’s point of view."},"scoreB":{"type":["integer","null"],"description":"Club B’s score."},"playedAt":{"type":["string","null"],"description":"When it was played."},"completedAt":{"type":["string","null"],"description":"When it entered `completed`, which is when rating applied."},"scored":{"type":"boolean","description":"Pending results only: whether it was sent from the match room’s scoresheet. Confirming one takes the lineup and stats from the scoresheet, so a lineup sent with it is ignored."}},"required":["slug","state"]},"Match":{"type":"object","properties":{"slug":{"type":"string","description":"The match’s address."},"state":{"type":"string","description":"`pending`, `completed`, `disputed`, `declined` or `voided`."},"gameSlug":{"type":"string","description":"The title."},"clubA":{"$ref":"#/components/schemas/Club"},"clubB":{"$ref":"#/components/schemas/Club"},"scoreA":{"type":["integer","null"],"description":"Club A’s score."},"scoreB":{"type":["integer","null"],"description":"Club B’s score."},"lineupA":{"type":"array","items":{"$ref":"#/components/schemas/Member"}},"lineupB":{"type":"array","items":{"$ref":"#/components/schemas/Member"}},"ratingDeltaA":{"type":["integer","null"],"description":"Elo movement for club A, present once rated."},"ratingDeltaB":{"type":["integer","null"],"description":"Elo movement for club B, present once rated."},"reportedAt":{"type":["string","null"],"description":"When a result was filed."},"completedAt":{"type":["string","null"],"description":"When it entered `completed`."}},"required":["slug","state","clubA","clubB"]},"Fixture":{"type":"object","properties":{"id":{"type":"string","description":"The fixture’s uuid.","format":"uuid"},"state":{"type":"string","description":"`proposed`, `accepted`, `completed`, `declined` or `expired`. The last three are terminal, and reaching one is what frees both clubs to queue again."},"clubA":{"$ref":"#/components/schemas/Club"},"clubB":{"$ref":"#/components/schemas/Club"},"acceptedA":{"type":"boolean","description":"Whether club A has accepted."},"acceptedB":{"type":"boolean","description":"Whether club B has accepted."},"createdAt":{"type":"string","description":"When the pairing was made.","format":"date-time"},"reportableAt":{"type":["string","null"],"description":"When reporting opens — set at the second acceptance. Null means not yet."}},"required":["id","state","clubA","clubB"]},"QueueSnapshot":{"type":"object","properties":{"queueMode":{"type":"string","description":"`open`, `closed_beta` or `closed`.","enum":["open","closed_beta","closed"]},"queueClosedReason":{"type":["string","null"],"description":"Why, when not open."},"waiting":{"type":"integer","description":"Clubs currently searching."}},"required":["queueMode","waiting"]},"Invitation":{"type":"object","properties":{"id":{"type":"string","description":"The invitation’s uuid.","format":"uuid"},"club":{"$ref":"#/components/schemas/Club"},"gameSlug":{"type":"string","description":"The title the roster seat is in."},"invitedByName":{"type":["string","null"],"description":"Who sent it."},"createdAt":{"type":"string","description":"When it was sent.","format":"date-time"}},"required":["id","club","gameSlug"]},"Profile":{"type":"object","properties":{"id":{"type":"string","description":"Internal uuid."},"userNumber":{"type":"integer","description":"Public player number."},"displayName":{"type":"string","description":"From Discord. Read-only — `users/sync` overwrites it at every sign-in."},"avatarUrl":{"type":["string","null"],"description":"Discord avatar."},"bio":{"type":["string","null"],"description":"Free text, the caller’s own."},"timezone":{"type":["string","null"],"description":"IANA zone. NULL means never asked, and is what the profile-setup gate reads."},"notificationPrefs":{"type":"object","properties":{}},"permissions":{"type":"array","items":{"type":"string","description":"A platform permission the caller holds. For drawing UI only — every admin route re-checks server-side."}}},"required":["id","userNumber","displayName"]},"ApiKey":{"type":"object","properties":{"id":{"type":"string","description":"The key’s uuid. What you pass to revoke it.","format":"uuid"},"name":{"type":"string","description":"What the owner called it."},"prefix":{"type":"string","description":"The public half of the token, e.g. `a3f9c1d20b7e`. Safe to log; it identifies the key without being it."},"scopes":{"type":"array","items":{"type":"string","description":"A scope the key carries.","enum":["ladder.read","clubs.read","players.read","matches.read","queue.read"]}},"expiresAt":{"type":["string","null"],"description":"When it stops working on its own, or null for never."},"lastUsedAt":{"type":["string","null"],"description":"Accurate to the minute, not a request log — the column is written at most once a minute per key."},"createdAt":{"type":"string","description":"When it was minted.","format":"date-time"},"revokedAt":{"type":["string","null"],"description":"When it was revoked, or null while live."}},"required":["id","name","prefix","scopes","createdAt"]},"RoadmapItem":{"type":"object","properties":{"id":{"type":"string","description":"Internal uuid. Addresses nothing publicly — use `itemNumber`."},"itemNumber":{"type":"integer","description":"The public, routable identifier: https://clubbattles.com/roadmap/{itemNumber}."},"title":{"type":"string","description":"One line."},"body":{"type":"string","description":"The detail, possibly empty."},"area":{"type":"string","description":"Which part of the product it is about.","enum":["ladder","queue","clubs","matches","players","discord","circuits","mobile","developers","general"]},"status":{"type":"string","description":"`open` is an idea waiting for an answer; `planned`, `in_progress` and `shipped` are the board; `declined` carries the reason in `reply`.","enum":["open","planned","in_progress","shipped","declined"]},"origin":{"type":"string","description":"`community` when somebody suggested it, `staff` when ClubBattles posted it.","enum":["community","staff"]},"author":{"type":["object","null"],"description":"Who suggested it. Null for entries ClubBattles posted, and for a suggestion whose author’s account is gone.","properties":{"userNumber":{"type":"integer","description":"The author’s player number."},"displayName":{"type":"string","description":"From Discord."}}},"target":{"type":["string","null"],"description":"A loose “when” for work not yet shipped — a word like `Next`, never a promised date."},"reply":{"type":["string","null"],"description":"ClubBattles’ public reply. Always present on a declined idea."},"repliedAt":{"type":["string","null"],"description":"When that reply was last written."},"shippedOn":{"type":["string","null"],"description":"`YYYY-MM-DD`, UTC. Set exactly when `status` is `shipped`; this is what the changelog sorts on."},"votes":{"type":"integer","description":"Derived from the vote rows on every read, never stored. Frozen once an item ships or is declined."},"viewerVoted":{"type":"boolean","description":"Whether the signed-in caller has voted for it. Always false without a session."},"viewerIsAuthor":{"type":"boolean","description":"Whether the signed-in caller suggested it. Always false without a session."},"statusChangedAt":{"type":"string","description":"When the status last moved.","format":"date-time"},"createdAt":{"type":"string","description":"When it was suggested or posted.","format":"date-time"},"hiddenAt":{"type":["string","null"],"description":"Always null here — hidden items are not returned at all."}},"required":["id","itemNumber","title","body","area","status","origin","votes","viewerVoted","viewerIsAuthor","createdAt"]},"CalendarEvent":{"type":"object","properties":{"eventNumber":{"type":"integer","description":"The public identifier: https://clubbattles.com/calendar?event={eventNumber}."},"title":{"type":"string","description":"One line."},"body":{"type":"string","description":"The detail, possibly empty."},"kind":{"type":"string","description":"What sort of event it is.","enum":["tournament","community","maintenance","announcement"]},"startsOn":{"type":"string","description":"`YYYY-MM-DD`, a UTC calendar day."},"endsOn":{"type":["string","null"],"description":"`YYYY-MM-DD`, the last day of a multi-day event; null for a single day."},"startsAt":{"type":["string","null"],"description":"`HH:MM`, 24-hour, UTC; null for an all-day event."},"game":{"type":["object","null"],"description":"The title it is for, or null for every title.","properties":{"slug":{"type":"string","description":"The title’s slug."},"name":{"type":"string","description":"The title’s name."}}},"link":{"type":["string","null"],"description":"Somewhere to read more or sign up — an http(s) URL."},"published":{"type":"boolean","description":"Always true here — drafts are not returned."},"createdAt":{"type":"string","description":"When it was added.","format":"date-time"},"updatedAt":{"type":"string","description":"When it last changed.","format":"date-time"}},"required":["eventNumber","title","body","kind","startsOn","published"]}}}}