Reference
API reference
Every HTTP endpoint the backend exposes, grouped by who calls it. The launcher, the dashboard and CS2 servers each authenticate differently, and no endpoint accepts more than one kind of caller.
The API serves on http://localhost:5165 in development. In the Development environment an OpenAPI
document is mapped as well. Request and response bodies are JSON with camelCase properties.
Authentication#
| Caller | Sends | Resolved by |
|---|---|---|
| Player | Authorization: Bearer <token> from the launcher | CurrentPlayer |
| Admin | The flicked.session HttpOnly cookie from the dashboard, for a player listed in FLICKED_ADMIN_STEAM_IDS | CurrentPlayer + Admins |
| Server | X-Server-Token, issued when an admin registers the server | ServerAuth |
| Match | X-Match-Token, minted per match and sent to the server over RCON | Compared against Matches.ConfigTokenHash |
Player endpoints accept either a bearer token or the cookie, since both resolve to the same
Sessions row. Every action works on the signed-in player: there is no "on behalf of" anywhere, so
the launcher cannot queue, accept or vote for somebody else. All tokens are stored as SHA-256 hashes.
Getting a launcher token#
- Open the sign-in page
The launcher listens on a free loopback port and opens
/auth/steam/login?port=<port>in the system browser. - Steam verifies the player
The backend checks the assertion with Steam server to server, then redirects to
127.0.0.1:<port>?code=…with a single-use code that expires in two minutes. - Exchange the code
POST /auth/exchange
{ "code": "…" }200 OK{ "token": "…", "expiresAt": "2026-11-09T18:00:00Z", "playerId": 12, "name": "Nyx", "steamId": "76561198000000000", "avatarUrl": "https://avatars.steamstatic.com/…" }The token is shown once. The launcher keeps it in the OS keychain, on the Rust side.
Auth#
Public#
{ slug, label }: patch, update, eventQueue#
All Player. The queue holds parties, so the leader's join puts the whole party in it. Accepting and voting stay personal.
{ "mode": "Competitive" | "Wingman" }. Leader only. 409 if the party is too big for the mode, already queued, or anyone is in a match{ "map": "de_mirage" }{
"phase": "vote", // idle | searching | found | vote | connecting | live
"mode": "Competitive",
"since": "2026-10-10T19:04:12Z", // when this phase began; the launcher counts up from it
"matchId": 27,
"accepted": 10,
"needed": 10,
"youAccepted": true,
"yourVote": "de_mirage",
"map": null, // once the vote is settled
"connect": null, // host:port, once there is a server
"connectPassword": null,
"votes": { "de_mirage": 4, "de_inferno": 3 }
}Party#
All Player. Rules and reasoning are in Matchmaking & parties.
playerId) cancel one you sentFriends and social#
All Player.
304Matches#
W, L or D), score from your side, K/D, damage per round and rating deltaAdmin: servers#
All Admin. The API never hands a secret back: responses say whether an RCON or game
password is stored (hasRcon, hasGamePassword), never what it is.
409 while the server is in a match{ "matchId": 27 }{
"name": "EU #1",
"region": "eu-west",
"type": "Competitive", // Competitive (5v5) or Wingman (2v2)
"host": "203.0.113.10",
"port": 27015,
"rconPassword": "…",
"gamePassword": null,
"isEnabled": true
}Admin: news#
All Admin. There is no admin GET on purpose: the dashboard reads the public
/api/news, so it can never show something the launcher would not.
{
"category": "patch",
"title": "Parties are here",
"excerpt": "Queue with up to four friends.",
"body": ["First paragraph.", "Second paragraph."],
"date": "2026-10-03",
"badge": "v0.1"
}Game servers#
Called by CS2 servers, never by people. Nothing here accepts a player session: a person should never be able to speak as a server, and a server has no business reading a player's data. See MatchZy integration for payloads.
series_end changes nothing{ status, matchId }