From 319b61f8dac635a76308762b1c9eec696e12f826 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Sun, 2 Aug 2026 18:43:55 +0100 Subject: [PATCH 01/13] rework and add public facing endpoints --- docs/API.md | 1538 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 1341 insertions(+), 197 deletions(-) diff --git a/docs/API.md b/docs/API.md index 1497312de8..a841f07c35 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,284 +1,1428 @@ -# API Usage +# OpenFront API + +This is the public HTTP and WebSocket API exposed by the OpenFront API +worker. It documents endpoints intended for the game client, public websites, +and player integrations. It is kept aligned with the route registry and +endpoint schemas in the infra repository. + +## Base URLs + +Production: + + https://api.openfront.io + +Development: + + https://api.openfront.dev + +All examples below are relative to one of these hosts. JSON responses use +UTF-8 and the API sends CORS credentials when the request origin is allowed. +The API exposes Content-Range and accepts these request headers: + +- Content-Type +- Authorization +- x-persistent-id +- Idempotency-Key +- Content-Encoding + +## Quick reference + +### Public endpoints + +| Method | Path | Purpose | +| ------ | --------------------------------- | --------------------------------------------------- | +| GET | /.well-known/jwks.json | JWT verification keys | +| GET | /ping | Health check | +| GET | /cosmetics.json | Public shop/catalog configuration | +| GET | /news.json | Published news | +| GET | /featured-stream.json | Featured stream configuration | +| GET | /live-streams.json | Current live-stream configuration | +| GET | /public/games | Archived game summaries | +| GET | /public/game/:gameId | Archived game record | +| GET | /game/:gameId | Legacy alias for the archived game record | +| GET | /public/player/:publicId | Public player profile | +| GET | /player/:publicId | Legacy alias for the public player profile | +| GET | /public/player/:publicId/sessions | Public player sessions | +| GET | /public/player/:publicId/games | Public player game history | +| GET | /public/clans/leaderboard | Rolling clan leaderboard | +| GET | /public/clan/:clanTag | Clan statistics | +| GET | /public/clan/:clanTag/exists | Clan existence check | +| GET | /public/clan/:clanTag/sessions | Clan game sessions | +| GET | /leaderboard/public/ffa | Public free-for-all leaderboard | +| GET | /leaderboard/ranked | Ranked 1v1 and 2v2 leaderboards | +| GET | /leaderboard/tribes | Custom tribe-name leaderboard | +| GET | /users/:persistentId | Anonymous persistent-player lookup | +| GET | /matchmaking/join | Matchmaking WebSocket | +| GET | /public/\* | Public asset fallback, mainly for local development | + +The OAuth and session endpoints are also unauthenticated at the HTTP layer: + +| Method | Path | Purpose | +| ------ | ---------------------- | ----------------------------------- | +| POST | /auth/logout | Clear the current refresh session | +| POST | /auth/refresh | Refresh or create a guest session | +| POST | /auth/revoke | Revoke the current provider session | +| GET | /auth/login/discord | Start Discord login | +| GET | /auth/login/google | Start Google login | +| GET | /auth/login/token | Consume a one-time login token | +| POST | /auth/magic-link | Send a magic-link login | +| GET | /auth/callback/discord | Discord OAuth callback | +| GET | /auth/callback/google | Google OAuth callback | +| POST | /auth/crazygames | Exchange a CrazyGames token | +| POST | /auth/steam | Exchange a Steam ticket | + +### Authenticated player endpoints + +Every endpoint in this table requires a user JWT unless noted otherwise. +Clan endpoints additionally require the role shown in the detailed reference. + +| Method | Path | Required role or purpose | +| ------------ | ---------------------------------- | ----------------------------------- | +| GET, POST | /users/@me | Read or change profile visibility | +| PUT | /users/@me/username | Change username | +| GET, POST | /users/@me/tribe_names | Read or buy custom tribe names | +| POST | /users/@me/tribe_names/:id/boosts | Boost one owned tribe name | +| GET, POST | /marketing/consent | Read or change email consent | +| GET | /auth/link/google | Start Google account linking | +| POST | /colors/random | Generate a random player color | +| POST | /archive_singleplayer_game | Archive a browser singleplayer game | +| POST | /flares_granted/temporary | Grant a short-lived cosmetic trial | +| GET | /friends | List friends | +| GET | /friends/requests | List friend requests | +| POST, DELETE | /friends/requests/:publicId | Create or withdraw a request | +| POST | /friends/requests/:publicId/accept | Accept a request | +| DELETE | /friends/:publicId | Remove a friend | +| GET | /clans | Browse clans | +| GET | /clans/:clanTag | Read clan details | +| GET | /clans/:clanTag/members | Member list (member) | +| GET | /clans/:clanTag/games | Clan game history (member) | +| PATCH | /clans/:clanTag | Update clan (officer) | +| DELETE | /clans/:clanTag | Disband clan (leader) | +| POST | /clans/:clanTag/join | Join or request to join | +| POST | /clans/:clanTag/leave | Leave clan (member) | +| POST | /clans/:clanTag/kick | Kick a member (officer) | +| POST | /clans/:clanTag/ban | Ban a player (officer) | +| POST | /clans/:clanTag/unban | Remove a clan ban (officer) | +| POST | /clans/:clanTag/promote | Promote a member (leader) | +| POST | /clans/:clanTag/demote | Demote an officer (leader) | +| POST | /clans/:clanTag/transfer | Transfer leadership (leader) | +| GET | /clans/:clanTag/bans | List bans (officer) | +| GET | /clans/:clanTag/requests | List join requests (officer) | +| POST | /clans/:clanTag/requests/approve | Approve a join request (officer) | +| POST | /clans/:clanTag/requests/deny | Deny a join request (officer) | +| POST | /clans/:clanTag/requests/withdraw | Withdraw your join request | +| POST | /subscriptions/@me/cancel | Cancel a subscription | +| POST | /subscriptions/@me/change-tier | Change subscription tier | +| POST | /subscriptions/@me/portal | Create a billing-portal session | +| POST | /rewards/claim-all | Claim all available rewards | +| POST | /rewards/:rewardId/claim | Claim one reward | +| POST | /shop/purchase | Spend in-game currency | + +## Common conventions + +### Authentication + +Send a short-lived player JWT as: + + Authorization: Bearer + +The API also sets an HttpOnly refresh-session cookie. Access tokens expire +after about 15 minutes; refresh sessions expire after about 30 days. Browser +clients should send credentials on cross-origin requests. + +The JWKS endpoint is: + + GET /.well-known/jwks.json + +It returns a standard JSON Web Key Set for verifying API-issued JWTs. JWT +claims include a subject, issuer, audience, issued-at time, expiry, and a +session identifier (jti). Do not put refresh cookies or signing keys in +client-visible documentation or logs. + +### Responses and errors + +Successful JSON responses are normally 200. Other success statuses used by +the API are: + +- 201 for a created resource +- 202 when an operation is accepted for later processing +- 204 when there is no response body + +Errors are JSON objects. Common shapes are: + + { "error": "Bad request", "message": "..." } + { "error": "Unauthorized", "message": "..." } + { "error": "Forbidden", "message": "..." } + { "error": "Not found", "message": "..." } + { "error": "Conflict", "message": "..." } + { "error": "Too many requests", "message": "..." } + +Some validation errors add fields such as code, reason, or details. A 429 may +also include Retry-After. + +### Dates, identifiers, and pagination + +Unless an endpoint says otherwise, timestamps are ISO 8601 strings. Public +player references accept a public ID, a full display name in base.disc form, +or a bare premium name. Player references are limited to 25 characters. + +Page-based endpoints use page numbers starting at 1. Cursor values are opaque: +clients must not parse or manufacture them, and should retain the cursor +alongside the filters that produced it. + +## Authentication and sessions + +### Refresh, logout, and revoke + +#### POST /auth/refresh + +Refresh the session represented by the refresh cookie. If there is no refresh +cookie, the endpoint creates a guest session and sets one. + +Response: -> **Warning:** Rate limits are very strict. Join the [Discord](https://discord.gg/K9zernJB5z) to request higher rate limits. + { + "jwt": "eyJ...", + "expiresIn": 900 + } -## Games +#### POST /auth/logout -### List Game Metadata +Deletes the current refresh session and clears the refresh cookie. The response +has no body. -Get game IDs and basic metadata for games that started within a specified time range. Results are sorted by start time and paginated. +#### POST /auth/revoke -**Constraints:** +Deletes the current refresh session. For a Discord-backed session it also +revokes the provider token when available. The response has no body. -- Maximum time range: 2 days -- Maximum limit per request: 1000 games +### OAuth login -**Endpoint:** +#### GET /auth/login/discord -``` -GET https://api.openfront.io/public/games -``` +#### GET /auth/login/google -**Query Parameters:** +Query parameters: -- `start` (required): ISO 8601 timestamp -- `end` (required): ISO 8601 timestamp -- `type` (optional): Game type, must be one of `[Private, Public, Singleplayer]` -- `mode` (optional): Game mode, must be one of `[Free For All, Team]` -- `rankedType` (optional): Ranked type, must be one of `[unranked, 1v1, 2v2]` -- `playerTeams` (optional): Player team configuration (e.g. `Duos`) -- `limit` (optional): Number of results (max 1000, default 50) -- `offset` (optional): Pagination offset +- redirect_uri: an allowlisted callback destination -**Example Request:** +The API generates and stores the OAuth state; callers do not supply it. +These endpoints redirect to the provider. The corresponding callbacks are: -```bash -curl "https://api.openfront.io/public/games?start=2025-10-25T00:00:00Z&end=2025-10-26T23:59:59Z&type=Public&mode=Team&rankedType=unranked&limit=10&offset=5" -``` +#### GET /auth/callback/discord -**Response:** +#### GET /auth/callback/google -```json -[ - { - "game": "ABSgwin6", - "start": "2025-10-25T00:00:10.526Z", - "end": "2025-10-25T00:19:45.187Z", - "type": "Public", - "mode": "Team", - "difficulty": "Medium", - "numPlayers": 6, - "maxPlayers": 8, - "lobbyFillTime": 45000, - "playerTeams": "Duos", - "rankedType": "unranked" - } -] -``` +The callbacks validate the state and provider response, set the refresh +cookie, and redirect to the original allowlisted destination. Integrations +should use the documented redirect flow rather than expecting a JWT in a URL +fragment. -The response includes a `Content-Range` header indicating pagination (e.g., `games 5-15/399`). +### One-time login links ---- +#### POST /auth/magic-link -### Get Game Info +Body: -Retrieve detailed information about a specific game. + { + "email": "player@example.com", + "redirectDomain": "https://example.com" + } -**Endpoint:** +The endpoint sends a one-time link when the address is eligible. The token is +valid for 15 minutes. -``` -GET https://api.openfront.io/public/game/:gameId -``` +#### GET /auth/login/token?login-token= -**Query Parameters:** +Consumes the one-time token, sets the refresh cookie, and returns the +authenticated email. A successful response is: -- `turns` (optional): Set to `false` to exclude turn data and reduce response size + { "email": "player@example.com" } -**Examples:** +A token cannot be reused. This endpoint does not perform an additional +redirect; the link's client can navigate after receiving the response. -```bash -# Full game data -curl "https://api.openfront.io/public/game/ABSgwin6" +### Platform login -# Without turn data -curl "https://api.openfront.io/public/game/ABSgwin6?turns=false" -``` +#### POST /auth/crazygames -**Note:** Public player IDs are stripped from game records for privacy. +Body: -## Players + { "token": "crazygames-sdk-token" } -### Get Player Info +#### POST /auth/steam -Retrieve information and stats for a specific player. +Body: -**Endpoint:** + { "ticket": "steamworks-auth-ticket" } -``` -GET https://api.openfront.io/public/player/:playerId -``` +Both successful exchanges return the same session shape: -**Example:** + { + "jwt": "eyJ...", + "expiresIn": 900 + } -```bash -curl "https://api.openfront.io/public/player/HabCsQYR" -``` +They also set the refresh-session cookie. -### Get Player Sessions +### Link Google -Retrieve a list of games & client ids (session ids) for a specific player. +#### GET /auth/link/google?redirect_uri= -**Endpoint:** +Requires a user JWT. Returns: -``` -GET https://api.openfront.io/public/player/:playerId/sessions -``` + { "url": "https://accounts.google.com/..." } -**Example:** +The returned URL starts the linking flow. The callback redirects with a +completion status such as link=google, cancel, already_linked, or error. -```bash -curl "https://api.openfront.io/public/player/HabCsQYR/sessions" -``` +## Public games and players -### Get Player Games +### GET /public/games -Retrieve a player's personal game history, newest first. Uses keyset (cursor) -pagination rather than the `page`/`limit` scheme used elsewhere. +Lists archived games in ascending start-time order. -**Endpoint:** +Required query parameters: -``` -GET https://api.openfront.io/public/player/:playerId/games -``` +- start: ISO 8601 lower bound +- end: ISO 8601 upper bound -**Query Parameters:** +The requested range may be at most two days. Optional filters: -- `filter` (optional): Mode bucket, one of `[ffa, team, hvn, ranked]`. Omit for all modes. -- `type` (optional): Game type, one of `[public, private, singleplayer]`. Omit for all types. `filter` and `type` are orthogonal and may be combined. -- `cursor` (optional): Opaque continuation token. Pass the `nextCursor` value from the previous response verbatim to fetch the next page — do not construct or parse it. +- type: Singleplayer, Public, or Private +- mode: Free For All or Team +- rankedType: unranked, 1v1, or 2v2 +- playerTeams: team filter, up to 20 characters +- limit: 1–1000, default 50 +- offset: non-negative integer, default 0 -**Response:** +Each result contains: -```json -{ - "results": [ { - "gameId": "abc123", - "start": "2026-05-17T21:04:00.000Z", - "durationSeconds": 1234, - "map": "World", - "mode": "Team", + "game": "game-id", + "start": "2026-01-01T12:00:00.000Z", + "end": "2026-01-01T12:20:00.000Z", "type": "Public", - "playerTeams": "Duos", - "rankedType": "unranked", - "result": "victory", - "totalPlayers": 8, - "username": "alice", - "clanTag": "ABC" + "mode": "Team", + "difficulty": null, + "numPlayers": 10, + "maxPlayers": 20, + "lobbyFillTime": 15, + "playerTeams": "2v2", + "rankedType": "unranked" + } + +Values such as end, difficulty, player counts, lobbyFillTime, +playerTeams, and rankedType may be null. The response includes: + + Content-Range: games -/ + +### GET /public/game/:gameId + +### GET /game/:gameId + +Returns the archived GameRecord for a game. The second path is a legacy alias. +Pass turns=false to omit the potentially large turns array: + + GET /public/game/?turns=false + +The identifier may be an older eight-character ID or a newer encoded game ID. +Unknown games return 404. + +### GET /public/player/:publicId + +### GET /player/:publicId + +Returns a public player profile. The second path is a legacy alias. A profile +contains: + +- publicId and createdAt +- username, when one is set +- the linked Discord user object when the player has made the profile public +- aggregated stats, including wins, losses, total games, and nested + FFA/team/Humans-vs-Nations/ranked counters +- current clan memberships with tag, name, role, joinedAt, and memberCount + +Private Discord identity data is omitted for a private profile. The profile +stats exclude singleplayer games from the public unranked aggregates. + +### GET /public/player/:publicId/sessions + +Returns the player's recorded sessions. A result has this shape: + + { + "gameId": "game-id", + "gameStart": "2026-01-01T12:00:00.000Z", + "gameEnd": "2026-01-01T12:20:00.000Z", + "gameType": "Public", + "gameMode": "Team", + "gameRankedType": "unranked", + "clientId": "client-id", + "username": "Player", + "clanTag": "ABC", + "hasWon": true + } + +Nullable session fields may be null. A player with no sessions returns 404. + +### GET /public/player/:publicId/games + +Returns a keyset-paginated public game history. The page size is fixed at 10. + +Query parameters: + +- filter: ffa, team, hvn, or ranked +- type: public, private, or singleplayer +- cursor: opaque cursor from the previous response + +The cursor is tied to filter and type; changing either while reusing a cursor +returns 400. The response is: + + { + "results": [ + { + "gameId": "game-id", + "start": "2026-01-01T12:00:00.000Z", + "durationSeconds": 1200, + "map": "map-id", + "mode": "Team", + "type": "Public", + "playerTeams": "2v2", + "rankedType": "unranked", + "result": "victory", + "totalPlayers": 10, + "username": "Player", + "clanTag": "ABC" + } + ], + "nextCursor": "opaque-cursor-or-null" + } + +result is victory, defeat, or incomplete. totalPlayers, playerTeams, and +clanTag can be null. username and clanTag reflect the identity recorded in +that game session. Unknown players return 404. + +### GET /users/:persistentId + +Looks up the public player ID associated with an anonymous persistent player. +The path accepts a UUID or the literal REDACTED. A successful response is: + + { "player": { "publicId": "..." } } + +This endpoint only resolves anonymous players without a linked Discord +identity. Other cases return 404. + +## Public clans and leaderboards + +Clan tags are case-insensitive in lookup paths and are returned uppercase. +Public clan statistics use only public, unranked Team games and exclude Humans +vs Nations games. + +### GET /public/clan/:clanTag/exists + +Returns a minimal existence check: + + { "exists": true } + +An existing clan returns 200. A missing clan returns 404. + +### GET /public/clan/:clanTag + +Required query parameters: + +- start: ISO 8601 interval start +- end: ISO 8601 interval end + +The interval must be no longer than one day and end must not precede start. +Both bounds are required; neither may be omitted. + +Response: + + { + "start": "2026-01-01T00:00:00.000Z", + "end": "2026-01-02T00:00:00.000Z", + "clan": { + "clanTag": "ABC", + "games": 12, + "playerSessions": 45, + "wins": 8, + "losses": 4, + "weightedWins": 7.2, + "weightedLosses": 4.8, + "weightedWLRatio": 1.5, + "teamTypeWL": { + "2v2": { + "wl": [5, 2], + "weightedWL": [4.6, 2.4] + } + }, + "teamCountWL": { + "2": { + "wl": [5, 2], + "weightedWL": [4.6, 2.4] + } + } + } + } + +teamTypeWL keys are player-team labels, such as 2v2. teamCountWL keys are +team-count labels. Each wl and weightedWL value is [wins, losses]. The +weighted values use the clan's team-size ratio and game difficulty; this +endpoint does not apply the rolling leaderboard's time decay. + +### GET /public/clan/:clanTag/sessions + +Uses the same required start and end parameters and one-day maximum as the +clan statistics endpoint. Optional pagination: + +- page: positive integer, default 1 +- limit: 1–50, default 10 + +Response: + + { + "results": [ + { + "gameId": "game-id", + "clanTag": "ABC", + "clanPlayerCount": 4, + "hasWon": true, + "numTeams": 2, + "playerTeams": "2v2", + "totalPlayerCount": 10, + "gameStart": "2026-01-01T12:00:00.000Z", + "score": 2.1 + } + ], + "total": 12, + "page": 1, + "limit": 10 + } + +Sessions are newest first. score is positive for a win and negative for a +loss. A session can include historical clan-member counts even when the +player's current membership has changed. + +### GET /public/clans/leaderboard + +Returns the public clan leaderboard for a rolling 90-day window. The window +uses a 30-day half-life for time decay. It contains public, unranked Team +games and excludes Humans vs Nations games. + +The leaderboard normally requires at least 100 games per clan, while always +retaining the top 10 clans so a new/low-volume leaderboard is useful. Results +are sorted by weightedWins and the response is: + + { + "start": "2025-12-01T00:00:00.000Z", + "end": "2026-03-01T00:00:00.000Z", + "clans": [ + { + "clanTag": "ABC", + "games": 120, + "wins": 75, + "losses": 45, + "playerSessions": 500, + "weightedWins": 62.4, + "weightedLosses": 38.1, + "weightedWLRatio": 1.64 + } + ] + } + +The response is cached for about one hour. The implementation also applies +the configured historical cutoff when calculating the rolling window. + +### GET /leaderboard/:type/:mode + +The legacy leaderboard route only accepts type=public and mode=ffa, so the +canonical URL is /leaderboard/public/ffa. It covers public Free For All games, +requires more than 20 games, excludes banned players, and returns at most 40 +entries. + +Each entry contains: + + { + "wlr": 1.75, + "wins": 35, + "losses": 20, + "total": 55, + "public_id": "player-public-id", + "username": "Player", + "user": { + "id": "discord-id", + "username": "discord-name", + "global_name": "Display name" + } + } + +user may be null when there is no public linked Discord profile. This route is +cached briefly (about one minute). + +### GET /leaderboard/ranked + +Query parameter: + +- page: 1 or 2, default 1 + +Each page has up to 50 entries for both ranked ladders. The response has +separate 1v1 and 2v2 arrays: + + { + "1v1": [ + { + "rank": 1, + "elo": 1500, + "peakElo": 1600, + "wins": 20, + "losses": 5, + "total": 25, + "public_id": "player-public-id", + "accountUsername": "Player", + "username": "Player" + } + ], + "2v2": [] + } + +peakElo and accountUsername can be null. username is the display-name +fallback used by clients. This leaderboard is cached for about one hour. + +### GET /leaderboard/tribes + +Query parameter: + +- page: 1 or 2, default 1 + +This is a rolling 30-day leaderboard for purchased custom tribe names. The +response is: + + { + "windowDays": 30, + "start": "2026-01-01", + "end": "2026-01-31", + "tribes": [ + { + "rank": 1, + "name": "Example Tribe", + "gamesAppeared": 42, + "playerReach": 1000, + "ownerPublicId": "player-public-id", + "ownerUsername": "Player", + "activeBoosts": 2 + } + ] + } + +playerReach is the accumulated impression/reach metric, not a distinct-player +count. ownerUsername can be null. This leaderboard is cached for about one +hour. + +## Public feeds and catalog + +These feeds are intentionally unauthenticated and are suitable for loading +the public website or game client. They are normally cached for about one +minute unless stated otherwise. + +### GET /cosmetics.json + +Returns the public catalog grouped by: + +- patterns +- flags +- skins +- crowns +- effects +- colorPalettes +- currencyPacks +- subscriptions +- tribeNames + +Purchasable entries include price as a display string, priceInCents, +productId, and priceId when applicable; unavailable products have a null +product entry. Cosmetic entries also expose their name, rarity, optional +affiliateCode, and soft/hard in-game prices. Patterns include pattern, +description, and optional color-palette availability. Flags, skins, and crowns +include a public url. Effects are grouped by effect type; current groups +include transportShipTrail, nukeTrail, nukeExplosion, structures, and warship, +with type-specific attributes. + +Color palettes contain name, primaryColor, and nullable secondaryColor. +Currency packs contain name, displayName, currency, amount, bonusAmount, +rarity, and a product when purchasable. Subscription entries contain name, +description, priceMonthly, daily soft/hard currency, lobby/ranked +entitlements, signup bonus, rarity, and a product. Tribe-name catalog entries +include the current hard-currency name price, boost price, and boost duration. +Clients should use this feed instead of hard-coding catalog prices or asset +URLs. + +### GET /ping + +Returns 204 when the API worker is reachable. + +### GET /news.json + +Returns published news, omitting disabled entries: + + [ + { + "id": "news-id", + "title": "Headline", + "description": "Short description", + "url": "https://example.com/article", + "type": "news" + } + ] + +url may be null. The exact type values are managed by the API catalog. + +### GET /featured-stream.json + +Returns: + + { + "enabled": true, + "channels": ["openfrontio"] + } + +channels contains valid Twitch login names. + +### GET /live-streams.json + +Returns the current configured roster: + + { + "enabled": true, + "streams": [ + { + "platform": "twitch", + "channel": "openfrontio", + "displayName": "OpenFrontIO", + "title": "Playing OpenFront", + "viewers": 42, + "avatarUrl": "https://...", + "url": "https://twitch.tv/openfrontio" + } + ] + } + +platform is twitch or youtube. title, viewers, avatarUrl, and url may be +omitted when the provider has not supplied them. + +### GET /public/\* + +Serves an asset from the configured public bucket when the API is running in +a mode with public-bucket fallback enabled. Production clients should use the +asset URLs returned by /cosmetics.json; they should not construct bucket keys +or depend on this fallback route. + +## Authenticated account endpoints + +The endpoints in this section require Authorization: Bearer . + +### GET /users/@me + +Returns the authenticated account and player state. The response has this +shape; dates are serialized as ISO strings: + + { + "user": { + "discord": { + "id": "discord-id", + "avatar": "avatar-hash-or-null", + "username": "discord-name", + "global_name": "Display name", + "discriminator": "0", + "locale": "en-US" + }, + "google": { "email": "player@example.com" }, + "email": "player@example.com", + "steam": { + "steamId": "steam-id", + "personaName": "Steam name", + "avatarUrl": "https://..." + } + }, + "ban": null, + "player": { + "adfree": false, + "username": "Player.1234", + "usernameBase": "Player", + "usernameDiscriminator": "1234", + "usernameStatus": "unclaimed", + "usernameClaimExpiresAt": null, + "nextUsernameChangeAt": null, + "canCreatePublicLobbies": false, + "unlimitedRanked": false, + "publicId": "player-public-id", + "flares": ["pattern:example"], + "flareExpiration": {}, + "tempFlaresCooldown": false, + "achievements": {}, + "leaderboard": { + "oneVone": { "elo": 1000, "maxElo": 1000 }, + "twoVtwo": { "elo": 1000, "maxElo": 1000 } + }, + "currency": { "soft": "0", "hard": "0" }, + "rewards": [], + "clans": [], + "clanRequests": [], + "friends": [], + "subscription": null, + "marketingConsent": { + "consented": "no_response", + "hasEmail": false + } + } + } + +user contains only the identity providers linked to the account. ban is null +or an object with category, reason, and expiresAt. The player fields report +entitlements, cosmetics, achievements, ranked ELO, currency balances, pending +rewards, clan memberships, pending clan requests, friend public IDs, +subscription status, and marketing-consent state. + +currency and reward amounts are decimal strings to preserve integer precision. +rewards are not included in the balance until claimed. subscription is null or +contains tier, status, currentPeriodEnd, and cancelAtPeriodEnd. The four +usernameStatus values are unclaimed, claimed, premium, and indefinite. + +### POST /users/@me + +Changes profile visibility. + +Body: + + { "public": true } + +Returns 204. + +### PUT /users/@me/username + +Body: + + { "username": "NewName" } + +username is trimmed, must contain 3–20 ASCII letters, numbers, underscores, +or hyphens, and is subject to the username moderation and namespace checks. +The change cooldown is 30 days. + +Response: + + { + "username": "NewName.1234", + "base": "NewName", + "discriminator": "1234", + "usernameStatus": "unclaimed", + "nextUsernameChangeAt": "2026-02-01T00:00:00.000Z" + } + +A profane or invalid name returns 400, an unavailable name returns 409, and a +cooldown returns 429 with Retry-After when available. + +### GET /users/@me/tribe_names + +Returns at most the 100 most recent purchased names: + + { + "names": [ + { + "id": "123", + "displayName": "Example Tribe", + "status": "pending", + "rejectionKind": null, + "reviewReason": null, + "pricePaid": "200", + "baseWeight": 1, + "activeBoosts": 0, + "boostExpiresAt": null, + "createdAt": "2026-01-01T00:00:00.000Z", + "approvedAt": null, + "gamesAppeared": 0, + "playerReach": 0 + } + ] + } + +status is managed by moderation and can be pending, live, rejected, or +revoked. rejectionKind and reviewReason can be null. activeBoosts counts +unexpired boosts, and boostExpiresAt is the next boost expiry. playerReach is +an impression metric, not a distinct-player count. + +### POST /users/@me/tribe_names + +Purchases a custom tribe name and puts it into the moderation queue. + +Body: + + { "name": "Example Tribe" } + +The name is limited to 100 characters and is screened before purchase. The +current hard-currency price is published by cosmetics.json. A successful +purchase returns 201: + + { + "id": "123", + "displayName": "Example Tribe", + "status": "pending", + "pricePaid": "200" + } + +Active names are globally unique; duplicate names return 409. + +### POST /users/@me/tribe_names/:id/boosts + +Adds a 30-day rotation boost to an owned active name. Boosts stack. + +The current hard-currency price is published by cosmetics.json (currently 100). +The optional Idempotency-Key header makes a retry safe for the same purchase. + +Response: + + { + "id": "456", + "customTribeNameId": "123", + "expiresAt": "2026-02-01T00:00:00.000Z", + "pricePaid": "100" + } + +Insufficient currency or an inactive/non-owned name returns 400 or 404 as +appropriate. + +### GET /marketing/consent + +Returns: + + { + "consented": "approved", + "hasEmail": true + } + +consented is approved, denied, or no_response. The state is associated with +the account's verified contact email. + +### POST /marketing/consent + +Body: + + { "consented": true } + +Returns the normalized state: + + { "consented": "approved" } + +An account without a verified email returns 404. + +### POST /colors/random + +Generates and stores a random player color: + + { "color": "#A1B2C3" } + +The endpoint is limited to once per minute per player and returns 429 when +called sooner. + +### POST /flares_granted/temporary + +Grants a six-minute trial for a shop pattern. Body: + + { "flare": "pattern:example" } + +The pattern must exist and be for sale. Each player can use this trial once +per 24 hours. Response: + + { "expiresAt": "2026-01-01T12:06:00.000Z" } + +### POST /archive_singleplayer_game + +Archives a client-authored singleplayer GameRecord. The payload must pass the +game-record schema, have config.gameType=Singleplayer, and contain exactly one +player. The server stamps that player's persistent identity from the JWT and +removes untrusted external flag URLs. + +Clients may gzip the JSON body and send: + + Content-Encoding: gzip + +Success returns 204. Duplicate game IDs return 409; malformed or invalid +records return 400. This endpoint is rate-limited to one archive per minute. + +## Friends + +### GET /friends + +Query parameters: + +- page: positive integer, default 1 +- limit: 1–50, default 10 + +Response: + + { + "results": [ + { + "publicId": "player-public-id", + "username": "Friend.1234", + "createdAt": "2026-01-01T00:00:00.000Z" + } + ], + "total": 1, + "page": 1, + "limit": 10 + } + +Friends are newest first. username may be null. + +### GET /friends/requests + +Returns both directions. publicId in each entry identifies the other player: + + { + "incoming": [ + { + "publicId": "player-public-id", + "username": "Player", + "createdAt": "2026-01-01T00:00:00.000Z" + } + ], + "outgoing": [] + } + +### POST /friends/requests/:publicId + +Sends a request to a public ID, full display name, or bare premium name. The +body is empty. + +Normally the response is 202: + + { "status": "requested" } + +If the other player already requested you, the inverse request is accepted +and the response is 201: + + { "status": "accepted" } + +Self-targeting returns 400. Already-friends, duplicate-request, and a full +recipient inbox return 409. + +### POST /friends/requests/:publicId/accept + +Accepts an incoming request. The body is empty and success returns 204. + +### DELETE /friends/requests/:publicId + +Denies an incoming request or withdraws an outgoing request. The body is empty +and success returns 204. + +### DELETE /friends/:publicId + +Removes an existing friendship. The body is empty and success returns 204. + +The request, accept, and delete paths accept the same player-reference forms +as the public player endpoints. Missing relationships return 404. + +## Player-facing clans + +Clan tags are 2–5 uppercase ASCII letters or digits. Lookup is +case-insensitive. A user JWT is required for all endpoints in this section; +the member, officer, and leader roles are checked against the target clan. + +### GET /clans + +Browse and search clans. + +Query parameters: + +- page: positive integer, default 1 +- limit: 1–50, default 10 +- search: optional, 2–100 characters; searches tag and name +- sortField: tag, name, or memberCount +- sortOrder: ASC or DESC + +Response: + + { + "results": [ + { + "name": "Example Clan", + "tag": "ABC", + "description": "A description", + "isOpen": true, + "createdAt": "2026-01-01T00:00:00.000Z", + "memberCount": 12 + } + ], + "total": 1, + "page": 1, + "limit": 10 + } + +### GET /clans/:clanTag + +Returns: + + { + "name": "Example Clan", + "tag": "ABC", + "description": "A description", + "isOpen": true, + "createdAt": "2026-01-01T00:00:00.000Z", + "memberCount": 12, + "discordUrl": "https://discord.gg/example" + } + +discordUrl can be null. + +### GET /clans/:clanTag/members + +Requires clan membership. Query parameters: + +- page: positive integer, default 1 +- limit: 1–50, default 10 +- sort: default, winsTotal, lossesTotal, winsFfa, lossesFfa, winsTeam, + lossesTeam, winsHvn, lossesHvn, winsRanked, lossesRanked, wins1v1, or + losses1v1 +- order: asc or desc + +Response: + + { + "results": [ + { + "role": "member", + "joinedAt": "2026-01-01T00:00:00.000Z", + "publicId": "player-public-id", + "username": "Player", + "stats": { + "total": { "wins": 10, "losses": 5 }, + "ffa": { "wins": 2, "losses": 1 }, + "team": { "wins": 8, "losses": 4 }, + "hvn": { "wins": 0, "losses": 0 }, + "duos": { "wins": 3, "losses": 2 }, + "trios": { "wins": 2, "losses": 1 }, + "quads": { "wins": 1, "losses": 0 }, + "2": { "wins": 0, "losses": 0 }, + "3": { "wins": 0, "losses": 0 }, + "4": { "wins": 0, "losses": 0 }, + "5": { "wins": 0, "losses": 0 }, + "6": { "wins": 0, "losses": 0 }, + "7": { "wins": 0, "losses": 0 }, + "ranked": { "wins": 1, "losses": 0 }, + "1v1": { "wins": 1, "losses": 0 } + } + } + ], + "total": 1, + "page": 1, + "limit": 10, + "pendingRequests": 0 + } + +username can be null. pendingRequests is included for managers and can be +omitted or null for ordinary members. All stats are public-game clan stats; +the bucket names describe the aggregation used by the API. + +### GET /clans/:clanTag/games + +Requires clan membership. Query parameters: + +- filter: ffa, team, hvn, or ranked +- cursor: opaque cursor from the previous response + +The page size is fixed at 10. The response is: + + { + "results": [ + { + "gameId": "game-id", + "start": "2026-01-01T12:00:00.000Z", + "durationSeconds": 1200, + "map": "map-id", + "mode": "Team", + "playerTeams": "2v2", + "rankedType": "unranked", + "result": "victory", + "totalPlayers": 10, + "clanPlayers": [ + { + "publicId": "player-public-id", + "username": "Player", + "verified": true, + "won": true + } + ] + } + ], + "nextCursor": "opaque-cursor-or-null" + } + +result is victory, defeat, or incomplete. totalPlayers and playerTeams can be +null; rankedType is a string. The cursor is tied to filter. + +### PATCH /clans/:clanTag + +Requires an officer. Send one or more fields: + + { + "name": "New Clan Name", + "description": "Updated description", + "discordUrl": "https://discord.gg/example", + "isOpen": false } - ], - "nextCursor": "opaque-token" -} -``` -- `result` is one of `[victory, defeat, incomplete]` (`incomplete` = no recorded winner). -- `playerTeams`, `totalPlayers`, and `clanTag` may be `null`. -- `nextCursor` is `null` when there are no more games. -- `username`/`clanTag` reflect the identity the player used in that specific game. +name is 1–30 characters using ASCII letters, digits, spaces, underscores, or +hyphens. description is at most 200 characters. Set discordUrl to null or an +empty string to clear it. Only the leader can change isOpen or discordUrl; +Discord invites must be valid and never-expiring. -**Example:** +Response: -```bash -curl "https://api.openfront.io/public/player/HabCsQYR/games?filter=team&type=public" -``` + { + "name": "New Clan Name", + "tag": "ABC", + "description": "Updated description", + "discordUrl": "https://discord.gg/example", + "isOpen": false + } + +### DELETE /clans/:clanTag + +Requires the leader and disbands the clan. Leadership must be transferred +before a leader can leave. Success returns 204. + +### POST /clans/:clanTag/join + +The body is empty. An open clan adds the player immediately and returns 201: + + { "status": "joined" } + +A closed clan creates a pending request and returns 202: + + { "status": "requested" } + +The endpoint is rate-limited to one join attempt per minute. A banned player +gets 403 with code BANNED and an optional reason. Existing membership or a +duplicate request returns 409. -## Clans +### POST /clans/:clanTag/leave -### Clan Leaderboard +Requires membership, has an empty body, and returns 204. Leaders receive 400 +until they transfer leadership or disband the clan. -Shows the top 100 clans by `weighted wins`. +### Clan member actions -**Endpoint:** +The following endpoints take: -``` -GET https://api.openfront.io/public/clans/leaderboard -``` + { "targetPublicId": "player-public-id" } -Weighted wins have a half-life of 30 days to favor recent wins. +- POST /clans/:clanTag/kick — officer; leaders can kick officers and members, + officers can kick members +- POST /clans/:clanTag/ban — officer; uses the same target plus an optional + reason of at most 200 characters +- POST /clans/:clanTag/unban — officer +- POST /clans/:clanTag/promote — leader; member to officer +- POST /clans/:clanTag/demote — leader; officer to member +- POST /clans/:clanTag/transfer — leader; transfers leadership to a member -Weighted wins are calculated using the following formula: +These successful mutations return 204. Self-targeting and invalid role +transitions return 400 or 403; missing players/members return 404. Banning an +already-banned player returns 409. -``` -FUNCTION calculateScore(session: ClanSession, decay: NUMBER = 1) → NUMBER - // 1. Calculate average team size - avgTeamSize ← session.totalPlayerCount ÷ session.numTeams +### GET /clans/:clanTag/bans - // 2. Determine how much the clan contributed to their team - // (clan players divided by average players per team) - clanMemberRatio ← session.clanPlayerCount ÷ avgTeamSize +Requires an officer. Query parameters page and limit have the standard clan +pagination defaults (page 1, limit 10, maximum 50). - // 3. Apply decay factor (e.g., for older sessions) - weightedValue ← clanMemberRatio × decay +Response entries contain publicId, username, bannedBy, bannedByUsername, +reason, and createdAt. Usernames may be null. - // 4. Calculate match difficulty based on number of teams - // More teams → harder to win → higher reward for victory - // Uses square root to avoid extreme scaling - difficulty ← MAX(1, √(session.numTeams - 1)) +### GET /clans/:clanTag/requests - // 5. Return final score: - // - Win: reward is multiplied by difficulty - // - Loss: penalty is divided by difficulty (less punishment in harder matches) - IF session.hasWon THEN - RETURN weightedValue × difficulty - ELSE - RETURN weightedValue ÷ difficulty - END IF -END FUNCTION -``` +Requires an officer. Standard page and limit parameters are supported. +Response: -### Clan stats + { + "results": [ + { + "publicId": "player-public-id", + "username": "Player", + "createdAt": "2026-01-01T00:00:00.000Z" + } + ], + "total": 1, + "page": 1, + "limit": 10 + } + +### POST /clans/:clanTag/requests/approve + +### POST /clans/:clanTag/requests/deny + +Require an officer and take the targetPublicId body shown above. Success +returns 204. Approving a banned player returns 409; missing requests return 404. -Displays comprehensive clan performance statistics for a specified clan over a chosen time range. If no time range is provided, it shows lifetime stats (starting from early November 2025). +### POST /clans/:clanTag/requests/withdraw -Key metrics include: +Requires a user JWT, takes an empty body, and withdraws the caller's pending +request. Success returns 204; no pending request returns 404. -- Total games, wins, losses, and win rate -- Win/loss ratio and weighted win/loss ratio\* broken down by: - - Team type (e.g., 2 teams, 3 teams, duos, trios, etc) - - Number of teams in the game (2 teams, 5 teams, 20 teams, etc) +## Currency, rewards, and subscriptions -**Note:** No decay is used, so weighted wins will be different from in the leaderboard. +### POST /rewards/:rewardId/claim -**Endpoint** +Claims one pending reward and credits the balance atomically. The response is: -``` -GET https://openfront.io/public/clan/:clanTag -``` + { + "id": "123", + "currencyType": "hard", + "amount": "100", + "reason": "subscription_signup_bonus", + "note": "Subscription signup bonus", + "claimedAt": "2026-01-01T00:00:00.000Z", + "currency": { + "soft": "2500", + "hard": "100" + } + } -**Query Parameters:** +Amounts are decimal strings. Unknown, already-claimed, or another player's +reward returns 404. -- `start` (optional): ISO 8601 timestamp -- `end` (optional): ISO 8601 timestamp +### POST /rewards/claim-all -**Example** +Claims all pending rewards in one transaction. The response is: -```bash -curl https://api.openfront.io/public/clan/UN?start=2025-11-15T00:00:00Z & -end=2025-11-18T23:59:59Z -``` + { + "claimed": [ + { + "id": "123", + "currencyType": "hard", + "amount": "100", + "reason": "subscription_daily", + "note": "Daily subscription reward", + "claimedAt": "2026-01-01T00:00:00.000Z" + } + ], + "currency": { + "soft": "2500", + "hard": "100" + } + } -### Clan Sessions +Calling this with no pending rewards is successful and returns an empty +claimed array. -A clan session is created any time a player with that clan tag is in a public team game. If no start or end query parameter is provided, lifetime sessions (starting early November 2025) are shown. +### POST /shop/purchase -**Endpoint** +Purchases a cosmetic with in-game currency. -``` -GET https://api.openfront.io/public/clan/:clanTag/sessions -``` +Body: -**Query Parameters:** + { + "cosmeticType": "pattern", + "cosmeticName": "example", + "currencyType": "hard", + "colorPaletteName": "sunset" + } + +cosmeticType is pattern, skin, flag, effect, or crown. colorPaletteName is +optional and is used for palette variants. The chosen cosmetic must have a +positive price in the requested soft or hard currency. + +Response: + + { + "flareName": "pattern:example", + "currencyType": "hard", + "amount": "100" + } + +Already-owned items return 409. Insufficient balance or unavailable items +return 400. + +### POST /subscriptions/@me/cancel + +Cancels the current subscription at the end of its paid period. Response: + + { + "status": "active", + "currentPeriodEnd": "2026-02-01T00:00:00.000Z", + "cancelAtPeriodEnd": true + } + +The player retains entitlements until period end. An already-pending +cancellation returns 409; no entitled subscription returns 404. An +administrator-granted subscription has no Stripe period and is revoked +immediately. + +### POST /subscriptions/@me/change-tier + +Body: + + { "tierName": "premium" } + +The target tier must be active and different from the current tier. Response: + + { + "tier": "premium", + "cancelAtPeriodEnd": false + } + +Upgrades invoice the difference immediately; downgrades use Stripe +proration. The local tier is canonical after the Stripe webhook, so clients +should refetch /users/@me. This operation is rate-limited to once per minute. + +### POST /subscriptions/@me/portal + +Body: + + { "returnUrl": "https://openfront.io/account" } + +returnUrl must be an allowlisted URL. Response: + + { "url": "https://billing.stripe.com/..." } + +The endpoint requires an active Stripe-backed subscription. Admin-granted +subscriptions do not have a Stripe billing portal. + +## Matchmaking WebSocket + +### GET /matchmaking/join + +This endpoint upgrades to a WebSocket. It is not authenticated by an HTTP +Authorization header; authenticate the socket by sending the JWT in the first +message. + +Query parameters: + +- instance_id: matchmaking instance name +- mode: 1v1 or 2v2, default 1v1 + +Production example: + + wss://api.openfront.io/matchmaking/join?instance_id=eu-west&mode=1v1 + +After the socket opens, send: + + { + "type": "join", + "jwt": "eyJ...", + "clanTag": "ABC" + } -- `start` (optional): ISO 8601 timestamp -- `end` (optional): ISO 8601 timestamp -- `page` (optional): Page number, 1-200 (default: 1) -- `limit` (optional): Results per page, 1-50 (default: 20) +clanTag is optional and is relevant to 2v2 matching. When supplied for 2v2, +the player must be a member of that clan. The server also checks the player's +ranked-play allowance. -**Response:** +While queued, the server may send: -```json -{ - "results": [ ... ], - "total": 150, - "page": 1, - "limit": 20 -} -``` + { "type": "queue-size", "count": 4 } -Results are ordered by game start time, newest first. +When a match is assigned: -**Example** + { "type": "match-assignment", "gameId": "game-id" } -```bash -curl "https://api.openfront.io/public/clan/UN/sessions?start=2025-11-15T00:00:00Z&end=2025-11-18T23:59:59Z&limit=10&page=1" -``` +Invalid JWT, ranked-play limits, or an invalid clan close the socket with +policy code 1008. A failed clan verification can use 1011. Missing +instance_id or an invalid mode returns HTTP 400; a non-WebSocket request +returns HTTP 426. From edcbd9b6e896688eca0fa31f424e6d4f44bc855a Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Sun, 2 Aug 2026 18:47:25 +0100 Subject: [PATCH 02/13] remove persistentId --- docs/API.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/docs/API.md b/docs/API.md index a841f07c35..d72f670cbc 100644 --- a/docs/API.md +++ b/docs/API.md @@ -51,7 +51,6 @@ The API exposes Content-Range and accepts these request headers: | GET | /leaderboard/public/ffa | Public free-for-all leaderboard | | GET | /leaderboard/ranked | Ranked 1v1 and 2v2 leaderboards | | GET | /leaderboard/tribes | Custom tribe-name leaderboard | -| GET | /users/:persistentId | Anonymous persistent-player lookup | | GET | /matchmaking/join | Matchmaking WebSocket | | GET | /public/\* | Public asset fallback, mainly for local development | @@ -402,16 +401,6 @@ result is victory, defeat, or incomplete. totalPlayers, playerTeams, and clanTag can be null. username and clanTag reflect the identity recorded in that game session. Unknown players return 404. -### GET /users/:persistentId - -Looks up the public player ID associated with an anonymous persistent player. -The path accepts a UUID or the literal REDACTED. A successful response is: - - { "player": { "publicId": "..." } } - -This endpoint only resolves anonymous players without a linked Discord -identity. Other cases return 404. - ## Public clans and leaderboards Clan tags are case-insensitive in lookup paths and are returned uppercase. From 81b4e99e1e50bb439be7768f82d90be09026a0bc Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:12:52 +0100 Subject: [PATCH 03/13] remove sp archive and temp flare --- docs/API.md | 105 +++++++++++++++++++--------------------------------- 1 file changed, 39 insertions(+), 66 deletions(-) diff --git a/docs/API.md b/docs/API.md index d72f670cbc..688150b548 100644 --- a/docs/API.md +++ b/docs/API.md @@ -75,47 +75,45 @@ The OAuth and session endpoints are also unauthenticated at the HTTP layer: Every endpoint in this table requires a user JWT unless noted otherwise. Clan endpoints additionally require the role shown in the detailed reference. -| Method | Path | Required role or purpose | -| ------------ | ---------------------------------- | ----------------------------------- | -| GET, POST | /users/@me | Read or change profile visibility | -| PUT | /users/@me/username | Change username | -| GET, POST | /users/@me/tribe_names | Read or buy custom tribe names | -| POST | /users/@me/tribe_names/:id/boosts | Boost one owned tribe name | -| GET, POST | /marketing/consent | Read or change email consent | -| GET | /auth/link/google | Start Google account linking | -| POST | /colors/random | Generate a random player color | -| POST | /archive_singleplayer_game | Archive a browser singleplayer game | -| POST | /flares_granted/temporary | Grant a short-lived cosmetic trial | -| GET | /friends | List friends | -| GET | /friends/requests | List friend requests | -| POST, DELETE | /friends/requests/:publicId | Create or withdraw a request | -| POST | /friends/requests/:publicId/accept | Accept a request | -| DELETE | /friends/:publicId | Remove a friend | -| GET | /clans | Browse clans | -| GET | /clans/:clanTag | Read clan details | -| GET | /clans/:clanTag/members | Member list (member) | -| GET | /clans/:clanTag/games | Clan game history (member) | -| PATCH | /clans/:clanTag | Update clan (officer) | -| DELETE | /clans/:clanTag | Disband clan (leader) | -| POST | /clans/:clanTag/join | Join or request to join | -| POST | /clans/:clanTag/leave | Leave clan (member) | -| POST | /clans/:clanTag/kick | Kick a member (officer) | -| POST | /clans/:clanTag/ban | Ban a player (officer) | -| POST | /clans/:clanTag/unban | Remove a clan ban (officer) | -| POST | /clans/:clanTag/promote | Promote a member (leader) | -| POST | /clans/:clanTag/demote | Demote an officer (leader) | -| POST | /clans/:clanTag/transfer | Transfer leadership (leader) | -| GET | /clans/:clanTag/bans | List bans (officer) | -| GET | /clans/:clanTag/requests | List join requests (officer) | -| POST | /clans/:clanTag/requests/approve | Approve a join request (officer) | -| POST | /clans/:clanTag/requests/deny | Deny a join request (officer) | -| POST | /clans/:clanTag/requests/withdraw | Withdraw your join request | -| POST | /subscriptions/@me/cancel | Cancel a subscription | -| POST | /subscriptions/@me/change-tier | Change subscription tier | -| POST | /subscriptions/@me/portal | Create a billing-portal session | -| POST | /rewards/claim-all | Claim all available rewards | -| POST | /rewards/:rewardId/claim | Claim one reward | -| POST | /shop/purchase | Spend in-game currency | +| Method | Path | Required role or purpose | +| ------------ | ---------------------------------- | --------------------------------- | +| GET, POST | /users/@me | Read or change profile visibility | +| PUT | /users/@me/username | Change username | +| GET, POST | /users/@me/tribe_names | Read or buy custom tribe names | +| POST | /users/@me/tribe_names/:id/boosts | Boost one owned tribe name | +| GET, POST | /marketing/consent | Read or change email consent | +| GET | /auth/link/google | Start Google account linking | +| POST | /colors/random | Generate a random player color | +| GET | /friends | List friends | +| GET | /friends/requests | List friend requests | +| POST, DELETE | /friends/requests/:publicId | Create or withdraw a request | +| POST | /friends/requests/:publicId/accept | Accept a request | +| DELETE | /friends/:publicId | Remove a friend | +| GET | /clans | Browse clans | +| GET | /clans/:clanTag | Read clan details | +| GET | /clans/:clanTag/members | Member list (member) | +| GET | /clans/:clanTag/games | Clan game history (member) | +| PATCH | /clans/:clanTag | Update clan (officer) | +| DELETE | /clans/:clanTag | Disband clan (leader) | +| POST | /clans/:clanTag/join | Join or request to join | +| POST | /clans/:clanTag/leave | Leave clan (member) | +| POST | /clans/:clanTag/kick | Kick a member (officer) | +| POST | /clans/:clanTag/ban | Ban a player (officer) | +| POST | /clans/:clanTag/unban | Remove a clan ban (officer) | +| POST | /clans/:clanTag/promote | Promote a member (leader) | +| POST | /clans/:clanTag/demote | Demote an officer (leader) | +| POST | /clans/:clanTag/transfer | Transfer leadership (leader) | +| GET | /clans/:clanTag/bans | List bans (officer) | +| GET | /clans/:clanTag/requests | List join requests (officer) | +| POST | /clans/:clanTag/requests/approve | Approve a join request (officer) | +| POST | /clans/:clanTag/requests/deny | Deny a join request (officer) | +| POST | /clans/:clanTag/requests/withdraw | Withdraw your join request | +| POST | /subscriptions/@me/cancel | Cancel a subscription | +| POST | /subscriptions/@me/change-tier | Change subscription tier | +| POST | /subscriptions/@me/portal | Create a billing-portal session | +| POST | /rewards/claim-all | Claim all available rewards | +| POST | /rewards/:rewardId/claim | Claim one reward | +| POST | /shop/purchase | Spend in-game currency | ## Common conventions @@ -911,31 +909,6 @@ Generates and stores a random player color: The endpoint is limited to once per minute per player and returns 429 when called sooner. -### POST /flares_granted/temporary - -Grants a six-minute trial for a shop pattern. Body: - - { "flare": "pattern:example" } - -The pattern must exist and be for sale. Each player can use this trial once -per 24 hours. Response: - - { "expiresAt": "2026-01-01T12:06:00.000Z" } - -### POST /archive_singleplayer_game - -Archives a client-authored singleplayer GameRecord. The payload must pass the -game-record schema, have config.gameType=Singleplayer, and contain exactly one -player. The server stamps that player's persistent identity from the JWT and -removes untrusted external flag URLs. - -Clients may gzip the JSON body and send: - - Content-Encoding: gzip - -Success returns 204. Duplicate game IDs return 409; malformed or invalid -records return 400. This endpoint is rate-limited to one archive per minute. - ## Friends ### GET /friends From 8ea86ca62b2614b201b4596392b1afffbfd65314 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:37:43 +0100 Subject: [PATCH 04/13] update --- docs/API.md | 138 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 91 insertions(+), 47 deletions(-) diff --git a/docs/API.md b/docs/API.md index 688150b548..386db53ebb 100644 --- a/docs/API.md +++ b/docs/API.md @@ -75,45 +75,47 @@ The OAuth and session endpoints are also unauthenticated at the HTTP layer: Every endpoint in this table requires a user JWT unless noted otherwise. Clan endpoints additionally require the role shown in the detailed reference. -| Method | Path | Required role or purpose | -| ------------ | ---------------------------------- | --------------------------------- | -| GET, POST | /users/@me | Read or change profile visibility | -| PUT | /users/@me/username | Change username | -| GET, POST | /users/@me/tribe_names | Read or buy custom tribe names | -| POST | /users/@me/tribe_names/:id/boosts | Boost one owned tribe name | -| GET, POST | /marketing/consent | Read or change email consent | -| GET | /auth/link/google | Start Google account linking | -| POST | /colors/random | Generate a random player color | -| GET | /friends | List friends | -| GET | /friends/requests | List friend requests | -| POST, DELETE | /friends/requests/:publicId | Create or withdraw a request | -| POST | /friends/requests/:publicId/accept | Accept a request | -| DELETE | /friends/:publicId | Remove a friend | -| GET | /clans | Browse clans | -| GET | /clans/:clanTag | Read clan details | -| GET | /clans/:clanTag/members | Member list (member) | -| GET | /clans/:clanTag/games | Clan game history (member) | -| PATCH | /clans/:clanTag | Update clan (officer) | -| DELETE | /clans/:clanTag | Disband clan (leader) | -| POST | /clans/:clanTag/join | Join or request to join | -| POST | /clans/:clanTag/leave | Leave clan (member) | -| POST | /clans/:clanTag/kick | Kick a member (officer) | -| POST | /clans/:clanTag/ban | Ban a player (officer) | -| POST | /clans/:clanTag/unban | Remove a clan ban (officer) | -| POST | /clans/:clanTag/promote | Promote a member (leader) | -| POST | /clans/:clanTag/demote | Demote an officer (leader) | -| POST | /clans/:clanTag/transfer | Transfer leadership (leader) | -| GET | /clans/:clanTag/bans | List bans (officer) | -| GET | /clans/:clanTag/requests | List join requests (officer) | -| POST | /clans/:clanTag/requests/approve | Approve a join request (officer) | -| POST | /clans/:clanTag/requests/deny | Deny a join request (officer) | -| POST | /clans/:clanTag/requests/withdraw | Withdraw your join request | -| POST | /subscriptions/@me/cancel | Cancel a subscription | -| POST | /subscriptions/@me/change-tier | Change subscription tier | -| POST | /subscriptions/@me/portal | Create a billing-portal session | -| POST | /rewards/claim-all | Claim all available rewards | -| POST | /rewards/:rewardId/claim | Claim one reward | -| POST | /shop/purchase | Spend in-game currency | +| Method | Path | Required role or purpose | +| ------------ | --------------------------------------- | --------------------------------- | +| GET, POST | /users/@me | Read or change profile visibility | +| PUT | /users/@me/username | Change username | +| GET, POST | /users/@me/tribe_names | Read or buy custom tribe names | +| POST | /users/@me/tribe_names/:id/boosts | Boost one owned tribe name | +| GET, POST | /marketing/consent | Read or change email consent | +| GET | /auth/link/google | Start Google account linking | +| POST | /colors/random | Generate a random player color | +| GET | /friends | List friends | +| GET | /friends/requests | List friend requests | +| POST, DELETE | /friends/requests/:publicId | Create or withdraw a request | +| POST | /friends/requests/:publicId/accept | Accept a request | +| DELETE | /friends/:publicId | Remove a friend | +| GET | /clans | Browse clans | +| GET | /clans/:clanTag | Read clan details | +| GET | /clans/:clanTag/members | Member list (member) | +| GET | /clans/:clanTag/games | Clan game history (member) | +| PATCH | /clans/:clanTag | Update clan (officer) | +| DELETE | /clans/:clanTag | Disband clan (leader) | +| POST | /clans/:clanTag/join | Join or request to join | +| POST | /clans/:clanTag/leave | Leave clan (member) | +| POST | /clans/:clanTag/kick | Kick a member (officer) | +| POST | /clans/:clanTag/ban | Ban a player (officer) | +| POST | /clans/:clanTag/unban | Remove a clan ban (officer) | +| POST | /clans/:clanTag/promote | Promote a member (leader) | +| POST | /clans/:clanTag/demote | Demote an officer (leader) | +| POST | /clans/:clanTag/transfer | Transfer leadership (leader) | +| GET | /clans/:clanTag/bans | List bans (officer) | +| GET | /clans/:clanTag/requests | List join requests (officer) | +| POST | /clans/:clanTag/requests/approve | Approve a join request (officer) | +| POST | /clans/:clanTag/requests/deny | Deny a join request (officer) | +| POST | /clans/:clanTag/requests/withdraw | Withdraw your join request | +| POST | /subscriptions/@me/cancel | Cancel a subscription | +| POST | /subscriptions/@me/change-tier | Change subscription tier | +| POST | /subscriptions/@me/portal | Create a billing-portal session | +| POST | /stripe/create-checkout-session | Create a catalog checkout | +| POST | /stripe/create-custom-currency-checkout | Create a hard-currency checkout | +| POST | /rewards/claim-all | Claim all available rewards | +| POST | /rewards/:rewardId/claim | Claim one reward | +| POST | /shop/purchase | Spend in-game currency | ## Common conventions @@ -304,13 +306,14 @@ Each result contains: "difficulty": null, "numPlayers": 10, "maxPlayers": 20, - "lobbyFillTime": 15, + "lobbyFillTime": 15000, "playerTeams": "2v2", "rankedType": "unranked" } Values such as end, difficulty, player counts, lobbyFillTime, -playerTeams, and rankedType may be null. The response includes: +playerTeams, and rankedType may be null. lobbyFillTime is milliseconds from +lobby visibility or creation until the game starts. The response includes: Content-Range: games -/ @@ -661,7 +664,9 @@ Returns published news, omitting disabled entries: } ] -url may be null. The exact type values are managed by the API catalog. +Entries may provide either a literal description or a +descriptionTranslationKey for client-side localization. url may be null. The +exact type values are managed by the API catalog. ### GET /featured-stream.json @@ -745,7 +750,9 @@ shape; dates are serialized as ISO strings: "flares": ["pattern:example"], "flareExpiration": {}, "tempFlaresCooldown": false, - "achievements": {}, + "achievements": { + "singleplayerMap": [] + }, "leaderboard": { "oneVone": { "elo": 1000, "maxElo": 1000 }, "twoVtwo": { "elo": 1000, "maxElo": 1000 } @@ -838,7 +845,9 @@ an impression metric, not a distinct-player count. ### POST /users/@me/tribe_names -Purchases a custom tribe name and puts it into the moderation queue. +Purchases a custom tribe name. It enters game rotation immediately with +status pending; moderation is post-purchase and may later reject or revoke +the name. Body: @@ -1080,8 +1089,8 @@ Response: "pendingRequests": 0 } -username can be null. pendingRequests is included for managers and can be -omitted or null for ordinary members. All stats are public-game clan stats; +username can be null. pendingRequests is included for managers and is omitted +for ordinary members. All stats are public-game clan stats; the bucket names describe the aggregation used by the API. ### GET /clans/:clanTag/games @@ -1347,6 +1356,39 @@ returnUrl must be an allowlisted URL. Response: The endpoint requires an active Stripe-backed subscription. Admin-granted subscriptions do not have a Stripe billing portal. +### POST /stripe/create-checkout-session + +Creates a Stripe Checkout session for a catalog product identified by +priceId. The product may be a cosmetic, currency pack, or subscription. The +hostname must be an allowlisted game origin; colorPaletteName is optional for +palette variants. + +Body: + + { + "priceId": "price_...", + "hostname": "https://openfront.io", + "colorPaletteName": "sunset" + } + +The response contains a Stripe Checkout URL. Invalid products or redirect +hostnames return 400. + +### POST /stripe/create-custom-currency-checkout + +Creates a Stripe Checkout session for a custom hard-currency purchase. + +Body: + + { + "hardAmount": 100, + "hostname": "https://openfront.io" + } + +hardAmount must be an integer from 20 through 2000; the current rate is 20 +hard currency per US dollar. The response contains a Stripe Checkout URL. +Invalid amounts or redirect hostnames return 400. + ## Matchmaking WebSocket ### GET /matchmaking/join @@ -1387,4 +1429,6 @@ When a match is assigned: Invalid JWT, ranked-play limits, or an invalid clan close the socket with policy code 1008. A failed clan verification can use 1011. Missing instance_id or an invalid mode returns HTTP 400; a non-WebSocket request -returns HTTP 426. +returns HTTP 426. When the same player joins from a newer socket, the older +socket closes normally with code 1000 and reason `Replaced by newer +connection`; only the newest socket remains queued. From b46b6ee29d02b40dc49ba8b0be95490e387a0358 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:08:05 +0100 Subject: [PATCH 05/13] comments + remove stripe --- docs/API.md | 130 +++++++++++++++++++++------------------------------- 1 file changed, 51 insertions(+), 79 deletions(-) diff --git a/docs/API.md b/docs/API.md index 386db53ebb..ac35c7968a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -75,47 +75,45 @@ The OAuth and session endpoints are also unauthenticated at the HTTP layer: Every endpoint in this table requires a user JWT unless noted otherwise. Clan endpoints additionally require the role shown in the detailed reference. -| Method | Path | Required role or purpose | -| ------------ | --------------------------------------- | --------------------------------- | -| GET, POST | /users/@me | Read or change profile visibility | -| PUT | /users/@me/username | Change username | -| GET, POST | /users/@me/tribe_names | Read or buy custom tribe names | -| POST | /users/@me/tribe_names/:id/boosts | Boost one owned tribe name | -| GET, POST | /marketing/consent | Read or change email consent | -| GET | /auth/link/google | Start Google account linking | -| POST | /colors/random | Generate a random player color | -| GET | /friends | List friends | -| GET | /friends/requests | List friend requests | -| POST, DELETE | /friends/requests/:publicId | Create or withdraw a request | -| POST | /friends/requests/:publicId/accept | Accept a request | -| DELETE | /friends/:publicId | Remove a friend | -| GET | /clans | Browse clans | -| GET | /clans/:clanTag | Read clan details | -| GET | /clans/:clanTag/members | Member list (member) | -| GET | /clans/:clanTag/games | Clan game history (member) | -| PATCH | /clans/:clanTag | Update clan (officer) | -| DELETE | /clans/:clanTag | Disband clan (leader) | -| POST | /clans/:clanTag/join | Join or request to join | -| POST | /clans/:clanTag/leave | Leave clan (member) | -| POST | /clans/:clanTag/kick | Kick a member (officer) | -| POST | /clans/:clanTag/ban | Ban a player (officer) | -| POST | /clans/:clanTag/unban | Remove a clan ban (officer) | -| POST | /clans/:clanTag/promote | Promote a member (leader) | -| POST | /clans/:clanTag/demote | Demote an officer (leader) | -| POST | /clans/:clanTag/transfer | Transfer leadership (leader) | -| GET | /clans/:clanTag/bans | List bans (officer) | -| GET | /clans/:clanTag/requests | List join requests (officer) | -| POST | /clans/:clanTag/requests/approve | Approve a join request (officer) | -| POST | /clans/:clanTag/requests/deny | Deny a join request (officer) | -| POST | /clans/:clanTag/requests/withdraw | Withdraw your join request | -| POST | /subscriptions/@me/cancel | Cancel a subscription | -| POST | /subscriptions/@me/change-tier | Change subscription tier | -| POST | /subscriptions/@me/portal | Create a billing-portal session | -| POST | /stripe/create-checkout-session | Create a catalog checkout | -| POST | /stripe/create-custom-currency-checkout | Create a hard-currency checkout | -| POST | /rewards/claim-all | Claim all available rewards | -| POST | /rewards/:rewardId/claim | Claim one reward | -| POST | /shop/purchase | Spend in-game currency | +| Method | Path | Required role or purpose | +| ------------ | ---------------------------------- | --------------------------------- | +| GET, POST | /users/@me | Read or change profile visibility | +| PUT | /users/@me/username | Change username | +| GET, POST | /users/@me/tribe_names | Read or buy custom tribe names | +| POST | /users/@me/tribe_names/:id/boosts | Boost one owned tribe name | +| GET, POST | /marketing/consent | Read or change email consent | +| GET | /auth/link/google | Start Google account linking | +| POST | /colors/random | Generate a random player color | +| GET | /friends | List friends | +| GET | /friends/requests | List friend requests | +| POST, DELETE | /friends/requests/:publicId | Create or withdraw a request | +| POST | /friends/requests/:publicId/accept | Accept a request | +| DELETE | /friends/:publicId | Remove a friend | +| GET | /clans | Browse clans | +| GET | /clans/:clanTag | Read clan details | +| GET | /clans/:clanTag/members | Member list (member) | +| GET | /clans/:clanTag/games | Clan game history (member) | +| PATCH | /clans/:clanTag | Update clan (officer) | +| DELETE | /clans/:clanTag | Disband clan (leader) | +| POST | /clans/:clanTag/join | Join or request to join | +| POST | /clans/:clanTag/leave | Leave clan (member) | +| POST | /clans/:clanTag/kick | Kick a member (officer) | +| POST | /clans/:clanTag/ban | Ban a player (officer) | +| POST | /clans/:clanTag/unban | Remove a clan ban (officer) | +| POST | /clans/:clanTag/promote | Promote a member (leader) | +| POST | /clans/:clanTag/demote | Demote an officer (leader) | +| POST | /clans/:clanTag/transfer | Transfer leadership (leader) | +| GET | /clans/:clanTag/bans | List bans (officer) | +| GET | /clans/:clanTag/requests | List join requests (officer) | +| POST | /clans/:clanTag/requests/approve | Approve a join request (officer) | +| POST | /clans/:clanTag/requests/deny | Deny a join request (officer) | +| POST | /clans/:clanTag/requests/withdraw | Withdraw your join request | +| POST | /subscriptions/@me/cancel | Cancel a subscription | +| POST | /subscriptions/@me/change-tier | Change subscription tier | +| POST | /subscriptions/@me/portal | Create a billing-portal session | +| POST | /rewards/claim-all | Claim all available rewards | +| POST | /rewards/:rewardId/claim | Claim one reward | +| POST | /shop/purchase | Spend in-game currency | ## Common conventions @@ -637,7 +635,7 @@ include a public url. Effects are grouped by effect type; current groups include transportShipTrail, nukeTrail, nukeExplosion, structures, and warship, with type-specific attributes. -Color palettes contain name, primaryColor, and nullable secondaryColor. +Color palettes contain name, primaryColor, and secondaryColor as strings. Currency packs contain name, displayName, currency, amount, bonusAmount, rarity, and a product when purchasable. Subscription entries contain name, description, priceMonthly, daily soft/hard currency, lobby/ranked @@ -770,7 +768,9 @@ shape; dates are serialized as ISO strings: } } -user contains only the identity providers linked to the account. ban is null +user contains only the identity providers linked to the account. For Steam +identities, personaName and avatarUrl may be null when profile metadata is +unavailable. ban is null or an object with category, reason, and expiresAt. The player fields report entitlements, cosmetics, achievements, ranked ELO, currency balances, pending rewards, clan memberships, pending clan requests, friend public IDs, @@ -1205,7 +1205,8 @@ Requires an officer. Query parameters page and limit have the standard clan pagination defaults (page 1, limit 10, maximum 50). Response entries contain publicId, username, bannedBy, bannedByUsername, -reason, and createdAt. Usernames may be null. +reason, and createdAt. Usernames may be null, and reason is null when no ban +reason was supplied. ### GET /clans/:clanTag/requests @@ -1225,6 +1226,8 @@ Response: "limit": 10 } +username may be null when the requester has not set an account username. + ### POST /clans/:clanTag/requests/approve ### POST /clans/:clanTag/requests/deny @@ -1256,8 +1259,10 @@ Claims one pending reward and credits the balance atomically. The response is: } } -Amounts are decimal strings. Unknown, already-claimed, or another player's -reward returns 404. +Amounts are decimal strings. note may be null. The current server also +returns the reward metadata shown above, while the updated currency balances +are the required result for clients. Unknown, already-claimed, or another +player's reward returns 404. ### POST /rewards/claim-all @@ -1356,39 +1361,6 @@ returnUrl must be an allowlisted URL. Response: The endpoint requires an active Stripe-backed subscription. Admin-granted subscriptions do not have a Stripe billing portal. -### POST /stripe/create-checkout-session - -Creates a Stripe Checkout session for a catalog product identified by -priceId. The product may be a cosmetic, currency pack, or subscription. The -hostname must be an allowlisted game origin; colorPaletteName is optional for -palette variants. - -Body: - - { - "priceId": "price_...", - "hostname": "https://openfront.io", - "colorPaletteName": "sunset" - } - -The response contains a Stripe Checkout URL. Invalid products or redirect -hostnames return 400. - -### POST /stripe/create-custom-currency-checkout - -Creates a Stripe Checkout session for a custom hard-currency purchase. - -Body: - - { - "hardAmount": 100, - "hostname": "https://openfront.io" - } - -hardAmount must be an integer from 20 through 2000; the current rate is 20 -hard currency per US dollar. The response contains a Stripe Checkout URL. -Invalid amounts or redirect hostnames return 400. - ## Matchmaking WebSocket ### GET /matchmaking/join From fa41008fb8a0a60b9c2c2353953e9afba1422c2f Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:16:35 +0100 Subject: [PATCH 06/13] remove sub references --- docs/API.md | 50 +------------------------------------------------- 1 file changed, 1 insertion(+), 49 deletions(-) diff --git a/docs/API.md b/docs/API.md index ac35c7968a..acc0fcf663 100644 --- a/docs/API.md +++ b/docs/API.md @@ -108,9 +108,6 @@ Clan endpoints additionally require the role shown in the detailed reference. | POST | /clans/:clanTag/requests/approve | Approve a join request (officer) | | POST | /clans/:clanTag/requests/deny | Deny a join request (officer) | | POST | /clans/:clanTag/requests/withdraw | Withdraw your join request | -| POST | /subscriptions/@me/cancel | Cancel a subscription | -| POST | /subscriptions/@me/change-tier | Change subscription tier | -| POST | /subscriptions/@me/portal | Create a billing-portal session | | POST | /rewards/claim-all | Claim all available rewards | | POST | /rewards/:rewardId/claim | Claim one reward | | POST | /shop/purchase | Spend in-game currency | @@ -1240,7 +1237,7 @@ returns 204. Approving a banned player returns 409; missing requests return 404. Requires a user JWT, takes an empty body, and withdraws the caller's pending request. Success returns 204; no pending request returns 404. -## Currency, rewards, and subscriptions +## Currency and rewards ### POST /rewards/:rewardId/claim @@ -1316,51 +1313,6 @@ Response: Already-owned items return 409. Insufficient balance or unavailable items return 400. -### POST /subscriptions/@me/cancel - -Cancels the current subscription at the end of its paid period. Response: - - { - "status": "active", - "currentPeriodEnd": "2026-02-01T00:00:00.000Z", - "cancelAtPeriodEnd": true - } - -The player retains entitlements until period end. An already-pending -cancellation returns 409; no entitled subscription returns 404. An -administrator-granted subscription has no Stripe period and is revoked -immediately. - -### POST /subscriptions/@me/change-tier - -Body: - - { "tierName": "premium" } - -The target tier must be active and different from the current tier. Response: - - { - "tier": "premium", - "cancelAtPeriodEnd": false - } - -Upgrades invoice the difference immediately; downgrades use Stripe -proration. The local tier is canonical after the Stripe webhook, so clients -should refetch /users/@me. This operation is rate-limited to once per minute. - -### POST /subscriptions/@me/portal - -Body: - - { "returnUrl": "https://openfront.io/account" } - -returnUrl must be an allowlisted URL. Response: - - { "url": "https://billing.stripe.com/..." } - -The endpoint requires an active Stripe-backed subscription. Admin-granted -subscriptions do not have a Stripe billing portal. - ## Matchmaking WebSocket ### GET /matchmaking/join From 15272fca1532061e9df6ce28de240a7edaf4a571 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:22:33 +0100 Subject: [PATCH 07/13] comments --- docs/API.md | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/API.md b/docs/API.md index acc0fcf663..8680678872 100644 --- a/docs/API.md +++ b/docs/API.md @@ -286,7 +286,7 @@ The requested range may be at most two days. Optional filters: - type: Singleplayer, Public, or Private - mode: Free For All or Team - rankedType: unranked, 1v1, or 2v2 -- playerTeams: team filter, up to 20 characters +- playerTeams: Duos, Trios, Quads, HumansVsNations, or a numeric team count - limit: 1–1000, default 50 - offset: non-negative integer, default 0 @@ -302,7 +302,7 @@ Each result contains: "numPlayers": 10, "maxPlayers": 20, "lobbyFillTime": 15000, - "playerTeams": "2v2", + "playerTeams": "Duos", "rankedType": "unranked" } @@ -339,7 +339,9 @@ contains: - current clan memberships with tag, name, role, joinedAt, and memberCount Private Discord identity data is omitted for a private profile. The profile -stats exclude singleplayer games from the public unranked aggregates. +stats exclude singleplayer games from the public unranked aggregates. The +wins, losses, and total counters in each populated stats leaf are decimal +strings to preserve integer precision. ### GET /public/player/:publicId/sessions @@ -382,7 +384,7 @@ returns 400. The response is: "map": "map-id", "mode": "Team", "type": "Public", - "playerTeams": "2v2", + "playerTeams": "Duos", "rankedType": "unranked", "result": "victory", "totalPlayers": 10, @@ -765,16 +767,20 @@ shape; dates are serialized as ISO strings: } } -user contains only the identity providers linked to the account. For Steam -identities, personaName and avatarUrl may be null when profile metadata is -unavailable. ban is null +user contains only the identity providers linked to the account. Discord +global_name may be null. For Steam identities, personaName and avatarUrl may +be null when profile metadata is unavailable. username, usernameBase, and +usernameDiscriminator may be null or omitted when no account username has been +chosen. ban is null or an object with category, reason, and expiresAt. The player fields report entitlements, cosmetics, achievements, ranked ELO, currency balances, pending rewards, clan memberships, pending clan requests, friend public IDs, subscription status, and marketing-consent state. currency and reward amounts are decimal strings to preserve integer precision. -rewards are not included in the balance until claimed. subscription is null or +Each clanRequests entry contains tag, name, and an ISO createdAt timestamp; the +array is empty when there are no pending requests. rewards are not included in +the balance until claimed. subscription is null or contains tier, status, currentPeriodEnd, and cancelAtPeriodEnd. The four usernameStatus values are unclaimed, claimed, premium, and indefinite. @@ -956,6 +962,8 @@ Returns both directions. publicId in each entry identifies the other player: "outgoing": [] } +username may be null when the other player has not chosen an account username. + ### POST /friends/requests/:publicId Sends a request to a public ID, full display name, or bare premium name. The @@ -1107,7 +1115,7 @@ The page size is fixed at 10. The response is: "durationSeconds": 1200, "map": "map-id", "mode": "Team", - "playerTeams": "2v2", + "playerTeams": "Duos", "rankedType": "unranked", "result": "victory", "totalPlayers": 10, From 9d3730156298348b4da83a8bbcf5a07223bcbfcf Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:36:58 +0100 Subject: [PATCH 08/13] comments --- docs/API.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/API.md b/docs/API.md index 8680678872..48e3aada3d 100644 --- a/docs/API.md +++ b/docs/API.md @@ -286,7 +286,7 @@ The requested range may be at most two days. Optional filters: - type: Singleplayer, Public, or Private - mode: Free For All or Team - rankedType: unranked, 1v1, or 2v2 -- playerTeams: Duos, Trios, Quads, HumansVsNations, or a numeric team count +- playerTeams: Duos, Trios, Quads, Humans Vs Nations, or a numeric team count - limit: 1–1000, default 50 - offset: non-negative integer, default 0 @@ -628,8 +628,8 @@ Returns the public catalog grouped by: Purchasable entries include price as a display string, priceInCents, productId, and priceId when applicable; unavailable products have a null product entry. Cosmetic entries also expose their name, rarity, optional -affiliateCode, and soft/hard in-game prices. Patterns include pattern, -description, and optional color-palette availability. Flags, skins, and crowns +affiliateCode, and soft/hard in-game prices. Patterns include pattern and +optional color-palette availability. Flags, skins, and crowns include a public url. Effects are grouped by effect type; current groups include transportShipTrail, nukeTrail, nukeExplosion, structures, and warship, with type-specific attributes. @@ -1094,8 +1094,9 @@ Response: "pendingRequests": 0 } -username can be null. pendingRequests is included for managers and is omitted -for ordinary members. All stats are public-game clan stats; +username can be null. stats is optional and may be omitted for compatibility +members. pendingRequests is included for managers and is omitted for ordinary +members. All stats are public-game clan stats; the bucket names describe the aggregation used by the API. ### GET /clans/:clanTag/games @@ -1290,8 +1291,9 @@ Claims all pending rewards in one transaction. The response is: } } -Calling this with no pending rewards is successful and returns an empty -claimed array. +Each claimed entry always includes id. currencyType, amount, reason, note, and +claimedAt are optional server metadata. Calling this with no pending rewards is +successful and returns an empty claimed array. ### POST /shop/purchase From 1b6c89e317d10787acf240fbc5ad3944b803e364 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:57:32 +0100 Subject: [PATCH 09/13] comments --- docs/API.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/API.md b/docs/API.md index 48e3aada3d..c8094284fd 100644 --- a/docs/API.md +++ b/docs/API.md @@ -751,8 +751,8 @@ shape; dates are serialized as ISO strings: "singleplayerMap": [] }, "leaderboard": { - "oneVone": { "elo": 1000, "maxElo": 1000 }, - "twoVtwo": { "elo": 1000, "maxElo": 1000 } + "oneVone": { "elo": 1000 }, + "twoVtwo": { "elo": 1000 } }, "currency": { "soft": "0", "hard": "0" }, "rewards": [], @@ -1199,7 +1199,8 @@ The following endpoints take: - POST /clans/:clanTag/unban — officer - POST /clans/:clanTag/promote — leader; member to officer - POST /clans/:clanTag/demote — leader; officer to member -- POST /clans/:clanTag/transfer — leader; transfers leadership to a member +- POST /clans/:clanTag/transfer — leader; transfers leadership to any + non-leader member, including officers These successful mutations return 204. Self-targeting and invalid role transitions return 400 or 403; missing players/members return 404. Banning an From 50dcd6e139b1a6dce07173cb20c51326800c325e Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:45:01 +0100 Subject: [PATCH 10/13] comment --- docs/API.md | 301 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 185 insertions(+), 116 deletions(-) diff --git a/docs/API.md b/docs/API.md index c8094284fd..6272a4d214 100644 --- a/docs/API.md +++ b/docs/API.md @@ -121,8 +121,9 @@ Send a short-lived player JWT as: Authorization: Bearer The API also sets an HttpOnly refresh-session cookie. Access tokens expire -after about 15 minutes; refresh sessions expire after about 30 days. Browser -clients should send credentials on cross-origin requests. +after about 15 minutes. Refresh sessions expire after 30 days of inactivity; +successful refreshes renew the cookie and periodically rotate the session +token. Browser clients should send credentials on cross-origin requests. The JWKS endpoint is: @@ -156,9 +157,10 @@ also include Retry-After. ### Dates, identifiers, and pagination -Unless an endpoint says otherwise, timestamps are ISO 8601 strings. Public -player references accept a public ID, a full display name in base.disc form, -or a bare premium name. Player references are limited to 25 characters. +Unless an endpoint says otherwise, timestamps are ISO 8601 strings. Where a +path accepts a player reference rather than a literal public ID, it accepts a +public ID, a full display name in base.disc form, or a bare premium name. +Player references are limited to 25 characters. Page-based endpoints use page numbers starting at 1. Cursor values are opaque: clients must not parse or manufacture them, and should retain the cursor @@ -187,8 +189,10 @@ has no body. #### POST /auth/revoke -Deletes the current refresh session. For a Discord-backed session it also -revokes the provider token when available. The response has no body. +Deletes the current refresh session and clears the refresh cookie. For a +Discord-backed session it also attempts to revoke the provider token. Discord, +Google, guest, email, and CrazyGames sessions return 204 with no body. A Steam +session is deleted but currently returns 500 with reason unsupported provider. ### OAuth login @@ -196,21 +200,24 @@ revokes the provider token when available. The response has no body. #### GET /auth/login/google -Query parameters: +Required query parameter: - redirect_uri: an allowlisted callback destination -The API generates and stores the OAuth state; callers do not supply it. -These endpoints redirect to the provider. The corresponding callbacks are: +Missing or invalid values return 400. The API generates and stores the OAuth +state; callers do not supply it. Success returns a 302 redirect to the +provider. The corresponding callbacks are: #### GET /auth/callback/discord #### GET /auth/callback/google -The callbacks validate the state and provider response, set the refresh -cookie, and redirect to the original allowlisted destination. Integrations -should use the documented redirect flow rather than expecting a JWT in a URL -fragment. +The callbacks validate the state and provider response. Successful login +callbacks set the refresh cookie and 302-redirect to the original allowlisted +destination. A Google account-link callback instead requires the original live +refresh cookie, sets no new cookie, and redirects with link=google, +link=cancel, link=already_linked, or link=error. Integrations should use this +redirect flow rather than expecting a JWT in a URL fragment. ### One-time login links @@ -223,8 +230,11 @@ Body: "redirectDomain": "https://example.com" } -The endpoint sends a one-time link when the address is eligible. The token is -valid for 15 minutes. +The endpoint sends a one-time link when the address is eligible and returns 204 +with no body. redirectDomain must be an allowlisted full redirect URI. The +email must already belong to an account, or the request must include a valid +refresh session to associate a new email; otherwise the endpoint returns 400. +The token is valid for 15 minutes. #### GET /auth/login/token?login-token= @@ -267,8 +277,8 @@ Requires a user JWT. Returns: { "url": "https://accounts.google.com/..." } -The returned URL starts the linking flow. The callback redirects with a -completion status such as link=google, cancel, already_linked, or error. +The returned URL starts the linking flow. The callback behavior and exact +completion statuses are described in the OAuth callback section above. ## Public games and players @@ -286,7 +296,8 @@ The requested range may be at most two days. Optional filters: - type: Singleplayer, Public, or Private - mode: Free For All or Team - rankedType: unranked, 1v1, or 2v2 -- playerTeams: Duos, Trios, Quads, Humans Vs Nations, or a numeric team count +- playerTeams: exact non-empty player-team label, at most 20 characters (for + example Duos, Trios, Quads, Humans Vs Nations, 5v5, or 4v4v4) - limit: 1–1000, default 50 - offset: non-negative integer, default 0 @@ -310,7 +321,9 @@ Values such as end, difficulty, player counts, lobbyFillTime, playerTeams, and rankedType may be null. lobbyFillTime is milliseconds from lobby visibility or creation until the game starts. The response includes: - Content-Range: games -/ + Content-Range: games -/ + +exclusiveEnd is offset plus the number of entries returned. ### GET /public/game/:gameId @@ -341,7 +354,10 @@ contains: Private Discord identity data is omitted for a private profile. The profile stats exclude singleplayer games from the public unranked aggregates. The wins, losses, and total counters in each populated stats leaf are decimal -strings to preserve integer precision. +strings to preserve integer precision. The path segment also accepts a +base.disc display name or bare premium username and returns the canonical +publicId. The sessions and games subroutes below require that canonical public +ID rather than a username reference. ### GET /public/player/:publicId/sessions @@ -360,7 +376,8 @@ Returns the player's recorded sessions. A result has this shape: "hasWon": true } -Nullable session fields may be null. A player with no sessions returns 404. +Nullable session fields may be null. Session order is unspecified. A player +with no sessions returns 404. ### GET /public/player/:publicId/games @@ -381,7 +398,7 @@ returns 400. The response is: "gameId": "game-id", "start": "2026-01-01T12:00:00.000Z", "durationSeconds": 1200, - "map": "map-id", + "map": "TestMap", "mode": "Team", "type": "Public", "playerTeams": "Duos", @@ -397,7 +414,8 @@ returns 400. The response is: result is victory, defeat, or incomplete. totalPlayers, playerTeams, and clanTag can be null. username and clanTag reflect the identity recorded in -that game session. Unknown players return 404. +that game session. Results are ordered by descending session game ID, normally +newest first. Unknown players return 404. ## Public clans and leaderboards @@ -457,6 +475,9 @@ team-count labels. Each wl and weightedWL value is [wins, losses]. The weighted values use the clan's team-size ratio and game difficulty; this endpoint does not apply the rolling leaderboard's time decay. +A valid tag with no matching sessions, including an unregistered tag, returns +200 with zero-valued statistics. + ### GET /public/clan/:clanTag/sessions Uses the same required start and end parameters and one-day maximum as the @@ -488,7 +509,9 @@ Response: Sessions are newest first. score is positive for a win and negative for a loss. A session can include historical clan-member counts even when the -player's current membership has changed. +player's current membership has changed. A valid tag with no matching +sessions, including an unregistered tag, returns 200 with results: [] and +total: 0. ### GET /public/clans/leaderboard @@ -517,8 +540,8 @@ are sorted by weightedWins and the response is: ] } -The response is cached for about one hour. The implementation also applies -the configured historical cutoff when calculating the rolling window. +The response is cached for about one hour. The implementation also applies a +historical cutoff of 2025-11-12 when calculating the rolling window. ### GET /leaderboard/:type/:mode @@ -543,8 +566,9 @@ Each entry contains: } } -user may be null when there is no public linked Discord profile. This route is -cached briefly (about one minute). +user may be null when there is no public linked Discord profile. username may +be null when the player has not set an account username. This route is cached +briefly (about one minute). ### GET /leaderboard/ranked @@ -601,15 +625,19 @@ response is: ] } -playerReach is the accumulated impression/reach metric, not a distinct-player -count. ownerUsername can be null. This leaderboard is cached for about one -hour. +Each page contains up to 50 entries, and ranks are absolute across pages. +Entries are sorted by playerReach descending, then gamesAppeared descending, +then stable internal ID order. playerReach is the accumulated impression/reach +metric, not a distinct-player count. ownerUsername can be null. This +leaderboard is cached for about one hour. ## Public feeds and catalog -These feeds are intentionally unauthenticated and are suitable for loading -the public website or game client. They are normally cached for about one -minute unless stated otherwise. +The four JSON feeds below are unauthenticated and normally return +Cache-Control: public, max-age=60. The worker retains successful values for up +to 120 seconds and may serve an expired value with max-age=0 while revalidating +in the background. Local development bypasses this cache. /ping is not +cache-wrapped, and /public/\* has its own asset cache policy. ### GET /cosmetics.json @@ -625,23 +653,26 @@ Returns the public catalog grouped by: - subscriptions - tribeNames -Purchasable entries include price as a display string, priceInCents, -productId, and priceId when applicable; unavailable products have a null -product entry. Cosmetic entries also expose their name, rarity, optional -affiliateCode, and soft/hard in-game prices. Patterns include pattern and -optional color-palette availability. Flags, skins, and crowns -include a public url. Effects are grouped by effect type; current groups -include transportShipTrail, nukeTrail, nukeExplosion, structures, and warship, -with type-specific attributes. - -Color palettes contain name, primaryColor, and secondaryColor as strings. -Currency packs contain name, displayName, currency, amount, bonusAmount, -rarity, and a product when purchasable. Subscription entries contain name, -description, priceMonthly, daily soft/hard currency, lobby/ranked -entitlements, signup bonus, rarity, and a product. Tribe-name catalog entries -include the current hard-currency name price, boost price, and boost duration. -Clients should use this feed instead of hard-coding catalog prices or asset -URLs. +Cosmetic entries always include name, rarity, affiliateCode (a string or +null), and product (an object containing price, priceInCents, productId, and +priceId, or null). priceSoft, priceHard, and artist are omitted when unset. +Patterns also include pattern, description, and a colorPalettes array of +objects containing name and isArchived. Flags, skins, and crowns include a url +string, which is empty when no URL is configured. Effects are grouped by +effect type; current groups include transportShipTrail, nukeTrail, +nukeExplosion, structures, and warship. Each effect contains its effectType +and type-specific attributes. + +Color palettes contain name, primaryColor, and secondaryColor, where +secondaryColor may be null. Only active, fully configured currency packs are +emitted, so each has a non-null product; they also contain name, displayName, +currency, amount, bonusAmount, affiliateCode: null, and rarity. Active +subscription tiers are emitted even when their product is null. Their fields +include name, description, priceMonthly, dailySoftCurrency, +dailyHardCurrency, canCreatePublicLobbies, unlimitedRanked, +hardCurrencySignupBonus, rarity, and product. tribeNames contains priceHard, +boostPriceHard, and boostDurationDays. Clients should use this feed instead of +hard-coding catalog prices or asset URLs. ### GET /ping @@ -661,9 +692,8 @@ Returns published news, omitting disabled entries: } ] -Entries may provide either a literal description or a -descriptionTranslationKey for client-side localization. url may be null. The -exact type values are managed by the API catalog. +description is always a literal string. url may be null. type is a string whose +managed values are controlled by the API catalog. ### GET /featured-stream.json @@ -678,7 +708,8 @@ channels contains valid Twitch login names. ### GET /live-streams.json -Returns the current configured roster: +Returns live Twitch streams from the latest provider snapshot, followed by +configured YouTube entries. enabled is true exactly when streams is non-empty: { "enabled": true, @@ -695,15 +726,19 @@ Returns the current configured roster: ] } -platform is twitch or youtube. title, viewers, avatarUrl, and url may be -omitted when the provider has not supplied them. +platform is twitch or youtube. viewers is always a non-negative integer and is +0 when a configured YouTube entry has no count. title, avatarUrl, and url may +be omitted. ### GET /public/\* -Serves an asset from the configured public bucket when the API is running in -a mode with public-bucket fallback enabled. Production clients should use the -asset URLs returned by /cosmetics.json; they should not construct bucket keys -or depend on this fallback route. +This fallback serves objects only when PUBLIC_BUCKET_URL points to a URL whose +pathname is /public, which is the local-development configuration. Other +deployments, empty keys, and missing objects return 404. Successful responses +use the stored content type or application/octet-stream and include +Cache-Control: public, max-age=31536000, Access-Control-Allow-Origin: \*, CSP +sandboxing, and X-Content-Type-Options: nosniff. Production clients should use +the asset URLs returned by /cosmetics.json. ## Authenticated account endpoints @@ -748,11 +783,12 @@ shape; dates are serialized as ISO strings: "flareExpiration": {}, "tempFlaresCooldown": false, "achievements": { - "singleplayerMap": [] + "singleplayerMap": [], + "player": [] }, "leaderboard": { - "oneVone": { "elo": 1000 }, - "twoVtwo": { "elo": 1000 } + "oneVone": { "elo": 1000, "maxElo": 1100 }, + "twoVtwo": { "elo": 1000, "maxElo": 1050 } }, "currency": { "soft": "0", "hard": "0" }, "rewards": [], @@ -767,17 +803,23 @@ shape; dates are serialized as ISO strings: } } -user contains only the identity providers linked to the account. Discord -global_name may be null. For Steam identities, personaName and avatarUrl may -be null when profile metadata is unavailable. username, usernameBase, and -usernameDiscriminator may be null or omitted when no account username has been -chosen. ban is null +Identity-provider properties are independently optional. A present google +object may omit email. Discord global_name may be null. For Steam identities, +personaName and avatarUrl may be null when profile metadata is unavailable. +username, usernameBase, and usernameDiscriminator are always present and are +null when no account username has been chosen. ban is null or an object with category, reason, and expiresAt. The player fields report entitlements, cosmetics, achievements, ranked ELO, currency balances, pending rewards, clan memberships, pending clan requests, friend public IDs, subscription status, and marketing-consent state. currency and reward amounts are decimal strings to preserve integer precision. +Each leaderboard ladder contains numeric elo and maxElo values. +flareExpiration maps flare names to Unix-epoch millisecond timestamps. Each +reward entry contains id, currencyType, amount, reason, note, and an ISO +createdAt timestamp; note may be null. player.friends contains at most 250 +public IDs in ascending public-ID order; use GET /friends for the complete +paginated list. Each clanRequests entry contains tag, name, and an ISO createdAt timestamp; the array is empty when there are no pending requests. rewards are not included in the balance until claimed. subscription is null or @@ -800,9 +842,10 @@ Body: { "username": "NewName" } -username is trimmed, must contain 3–20 ASCII letters, numbers, underscores, -or hyphens, and is subject to the username moderation and namespace checks. -The change cooldown is 30 days. +username is trimmed and must be 3–20 characters. It may contain ASCII letters, +numbers, underscores, and hyphens, with single spaces between non-empty word +segments. It is subject to the username moderation and namespace checks. The +change cooldown is 30 days. Response: @@ -876,7 +919,7 @@ Adds a 30-day rotation boost to an owned active name. Boosts stack. The current hard-currency price is published by cosmetics.json (currently 100). The optional Idempotency-Key header makes a retry safe for the same purchase. -Response: +A successful purchase returns 201: { "id": "456", @@ -963,6 +1006,7 @@ Returns both directions. publicId in each entry identifies the other player: } username may be null when the other player has not chosen an account username. +incoming and outgoing are each ordered oldest first. ### POST /friends/requests/:publicId @@ -978,8 +1022,9 @@ and the response is 201: { "status": "accepted" } -Self-targeting returns 400. Already-friends, duplicate-request, and a full -recipient inbox return 409. +Self-targeting returns 400. Already-friends and duplicate requests return 409. +A recipient may have at most 250 pending incoming requests; additional +requests return 409. ### POST /friends/requests/:publicId/accept @@ -1033,6 +1078,10 @@ Response: "limit": 10 } +Without sortField, results are ordered by name ascending, then clan ID +ascending. With sortField, sortOrder defaults to ASC; clan ID ascending breaks +ties. + ### GET /clans/:clanTag Returns: @@ -1094,10 +1143,14 @@ Response: "pendingRequests": 0 } -username can be null. stats is optional and may be omitted for compatibility -members. pendingRequests is included for managers and is omitted for ordinary -members. All stats are public-game clan stats; -the bucket names describe the aggregation used by the API. +username can be null. stats is included in every current response, with +zero-valued buckets for members without qualifying games; clients may tolerate +its absence only for compatibility with older deployments. With sort=default, +members are ordered leader, officer, member, then joinedAt ascending. For +statistic sorts, order defaults to desc; ties are ordered by role then joinedAt +ascending. pendingRequests is included for managers and omitted for ordinary +members. All stats are public-game clan stats; the bucket names describe the +aggregation used by the API. ### GET /clans/:clanTag/games @@ -1114,7 +1167,7 @@ The page size is fixed at 10. The response is: "gameId": "game-id", "start": "2026-01-01T12:00:00.000Z", "durationSeconds": 1200, - "map": "map-id", + "map": "TestMap", "mode": "Team", "playerTeams": "Duos", "rankedType": "unranked", @@ -1134,7 +1187,9 @@ The page size is fixed at 10. The response is: } result is victory, defeat, or incomplete. totalPlayers and playerTeams can be -null; rankedType is a string. The cursor is tied to filter. +null; rankedType is a string. Games are returned newest first, with game ID +descending as the timestamp tie-breaker. A malformed cursor, or a cursor used +with a different filter, returns 400. ### PATCH /clans/:clanTag @@ -1150,7 +1205,10 @@ Requires an officer. Send one or more fields: name is 1–30 characters using ASCII letters, digits, spaces, underscores, or hyphens. description is at most 200 characters. Set discordUrl to null or an empty string to clear it. Only the leader can change isOpen or discordUrl; -Discord invites must be valid and never-expiring. +Discord invites must be valid and never-expiring. A non-empty invite is +normalized to https://discord.gg/. Verification of a changed invite is +limited to once per player per minute and may return 429. Changing isOpen to +true automatically approves all pending requests as members. Response: @@ -1177,9 +1235,9 @@ A closed clan creates a pending request and returns 202: { "status": "requested" } -The endpoint is rate-limited to one join attempt per minute. A banned player -gets 403 with code BANNED and an optional reason. Existing membership or a -duplicate request returns 409. +A successful join or request creation is limited to one per minute; failed +attempts do not consume the limit. A banned player gets 403 with code BANNED +and an optional reason. Existing membership or a duplicate request returns 409. ### POST /clans/:clanTag/leave @@ -1202,9 +1260,13 @@ The following endpoints take: - POST /clans/:clanTag/transfer — leader; transfers leadership to any non-leader member, including officers -These successful mutations return 204. Self-targeting and invalid role -transitions return 400 or 403; missing players/members return 404. Banning an -already-banned player returns 409. +These successful mutations return 204. Kick, ban, and transfer reject +self-targeting with 400. Invalid role transitions return 400 or 403; missing +players/members return 404. Banning an already-banned player returns 409. A +successful ban removes the target's pending request and, when the caller may +remove their role, their membership. Unbanning does not restore either; +unbanning a player who is not banned returns 409. A leadership transfer makes +the target leader and the former leader a member. ### GET /clans/:clanTag/bans @@ -1213,12 +1275,12 @@ pagination defaults (page 1, limit 10, maximum 50). Response entries contain publicId, username, bannedBy, bannedByUsername, reason, and createdAt. Usernames may be null, and reason is null when no ban -reason was supplied. +reason was supplied. Bans are ordered newest first by createdAt. ### GET /clans/:clanTag/requests -Requires an officer. Standard page and limit parameters are supported. -Response: +Requires an officer. page is a positive integer (default 1), and limit is +1–50 (default 10). Requests are ordered oldest first by createdAt. Response: { "results": [ @@ -1240,7 +1302,8 @@ username may be null when the requester has not set an account username. ### POST /clans/:clanTag/requests/deny Require an officer and take the targetPublicId body shown above. Success -returns 204. Approving a banned player returns 409; missing requests return 404. +returns 204. Approving a banned player returns 409, does not add membership, +and removes that pending request. Missing requests return 404. ### POST /clans/:clanTag/requests/withdraw @@ -1266,10 +1329,10 @@ Claims one pending reward and credits the balance atomically. The response is: } } -Amounts are decimal strings. note may be null. The current server also +id and amount are decimal strings. note may be null. The current server also returns the reward metadata shown above, while the updated currency balances -are the required result for clients. Unknown, already-claimed, or another -player's reward returns 404. +are the required result for clients. A malformed rewardId returns 400. Unknown, +already-claimed, or another player's reward returns 404. ### POST /rewards/claim-all @@ -1292,9 +1355,9 @@ Claims all pending rewards in one transaction. The response is: } } -Each claimed entry always includes id. currencyType, amount, reason, note, and -claimedAt are optional server metadata. Calling this with no pending rewards is -successful and returns an empty claimed array. +Each claimed entry includes id, currencyType, amount, reason, note, and +claimedAt. id and amount are decimal strings, and note may be null. Calling +this with no pending rewards is successful and returns an empty claimed array. ### POST /shop/purchase @@ -1322,26 +1385,27 @@ Response: } Already-owned items return 409. Insufficient balance or unavailable items -return 400. +return 400. On success, the cosmetic is granted, the requested currency is +debited atomically, and the player's adfree flag is set to true. ## Matchmaking WebSocket ### GET /matchmaking/join -This endpoint upgrades to a WebSocket. It is not authenticated by an HTTP -Authorization header; authenticate the socket by sending the JWT in the first -message. +This endpoint upgrades only when Upgrade: websocket is supplied; otherwise it +returns HTTP 426. The HTTP Authorization header is not used. Query parameters: -- instance_id: matchmaking instance name -- mode: 1v1 or 2v2, default 1v1 +- instance_id: required non-empty matchmaking instance name +- mode: case-sensitive 1v1 or 2v2, default 1v1 Production example: wss://api.openfront.io/matchmaking/join?instance_id=eu-west&mode=1v1 -After the socket opens, send: +After the socket opens, send a join message to authenticate and enter the +queue: { "type": "join", @@ -1349,11 +1413,16 @@ After the socket opens, send: "clanTag": "ABC" } -clanTag is optional and is relevant to 2v2 matching. When supplied for 2v2, -the player must be a member of that clan. The server also checks the player's -ranked-play allowance. +clanTag is used only for 2v2. When supplied, it is normalized and the player +must be a member of that clan. The server also checks the player's ranked-play +allowance. Only messages with type join enter this flow; other message types +are ignored. The server imposes no join-message or queue timeout. + +Queues are independent for each instance_id and mode pair. A match requires +compatible queued players and an available game-server check-in. An unmatched +game-server check-in ends after about 15 seconds with hasAssignment: false. -While queued, the server may send: +On its roughly three-second alarm cadence, the server broadcasts: { "type": "queue-size", "count": 4 } @@ -1361,9 +1430,9 @@ When a match is assigned: { "type": "match-assignment", "gameId": "game-id" } -Invalid JWT, ranked-play limits, or an invalid clan close the socket with -policy code 1008. A failed clan verification can use 1011. Missing -instance_id or an invalid mode returns HTTP 400; a non-WebSocket request -returns HTTP 426. When the same player joins from a newer socket, the older -socket closes normally with code 1000 and reason `Replaced by newer -connection`; only the newest socket remains queued. +Missing instance_id or an invalid mode returns HTTP 400. Socket close reasons +are 1008 Invalid session, 1008 ranked_limit_reached, 1008 invalid_clan, and +1011 clan_verification_failed. When the same player joins from a newer socket +in the same instance_id and mode queue, the older socket closes with code 1000 +and reason Replaced by newer connection; only the newest socket remains in +that queue. From 7a215599424ca338ccab863952abc2e392f6c7c8 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Mon, 3 Aug 2026 00:55:44 +0100 Subject: [PATCH 11/13] evan comments --- docs/API.md | 1024 +-------------------------------------------------- 1 file changed, 18 insertions(+), 1006 deletions(-) diff --git a/docs/API.md b/docs/API.md index 6272a4d214..d2f9651896 100644 --- a/docs/API.md +++ b/docs/API.md @@ -29,88 +29,23 @@ The API exposes Content-Range and accepts these request headers: ### Public endpoints -| Method | Path | Purpose | -| ------ | --------------------------------- | --------------------------------------------------- | -| GET | /.well-known/jwks.json | JWT verification keys | -| GET | /ping | Health check | -| GET | /cosmetics.json | Public shop/catalog configuration | -| GET | /news.json | Published news | -| GET | /featured-stream.json | Featured stream configuration | -| GET | /live-streams.json | Current live-stream configuration | -| GET | /public/games | Archived game summaries | -| GET | /public/game/:gameId | Archived game record | -| GET | /game/:gameId | Legacy alias for the archived game record | -| GET | /public/player/:publicId | Public player profile | -| GET | /player/:publicId | Legacy alias for the public player profile | -| GET | /public/player/:publicId/sessions | Public player sessions | -| GET | /public/player/:publicId/games | Public player game history | -| GET | /public/clans/leaderboard | Rolling clan leaderboard | -| GET | /public/clan/:clanTag | Clan statistics | -| GET | /public/clan/:clanTag/exists | Clan existence check | -| GET | /public/clan/:clanTag/sessions | Clan game sessions | -| GET | /leaderboard/public/ffa | Public free-for-all leaderboard | -| GET | /leaderboard/ranked | Ranked 1v1 and 2v2 leaderboards | -| GET | /leaderboard/tribes | Custom tribe-name leaderboard | -| GET | /matchmaking/join | Matchmaking WebSocket | -| GET | /public/\* | Public asset fallback, mainly for local development | - -The OAuth and session endpoints are also unauthenticated at the HTTP layer: - -| Method | Path | Purpose | -| ------ | ---------------------- | ----------------------------------- | -| POST | /auth/logout | Clear the current refresh session | -| POST | /auth/refresh | Refresh or create a guest session | -| POST | /auth/revoke | Revoke the current provider session | -| GET | /auth/login/discord | Start Discord login | -| GET | /auth/login/google | Start Google login | -| GET | /auth/login/token | Consume a one-time login token | -| POST | /auth/magic-link | Send a magic-link login | -| GET | /auth/callback/discord | Discord OAuth callback | -| GET | /auth/callback/google | Google OAuth callback | -| POST | /auth/crazygames | Exchange a CrazyGames token | -| POST | /auth/steam | Exchange a Steam ticket | - -### Authenticated player endpoints - -Every endpoint in this table requires a user JWT unless noted otherwise. -Clan endpoints additionally require the role shown in the detailed reference. - -| Method | Path | Required role or purpose | -| ------------ | ---------------------------------- | --------------------------------- | -| GET, POST | /users/@me | Read or change profile visibility | -| PUT | /users/@me/username | Change username | -| GET, POST | /users/@me/tribe_names | Read or buy custom tribe names | -| POST | /users/@me/tribe_names/:id/boosts | Boost one owned tribe name | -| GET, POST | /marketing/consent | Read or change email consent | -| GET | /auth/link/google | Start Google account linking | -| POST | /colors/random | Generate a random player color | -| GET | /friends | List friends | -| GET | /friends/requests | List friend requests | -| POST, DELETE | /friends/requests/:publicId | Create or withdraw a request | -| POST | /friends/requests/:publicId/accept | Accept a request | -| DELETE | /friends/:publicId | Remove a friend | -| GET | /clans | Browse clans | -| GET | /clans/:clanTag | Read clan details | -| GET | /clans/:clanTag/members | Member list (member) | -| GET | /clans/:clanTag/games | Clan game history (member) | -| PATCH | /clans/:clanTag | Update clan (officer) | -| DELETE | /clans/:clanTag | Disband clan (leader) | -| POST | /clans/:clanTag/join | Join or request to join | -| POST | /clans/:clanTag/leave | Leave clan (member) | -| POST | /clans/:clanTag/kick | Kick a member (officer) | -| POST | /clans/:clanTag/ban | Ban a player (officer) | -| POST | /clans/:clanTag/unban | Remove a clan ban (officer) | -| POST | /clans/:clanTag/promote | Promote a member (leader) | -| POST | /clans/:clanTag/demote | Demote an officer (leader) | -| POST | /clans/:clanTag/transfer | Transfer leadership (leader) | -| GET | /clans/:clanTag/bans | List bans (officer) | -| GET | /clans/:clanTag/requests | List join requests (officer) | -| POST | /clans/:clanTag/requests/approve | Approve a join request (officer) | -| POST | /clans/:clanTag/requests/deny | Deny a join request (officer) | -| POST | /clans/:clanTag/requests/withdraw | Withdraw your join request | -| POST | /rewards/claim-all | Claim all available rewards | -| POST | /rewards/:rewardId/claim | Claim one reward | -| POST | /shop/purchase | Spend in-game currency | +| Method | Path | Purpose | +| ------ | --------------------------------- | ------------------------------------------ | +| GET | /ping | Health check | +| GET | /public/games | Archived game summaries | +| GET | /public/game/:gameId | Archived game record | +| GET | /game/:gameId | Legacy alias for the archived game record | +| GET | /public/player/:publicId | Public player profile | +| GET | /player/:publicId | Legacy alias for the public player profile | +| GET | /public/player/:publicId/sessions | Public player sessions | +| GET | /public/player/:publicId/games | Public player game history | +| GET | /public/clans/leaderboard | Rolling clan leaderboard | +| GET | /public/clan/:clanTag | Clan statistics | +| GET | /public/clan/:clanTag/exists | Clan existence check | +| GET | /public/clan/:clanTag/sessions | Clan game sessions | +| GET | /leaderboard/public/ffa | Public free-for-all leaderboard | +| GET | /leaderboard/ranked | Ranked 1v1 and 2v2 leaderboards | +| GET | /leaderboard/tribes | Custom tribe-name leaderboard | ## Common conventions @@ -125,15 +60,6 @@ after about 15 minutes. Refresh sessions expire after 30 days of inactivity; successful refreshes renew the cookie and periodically rotate the session token. Browser clients should send credentials on cross-origin requests. -The JWKS endpoint is: - - GET /.well-known/jwks.json - -It returns a standard JSON Web Key Set for verifying API-issued JWTs. JWT -claims include a subject, issuer, audience, issued-at time, expiry, and a -session identifier (jti). Do not put refresh cookies or signing keys in -client-visible documentation or logs. - ### Responses and errors Successful JSON responses are normally 200. Other success statuses used by @@ -166,120 +92,6 @@ Page-based endpoints use page numbers starting at 1. Cursor values are opaque: clients must not parse or manufacture them, and should retain the cursor alongside the filters that produced it. -## Authentication and sessions - -### Refresh, logout, and revoke - -#### POST /auth/refresh - -Refresh the session represented by the refresh cookie. If there is no refresh -cookie, the endpoint creates a guest session and sets one. - -Response: - - { - "jwt": "eyJ...", - "expiresIn": 900 - } - -#### POST /auth/logout - -Deletes the current refresh session and clears the refresh cookie. The response -has no body. - -#### POST /auth/revoke - -Deletes the current refresh session and clears the refresh cookie. For a -Discord-backed session it also attempts to revoke the provider token. Discord, -Google, guest, email, and CrazyGames sessions return 204 with no body. A Steam -session is deleted but currently returns 500 with reason unsupported provider. - -### OAuth login - -#### GET /auth/login/discord - -#### GET /auth/login/google - -Required query parameter: - -- redirect_uri: an allowlisted callback destination - -Missing or invalid values return 400. The API generates and stores the OAuth -state; callers do not supply it. Success returns a 302 redirect to the -provider. The corresponding callbacks are: - -#### GET /auth/callback/discord - -#### GET /auth/callback/google - -The callbacks validate the state and provider response. Successful login -callbacks set the refresh cookie and 302-redirect to the original allowlisted -destination. A Google account-link callback instead requires the original live -refresh cookie, sets no new cookie, and redirects with link=google, -link=cancel, link=already_linked, or link=error. Integrations should use this -redirect flow rather than expecting a JWT in a URL fragment. - -### One-time login links - -#### POST /auth/magic-link - -Body: - - { - "email": "player@example.com", - "redirectDomain": "https://example.com" - } - -The endpoint sends a one-time link when the address is eligible and returns 204 -with no body. redirectDomain must be an allowlisted full redirect URI. The -email must already belong to an account, or the request must include a valid -refresh session to associate a new email; otherwise the endpoint returns 400. -The token is valid for 15 minutes. - -#### GET /auth/login/token?login-token= - -Consumes the one-time token, sets the refresh cookie, and returns the -authenticated email. A successful response is: - - { "email": "player@example.com" } - -A token cannot be reused. This endpoint does not perform an additional -redirect; the link's client can navigate after receiving the response. - -### Platform login - -#### POST /auth/crazygames - -Body: - - { "token": "crazygames-sdk-token" } - -#### POST /auth/steam - -Body: - - { "ticket": "steamworks-auth-ticket" } - -Both successful exchanges return the same session shape: - - { - "jwt": "eyJ...", - "expiresIn": 900 - } - -They also set the refresh-session cookie. - -### Link Google - -#### GET /auth/link/google?redirect_uri= - -Requires a user JWT. Returns: - - { "url": "https://accounts.google.com/..." } - -The returned URL starts the linking flow. The callback behavior and exact -completion statuses are described in the OAuth callback section above. - ## Public games and players ### GET /public/games @@ -631,808 +443,8 @@ then stable internal ID order. playerReach is the accumulated impression/reach metric, not a distinct-player count. ownerUsername can be null. This leaderboard is cached for about one hour. -## Public feeds and catalog - -The four JSON feeds below are unauthenticated and normally return -Cache-Control: public, max-age=60. The worker retains successful values for up -to 120 seconds and may serve an expired value with max-age=0 while revalidating -in the background. Local development bypasses this cache. /ping is not -cache-wrapped, and /public/\* has its own asset cache policy. - -### GET /cosmetics.json - -Returns the public catalog grouped by: - -- patterns -- flags -- skins -- crowns -- effects -- colorPalettes -- currencyPacks -- subscriptions -- tribeNames - -Cosmetic entries always include name, rarity, affiliateCode (a string or -null), and product (an object containing price, priceInCents, productId, and -priceId, or null). priceSoft, priceHard, and artist are omitted when unset. -Patterns also include pattern, description, and a colorPalettes array of -objects containing name and isArchived. Flags, skins, and crowns include a url -string, which is empty when no URL is configured. Effects are grouped by -effect type; current groups include transportShipTrail, nukeTrail, -nukeExplosion, structures, and warship. Each effect contains its effectType -and type-specific attributes. - -Color palettes contain name, primaryColor, and secondaryColor, where -secondaryColor may be null. Only active, fully configured currency packs are -emitted, so each has a non-null product; they also contain name, displayName, -currency, amount, bonusAmount, affiliateCode: null, and rarity. Active -subscription tiers are emitted even when their product is null. Their fields -include name, description, priceMonthly, dailySoftCurrency, -dailyHardCurrency, canCreatePublicLobbies, unlimitedRanked, -hardCurrencySignupBonus, rarity, and product. tribeNames contains priceHard, -boostPriceHard, and boostDurationDays. Clients should use this feed instead of -hard-coding catalog prices or asset URLs. +## Health ### GET /ping Returns 204 when the API worker is reachable. - -### GET /news.json - -Returns published news, omitting disabled entries: - - [ - { - "id": "news-id", - "title": "Headline", - "description": "Short description", - "url": "https://example.com/article", - "type": "news" - } - ] - -description is always a literal string. url may be null. type is a string whose -managed values are controlled by the API catalog. - -### GET /featured-stream.json - -Returns: - - { - "enabled": true, - "channels": ["openfrontio"] - } - -channels contains valid Twitch login names. - -### GET /live-streams.json - -Returns live Twitch streams from the latest provider snapshot, followed by -configured YouTube entries. enabled is true exactly when streams is non-empty: - - { - "enabled": true, - "streams": [ - { - "platform": "twitch", - "channel": "openfrontio", - "displayName": "OpenFrontIO", - "title": "Playing OpenFront", - "viewers": 42, - "avatarUrl": "https://...", - "url": "https://twitch.tv/openfrontio" - } - ] - } - -platform is twitch or youtube. viewers is always a non-negative integer and is -0 when a configured YouTube entry has no count. title, avatarUrl, and url may -be omitted. - -### GET /public/\* - -This fallback serves objects only when PUBLIC_BUCKET_URL points to a URL whose -pathname is /public, which is the local-development configuration. Other -deployments, empty keys, and missing objects return 404. Successful responses -use the stored content type or application/octet-stream and include -Cache-Control: public, max-age=31536000, Access-Control-Allow-Origin: \*, CSP -sandboxing, and X-Content-Type-Options: nosniff. Production clients should use -the asset URLs returned by /cosmetics.json. - -## Authenticated account endpoints - -The endpoints in this section require Authorization: Bearer . - -### GET /users/@me - -Returns the authenticated account and player state. The response has this -shape; dates are serialized as ISO strings: - - { - "user": { - "discord": { - "id": "discord-id", - "avatar": "avatar-hash-or-null", - "username": "discord-name", - "global_name": "Display name", - "discriminator": "0", - "locale": "en-US" - }, - "google": { "email": "player@example.com" }, - "email": "player@example.com", - "steam": { - "steamId": "steam-id", - "personaName": "Steam name", - "avatarUrl": "https://..." - } - }, - "ban": null, - "player": { - "adfree": false, - "username": "Player.1234", - "usernameBase": "Player", - "usernameDiscriminator": "1234", - "usernameStatus": "unclaimed", - "usernameClaimExpiresAt": null, - "nextUsernameChangeAt": null, - "canCreatePublicLobbies": false, - "unlimitedRanked": false, - "publicId": "player-public-id", - "flares": ["pattern:example"], - "flareExpiration": {}, - "tempFlaresCooldown": false, - "achievements": { - "singleplayerMap": [], - "player": [] - }, - "leaderboard": { - "oneVone": { "elo": 1000, "maxElo": 1100 }, - "twoVtwo": { "elo": 1000, "maxElo": 1050 } - }, - "currency": { "soft": "0", "hard": "0" }, - "rewards": [], - "clans": [], - "clanRequests": [], - "friends": [], - "subscription": null, - "marketingConsent": { - "consented": "no_response", - "hasEmail": false - } - } - } - -Identity-provider properties are independently optional. A present google -object may omit email. Discord global_name may be null. For Steam identities, -personaName and avatarUrl may be null when profile metadata is unavailable. -username, usernameBase, and usernameDiscriminator are always present and are -null when no account username has been chosen. ban is null -or an object with category, reason, and expiresAt. The player fields report -entitlements, cosmetics, achievements, ranked ELO, currency balances, pending -rewards, clan memberships, pending clan requests, friend public IDs, -subscription status, and marketing-consent state. - -currency and reward amounts are decimal strings to preserve integer precision. -Each leaderboard ladder contains numeric elo and maxElo values. -flareExpiration maps flare names to Unix-epoch millisecond timestamps. Each -reward entry contains id, currencyType, amount, reason, note, and an ISO -createdAt timestamp; note may be null. player.friends contains at most 250 -public IDs in ascending public-ID order; use GET /friends for the complete -paginated list. -Each clanRequests entry contains tag, name, and an ISO createdAt timestamp; the -array is empty when there are no pending requests. rewards are not included in -the balance until claimed. subscription is null or -contains tier, status, currentPeriodEnd, and cancelAtPeriodEnd. The four -usernameStatus values are unclaimed, claimed, premium, and indefinite. - -### POST /users/@me - -Changes profile visibility. - -Body: - - { "public": true } - -Returns 204. - -### PUT /users/@me/username - -Body: - - { "username": "NewName" } - -username is trimmed and must be 3–20 characters. It may contain ASCII letters, -numbers, underscores, and hyphens, with single spaces between non-empty word -segments. It is subject to the username moderation and namespace checks. The -change cooldown is 30 days. - -Response: - - { - "username": "NewName.1234", - "base": "NewName", - "discriminator": "1234", - "usernameStatus": "unclaimed", - "nextUsernameChangeAt": "2026-02-01T00:00:00.000Z" - } - -A profane or invalid name returns 400, an unavailable name returns 409, and a -cooldown returns 429 with Retry-After when available. - -### GET /users/@me/tribe_names - -Returns at most the 100 most recent purchased names: - - { - "names": [ - { - "id": "123", - "displayName": "Example Tribe", - "status": "pending", - "rejectionKind": null, - "reviewReason": null, - "pricePaid": "200", - "baseWeight": 1, - "activeBoosts": 0, - "boostExpiresAt": null, - "createdAt": "2026-01-01T00:00:00.000Z", - "approvedAt": null, - "gamesAppeared": 0, - "playerReach": 0 - } - ] - } - -status is managed by moderation and can be pending, live, rejected, or -revoked. rejectionKind and reviewReason can be null. activeBoosts counts -unexpired boosts, and boostExpiresAt is the next boost expiry. playerReach is -an impression metric, not a distinct-player count. - -### POST /users/@me/tribe_names - -Purchases a custom tribe name. It enters game rotation immediately with -status pending; moderation is post-purchase and may later reject or revoke -the name. - -Body: - - { "name": "Example Tribe" } - -The name is limited to 100 characters and is screened before purchase. The -current hard-currency price is published by cosmetics.json. A successful -purchase returns 201: - - { - "id": "123", - "displayName": "Example Tribe", - "status": "pending", - "pricePaid": "200" - } - -Active names are globally unique; duplicate names return 409. - -### POST /users/@me/tribe_names/:id/boosts - -Adds a 30-day rotation boost to an owned active name. Boosts stack. - -The current hard-currency price is published by cosmetics.json (currently 100). -The optional Idempotency-Key header makes a retry safe for the same purchase. - -A successful purchase returns 201: - - { - "id": "456", - "customTribeNameId": "123", - "expiresAt": "2026-02-01T00:00:00.000Z", - "pricePaid": "100" - } - -Insufficient currency or an inactive/non-owned name returns 400 or 404 as -appropriate. - -### GET /marketing/consent - -Returns: - - { - "consented": "approved", - "hasEmail": true - } - -consented is approved, denied, or no_response. The state is associated with -the account's verified contact email. - -### POST /marketing/consent - -Body: - - { "consented": true } - -Returns the normalized state: - - { "consented": "approved" } - -An account without a verified email returns 404. - -### POST /colors/random - -Generates and stores a random player color: - - { "color": "#A1B2C3" } - -The endpoint is limited to once per minute per player and returns 429 when -called sooner. - -## Friends - -### GET /friends - -Query parameters: - -- page: positive integer, default 1 -- limit: 1–50, default 10 - -Response: - - { - "results": [ - { - "publicId": "player-public-id", - "username": "Friend.1234", - "createdAt": "2026-01-01T00:00:00.000Z" - } - ], - "total": 1, - "page": 1, - "limit": 10 - } - -Friends are newest first. username may be null. - -### GET /friends/requests - -Returns both directions. publicId in each entry identifies the other player: - - { - "incoming": [ - { - "publicId": "player-public-id", - "username": "Player", - "createdAt": "2026-01-01T00:00:00.000Z" - } - ], - "outgoing": [] - } - -username may be null when the other player has not chosen an account username. -incoming and outgoing are each ordered oldest first. - -### POST /friends/requests/:publicId - -Sends a request to a public ID, full display name, or bare premium name. The -body is empty. - -Normally the response is 202: - - { "status": "requested" } - -If the other player already requested you, the inverse request is accepted -and the response is 201: - - { "status": "accepted" } - -Self-targeting returns 400. Already-friends and duplicate requests return 409. -A recipient may have at most 250 pending incoming requests; additional -requests return 409. - -### POST /friends/requests/:publicId/accept - -Accepts an incoming request. The body is empty and success returns 204. - -### DELETE /friends/requests/:publicId - -Denies an incoming request or withdraws an outgoing request. The body is empty -and success returns 204. - -### DELETE /friends/:publicId - -Removes an existing friendship. The body is empty and success returns 204. - -The request, accept, and delete paths accept the same player-reference forms -as the public player endpoints. Missing relationships return 404. - -## Player-facing clans - -Clan tags are 2–5 uppercase ASCII letters or digits. Lookup is -case-insensitive. A user JWT is required for all endpoints in this section; -the member, officer, and leader roles are checked against the target clan. - -### GET /clans - -Browse and search clans. - -Query parameters: - -- page: positive integer, default 1 -- limit: 1–50, default 10 -- search: optional, 2–100 characters; searches tag and name -- sortField: tag, name, or memberCount -- sortOrder: ASC or DESC - -Response: - - { - "results": [ - { - "name": "Example Clan", - "tag": "ABC", - "description": "A description", - "isOpen": true, - "createdAt": "2026-01-01T00:00:00.000Z", - "memberCount": 12 - } - ], - "total": 1, - "page": 1, - "limit": 10 - } - -Without sortField, results are ordered by name ascending, then clan ID -ascending. With sortField, sortOrder defaults to ASC; clan ID ascending breaks -ties. - -### GET /clans/:clanTag - -Returns: - - { - "name": "Example Clan", - "tag": "ABC", - "description": "A description", - "isOpen": true, - "createdAt": "2026-01-01T00:00:00.000Z", - "memberCount": 12, - "discordUrl": "https://discord.gg/example" - } - -discordUrl can be null. - -### GET /clans/:clanTag/members - -Requires clan membership. Query parameters: - -- page: positive integer, default 1 -- limit: 1–50, default 10 -- sort: default, winsTotal, lossesTotal, winsFfa, lossesFfa, winsTeam, - lossesTeam, winsHvn, lossesHvn, winsRanked, lossesRanked, wins1v1, or - losses1v1 -- order: asc or desc - -Response: - - { - "results": [ - { - "role": "member", - "joinedAt": "2026-01-01T00:00:00.000Z", - "publicId": "player-public-id", - "username": "Player", - "stats": { - "total": { "wins": 10, "losses": 5 }, - "ffa": { "wins": 2, "losses": 1 }, - "team": { "wins": 8, "losses": 4 }, - "hvn": { "wins": 0, "losses": 0 }, - "duos": { "wins": 3, "losses": 2 }, - "trios": { "wins": 2, "losses": 1 }, - "quads": { "wins": 1, "losses": 0 }, - "2": { "wins": 0, "losses": 0 }, - "3": { "wins": 0, "losses": 0 }, - "4": { "wins": 0, "losses": 0 }, - "5": { "wins": 0, "losses": 0 }, - "6": { "wins": 0, "losses": 0 }, - "7": { "wins": 0, "losses": 0 }, - "ranked": { "wins": 1, "losses": 0 }, - "1v1": { "wins": 1, "losses": 0 } - } - } - ], - "total": 1, - "page": 1, - "limit": 10, - "pendingRequests": 0 - } - -username can be null. stats is included in every current response, with -zero-valued buckets for members without qualifying games; clients may tolerate -its absence only for compatibility with older deployments. With sort=default, -members are ordered leader, officer, member, then joinedAt ascending. For -statistic sorts, order defaults to desc; ties are ordered by role then joinedAt -ascending. pendingRequests is included for managers and omitted for ordinary -members. All stats are public-game clan stats; the bucket names describe the -aggregation used by the API. - -### GET /clans/:clanTag/games - -Requires clan membership. Query parameters: - -- filter: ffa, team, hvn, or ranked -- cursor: opaque cursor from the previous response - -The page size is fixed at 10. The response is: - - { - "results": [ - { - "gameId": "game-id", - "start": "2026-01-01T12:00:00.000Z", - "durationSeconds": 1200, - "map": "TestMap", - "mode": "Team", - "playerTeams": "Duos", - "rankedType": "unranked", - "result": "victory", - "totalPlayers": 10, - "clanPlayers": [ - { - "publicId": "player-public-id", - "username": "Player", - "verified": true, - "won": true - } - ] - } - ], - "nextCursor": "opaque-cursor-or-null" - } - -result is victory, defeat, or incomplete. totalPlayers and playerTeams can be -null; rankedType is a string. Games are returned newest first, with game ID -descending as the timestamp tie-breaker. A malformed cursor, or a cursor used -with a different filter, returns 400. - -### PATCH /clans/:clanTag - -Requires an officer. Send one or more fields: - - { - "name": "New Clan Name", - "description": "Updated description", - "discordUrl": "https://discord.gg/example", - "isOpen": false - } - -name is 1–30 characters using ASCII letters, digits, spaces, underscores, or -hyphens. description is at most 200 characters. Set discordUrl to null or an -empty string to clear it. Only the leader can change isOpen or discordUrl; -Discord invites must be valid and never-expiring. A non-empty invite is -normalized to https://discord.gg/. Verification of a changed invite is -limited to once per player per minute and may return 429. Changing isOpen to -true automatically approves all pending requests as members. - -Response: - - { - "name": "New Clan Name", - "tag": "ABC", - "description": "Updated description", - "discordUrl": "https://discord.gg/example", - "isOpen": false - } - -### DELETE /clans/:clanTag - -Requires the leader and disbands the clan. Leadership must be transferred -before a leader can leave. Success returns 204. - -### POST /clans/:clanTag/join - -The body is empty. An open clan adds the player immediately and returns 201: - - { "status": "joined" } - -A closed clan creates a pending request and returns 202: - - { "status": "requested" } - -A successful join or request creation is limited to one per minute; failed -attempts do not consume the limit. A banned player gets 403 with code BANNED -and an optional reason. Existing membership or a duplicate request returns 409. - -### POST /clans/:clanTag/leave - -Requires membership, has an empty body, and returns 204. Leaders receive 400 -until they transfer leadership or disband the clan. - -### Clan member actions - -The following endpoints take: - - { "targetPublicId": "player-public-id" } - -- POST /clans/:clanTag/kick — officer; leaders can kick officers and members, - officers can kick members -- POST /clans/:clanTag/ban — officer; uses the same target plus an optional - reason of at most 200 characters -- POST /clans/:clanTag/unban — officer -- POST /clans/:clanTag/promote — leader; member to officer -- POST /clans/:clanTag/demote — leader; officer to member -- POST /clans/:clanTag/transfer — leader; transfers leadership to any - non-leader member, including officers - -These successful mutations return 204. Kick, ban, and transfer reject -self-targeting with 400. Invalid role transitions return 400 or 403; missing -players/members return 404. Banning an already-banned player returns 409. A -successful ban removes the target's pending request and, when the caller may -remove their role, their membership. Unbanning does not restore either; -unbanning a player who is not banned returns 409. A leadership transfer makes -the target leader and the former leader a member. - -### GET /clans/:clanTag/bans - -Requires an officer. Query parameters page and limit have the standard clan -pagination defaults (page 1, limit 10, maximum 50). - -Response entries contain publicId, username, bannedBy, bannedByUsername, -reason, and createdAt. Usernames may be null, and reason is null when no ban -reason was supplied. Bans are ordered newest first by createdAt. - -### GET /clans/:clanTag/requests - -Requires an officer. page is a positive integer (default 1), and limit is -1–50 (default 10). Requests are ordered oldest first by createdAt. Response: - - { - "results": [ - { - "publicId": "player-public-id", - "username": "Player", - "createdAt": "2026-01-01T00:00:00.000Z" - } - ], - "total": 1, - "page": 1, - "limit": 10 - } - -username may be null when the requester has not set an account username. - -### POST /clans/:clanTag/requests/approve - -### POST /clans/:clanTag/requests/deny - -Require an officer and take the targetPublicId body shown above. Success -returns 204. Approving a banned player returns 409, does not add membership, -and removes that pending request. Missing requests return 404. - -### POST /clans/:clanTag/requests/withdraw - -Requires a user JWT, takes an empty body, and withdraws the caller's pending -request. Success returns 204; no pending request returns 404. - -## Currency and rewards - -### POST /rewards/:rewardId/claim - -Claims one pending reward and credits the balance atomically. The response is: - - { - "id": "123", - "currencyType": "hard", - "amount": "100", - "reason": "subscription_signup_bonus", - "note": "Subscription signup bonus", - "claimedAt": "2026-01-01T00:00:00.000Z", - "currency": { - "soft": "2500", - "hard": "100" - } - } - -id and amount are decimal strings. note may be null. The current server also -returns the reward metadata shown above, while the updated currency balances -are the required result for clients. A malformed rewardId returns 400. Unknown, -already-claimed, or another player's reward returns 404. - -### POST /rewards/claim-all - -Claims all pending rewards in one transaction. The response is: - - { - "claimed": [ - { - "id": "123", - "currencyType": "hard", - "amount": "100", - "reason": "subscription_daily", - "note": "Daily subscription reward", - "claimedAt": "2026-01-01T00:00:00.000Z" - } - ], - "currency": { - "soft": "2500", - "hard": "100" - } - } - -Each claimed entry includes id, currencyType, amount, reason, note, and -claimedAt. id and amount are decimal strings, and note may be null. Calling -this with no pending rewards is successful and returns an empty claimed array. - -### POST /shop/purchase - -Purchases a cosmetic with in-game currency. - -Body: - - { - "cosmeticType": "pattern", - "cosmeticName": "example", - "currencyType": "hard", - "colorPaletteName": "sunset" - } - -cosmeticType is pattern, skin, flag, effect, or crown. colorPaletteName is -optional and is used for palette variants. The chosen cosmetic must have a -positive price in the requested soft or hard currency. - -Response: - - { - "flareName": "pattern:example", - "currencyType": "hard", - "amount": "100" - } - -Already-owned items return 409. Insufficient balance or unavailable items -return 400. On success, the cosmetic is granted, the requested currency is -debited atomically, and the player's adfree flag is set to true. - -## Matchmaking WebSocket - -### GET /matchmaking/join - -This endpoint upgrades only when Upgrade: websocket is supplied; otherwise it -returns HTTP 426. The HTTP Authorization header is not used. - -Query parameters: - -- instance_id: required non-empty matchmaking instance name -- mode: case-sensitive 1v1 or 2v2, default 1v1 - -Production example: - - wss://api.openfront.io/matchmaking/join?instance_id=eu-west&mode=1v1 - -After the socket opens, send a join message to authenticate and enter the -queue: - - { - "type": "join", - "jwt": "eyJ...", - "clanTag": "ABC" - } - -clanTag is used only for 2v2. When supplied, it is normalized and the player -must be a member of that clan. The server also checks the player's ranked-play -allowance. Only messages with type join enter this flow; other message types -are ignored. The server imposes no join-message or queue timeout. - -Queues are independent for each instance_id and mode pair. A match requires -compatible queued players and an available game-server check-in. An unmatched -game-server check-in ends after about 15 seconds with hasAssignment: false. - -On its roughly three-second alarm cadence, the server broadcasts: - - { "type": "queue-size", "count": 4 } - -When a match is assigned: - - { "type": "match-assignment", "gameId": "game-id" } - -Missing instance_id or an invalid mode returns HTTP 400. Socket close reasons -are 1008 Invalid session, 1008 ranked_limit_reached, 1008 invalid_clan, and -1011 clan_verification_failed. When the same player joins from a newer socket -in the same instance_id and mode queue, the older socket closes with code 1000 -and reason Replaced by newer connection; only the newest socket remains in -that queue. From ebca6b9339cb912d1d579d80d6e82fe3a0808c18 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Mon, 3 Aug 2026 01:00:23 +0100 Subject: [PATCH 12/13] add disclaimer --- docs/API.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/API.md b/docs/API.md index d2f9651896..6833382ea3 100644 --- a/docs/API.md +++ b/docs/API.md @@ -5,6 +5,10 @@ worker. It documents endpoints intended for the game client, public websites, and player integrations. It is kept aligned with the route registry and endpoint schemas in the infra repository. +## API Usage + +> **Warning:** Rate limits are very strict. Join the [Discord](https://discord.gg/K9zernJB5z) to request higher rate limits. + ## Base URLs Production: From f830d0ea4451239e856be079167b279ef5649df6 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Mon, 3 Aug 2026 01:02:04 +0100 Subject: [PATCH 13/13] add new endpoint --- docs/API.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/API.md b/docs/API.md index 6833382ea3..2f9af6c752 100644 --- a/docs/API.md +++ b/docs/API.md @@ -47,6 +47,7 @@ The API exposes Content-Range and accepts these request headers: | GET | /public/clan/:clanTag | Clan statistics | | GET | /public/clan/:clanTag/exists | Clan existence check | | GET | /public/clan/:clanTag/sessions | Clan game sessions | +| GET | /public/tribe/:name | Purchased tribe-name stats | | GET | /leaderboard/public/ffa | Public free-for-all leaderboard | | GET | /leaderboard/ranked | Ranked 1v1 and 2v2 leaderboards | | GET | /leaderboard/tribes | Custom tribe-name leaderboard | @@ -447,6 +448,41 @@ then stable internal ID order. playerReach is the accumulated impression/reach metric, not a distinct-player count. ownerUsername can be null. This leaderboard is cached for about one hour. +### GET /public/tribe/:name + +Looks up a purchased custom tribe name by its normalized name. Lookup is +case- and whitespace-insensitive, and the response preserves the canonical +display form. + +Only active names (`pending` or `live`) are visible. A name owned by an +actively banned player, a rejected or revoked name, and an unknown name all +return 404. A name that is empty after normalization returns 400. + +Response: + + { + "name": "Cool Tribe", + "ownerPublicId": "abc123", + "ownerUsername": "Ada.4821", + "activeBoosts": 2, + "lifetime": { + "gamesAppeared": 107, + "playerReach": 10699 + }, + "window": { + "days": 30, + "start": "2026-07-02", + "end": "2026-08-01", + "gamesAppeared": 7, + "playerReach": 700 + } + } + +`lifetime` contains all-time games-appeared and player-reach totals. +`window` contains the same metrics for the rolling 30-day leaderboard window. +`activeBoosts` counts only unexpired boosts. `ownerUsername` is null when the +owner has not set an account username. + ## Health ### GET /ping