Skip to content
FLICKED Docs

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#

CallerSendsResolved by
PlayerAuthorization: Bearer <token> from the launcherCurrentPlayer
AdminThe flicked.session HttpOnly cookie from the dashboard, for a player listed in FLICKED_ADMIN_STEAM_IDSCurrentPlayer + Admins
ServerX-Server-Token, issued when an admin registers the serverServerAuth
MatchX-Match-Token, minted per match and sent to the server over RCONCompared 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#

  1. Open the sign-in page

    The launcher listens on a free loopback port and opens /auth/steam/login?port=<port> in the system browser.

  2. 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.

  3. 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#

GET/auth/steam/login?port={port}PublicStarts launcher sign-in. Redirects to Steam
GET/auth/steam/callbackPublicSteam returns here. Verifies, finds or creates the player, redirects to the launcher's loopback port with a code
GET/auth/web/login?returnTo={path}PublicStarts dashboard sign-in
GET/auth/web/callbackPublicSets the HttpOnly session cookie and redirects back to the dashboard
POST/auth/exchangePublicTrades a one-time code for a session token
GET/auth/mePlayerYou: id, name, avatar, rating, division, admin flag, and derived stats (matches, win rate, K/D, damage per round)
POST/auth/logoutPlayerEnds the current session

Public#

GET/api/newsThe news feed every launcher reads. Posts are live the moment they are saved
GET/api/news/categoriesAllowed categories as { slug, label }: patch, update, event
GET/api/leaderboardRanked players with rating and division

Queue#

All Player. The queue holds parties, so the leader's join puts the whole party in it. Accepting and voting stay personal.

GET/api/queueWhere you are, in one shape for every phase (below)
POST/api/queueJoin, with { "mode": "Competitive" | "Wingman" }. Leader only. 409 if the party is too big for the mode, already queued, or anyone is in a match
DELETE/api/queueLeave the queue
POST/api/queue/acceptAccept the match you were offered
POST/api/queue/declineDecline it, which cancels it for everyone
POST/api/queue/voteVote for a map, with { "map": "de_mirage" }
GET /api/queue
{
  "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.

GET/api/partyYour party, its members, and your invites
POST/api/partyCreate one. Usually implicit, since inviting creates it
POST/api/party/invite/{playerId}Leader only. The invitee must be a friend. At most ten pending invites
POST/api/party/invites/{partyId}/acceptAccept an invite. Refused if the party is already a full team
DELETE/api/party/invites/{partyId}?playerId={id}Decline an invite sent to you, or (leader, with playerId) cancel one you sent
DELETE/api/party/members/{playerId}Leave (your own id) or kick (leader)

Friends and social#

All Player.

GET/api/social/stateFriends, requests, party and invites in one response, with an ETag. Polled every 5s while focused; unchanged state answers 304
GET/api/friendsYour friends with name, avatar and rating
GET/api/friends/requestsPending requests, incoming and outgoing
GET/api/friends/search?q={name}Find players to add
POST/api/friends/requests/{playerId}Send a friend request
POST/api/friends/requests/{playerId}/acceptAccept one
DELETE/api/friends/requests/{playerId}Decline an incoming request or cancel an outgoing one
DELETE/api/friends/{playerId}Remove a friend

Matches#

GET/api/matchesPlayerYour match history: result (W, L or D), score from your side, K/D, damage per round and rating delta

Admin: 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.

GET/api/admin/serversThe pool: status, current match, last seen
POST/api/admin/serversRegister a server. The RCON password is encrypted at rest
PATCH/api/admin/servers/{id}Edit any field; omitted fields are left as they are. 409 while the server is in a match
DELETE/api/admin/servers/{id}Remove a server from the pool
POST/api/admin/servers/{id}/tokenRotate the server token, when the old one is lost or should stop working
POST/api/admin/servers/{id}/start-matchPoint this server at a match by hand, with { "matchId": 27 }
POST /api/admin/servers
{
  "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.

POST/api/admin/newsPublish a post. Its id is a slug made from the title
PATCH/api/admin/news/{id}Edit one. The id stays put when the title is corrected
DELETE/api/admin/news/{id}Remove one
POST /api/admin/news
{
  "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.

GET/api/matches/{id}/configMatchThe match in MatchZy's config format: roster by Steam64 ID, voted map, and where to report
POST/api/matches/eventsServer or MatchEvery MatchZy event. Idempotent: a repeated series_end changes nothing
POST/api/servers/heartbeatServer"Still here", about every 30 seconds. Brings an Offline server back to Idle. Returns { status, matchId }
Esc
↑↓ navigate ↵ open