# Filudi developer docs How a game runs on filudi.com and on portals, the SDK and API references, and how a portal builds on the Filudi API. Four sections. Read the one that matches what you build; the references serve both. - [Games](/docs/games/): from the invitation to a version under review, and everything a game can do on filudi.com and on portals. - [SDK](/docs/sdk/): `@filudi/game-sdk`, one page per module, with the rules the host applies. - [Portals](/docs/portals/): the three ways to run a site on the catalog, approval, domains, server keys. - [API reference](/docs/api/): every route of the API, generated from the file the API runs on. ## Reading a page Code samples sit in the right column on a wide screen and under the text on a narrow one. The language tabs switch every sample on the site at once; the choice is kept in your browser. ```js import { sdk } from '@filudi/game-sdk'; const { player, locale } = await sdk.init({ gameId: 'gloop' }); sdk.ready(); ``` ```ts import { sdk, type InitResult } from '@filudi/game-sdk'; const result: InitResult = await sdk.init({ gameId: 'gloop' }); sdk.ready(); ``` Every page is also served as Markdown: add `index.md` to its address. [`/llms.txt`](/llms.txt) lists every page and [`/llms-full.txt`](/llms-full.txt) holds them all, for an agent that reads the docs in one go. Source: https://developers.filudi.com/docs/ --- # API reference Every route of the Filudi API with its parameters, bodies, responses and request samples, generated from openapi.yaml. Every route of the API, grouped by tag, with parameters, request bodies, responses and samples in curl, JavaScript and Python. The reference pages are generated at build time from `apps/api/openapi.yaml`, the file the API runs on, so what you read here is what the API answers. Start with the introduction pages, then the group that matches what you build. - [Authentication](/docs/api/authentication/): the four credentials and where each one is valid. - [Errors](/docs/api/errors/): the `{ error, message }` shape and every code. - [Rate limits](/docs/api/rate-limits/): what is limited, by what, and the 429 answer. - [Versioning](/docs/api/versioning/): the `/v1` prefix and how the API changes. - [CORS and origins](/docs/api/cors-and-origins/): which origins may call the API from a browser and frame a game. The reference itself: - [SDK](/docs/api/reference/sdk/): what a game or a hub calls with a player or game token: players, sessions, saves, leaderboards, events. - [Catalog](/docs/api/reference/catalog/) and [Bundles](/docs/api/reference/bundles/): public, cacheable. - [Account](/docs/api/reference/account/): sign-in by link, code or password, export, deletion. - [Community](/docs/api/reference/community/) and [Feedback](/docs/api/reference/feedback/): ratings, comments, bug reports. - [Console](/docs/api/reference/console/): sign-in to the developer console, invitations, sessions. - [Admin](/docs/api/reference/admin/): the console API, for developers and staff, on the session cookie. - [Server](/docs/api/reference/server/): for portal backends, on a server key. - [Schemas](/docs/api/reference/schemas/): every named object the routes read or answer. A route heading is a link: `/docs/api/reference/sdk/#post-v1-sdk-game-tokens` opens `POST /v1/sdk/game-tokens`. The anchor is the method and the path, lowercase, with dashes. Source: https://developers.filudi.com/docs/api/ --- # Authentication The four credentials of the Filudi API, where each one is valid, and how it travels. Four credentials open the API. Each one opens one group of routes and nothing else. The reference says, under every route, which one it takes. | Credential | Looks like | Sent as | Valid on | | --------------- | ----------------------------- | ----------------------------------------- | --------------------------------------------------------- | | Player token | `fp1..` | `Authorization: Bearer` | `/v1/sdk/*`, `/v1/account/*`, ratings, comments, feedback | | Game token | `fg1..` | `Authorization: Bearer` | Five `/v1/sdk/*` routes, for its own game | | Console session | cookie `filudi_console` | the cookie, with `credentials: 'include'` | `/v1/console/*`, `/v1/admin/*` | | Server key | `fsk__<32 base62>` | `Authorization: Bearer` | `/v1/server/*` | ## Player token A player is a guest from the first page view. `POST /v1/sdk/players` creates one for a site and answers `{ player, token }`; the magic link, its 6-digit code and the password sign-in answer the same shape for an account. The token has no expiry. It opens the player's saves, scores, events, profile and the whole `/v1/account/*` group, deletion included. ```sh curl -X POST 'https://api.filudi.com/v1/sdk/players' \ -H 'content-type: application/json' \ -d '{ "site": "filudi", "locale": "en" }' curl 'https://api.filudi.com/v1/sdk/players/me' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" ``` ```js const created = await fetch('https://api.filudi.com/v1/sdk/players', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ site: 'filudi', locale: 'en' }), }); const { token } = await created.json(); localStorage.setItem('filudi:player:filudi', token); const me = await fetch('https://api.filudi.com/v1/sdk/players/me', { headers: { authorization: `Bearer ${token}` }, }); ``` ```py import requests # From a server, every visitor shares your IP and the 21st guest of the hour is refused. # Create guests in the browser, or use a server key and POST /v1/server/players. created = requests.post( 'https://api.filudi.com/v1/sdk/players', json={'site': 'filudi', 'locale': 'en'}, ) token = created.json()['token'] me = requests.get( 'https://api.filudi.com/v1/sdk/players/me', headers={'authorization': f'Bearer {token}'}, ) ``` Rules for the token: - It lives in the browser, in `localStorage`. `PlayerClient` of `@filudi/hub-core` uses the key `filudi:player:`. Never a cookie, never a URL, never a log. - It never enters a game frame. A game gets a game token instead. - A 401 means the API refused it: forget it and create a new guest. When the owner rotates the API's token secret, every player is signed out this way. - A player belongs to one site. The same email on two sites makes two players. ## Game token A game is untrusted. The page that frames it exchanges the player token with `POST /v1/sdk/game-tokens { game }` and passes the result in the `filudi:init` message of the handshake. The token is `fg1`, valid 24 hours, and opens these routes for the game it names: | Route | With a game token | | --------------------------------------------- | ---------------------------------------------------------- | | `GET /v1/sdk/players/me` | Allowed; `emailMasked` is null and `hasPassword` false | | `GET`, `PUT`, `DELETE /v1/sdk/saves/{gameId}` | Its own game | | `POST /v1/sdk/sessions` | Its own game | | `/v1/sdk/leaderboards/{gameId}/*` | Its own game | | `POST /v1/sdk/events` | An item that names another game refuses the batch with 403 | Every other route answers 401 to a game token. The exchange is limited to 240 per player per hour. A hub sends a fresh token to the frame every 12 hours as `filudi:token`; the SDK swaps it without a reload. A portal backend gets the same token for one of its players with `POST /v1/server/game-tokens`. ```sh curl -X POST 'https://api.filudi.com/v1/sdk/game-tokens' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \ -H 'content-type: application/json' \ -d '{ "game": "gloop" }' ``` ```js const response = await fetch('https://api.filudi.com/v1/sdk/game-tokens', { method: 'POST', headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' }, body: JSON.stringify({ game: 'gloop' }), }); const { token, expiresAt } = await response.json(); // Post `token` to the frame in filudi:init; keep playerToken on your origin. ``` ```py import requests # A backend uses its server key instead: POST /v1/server/game-tokens { playerId, game }. response = requests.post( 'https://api.filudi.com/v1/server/game-tokens', headers={'authorization': f'Bearer {site_key}'}, json={'playerId': player_id, 'game': 'gloop'}, ) game_token = response.json()['token'] ``` ## Console session The developer console and the back office sign in with `POST /v1/console/sign-in` (email, password), then `POST /v1/console/sign-in/totp` when the account has an authenticator. The answer sets the cookie `filudi_console`: HttpOnly, Secure, SameSite Strict, host-only on the API, 14 days. The browser sends it on its own; a script adds `credentials: 'include'` to each `fetch`. Every non-GET request under `/v1/console/*` and `/v1/admin/*` must carry an `Origin` header listed in the API's console origins (developers.filudi.com and admin.filudi.com). A browser sets it. A request without one, or with another, answers 403 `forbidden`. Staff without an authenticator gets 403 on every `/v1/admin/*` route except `GET /v1/admin/me`. A staff member who opens an impersonation gets a second cookie, `filudi_impersonation`, valid one hour; a request with the header `X-Filudi-Impersonation: 1` is read from that cookie instead. ```sh curl -X POST 'https://api.filudi.com/v1/console/sign-in' \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -c cookies.txt \ -d '{ "email": "you@example.com", "password": "string" }' curl 'https://api.filudi.com/v1/console/me' -b cookies.txt ``` ```js const signIn = await fetch('https://api.filudi.com/v1/console/sign-in', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email, password }), }); const { totpRequired, me } = await signIn.json(); // totpRequired: finish with POST /v1/console/sign-in/totp { code } const account = await fetch('https://api.filudi.com/v1/console/me', { credentials: 'include' }); ``` ```py import requests # The console API is made for a browser. A script keeps the cookie in a session. session = requests.Session() session.headers['origin'] = 'https://developers.filudi.com' session.post( 'https://api.filudi.com/v1/console/sign-in', json={'email': 'you@example.com', 'password': password}, ).raise_for_status() me = session.get('https://api.filudi.com/v1/console/me').json() ``` ## Server key A portal backend calls `/v1/server/*` with a key created in the console, under the site, Keys. The key is `fsk__<32 base62 characters>`, shown once; the API stores a hash and the console shows the first 12 characters. Each key carries scopes (`catalog`, `players`, `tokens`, `leaderboards`, `stats`) and each route says which one it needs. 600 requests a minute per key; over that, 429 with `Retry-After`. Only an approved site has keys, and a revoked key answers 403. A server key never reaches a browser: `/v1/server/*` sends no CORS headers, so a page cannot call it. The key sits in your backend's environment. ```sh curl 'https://api.filudi.com/v1/server/catalog?locale=en' \ -H "authorization: Bearer $FILUDI_SITE_KEY" ``` ```js // Node, in your backend. siteKey comes from the environment. const response = await fetch('https://api.filudi.com/v1/server/catalog?locale=en', { headers: { authorization: `Bearer ${siteKey}` }, }); const catalog = await response.json(); ``` ```py import os import requests response = requests.get( 'https://api.filudi.com/v1/server/catalog', params={'locale': 'en'}, headers={'authorization': f"Bearer {os.environ['FILUDI_SITE_KEY']}"}, ) catalog = response.json() ``` ## Public routes `GET /v1/catalog/{site}`, `GET /v1/catalog/{site}/games/{slug}`, the reviews of a game, the bundle and media files, `POST /v1/sdk/players`, `POST /v1/feedback`, `POST /v1/console/invitation-requests` and the health routes take no credential. A few routes accept a player token and answer without one too; the reference marks them "Credential, optional". Source: https://developers.filudi.com/docs/api/authentication/ --- # CORS and origins Which origins may call the Filudi API from a browser and frame a game, how a site's domains join that list, and what a game origin may do. A browser call reaches the API only from an origin the API allows. The same list is the `frame-ancestors` of every game bundle: an origin that may call the API may frame a game, and no other. ## The list The allowed origins are the union of: - `https://` for every entry of `domains` of every approved site. A pending site has no origin, and a proposed domain (`pendingDomains`) joins the list once staff approves it. - The hub origins the API is started with (`HUB_URLS`): filudi.com, the playground and the local ports of the monorepo, `http://localhost:4321` and `http://localhost:5173`. - The console origins (`CONSOLE_URLS`), for `/v1/console/*` and `/v1/admin/*` only, with credentials. Each API pod reloads the site domains every 60 seconds. A domain approved in the back office works within a minute, on every pod, without a redeploy. `localhost` is never a site domain. For local work, run the API from the monorepo, which allows the two local ports above, or add your port to `HUB_URLS` on that API. ## Credentials and the console `/v1/console/*` and `/v1/admin/*` answer the console origins with `Access-Control-Allow-Credentials: true`, so the `filudi_console` cookie travels with `credentials: 'include'`. Every non-GET request there must carry an `Origin` from that list; otherwise 403 `forbidden`. A browser sets `Origin` on its own. A `curl` adds it by hand. ## Game origins Each live game runs on its own origin, `https://.`. That origin gets CORS on the five routes a game token opens (`GET /v1/sdk/players/me`, `/v1/sdk/saves/{gameId}`, `POST /v1/sdk/sessions`, `/v1/sdk/leaderboards/{gameId}/*`, `POST /v1/sdk/events`) and on nothing else. A game cannot read a fresh guest token, a sign-in answer, a rating or a feedback route from its frame. A bundle answers with a Content Security Policy that lets it fetch its own origin and the API only, with no remote script and no frame. `frame-ancestors` lists the allowed origins, so your page frames a game only once your domain is approved. ## Server routes `/v1/server/*` sends no CORS headers at all. A browser cannot call it, which is the point: the server key stays in your backend. Call these routes from your servers and hand the browser a player token (`POST /v1/server/players/{playerId}/tokens`) or a game token (`POST /v1/server/game-tokens`) over your own authenticated channel. ## Your page's policy Your pages send a CSP of their own. The one that lets a frame load and the page call the API: ``` frame-src https://play.filudi.com https://*. connect-src 'self' https://api.filudi.com img-src 'self' data: https://play.filudi.com media-src https://play.filudi.com ``` Never serve a bundle from your own origin: the frame has `allow-same-origin`, so a bundle on your origin could read your `localStorage`, where the player's token sits. `GameFrame` throws on a bundle from the page's origin. Source: https://developers.filudi.com/docs/api/cors-and-origins/ --- # Errors The error body of the Filudi API, its nine codes and the status of each, and the two routes that answer another shape. Every error is JSON with two fields and sometimes a third: ```json { "error": "invalid", "message": "nickname: 3 to 20 characters.", "details": { "field": "nickname" } } ``` `error` is one of nine codes. `message` is written for the developer who reads the response, in English, and says what failed. `details` is present when the API has something structured to add, such as the field at fault. ```sh curl -s -o body.json -w '%{http_code}\n' \ -X PATCH 'https://api.filudi.com/v1/sdk/players/me' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \ -H 'content-type: application/json' \ -d '{ "nickname": "a" }' cat body.json ``` ```js const response = await fetch('https://api.filudi.com/v1/sdk/players/me', { method: 'PATCH', headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' }, body: JSON.stringify({ nickname: 'a' }), }); if (!response.ok) { const { error, message, details } = await response.json(); if (error === 'unauthorized') forgetToken(); if (error === 'rate_limited') waitSeconds(Number(response.headers.get('retry-after'))); console.warn(error, message, details); } ``` ```py import requests response = requests.patch( 'https://api.filudi.com/v1/sdk/players/me', headers={'authorization': f'Bearer {player_token}'}, json={'nickname': 'a'}, ) if not response.ok: body = response.json() print(response.status_code, body['error'], body['message']) if body['error'] == 'rate_limited': retry_after = int(response.headers['retry-after']) ``` ## Codes | Code | Status | When | | -------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ | | `invalid` | 400 | The body or a parameter breaks the schema. `message` names the field | | `unauthorized` | 401 | No credential, or one the API refuses. For a player token: forget it and create a new guest | | `forbidden` | 403 | The credential is valid and does not open this action: another game's token, a role, a scope, an origin | | `not_found` | 404 | Unknown resource, or one your credential may not see. An unknown site slug and a game outside a site's catalog answer this too | | `conflict` | 409 | The state refuses the change: a slug taken, a version already live, a quota reached | | `too_large` | 413 | The body is over the route's limit (64 kB for a save or an event batch) | | `rate_limited` | 429 | Too many requests. `Retry-After` says how many seconds to wait; see [Rate limits](/docs/api/rate-limits/) | | `unavailable` | 503 | A dependency is down, usually Redis. Retry in a few seconds; `Retry-After` is set when the API knows | | `internal` | 500 | An unexpected failure. The API logged it | The write routes of the admin API answer 400 `invalid`, 403 `forbidden`, 404 `not_found` and 409 `conflict` with this shape even where the reference lists only the success code. ## Two routes with another body `PUT /v1/sdk/saves/{gameId}` answers 409 with the server's copy of the save, not an error: `{ gameId, version, etag, bytes, updatedAt, data }`, with `version` 0 and `data` null when nothing is stored yet. Take it, merge if the game can, and write again with `If-Match` set to that version. `POST /v1/sdk/leaderboards/{gameId}/{board}/scores` answers 400 with `{ error: "invalid", reason, message }`. `reason` is one of `bounds`, `signature`, `session`, `board`, `nonce`, `clock`, `meta`, so a game logs why a score was refused. ## Details worth reading - `details.field: "current"` on a 401 of `PUT /v1/account/password` and `PUT /v1/console/password`: the session or token is fine, the current password is wrong. Keep the token. - The issue code `game_host` on a 400 of `POST /v1/admin/sites` and its PATCH: a domain is on or under the play domain, where no site may run. - A 403 of `PUT /v1/admin/sites/{siteId}/catalog/{gameId}` carries in `message` why the site may not list the game: pending, no live version, or `first_party` on a site that is not first-party. Source: https://developers.filudi.com/docs/api/errors/ --- # Rate limits What the Filudi API limits, by IP, player, email or key, and how a client reads the 429 answer. Limits live in Redis and count per IP, per player, per email or per key, depending on the route. Over a limit the API answers 429 `rate_limited` with a `Retry-After` header in seconds. When Redis is down, a limited write route answers 503 `unavailable` instead of guessing. ## Per route | Route | Limit | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | | `POST /v1/sdk/players` | 20 per hour per IP. Create guests in the browser, where each visitor has their own IP | | `POST /v1/sdk/game-tokens` | 240 per player per hour | | `POST /v1/sdk/leaderboards/{gameId}/{board}/scores` | `ratePerMinute` of the board, 10 by default; a nonce is accepted once per session | | `POST /v1/sdk/events` | 50 items per batch, 64 kB per body | | `PUT /v1/sdk/games/{gameId}/rating` | 60 ratings per player per hour | | `POST /v1/sdk/games/{gameId}/comments` | 5 comments per player per hour | | `PATCH /v1/sdk/comments/{commentId}` | 30 edits per player per hour | | `POST /v1/account/magic-link` | 3 links per email per hour | | `POST /v1/account/magic-link/consume-code` | 20 attempts per IP per hour; the fifth wrong code burns the link | | `POST /v1/account/sign-in`, `POST /v1/console/sign-in` | Attempts counted per email and per IP; unknown email and wrong password answer the same 401 in the same time | | `POST /v1/feedback` | 5 per IP per hour and 10 per player per day | | `POST /v1/console/invitation-requests` | 5 per IP per day | | `/v1/server/*` | 600 requests a minute per key; no IP limit | The admin API has no request limit beyond the quotas of the data itself: 10 pending games and 3 pending sites per account, 20 keys per site, 10 proposed domains, 50 grants per invitation. ## What a client does Read `Retry-After` and wait that long before the same request. Do not retry a 429 in a loop: the counter does not reset on refused requests. ```js async function withRetry(request) { const response = await request(); if (response.status !== 429) return response; const seconds = Number(response.headers.get('retry-after') ?? '1'); await new Promise((resolve) => setTimeout(resolve, seconds * 1000)); return request(); } ``` `@filudi/hub-core` and `@filudi/game-sdk` follow this rule on their own. A score refused for rate reasons comes back to the SDK as `rejected`, never as an exception. ## Caches you can lean on Public reads carry `Cache-Control` and an `ETag`. `GET /v1/catalog/{site}` is `public, max-age=300, stale-while-revalidate=600`; the reviews of a game are `public, max-age=60`; a leaderboard top is `public, max-age=30`. Send `If-None-Match` and a 304 costs nothing. The statistics routes are cached 60 s in Redis per set of filters, so two identical calls in a minute return the same numbers. Source: https://developers.filudi.com/docs/api/rate-limits/ --- # Account routes The account routes of games-api, with parameters, bodies, responses and request samples. Generated from `openapi.yaml` of games-api 0.7.0: 8 routes. - [`POST /v1/account/magic-link`](#post-v1-account-magic-link): Email a sign-in link - [`POST /v1/account/magic-link/consume`](#post-v1-account-magic-link-consume): Exchange the link token for an account player token - [`POST /v1/account/magic-link/consume-code`](#post-v1-account-magic-link-consume-code): Exchange the code of a sign-in email for an account player token - [`POST /v1/account/sign-in`](#post-v1-account-sign-in): Sign in with email and password - [`PUT /v1/account/password`](#put-v1-account-password): Set or change the account's password - [`DELETE /v1/account/password`](#delete-v1-account-password): Remove the password; the account signs in by link or code only - [`GET /v1/account/export`](#get-v1-account-export): Everything stored about the player - [`DELETE /v1/account`](#delete-v1-account): Delete the player and all data ```sh curl -X POST 'https://api.filudi.com/v1/account/magic-link' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \ -H 'content-type: application/json' \ -d '{ "email": "you@example.com", "redirect": "string" }' ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players const response = await fetch('https://api.filudi.com/v1/account/magic-link', { method: 'POST', headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' }, body: JSON.stringify({ email: 'you@example.com', redirect: 'string', }), }); // No body: check response.ok. ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players response = requests.post( 'https://api.filudi.com/v1/account/magic-link', headers={'authorization': f'Bearer {player_token}'}, json={ 'email': 'you@example.com', 'redirect': 'string', }, ) response.raise_for_status() # No body: the status says it all. ``` **Email a sign-in link** Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) answers 401 here. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `email` | string (email) | yes | | | `redirect` | string | no | Path on the site to return to after verification | ### Responses | Status | Body | Description | | --- | --- | --- | | 202 | none | Sent, or silently not sent for an unknown reason the client must not learn. | | 403 | none | Accounts are off for this site. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | ```sh curl -X POST 'https://api.filudi.com/v1/account/magic-link/consume' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \ -H 'content-type: application/json' \ -d '{ "token": "string" }' ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players const response = await fetch('https://api.filudi.com/v1/account/magic-link/consume', { method: 'POST', headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' }, body: JSON.stringify({ token: 'string', }), }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players response = requests.post( 'https://api.filudi.com/v1/account/magic-link/consume', headers={'authorization': f'Bearer {player_token}'}, json={ 'token': 'string', }, ) response.raise_for_status() data = response.json() ``` **Exchange the link token for an account player token** With a guest token, the API merges the guest into the account player (ADR 0005). Credential, optional: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) answers 401 here. Without a token the answer is the anonymous one. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `token` | string | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [ConsumeResponse](/docs/api/reference/schemas/#schema-consumeresponse) | Account player and token. | | 400 | none | Link expired, used or unknown. | | 403 | none | Accounts are off for this site. | ```sh curl -X POST 'https://api.filudi.com/v1/account/magic-link/consume-code' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \ -H 'content-type: application/json' \ -d '{ "email": "you@example.com", "code": "string", "site": "filudi" }' ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players const response = await fetch('https://api.filudi.com/v1/account/magic-link/consume-code', { method: 'POST', headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' }, body: JSON.stringify({ email: 'you@example.com', code: 'string', site: 'filudi', }), }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players response = requests.post( 'https://api.filudi.com/v1/account/magic-link/consume-code', headers={'authorization': f'Bearer {player_token}'}, json={ 'email': 'you@example.com', 'code': 'string', 'site': 'filudi', }, ) response.raise_for_status() data = response.json() ``` **Exchange the code of a sign-in email for an account player token** Same effect as consuming the link (ADR 0011). The page that asked for the link sends the 6-digit code the email shows; spaces are ignored. The API checks it against the most recent link of this email on the site that is neither consumed nor expired. The fifth wrong code burns that link. The site is the token's; without a token, `site` names it. An invalid or dead bearer counts as no token, and `site` is then required. `redirect` is never set: it travels inside the link, not with the code. 20 attempts per IP per hour. Credential, optional: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) answers 401 here. Without a token the answer is the anonymous one. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `email` | string (email) | yes | | | `code` | string | yes | Six digits, "123456" or "123 456"; pattern `^[0-9 ]{6,7}$` | | `site` | string | no | Site slug. Required without a player token; with one, it must be the token's site | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [ConsumeResponse](/docs/api/reference/schemas/#schema-consumeresponse) | Account player and token. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | "This code is wrong, expired or already used. Ask for a new email." Also a missing or mismatched `site`, and an account scheduled for deletion. | | 403 | none | Accounts are off for this site. | | 404 | none | Unknown site slug. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | | 503 | none | Redis is down; the rate limit cannot be checked. | ```sh curl -X POST 'https://api.filudi.com/v1/account/sign-in' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \ -H 'content-type: application/json' \ -d '{ "email": "you@example.com", "password": "string", "site": "filudi" }' ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players const response = await fetch('https://api.filudi.com/v1/account/sign-in', { method: 'POST', headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' }, body: JSON.stringify({ email: 'you@example.com', password: 'string', site: 'filudi', }), }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players response = requests.post( 'https://api.filudi.com/v1/account/sign-in', headers={'authorization': f'Bearer {player_token}'}, json={ 'email': 'you@example.com', 'password': 'string', 'site': 'filudi', }, ) response.raise_for_status() data = response.json() ``` **Sign in with email and password** For an account that set a password (ADR 0011). With a guest token of the site, the API merges the guest into the account as consume does. An unknown email, an account without a password and a wrong password get the same 401 in the same time. The site is the token's; without a token, `site` names it. An invalid or dead bearer counts as no token, and `site` is then required. 10 attempts per email per 15 minutes and 30 per IP per hour, right or wrong. Credential, optional: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) answers 401 here. Without a token the answer is the anonymous one. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `email` | string (email) | yes | | | `password` | string | yes | A password over 128 characters answers 401; the 8 to 128 rule applies when it is set; 1 to 1024 characters; write only | | `site` | string | no | Site slug. Required without a player token; with one, it must be the token's site | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [ConsumeResponse](/docs/api/reference/schemas/#schema-consumeresponse) | Account player and token. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | A missing or mismatched `site`, or an account scheduled for deletion. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | "Email or password is wrong.". | | 403 | none | Accounts are off for this site. | | 404 | none | Unknown site slug. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | | 503 | none | Redis is down; the rate limit cannot be checked. | ```sh curl -X PUT 'https://api.filudi.com/v1/account/password' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \ -H 'content-type: application/json' \ -d '{ "password": "string", "current": "string" }' ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players const response = await fetch('https://api.filudi.com/v1/account/password', { method: 'PUT', headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' }, body: JSON.stringify({ password: 'string', current: 'string', }), }); // No body: check response.ok. ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players response = requests.put( 'https://api.filudi.com/v1/account/password', headers={'authorization': f'Bearer {player_token}'}, json={ 'password': 'string', 'current': 'string', }, ) response.raise_for_status() # No body: the status says it all. ``` **Set or change the account's password** Account players only (ADR 0011). 8 to 128 characters after NFC normalization, not equal to the account's email (case-insensitive), no other rule. When a password exists, `current` is required and checked. 20 attempts per player per hour, right or wrong. Other devices stay signed in. Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) answers 401 here. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `password` | string | yes | 8 to 128 characters; write only | | `current` | string | no | Required when the account has a password; 1 to 128 characters; write only | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Saved. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token, or `current` is wrong; then the body carries `details: { field: "current" }`. | | 403 | none | A guest: "Add your email first: a password needs an account.". | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | | 503 | none | Redis is down; the rate limit cannot be checked. | ```sh curl -X DELETE 'https://api.filudi.com/v1/account/password' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players const response = await fetch('https://api.filudi.com/v1/account/password', { method: 'DELETE', headers: { authorization: `Bearer ${playerToken}` }, }); // No body: check response.ok. ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players response = requests.delete( 'https://api.filudi.com/v1/account/password', headers={'authorization': f'Bearer {player_token}'}, ) response.raise_for_status() # No body: the status says it all. ``` **Remove the password; the account signs in by link or code only** Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) answers 401 here. ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Removed. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | | 403 | none | A guest has no password. | ```sh curl 'https://api.filudi.com/v1/account/export' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players const response = await fetch('https://api.filudi.com/v1/account/export', { headers: { authorization: `Bearer ${playerToken}` }, }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players response = requests.get( 'https://api.filudi.com/v1/account/export', headers={'authorization': f'Bearer {player_token}'}, ) response.raise_for_status() data = response.json() ``` **Everything stored about the player** Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) answers 401 here. ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [PlayerExport](/docs/api/reference/schemas/#schema-playerexport) | JSON file. Headers: `Content-Disposition`. | ```sh curl -X DELETE 'https://api.filudi.com/v1/account' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players const response = await fetch('https://api.filudi.com/v1/account', { method: 'DELETE', headers: { authorization: `Bearer ${playerToken}` }, }); // No body: check response.ok. ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players response = requests.delete( 'https://api.filudi.com/v1/account', headers={'authorization': f'Bearer {player_token}'}, ) response.raise_for_status() # No body: the status says it all. ``` **Delete the player and all data** Scheduled; `cron:purge` removes the rows within 24 hours. The token stops working now. Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) answers 401 here. ### Responses | Status | Body | Description | | --- | --- | --- | | 202 | none | Deletion scheduled. | Source: https://developers.filudi.com/docs/api/reference/account/ --- # Admin routes The admin routes of games-api, with parameters, bodies, responses and request samples. Generated from `openapi.yaml` of games-api 0.7.0: 93 routes. - [`GET /v1/admin/me`](#get-v1-admin-me): Who am I and what can I edit - [`GET /v1/admin/accounts`](#get-v1-admin-accounts): Staff only. Accounts, newest first - [`GET /v1/admin/accounts/{accountId}`](#get-v1-admin-accounts-accountid): Staff only. One account with its memberships - [`PATCH /v1/admin/accounts/{accountId}`](#patch-v1-admin-accounts-accountid): Staff only. Change name, email, flags, or disable the account - [`POST /v1/admin/accounts/{accountId}/reset-link`](#post-v1-admin-accounts-accountid-reset-link): Staff only. Email the account a password reset link - [`DELETE /v1/admin/accounts/{accountId}/totp`](#delete-v1-admin-accounts-accountid-totp): Staff only. Remove the account's TOTP - [`DELETE /v1/admin/accounts/{accountId}/sessions`](#delete-v1-admin-accounts-accountid-sessions): Staff only. Revoke every session of the account - [`POST /v1/admin/accounts/{accountId}/impersonate`](#post-v1-admin-accounts-accountid-impersonate): Staff only. Open a one-hour session on a developer's account - [`GET /v1/admin/invitations`](#get-v1-admin-invitations): Staff only. Invitations, newest first - [`POST /v1/admin/invitations`](#post-v1-admin-invitations): Staff only. Invite an email address - [`DELETE /v1/admin/invitations/{invitationId}`](#delete-v1-admin-invitations-invitationid): Staff only. Revoke an invitation; its link stops working - [`POST /v1/admin/invitations/{invitationId}/resend`](#post-v1-admin-invitations-invitationid-resend): Staff only. Send the invitation email again with a fresh 7-day token - [`GET /v1/admin/invitation-requests`](#get-v1-admin-invitation-requests): Staff only. Requests from the form on developers.filudi.com, newest first - [`PATCH /v1/admin/invitation-requests/{requestId}`](#patch-v1-admin-invitation-requests-requestid): Staff only. Dismiss a request - [`GET /v1/admin/approvals`](#get-v1-admin-approvals): Staff only. Everything that waits for approval, oldest first - [`GET /v1/admin/sites`](#get-v1-admin-sites): Sites the account can see; an account with no site membership gets an empty list - [`POST /v1/admin/sites`](#post-v1-admin-sites): Create a site - [`GET /v1/admin/sites/{siteId}`](#get-v1-admin-sites-siteid): One site - [`PATCH /v1/admin/sites/{siteId}`](#patch-v1-admin-sites-siteid): Update policy, domains, locales, name - [`POST /v1/admin/sites/{siteId}/approve`](#post-v1-admin-sites-siteid-approve): Staff only. Approve a pending site - [`POST /v1/admin/sites/{siteId}/reject`](#post-v1-admin-sites-siteid-reject): Staff only. Send the owners a reason; the site stays pending - [`POST /v1/admin/sites/{siteId}/domains/approve`](#post-v1-admin-sites-siteid-domains-approve): Staff only. Move proposed domains into the site's domains - [`POST /v1/admin/sites/{siteId}/domains/reject`](#post-v1-admin-sites-siteid-domains-reject): Staff only. Remove proposed domains and send the owners the reason - [`GET /v1/admin/sites/{siteId}/members`](#get-v1-admin-sites-siteid-members): Members of a site - [`POST /v1/admin/sites/{siteId}/members`](#post-v1-admin-sites-siteid-members): Add a member by email - [`DELETE /v1/admin/sites/{siteId}/members/{accountId}`](#delete-v1-admin-sites-siteid-members-accountid): Remove a member - [`DELETE /v1/admin/sites/{siteId}/members/invitations/{invitationId}`](#delete-v1-admin-sites-siteid-members-invitations-invitationid): Withdraw an invitation - [`GET /v1/admin/sites/{siteId}/keys`](#get-v1-admin-sites-siteid-keys): Server keys of a site, revoked ones included - [`POST /v1/admin/sites/{siteId}/keys`](#post-v1-admin-sites-siteid-keys): Create a server key; the answer shows it once - [`DELETE /v1/admin/sites/{siteId}/keys/{keyId}`](#delete-v1-admin-sites-siteid-keys-keyid): Revoke a server key - [`GET /v1/admin/sites/{siteId}/categories`](#get-v1-admin-sites-siteid-categories): Categories of a site, in site order, with the number of games in each - [`POST /v1/admin/sites/{siteId}/categories`](#post-v1-admin-sites-siteid-categories): Add a category at the end of the site order - [`PUT /v1/admin/sites/{siteId}/categories/order`](#put-v1-admin-sites-siteid-categories-order): Reorder the categories - [`GET /v1/admin/sites/{siteId}/categories/{slug}`](#get-v1-admin-sites-siteid-categories-slug): One category - [`PATCH /v1/admin/sites/{siteId}/categories/{slug}`](#patch-v1-admin-sites-siteid-categories-slug): Update texts and icon - [`DELETE /v1/admin/sites/{siteId}/categories/{slug}`](#delete-v1-admin-sites-siteid-categories-slug): Remove the category from the site, from its games and from the top menu - [`GET /v1/admin/sites/{siteId}/tags`](#get-v1-admin-sites-siteid-tags): Tags of a site, in site order, with the number of games carrying each - [`POST /v1/admin/sites/{siteId}/tags`](#post-v1-admin-sites-siteid-tags): Add a tag at the end of the site order - [`PUT /v1/admin/sites/{siteId}/tags/order`](#put-v1-admin-sites-siteid-tags-order): Reorder the tags - [`GET /v1/admin/sites/{siteId}/tags/{slug}`](#get-v1-admin-sites-siteid-tags-slug): One tag - [`PATCH /v1/admin/sites/{siteId}/tags/{slug}`](#patch-v1-admin-sites-siteid-tags-slug): Update the titles - [`DELETE /v1/admin/sites/{siteId}/tags/{slug}`](#delete-v1-admin-sites-siteid-tags-slug): Remove the tag from the site and from its games - [`GET /v1/admin/sites/{siteId}/catalog`](#get-v1-admin-sites-siteid-catalog): Site catalog entries with every locale - [`PUT /v1/admin/sites/{siteId}/catalog/order`](#put-v1-admin-sites-siteid-catalog-order): Reorder the catalog - [`PUT /v1/admin/sites/{siteId}/catalog/{gameId}`](#put-v1-admin-sites-siteid-catalog-gameid): Add or update a game in the site catalog - [`DELETE /v1/admin/sites/{siteId}/catalog/{gameId}`](#delete-v1-admin-sites-siteid-catalog-gameid): Remove a game from the site catalog - [`POST /v1/admin/sites/{siteId}/publish`](#post-v1-admin-sites-siteid-publish): Make the catalog changes visible on the site front - [`GET /v1/admin/games`](#get-v1-admin-games): Every game with its versions - [`POST /v1/admin/games`](#post-v1-admin-games): Create a game - [`GET /v1/admin/games/{gameId}`](#get-v1-admin-games-gameid): One game - [`PATCH /v1/admin/games/{gameId}`](#patch-v1-admin-games-gameid): Update a game - [`DELETE /v1/admin/games/{gameId}`](#delete-v1-admin-games-gameid): Delete a game and everything attached to it (staff only) - [`POST /v1/admin/games/{gameId}/approve`](#post-v1-admin-games-gameid-approve): Staff only. Approve a pending game - [`POST /v1/admin/games/{gameId}/reject`](#post-v1-admin-games-gameid-reject): Staff only. Send the owners a reason; the game stays pending - [`GET /v1/admin/games/{gameId}/members`](#get-v1-admin-games-gameid-members): Members of a game - [`POST /v1/admin/games/{gameId}/members`](#post-v1-admin-games-gameid-members): Add a member by email - [`DELETE /v1/admin/games/{gameId}/members/{accountId}`](#delete-v1-admin-games-gameid-members-accountid): Remove a member - [`DELETE /v1/admin/games/{gameId}/members/invitations/{invitationId}`](#delete-v1-admin-games-gameid-members-invitations-invitationid): Withdraw an invitation - [`GET /v1/admin/games/{gameId}/stats`](#get-v1-admin-games-gameid-stats): One game's numbers across every site that lists it, for a range of UTC days - [`POST /v1/admin/games/{gameId}/versions`](#post-v1-admin-games-gameid-versions): Start a version upload - [`POST /v1/admin/games/{gameId}/media/{slot}`](#post-v1-admin-games-gameid-media-slot): Start a media upload - [`DELETE /v1/admin/games/{gameId}/media/{slot}`](#delete-v1-admin-games-gameid-media-slot): Empty the slot - [`POST /v1/admin/games/{gameId}/media/{slot}/finalize`](#post-v1-admin-games-gameid-media-slot-finalize): Check the uploaded file and put it in the slot - [`POST /v1/admin/versions/{versionId}/finalize`](#post-v1-admin-versions-versionid-finalize): Unpack the uploaded zip into the bundle prefix - [`POST /v1/admin/versions/{versionId}/activate`](#post-v1-admin-versions-versionid-activate): Make this version live; the previous live one goes back to draft (staff only) - [`POST /v1/admin/versions/{versionId}/retire`](#post-v1-admin-versions-versionid-retire): Retire a version; its bundle stops being served (staff only) - [`POST /v1/admin/versions/{versionId}/review`](#post-v1-admin-versions-versionid-review): Ask staff to review a draft and make it live - [`POST /v1/admin/versions/{versionId}/review/reject`](#post-v1-admin-versions-versionid-review-reject): Staff only. Decline a review request and send the owners the reason - [`GET /v1/admin/versions/{versionId}/preview`](#get-v1-admin-versions-versionid-preview): Signed URL to play a draft version for 1 hour - [`GET /v1/admin/games/{gameId}/leaderboards`](#get-v1-admin-games-gameid-leaderboards): Boards of a game - [`PUT /v1/admin/games/{gameId}/leaderboards`](#put-v1-admin-games-gameid-leaderboards): Replace the boards of a game - [`GET /v1/admin/games/{gameId}/leaderboards/{board}/scores`](#get-v1-admin-games-gameid-leaderboards-board-scores): Scores for moderation - [`POST /v1/admin/scores/{scoreId}`](#post-v1-admin-scores-scoreid): Unhide a score - [`DELETE /v1/admin/scores/{scoreId}`](#delete-v1-admin-scores-scoreid): Hide a score - [`GET /v1/admin/sites/{siteId}/players`](#get-v1-admin-sites-siteid-players): List the players of a site, with an optional search - [`GET /v1/admin/players/{playerId}`](#get-v1-admin-players-playerid): Player detail with saves metadata and best scores - [`DELETE /v1/admin/players/{playerId}`](#delete-v1-admin-players-playerid): Schedule deletion (GDPR request handled by support) - [`GET /v1/admin/players/{playerId}/export`](#get-v1-admin-players-playerid-export): Export a player's data - [`GET /v1/admin/sites/{siteId}/comments`](#get-v1-admin-sites-siteid-comments): Comments of a site for moderation, newest first - [`DELETE /v1/admin/comments/{commentId}`](#delete-v1-admin-comments-commentid): Delete a comment for good - [`POST /v1/admin/comments/{commentId}/hide`](#post-v1-admin-comments-commentid-hide): Hide a comment from every player route - [`POST /v1/admin/comments/{commentId}/unhide`](#post-v1-admin-comments-commentid-unhide): Show a hidden comment again - [`GET /v1/admin/sites/{siteId}/ratings`](#get-v1-admin-sites-siteid-ratings): Rating of every rated game of a site - [`GET /v1/admin/sites/{siteId}/feedback`](#get-v1-admin-sites-siteid-feedback): Feedback of a site, newest first - [`GET /v1/admin/feedback/{feedbackId}`](#get-v1-admin-feedback-feedbackid): One feedback message - [`PATCH /v1/admin/feedback/{feedbackId}`](#patch-v1-admin-feedback-feedbackid): Change the status or the note - [`DELETE /v1/admin/feedback/{feedbackId}`](#delete-v1-admin-feedback-feedbackid): Delete a feedback message for good - [`GET /v1/admin/sites/{siteId}/events/today`](#get-v1-admin-sites-siteid-events-today): Counts since midnight UTC, live from the events table - [`GET /v1/admin/sites/{siteId}/events/daily`](#get-v1-admin-sites-siteid-events-daily): Daily aggregates - [`GET /v1/admin/sites/{siteId}/stats/overview`](#get-v1-admin-sites-siteid-stats-overview): The site dashboard numbers for a range of UTC days - [`GET /v1/admin/sites/{siteId}/stats/games/{gameId}`](#get-v1-admin-sites-siteid-stats-games-gameid): One game's numbers on this site for a range of UTC days - [`GET /v1/admin/sites/{siteId}/stats/web`](#get-v1-admin-sites-siteid-stats-web): Web audience from CausalIT (ADR 0009) - [`GET /v1/admin/audit`](#get-v1-admin-audit): Audit log ```sh curl 'https://api.filudi.com/v1/admin/me' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/me', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/me', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Who am I and what can I edit** The one `/v1/admin/*` route that answers for staff without TOTP. `roles.admin` is true for staff with TOTP enabled. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [ConsoleMe](/docs/api/reference/schemas/#schema-consoleme) | Identity. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | ```sh curl 'https://api.filudi.com/v1/admin/accounts' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/accounts', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/accounts', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Staff only. Accounts, newest first** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `q` | query | string | no | Case-insensitive substring of the name, or an email matched by its hash; 2 to 100 characters | | `cursor` | query | string | no | Opaque; the `nextCursor` of the previous page, with the same filters and sort; at most 200 characters | | `limit` | query | integer | no | 1 to 500; default `100` | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [AccountPage](/docs/api/reference/schemas/#schema-accountpage) | Accounts. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | ```sh curl 'https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Staff only. One account with its memberships** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `accountId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [AccountAdmin](/docs/api/reference/schemas/#schema-accountadmin) | Account. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X PATCH 'https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "name": "string", "email": "you@example.com", "staff": true, "canCreateGames": true, "canCreatePortals": true, "disabled": true }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000', { method: 'PATCH', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: 'string', email: 'you@example.com', staff: true, canCreateGames: true, canCreatePortals: true, disabled: true, }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.patch( 'https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'name': 'string', 'email': 'you@example.com', 'staff': True, 'canCreateGames': True, 'canCreatePortals': True, 'disabled': True, }, ) response.raise_for_status() data = response.json() ``` **Staff only. Change name, email, flags, or disable the account** Disabling an account revokes its sessions. Every change writes an audit row. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `accountId` | path | string (uuid) | yes | | ### Request body `application/json`, required. The body is a [AccountPatch](/docs/api/reference/schemas/#schema-accountpatch). At least one field; an empty body is a 400. | Property | Type | Required | Description | | --- | --- | --- | --- | | `name` | [AccountName](/docs/api/reference/schemas/#schema-accountname) | no | Display name, shown to other members and in the audit log; 1 to 80 characters | | `email` | string (email) | no | Changed at once | | `staff` | boolean | no | A new staff member gets admin rights once TOTP is enabled | | `canCreateGames` | boolean | no | | | `canCreatePortals` | boolean | no | | | `disabled` | boolean | no | True revokes every session and refuses sign-in; false enables again | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [AccountAdmin](/docs/api/reference/schemas/#schema-accountadmin) | Account. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 409 | none | The email is on another account. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000/reset-link' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000/reset-link', { method: 'POST', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000/reset-link', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Staff only. Email the account a password reset link** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `accountId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 202 | none | Sent. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X DELETE 'https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000/totp' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000/totp', { method: 'DELETE', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000/totp', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Staff only. Remove the account's TOTP** For a lost authenticator. A staff account loses its admin rights until it enrols again. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `accountId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Removed, or there was none. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X DELETE 'https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000/sessions' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000/sessions', { method: 'DELETE', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000/sessions', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Staff only. Revoke every session of the account** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `accountId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Revoked. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000/impersonate' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000/impersonate', { method: 'POST', credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000/impersonate', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() data = response.json() ``` **Staff only. Open a one-hour session on a developer's account** Sets the `filudi_impersonation` cookie; the console sends `X-Filudi-Impersonation: 1` to use it, the admin tab keeps acting as the staff member. Every audit row written during it carries the developer as `actor` and the staff member as `impersonator`. Refused on a staff account and on oneself. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `accountId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [ImpersonationStarted](/docs/api/reference/schemas/#schema-impersonationstarted) | Where to open the console. Headers: `Set-Cookie` (`filudi_impersonation`). | | 403 | none | Not staff, a staff account, or your own account. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl 'https://api.filudi.com/v1/admin/invitations' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/invitations', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/invitations', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Staff only. Invitations, newest first** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `status` | query | one of `"pending"`, `"accepted"`, `"revoked"`, `"expired"` | no | default `"pending"` | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Invitation](/docs/api/reference/schemas/#schema-invitation)[] | Invitations. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/invitations' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "email": "you@example.com", "staff": false, "canCreateGames": false, "canCreatePortals": false, "grants": [ { "game": "00000000-0000-0000-0000-000000000000", "site": "00000000-0000-0000-0000-000000000000", "role": "owner" } ] }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/invitations', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: 'you@example.com', staff: false, canCreateGames: false, canCreatePortals: false, grants: [{ game: '00000000-0000-0000-0000-000000000000', site: '00000000-0000-0000-0000-000000000000', role: 'owner', }], }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/invitations', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'email': 'you@example.com', 'staff': False, 'canCreateGames': False, 'canCreatePortals': False, 'grants': [{ 'game': '00000000-0000-0000-0000-000000000000', 'site': '00000000-0000-0000-0000-000000000000', 'role': 'owner', }], }, ) response.raise_for_status() data = response.json() ``` **Staff only. Invite an email address** The `notify` service sends the link, valid 7 days. A pending invitation for the same email is revoked first. An open invitation request for the email moves to `invited`. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Request body `application/json`, required. The body is a [InvitationInput](/docs/api/reference/schemas/#schema-invitationinput). | Property | Type | Required | Description | | --- | --- | --- | --- | | `email` | string (email) | yes | at most 254 characters | | `staff` | boolean | no | default `false` | | `canCreateGames` | boolean | no | default `false` | | `canCreatePortals` | boolean | no | default `false` | | `grants` | object[] | no | Memberships created on accept. Each item names exactly one of `game` and `site`.; at most 50 items | | `grants[].game` | string (uuid) | no | | | `grants[].site` | string (uuid) | no | | | `grants[].role` | [MembershipRole](/docs/api/reference/schemas/#schema-membershiprole) | yes | A member has every right of the owner except membership changes | ### Responses | Status | Body | Description | | --- | --- | --- | | 201 | [Invitation](/docs/api/reference/schemas/#schema-invitation) | Invitation. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 409 | none | An account with that email exists. | ```sh curl -X DELETE 'https://api.filudi.com/v1/admin/invitations/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/invitations/00000000-0000-0000-0000-000000000000', { method: 'DELETE', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/admin/invitations/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Staff only. Revoke an invitation; its link stops working** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `invitationId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Revoked. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 409 | none | Already accepted. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/invitations/00000000-0000-0000-0000-000000000000/resend' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/invitations/00000000-0000-0000-0000-000000000000/resend', { method: 'POST', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/invitations/00000000-0000-0000-0000-000000000000/resend', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Staff only. Send the invitation email again with a fresh 7-day token** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `invitationId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 202 | none | Sent. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 409 | none | Accepted or revoked. | ```sh curl 'https://api.filudi.com/v1/admin/invitation-requests' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/invitation-requests', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/invitation-requests', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Staff only. Requests from the form on developers.filudi.com, newest first** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `status` | query | one of `"new"`, `"invited"`, `"dismissed"` | no | Default `new` | | `cursor` | query | string | no | Opaque; the `nextCursor` of the previous page, with the same filters and sort; at most 200 characters | | `limit` | query | integer | no | 1 to 500; default `100` | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [InvitationRequestPage](/docs/api/reference/schemas/#schema-invitationrequestpage) | Requests. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | ```sh curl -X PATCH 'https://api.filudi.com/v1/admin/invitation-requests/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "status": "dismissed" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/invitation-requests/00000000-0000-0000-0000-000000000000', { method: 'PATCH', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ status: 'dismissed', }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.patch( 'https://api.filudi.com/v1/admin/invitation-requests/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'status': 'dismissed', }, ) response.raise_for_status() data = response.json() ``` **Staff only. Dismiss a request** The `invited` status is set by the API when an invitation is created for the email. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `requestId` | path | string (uuid) | yes | | ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `status` | one of `"dismissed"` | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [InvitationRequest](/docs/api/reference/schemas/#schema-invitationrequest) | Request. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl 'https://api.filudi.com/v1/admin/approvals' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/approvals', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/approvals', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Staff only. Everything that waits for approval, oldest first** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Approvals](/docs/api/reference/schemas/#schema-approvals) | Pending games, sites, domains and versions. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | ```sh curl 'https://api.filudi.com/v1/admin/sites' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/sites', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Sites the account can see; an account with no site membership gets an empty list** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Site](/docs/api/reference/schemas/#schema-site)[] | Sites. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/sites' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "slug": "gloop", "name": "string", "domains": [ "string" ], "pendingDomains": [ "string" ], "firstParty": true, "locales": [ "string" ], "accountsAllowed": true, "personalizedAdsAllowed": true, "ageBand": "all", "cdnPullZoneId": "string", "navCategories": [ "string" ], "otherSites": [ { "name": "string", "url": "https://example.com/", "description": "string" } ], "causalitDomain": "string", "causalitReadKey": "string", "causalitWriteKey": "string" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ slug: 'gloop', name: 'string', domains: ['string'], pendingDomains: ['string'], firstParty: true, locales: ['string'], accountsAllowed: true, personalizedAdsAllowed: true, ageBand: 'all', cdnPullZoneId: 'string', navCategories: ['string'], otherSites: [{ name: 'string', url: 'https://example.com/', description: 'string', }], causalitDomain: 'string', causalitReadKey: 'string', causalitWriteKey: 'string', }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/sites', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'slug': 'gloop', 'name': 'string', 'domains': ['string'], 'pendingDomains': ['string'], 'firstParty': True, 'locales': ['string'], 'accountsAllowed': True, 'personalizedAdsAllowed': True, 'ageBand': 'all', 'cdnPullZoneId': 'string', 'navCategories': ['string'], 'otherSites': [{ 'name': 'string', 'url': 'https://example.com/', 'description': 'string', }], 'causalitDomain': 'string', 'causalitReadKey': 'string', 'causalitWriteKey': 'string', }, ) response.raise_for_status() data = response.json() ``` **Create a site** Staff, or an account with `canCreatePortals`; anyone else gets 403. A staff creation is approved at once. A developer's site is created with `approvedAt` null and the creator as owner; staff approves it from the Approvals screen. A developer writes `pendingDomains`, never `domains`. One account holds at most 3 pending sites (409). A reserved slug answers 400: `admin`, `api`, `play`, `developers`, `filudi`, `www`, `static`. This route and PATCH answer 400 with issue code `game_host` when a domain is on or under the play domain, the `PLAY_URL` host or the preview host (ADR 0013). Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `slug` | string | yes | pattern `^[a-z][a-z0-9-]{1,39}$` | | `name` | string | yes | 1 to 80 characters | | `domains` | string[] | no | Lowercase hostnames, the first is canonical. Staff only.; at least 1 item | | `pendingDomains` | string[] | no | Lowercase hostnames the owner proposes; each works once staff approves it. Replaces the whole list.; at most 10 items | | `firstParty` | boolean | no | Staff only | | `locales` | string[] | yes | Lowercase BCP 47, unique, the first is the default; at least 1 item | | `accountsAllowed` | boolean | no | | | `personalizedAdsAllowed` | boolean | no | | | `ageBand` | [AgeBand](/docs/api/reference/schemas/#schema-ageband) | no | | | `cdnPullZoneId` | string or null | no | pattern `^[0-9]{1,12}$` | | `navCategories` | string[] | no | Slugs of existing categories of the site, unique, in menu order; at most 12 items | | `otherSites` | [OtherSite](/docs/api/reference/schemas/#schema-othersite)[] | no | at most 10 items | | `causalitDomain` | string or null | no | pattern `^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$` | | `causalitReadKey` | string or null | no | 8 to 200 characters; write only | | `causalitWriteKey` | string or null | no | 8 to 200 characters; write only | ### Responses | Status | Body | Description | | --- | --- | --- | | 201 | [Site](/docs/api/reference/schemas/#schema-site) | Created. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 409 | none | Slug taken, or the account already holds 3 pending sites. | ```sh curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **One site** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Site](/docs/api/reference/schemas/#schema-site) | Site. | ```sh curl -X PATCH 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "slug": "gloop", "name": "string", "domains": [ "string" ], "pendingDomains": [ "string" ], "firstParty": true, "locales": [ "string" ], "accountsAllowed": true, "personalizedAdsAllowed": true, "ageBand": "all", "cdnPullZoneId": "string", "navCategories": [ "string" ], "otherSites": [ { "name": "string", "url": "https://example.com/", "description": "string" } ], "causalitDomain": "string", "causalitReadKey": "string", "causalitWriteKey": "string" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000', { method: 'PATCH', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ slug: 'gloop', name: 'string', domains: ['string'], pendingDomains: ['string'], firstParty: true, locales: ['string'], accountsAllowed: true, personalizedAdsAllowed: true, ageBand: 'all', cdnPullZoneId: 'string', navCategories: ['string'], otherSites: [{ name: 'string', url: 'https://example.com/', description: 'string', }], causalitDomain: 'string', causalitReadKey: 'string', causalitWriteKey: 'string', }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.patch( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'slug': 'gloop', 'name': 'string', 'domains': ['string'], 'pendingDomains': ['string'], 'firstParty': True, 'locales': ['string'], 'accountsAllowed': True, 'personalizedAdsAllowed': True, 'ageBand': 'all', 'cdnPullZoneId': 'string', 'navCategories': ['string'], 'otherSites': [{ 'name': 'string', 'url': 'https://example.com/', 'description': 'string', }], 'causalitDomain': 'string', 'causalitReadKey': 'string', 'causalitWriteKey': 'string', }, ) response.raise_for_status() data = response.json() ``` **Update policy, domains, locales, name** Owners and members of the site write `pendingDomains`; a domain works once staff approves it. `domains` and `firstParty` are for staff (ADR 0015). Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Request body `application/json`, required. The body is a [SiteInput](/docs/api/reference/schemas/#schema-siteinput). On PATCH the slug may be repeated unchanged; the API refuses a different slug. `slug`, `domains`, `firstParty` and `cdnPullZoneId` are for staff only; owners and members write `pendingDomains` and the CausalIT fields (ADR 0015). A key field set to null clears the stored key; absent leaves it. | Property | Type | Required | Description | | --- | --- | --- | --- | | `slug` | string | no | pattern `^[a-z][a-z0-9-]{1,39}$` | | `name` | string | no | 1 to 80 characters | | `domains` | string[] | no | Lowercase hostnames, the first is canonical. Staff only.; at least 1 item | | `pendingDomains` | string[] | no | Lowercase hostnames the owner proposes; each works once staff approves it. Replaces the whole list.; at most 10 items | | `firstParty` | boolean | no | Staff only | | `locales` | string[] | no | Lowercase BCP 47, unique, the first is the default; at least 1 item | | `accountsAllowed` | boolean | no | | | `personalizedAdsAllowed` | boolean | no | | | `ageBand` | [AgeBand](/docs/api/reference/schemas/#schema-ageband) | no | | | `cdnPullZoneId` | string or null | no | pattern `^[0-9]{1,12}$` | | `navCategories` | string[] | no | Slugs of existing categories of the site, unique, in menu order; at most 12 items | | `otherSites` | [OtherSite](/docs/api/reference/schemas/#schema-othersite)[] | no | at most 10 items | | `causalitDomain` | string or null | no | pattern `^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$` | | `causalitReadKey` | string or null | no | 8 to 200 characters; write only | | `causalitWriteKey` | string or null | no | 8 to 200 characters; write only | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Site](/docs/api/reference/schemas/#schema-site) | Updated. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/approve' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/approve', { method: 'POST', credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/approve', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() data = response.json() ``` **Staff only. Approve a pending site** Sets `approvedAt`. Its approved domains become allowed origins within 60 s; the owners get an email. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Site](/docs/api/reference/schemas/#schema-site) | Site. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 409 | none | Already approved. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/reject' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "reason": "string" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/reject', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ reason: 'string', }), }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/reject', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'reason': 'string', }, ) response.raise_for_status() # No body: the status says it all. ``` **Staff only. Send the owners a reason; the site stays pending** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Request body `application/json`, required. The body is a [RejectInput](/docs/api/reference/schemas/#schema-rejectinput). | Property | Type | Required | Description | | --- | --- | --- | --- | | `reason` | string | yes | Sent to the owners by email; 1 to 1000 characters | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Reason sent. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 409 | none | The site is approved. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/domains/approve' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "domains": [ "string" ] }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/domains/approve', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ domains: ['string'], }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/domains/approve', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'domains': ['string'], }, ) response.raise_for_status() data = response.json() ``` **Staff only. Move proposed domains into the site's domains** Each listed domain must be in `pendingDomains`, else 400. The `game_host` rule of site creation applies. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `domains` | string[] | yes | at least 1 item | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Site](/docs/api/reference/schemas/#schema-site) | Site. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/domains/reject' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "domains": [ "string" ], "reason": "string" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/domains/reject', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ domains: ['string'], reason: 'string', }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/domains/reject', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'domains': ['string'], 'reason': 'string', }, ) response.raise_for_status() data = response.json() ``` **Staff only. Remove proposed domains and send the owners the reason** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `domains` | string[] | yes | at least 1 item | | `reason` | string | yes | 1 to 1000 characters | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Site](/docs/api/reference/schemas/#schema-site) | Site. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/members' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/members', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/members', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Members of a site** Owner of the site or staff. Invited emails without an account yet are listed with `invited` true. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Member](/docs/api/reference/schemas/#schema-member)[] | Members. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/members' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "email": "you@example.com", "role": "owner" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/members', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: 'you@example.com', role: 'owner', }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/members', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'email': 'you@example.com', 'role': 'owner', }, ) response.raise_for_status() data = response.json() ``` **Add a member by email** Owner of the site or staff. An existing account gets the membership at once. An unknown email gets an invitation that carries it; the membership exists once the invitation is accepted. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Request body `application/json`, required. The body is a [MemberInput](/docs/api/reference/schemas/#schema-memberinput). | Property | Type | Required | Description | | --- | --- | --- | --- | | `email` | string (email) | yes | at most 254 characters | | `role` | [MembershipRole](/docs/api/reference/schemas/#schema-membershiprole) | yes | A member has every right of the owner except membership changes | ### Responses | Status | Body | Description | | --- | --- | --- | | 201 | [Member](/docs/api/reference/schemas/#schema-member) | Member. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 409 | none | Already a member. | ```sh curl -X DELETE 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/members/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/members/00000000-0000-0000-0000-000000000000', { method: 'DELETE', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/members/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Remove a member** Owner of the site or staff. The last owner cannot be removed. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | | `accountId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Removed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 409 | none | `conflict`: "A site keeps at least one owner.". | ```sh curl -X DELETE 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/members/invitations/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/members/invitations/00000000-0000-0000-0000-000000000000', { method: 'DELETE', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/members/invitations/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Withdraw an invitation** Owner of the site or staff. Removes this site from the invitation's grants; an invitation left with no grant is revoked. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | | `invitationId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Withdrawn. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | none | No pending invitation to this site has this id. | ```sh curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/keys' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/keys', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/keys', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Server keys of a site, revoked ones included** Owner or member of the site, or staff. Approved sites only (409). The key itself is never returned again. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [SiteKey](/docs/api/reference/schemas/#schema-sitekey)[] | Keys. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 409 | none | The site is not approved. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/keys' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "name": "string", "scopes": [ "catalog" ] }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/keys', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: 'string', scopes: ['catalog'], }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/keys', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'name': 'string', 'scopes': ['catalog'], }, ) response.raise_for_status() data = response.json() ``` **Create a server key; the answer shows it once** Owner or member of the site, or staff. Approved sites only. 20 keys per site at most, revoked ones included. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Request body `application/json`, required. The body is a [SiteKeyInput](/docs/api/reference/schemas/#schema-sitekeyinput). | Property | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | 1 to 60 characters | | `scopes` | [SiteKeyScope](/docs/api/reference/schemas/#schema-sitekeyscope)[] | yes | at least 1 item; unique items | ### Responses | Status | Body | Description | | --- | --- | --- | | 201 | [SiteKeyCreated](/docs/api/reference/schemas/#schema-sitekeycreated) | The key, shown once. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 409 | none | The site is not approved, or already has 20 keys. | ```sh curl -X DELETE 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/keys/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/keys/00000000-0000-0000-0000-000000000000', { method: 'DELETE', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/keys/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Revoke a server key** The row stays with `revokedAt`; `/v1/server/*` answers 403 to the key from now on. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | | `keyId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Revoked, or already revoked. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Categories of a site, in site order, with the number of games in each** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Category](/docs/api/reference/schemas/#schema-category)[] | Categories. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "slug": "gloop", "icon": "puzzle", "texts": { "en": { "title": "string", "navTitle": "string", "description": "string" } } }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ slug: 'gloop', icon: 'puzzle', texts: { en: { title: 'string', navTitle: 'string', description: 'string', }, }, }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'slug': 'gloop', 'icon': 'puzzle', 'texts': { 'en': { 'title': 'string', 'navTitle': 'string', 'description': 'string', }, }, }, ) response.raise_for_status() data = response.json() ``` **Add a category at the end of the site order** The default locale needs a title. A category slug never changes once created (ADR 0008). Slugs the fronts use as routes are refused: `categories`, `game`, `games`, `search`, `account`, `privacy`, `terms`, `cookies`, `stats`, `cmp`, `api`, `assets`, and every locale of the site. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `slug` | string | yes | On create only; ignored on PATCH; pattern `^[a-z][a-z0-9-]{0,39}$` | | `icon` | [CategoryIcon](/docs/api/reference/schemas/#schema-categoryicon) or null | no | | | `texts` | object | yes | Keyed by locale; the site's default locale needs a title | | `texts.*.title` | string | no | at most 120 characters | | `texts.*.navTitle` | string | no | at most 24 characters | | `texts.*.description` | string | no | at most 1000 characters | ### Responses | Status | Body | Description | | --- | --- | --- | | 201 | [Category](/docs/api/reference/schemas/#schema-category) | Created. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 409 | none | Slug taken on this site. | ```sh curl -X PUT 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories/order' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "slugs": [ "string" ] }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories/order', { method: 'PUT', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ slugs: ['string'], }), }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.put( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories/order', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'slugs': ['string'], }, ) response.raise_for_status() # No body: the status says it all. ``` **Reorder the categories** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `slugs` | string[] | yes | Every category of the site exactly once | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Reordered. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | ```sh curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories/gloop' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories/gloop', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories/gloop', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **One category** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | | `slug` | path | string | yes | pattern `^[a-z][a-z0-9-]{0,39}$` | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Category](/docs/api/reference/schemas/#schema-category) | Category. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X PATCH 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories/gloop' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "slug": "gloop", "icon": "puzzle", "texts": { "en": { "title": "string", "navTitle": "string", "description": "string" } } }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories/gloop', { method: 'PATCH', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ slug: 'gloop', icon: 'puzzle', texts: { en: { title: 'string', navTitle: 'string', description: 'string', }, }, }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.patch( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories/gloop', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'slug': 'gloop', 'icon': 'puzzle', 'texts': { 'en': { 'title': 'string', 'navTitle': 'string', 'description': 'string', }, }, }, ) response.raise_for_status() data = response.json() ``` **Update texts and icon** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | | `slug` | path | string | yes | pattern `^[a-z][a-z0-9-]{0,39}$` | ### Request body `application/json`, required. The body is a [CategoryInput](/docs/api/reference/schemas/#schema-categoryinput). | Property | Type | Required | Description | | --- | --- | --- | --- | | `slug` | string | no | On create only; ignored on PATCH; pattern `^[a-z][a-z0-9-]{0,39}$` | | `icon` | [CategoryIcon](/docs/api/reference/schemas/#schema-categoryicon) or null | no | | | `texts` | object | no | Keyed by locale; the site's default locale needs a title | | `texts.*.title` | string | no | at most 120 characters | | `texts.*.navTitle` | string | no | at most 24 characters | | `texts.*.description` | string | no | at most 1000 characters | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Category](/docs/api/reference/schemas/#schema-category) | Updated. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X DELETE 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories/gloop' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories/gloop', { method: 'DELETE', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories/gloop', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Remove the category from the site, from its games and from the top menu** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | | `slug` | path | string | yes | pattern `^[a-z][a-z0-9-]{0,39}$` | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Removed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Tags of a site, in site order, with the number of games carrying each** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Tag](/docs/api/reference/schemas/#schema-tag)[] | Tags. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "slug": "gloop", "texts": { "en": { "title": "string" } } }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ slug: 'gloop', texts: { en: { title: 'string', }, }, }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'slug': 'gloop', 'texts': { 'en': { 'title': 'string', }, }, }, ) response.raise_for_status() data = response.json() ``` **Add a tag at the end of the site order** The default locale needs a title. A tag slug never changes once created (ADR 0008). Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `slug` | string | yes | On create only; ignored on PATCH; pattern `^[a-z][a-z0-9-]{0,39}$` | | `texts` | object | yes | Keyed by locale; the site's default locale needs a title | | `texts.*.title` | string | no | at most 60 characters | ### Responses | Status | Body | Description | | --- | --- | --- | | 201 | [Tag](/docs/api/reference/schemas/#schema-tag) | Created. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 409 | none | Slug taken on this site. | ```sh curl -X PUT 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags/order' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "slugs": [ "string" ] }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags/order', { method: 'PUT', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ slugs: ['string'], }), }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.put( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags/order', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'slugs': ['string'], }, ) response.raise_for_status() # No body: the status says it all. ``` **Reorder the tags** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `slugs` | string[] | yes | Every tag of the site exactly once | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Reordered. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | ```sh curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags/gloop' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags/gloop', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags/gloop', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **One tag** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | | `slug` | path | string | yes | pattern `^[a-z][a-z0-9-]{0,39}$` | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Tag](/docs/api/reference/schemas/#schema-tag) | Tag. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X PATCH 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags/gloop' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "slug": "gloop", "texts": { "en": { "title": "string" } } }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags/gloop', { method: 'PATCH', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ slug: 'gloop', texts: { en: { title: 'string', }, }, }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.patch( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags/gloop', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'slug': 'gloop', 'texts': { 'en': { 'title': 'string', }, }, }, ) response.raise_for_status() data = response.json() ``` **Update the titles** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | | `slug` | path | string | yes | pattern `^[a-z][a-z0-9-]{0,39}$` | ### Request body `application/json`, required. The body is a [TagInput](/docs/api/reference/schemas/#schema-taginput). | Property | Type | Required | Description | | --- | --- | --- | --- | | `slug` | string | no | On create only; ignored on PATCH; pattern `^[a-z][a-z0-9-]{0,39}$` | | `texts` | object | no | Keyed by locale; the site's default locale needs a title | | `texts.*.title` | string | no | at most 60 characters | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Tag](/docs/api/reference/schemas/#schema-tag) | Updated. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X DELETE 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags/gloop' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags/gloop', { method: 'DELETE', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags/gloop', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Remove the tag from the site and from its games** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | | `slug` | path | string | yes | pattern `^[a-z][a-z0-9-]{0,39}$` | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Removed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/catalog' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/catalog', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/catalog', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Site catalog entries with every locale** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [SiteGame](/docs/api/reference/schemas/#schema-sitegame)[] | Entries in order. | ```sh curl -X PUT 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/catalog/order' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "gameIds": [ "00000000-0000-0000-0000-000000000000" ] }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/catalog/order', { method: 'PUT', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ gameIds: ['00000000-0000-0000-0000-000000000000'], }), }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.put( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/catalog/order', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'gameIds': ['00000000-0000-0000-0000-000000000000'], }, ) response.raise_for_status() # No body: the status says it all. ``` **Reorder the catalog** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `gameIds` | array of string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Reordered. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | ```sh curl -X PUT 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/catalog/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "featured": true, "visible": true, "categories": [ "string" ], "tags": [ "string" ], "texts": { "en": { "title": "string", "tagline": "string", "description": "string", "howToPlay": [ "string" ], "controls": [ { "keys": [ "string" ], "does": "string" } ], "seoTitle": "string", "seoDescription": "string" } } }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/catalog/00000000-0000-0000-0000-000000000000', { method: 'PUT', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ featured: true, visible: true, categories: ['string'], tags: ['string'], texts: { en: { title: 'string', tagline: 'string', description: 'string', howToPlay: ['string'], controls: [{ keys: ['string'], does: 'string', }], seoTitle: 'string', seoDescription: 'string', }, }, }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.put( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/catalog/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'featured': True, 'visible': True, 'categories': ['string'], 'tags': ['string'], 'texts': { 'en': { 'title': 'string', 'tagline': 'string', 'description': 'string', 'howToPlay': ['string'], 'controls': [{ 'keys': ['string'], 'does': 'string', }], 'seoTitle': 'string', 'seoDescription': 'string', }, }, }, ) response.raise_for_status() data = response.json() ``` **Add or update a game in the site catalog** Adding a game the site may not list answers 403 with the reason in `message`: the game is pending, it has no live version, or it is `first_party` and the site is not first-party (ADR 0015). A pending site of the game's own developer may list the developer's pending games. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | | `gameId` | path | string (uuid) | yes | | ### Request body `application/json`, required. The body is a [SiteGameInput](/docs/api/reference/schemas/#schema-sitegameinput). | Property | Type | Required | Description | | --- | --- | --- | --- | | `featured` | boolean | no | | | `visible` | boolean | no | | | `categories` | string[] | no | | | `tags` | string[] | no | | | `texts` | object | no | | | `texts.*` | [GameTexts](/docs/api/reference/schemas/#schema-gametexts) | no | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [SiteGame](/docs/api/reference/schemas/#schema-sitegame) | Entry. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | The site may not list this game; `message` says why. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X DELETE 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/catalog/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/catalog/00000000-0000-0000-0000-000000000000', { method: 'DELETE', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/catalog/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Remove a game from the site catalog** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | | `gameId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Removed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/publish' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/publish', { method: 'POST', credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/publish', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() data = response.json() ``` **Make the catalog changes visible on the site front** Drops the API's cached catalog of the site, then purges the site's CDN pull zone (ADR 0010) when the site has `cdnPullZoneId` and the API has `BUNNY_API_KEY`. Without one of them nothing is purged and the answer says why: the front picks the change up on its own within its revalidation delay. Writes an audit row `site.publish`. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [PublishResult](/docs/api/reference/schemas/#schema-publishresult) | What was purged. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 503 | [Error](/docs/api/reference/schemas/#schema-error) | The CDN did not accept the purge; retry in a minute. | ```sh curl 'https://api.filudi.com/v1/admin/games' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/games', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Every game with its versions** Staff and a game's members get the full game. A site member gets a reader view of the others: the live version only, `notes` null, `createdBy` empty, `sites` limited to its own. An account with no site membership gets only its own games, and 403 on another game's id. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `available` | query | string (uuid) | no | A site id. Lists only the games that site may add: approved, with a live version, `any_portal` or the site is first-party. A pending site of the caller also gets the caller's own pending games. | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Game](/docs/api/reference/schemas/#schema-game)[] | Games. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/games' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "slug": "gloop", "name": "string", "orientation": "portrait", "minutes": 1, "mobile": true, "daily": true, "coverColor": "string", "distribution": "first_party" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ slug: 'gloop', name: 'string', orientation: 'portrait', minutes: 1, mobile: true, daily: true, coverColor: 'string', distribution: 'first_party', }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/games', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'slug': 'gloop', 'name': 'string', 'orientation': 'portrait', 'minutes': 1, 'mobile': True, 'daily': True, 'coverColor': 'string', 'distribution': 'first_party', }, ) response.raise_for_status() data = response.json() ``` **Create a game** Staff, or an account with `canCreateGames`; anyone else gets 403. A staff creation is approved at once. A developer's game is created with `approvedAt` null and the creator as owner; staff approves it from the Approvals screen. One account holds at most 10 pending games (409). A reserved slug answers 400: `admin`, `api`, `play`, `developers`, `filudi`, `www`, `static`. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `slug` | string | yes | pattern `^[a-z][a-z0-9-]{1,39}$` | | `name` | string | yes | | | `orientation` | one of `"portrait"`, `"landscape"`, `"any"` | no | | | `minutes` | integer or null | no | 1 to 600 | | `mobile` | boolean | no | | | `daily` | boolean | no | | | `coverColor` | string or null | no | pattern `^#[0-9A-Fa-f]{6}$` | | `distribution` | [Distribution](/docs/api/reference/schemas/#schema-distribution) | no | Where a game may be listed (ADR 0015). `first_party`: sites with `firstParty` only. `any_portal`: any approved site. | ### Responses | Status | Body | Description | | --- | --- | --- | | 201 | [Game](/docs/api/reference/schemas/#schema-game) | Created. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 409 | none | Slug taken, or the account already holds 10 pending games. | ```sh curl 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **One game** Members and staff get `listedOn`, the sites whose catalog carries the game. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Game](/docs/api/reference/schemas/#schema-game) | Game. | ```sh curl -X PATCH 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "slug": "gloop", "name": "string", "orientation": "portrait", "minutes": 1, "mobile": true, "daily": true, "coverColor": "string", "distribution": "first_party" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000', { method: 'PATCH', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ slug: 'gloop', name: 'string', orientation: 'portrait', minutes: 1, mobile: true, daily: true, coverColor: 'string', distribution: 'first_party', }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.patch( 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'slug': 'gloop', 'name': 'string', 'orientation': 'portrait', 'minutes': 1, 'mobile': True, 'daily': True, 'coverColor': 'string', 'distribution': 'first_party', }, ) response.raise_for_status() data = response.json() ``` **Update a game** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string (uuid) | yes | | ### Request body `application/json`, required. The body is a [GameInput](/docs/api/reference/schemas/#schema-gameinput). | Property | Type | Required | Description | | --- | --- | --- | --- | | `slug` | string | no | pattern `^[a-z][a-z0-9-]{1,39}$` | | `name` | string | no | | | `orientation` | one of `"portrait"`, `"landscape"`, `"any"` | no | | | `minutes` | integer or null | no | 1 to 600 | | `mobile` | boolean | no | | | `daily` | boolean | no | | | `coverColor` | string or null | no | pattern `^#[0-9A-Fa-f]{6}$` | | `distribution` | [Distribution](/docs/api/reference/schemas/#schema-distribution) | no | Where a game may be listed (ADR 0015). `first_party`: sites with `firstParty` only. `any_portal`: any approved site. | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Game](/docs/api/reference/schemas/#schema-game) | Updated. | ```sh curl -X DELETE 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000', { method: 'DELETE', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Delete a game and everything attached to it (staff only)** Refused while a version of the game is live. Deletes the versions, media, saves, scores, leaderboards, ratings, comments and catalog entries of every site, and sets `game` to null on its feedback (ADR 0012). Then removes the files under `games//` from the files service (best effort: a failure is logged, the deletion stands), removes the game's Redis leaderboard sets and drops the catalog cache of every site that listed it. Events and daily aggregates keep the game's id. Writes an audit row `game.delete`. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Deleted. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 409 | [Error](/docs/api/reference/schemas/#schema-error) | `conflict`: "Retire the live version first.". | ```sh curl -X POST 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/approve' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/approve', { method: 'POST', credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/approve', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() data = response.json() ``` **Staff only. Approve a pending game** Sets `approvedAt`. The game may now have a live version and be listed by approved sites; the owners get an email. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Game](/docs/api/reference/schemas/#schema-game) | Game. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 409 | none | Already approved. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/reject' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "reason": "string" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/reject', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ reason: 'string', }), }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/reject', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'reason': 'string', }, ) response.raise_for_status() # No body: the status says it all. ``` **Staff only. Send the owners a reason; the game stays pending** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string (uuid) | yes | | ### Request body `application/json`, required. The body is a [RejectInput](/docs/api/reference/schemas/#schema-rejectinput). | Property | Type | Required | Description | | --- | --- | --- | --- | | `reason` | string | yes | Sent to the owners by email; 1 to 1000 characters | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Reason sent. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 409 | none | The game is approved. | ```sh curl 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/members' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/members', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/members', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Members of a game** Owner of the game or staff. Invited emails without an account yet are listed with `invited` true. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Member](/docs/api/reference/schemas/#schema-member)[] | Members. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/members' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "email": "you@example.com", "role": "owner" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/members', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: 'you@example.com', role: 'owner', }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/members', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'email': 'you@example.com', 'role': 'owner', }, ) response.raise_for_status() data = response.json() ``` **Add a member by email** Owner of the game or staff. An existing account gets the membership at once. An unknown email gets an invitation that carries it; the membership exists once the invitation is accepted. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string (uuid) | yes | | ### Request body `application/json`, required. The body is a [MemberInput](/docs/api/reference/schemas/#schema-memberinput). | Property | Type | Required | Description | | --- | --- | --- | --- | | `email` | string (email) | yes | at most 254 characters | | `role` | [MembershipRole](/docs/api/reference/schemas/#schema-membershiprole) | yes | A member has every right of the owner except membership changes | ### Responses | Status | Body | Description | | --- | --- | --- | | 201 | [Member](/docs/api/reference/schemas/#schema-member) | Member. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 409 | none | Already a member. | ```sh curl -X DELETE 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/members/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/members/00000000-0000-0000-0000-000000000000', { method: 'DELETE', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/members/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Remove a member** Owner of the game or staff. The last owner cannot be removed. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string (uuid) | yes | | | `accountId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Removed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 409 | none | `conflict`: "A game keeps at least one owner.". | ```sh curl -X DELETE 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/members/invitations/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/members/invitations/00000000-0000-0000-0000-000000000000', { method: 'DELETE', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/members/invitations/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Withdraw an invitation** Owner of the game or staff. Removes this game from the invitation's grants; an invitation left with no grant is revoked. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string (uuid) | yes | | | `invitationId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Withdrawn. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | none | No pending invitation to this game has this id. | ```sh curl 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/stats?from=2026-10-30&to=2026-10-30' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/stats?from=2026-10-30&to=2026-10-30', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/stats?from=2026-10-30&to=2026-10-30', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **One game's numbers across every site that lists it, for a range of UTC days** Members of the game and staff. Same range rules as the site overview: `from` and `to` included, at most 400 days, `previous` covers the range of the same length that ends the day before `from`. Cached in Redis for 60 s per game, range and filter set. Nothing about ads or money (ADR 0015). Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string (uuid) | yes | | | `from` | query | string (date) | yes | First UTC day, included | | `to` | query | string (date) | yes | Last UTC day, included | | `site` | query | string (uuid) | no | Only the sessions of this site | | `host` | query | string | no | Only the sessions of this host | | `version` | query | string | no | Only the sessions of this bundle version. `events` and `funnel.loaded` come from `event_daily`, which has no version, so they ignore this filter | | `locale` | query | string | no | Only the players of this locale. `events` and `funnel.loaded` come from `event_daily`, which has no locale, so they ignore this filter | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [ConsoleGameStats](/docs/api/reference/schemas/#schema-consolegamestats) | Game stats. | | 400 | none | Range over 400 days, or `to` before `from`. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/versions' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "version": "string", "sizeBytes": 0, "notes": "string" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/versions', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ version: 'string', sizeBytes: 0, notes: 'string', }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/versions', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'version': 'string', 'sizeBytes': 0, 'notes': 'string', }, ) response.raise_for_status() data = response.json() ``` **Start a version upload** Returns a presigned PUT URL from the files service for a zip. 200 MB max. Answers 409 while a finalize runs or once the version is finalized, and 503 with `Retry-After` while Redis is down. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string (uuid) | yes | | ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `version` | string | yes | pattern `^\d+\.\d+\.\d+([-.][0-9A-Za-z.-]+)?$` | | `sizeBytes` | integer | yes | at most 209715200 | | `notes` | string | no | | ### Responses | Status | Body | Description | | --- | --- | --- | | 201 | object | Upload slot. | | 409 | none | Version exists. | **Body of 201** | Property | Type | Required | Description | | --- | --- | --- | --- | | `versionId` | string (uuid) | yes | | | `uploadUrl` | string (uri) | yes | | | `expiresAt` | string (date-time) | yes | | ```sh curl -X POST 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/media/image-square' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "contentType": "image/avif", "sizeBytes": 1 }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/media/image-square', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ contentType: 'image/avif', sizeBytes: 1, }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/media/image-square', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'contentType': 'image/avif', 'sizeBytes': 1, }, ) response.raise_for_status() data = response.json() ``` **Start a media upload** Returns a presigned PUT URL from the files service. The slot keeps its current file until finalize succeeds. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string (uuid) | yes | | | `slot` | path | one of `"image-square"`, `"image-landscape"`, `"image-portrait"`, `"video-landscape"`, `"video-portrait"` | yes | ADR 0007 gives the ratio, pixel range, file types and size limit of each slot | ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `contentType` | one of `"image/avif"`, `"image/webp"`, `"image/jpeg"`, `"image/png"`, `"video/mp4"` | yes | | | `sizeBytes` | integer | yes | Checked against the slot limit; 1 to 15728640 | ### Responses | Status | Body | Description | | --- | --- | --- | | 201 | object | Upload slot. | | 400 | none | Type not allowed in this slot, or file over the slot limit. | **Body of 201** | Property | Type | Required | Description | | --- | --- | --- | --- | | `uploadUrl` | string (uri) | yes | | | `expiresAt` | string (date-time) | yes | | ```sh curl -X DELETE 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/media/image-square' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/media/image-square', { method: 'DELETE', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/media/image-square', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Empty the slot** The file stays in the files service; built pages may still point to it (ADR 0007). Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string (uuid) | yes | | | `slot` | path | one of `"image-square"`, `"image-landscape"`, `"image-portrait"`, `"video-landscape"`, `"video-portrait"` | yes | ADR 0007 gives the ratio, pixel range, file types and size limit of each slot | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Emptied, or already empty. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/media/image-square/finalize' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/media/image-square/finalize', { method: 'POST', credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/media/image-square/finalize', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() data = response.json() ``` **Check the uploaded file and put it in the slot** Detects the type from the bytes, checks ratio, pixels, size, and for a video H.264, duration and `moov` before `mdat`. The message of a 400 says what to change in the file. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string (uuid) | yes | | | `slot` | path | one of `"image-square"`, `"image-landscape"`, `"image-portrait"`, `"video-landscape"`, `"video-portrait"` | yes | ADR 0007 gives the ratio, pixel range, file types and size limit of each slot | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [MediaFile](/docs/api/reference/schemas/#schema-mediafile) | The slot's new file. | | 400 | none | No upload, or a file that breaks the slot rules. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/finalize' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/finalize', { method: 'POST', credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/finalize', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() data = response.json() ``` **Unpack the uploaded zip into the bundle prefix** Checks the entry file exists, rejects paths with `..`, counts files and bytes. The version becomes `draft`. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `versionId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [GameVersion](/docs/api/reference/schemas/#schema-gameversion) | Version. | | 400 | none | Zip missing, invalid or without the entry file. | | 409 | none | Another finalize or an activation holds this version. | | 503 | none | Redis is down: the version lock is not available. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/activate' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/activate', { method: 'POST', credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/activate', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() data = response.json() ``` **Make this version live; the previous live one goes back to draft (staff only)** Clears `reviewRequestedAt`. Answers 409 on a game that is not approved, and while a finalize or another activation holds the version; 503 while Redis is down (ADR 0013). Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `versionId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [GameVersion](/docs/api/reference/schemas/#schema-gameversion) | Version. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 409 | [Error](/docs/api/reference/schemas/#schema-error) | The game is pending approval, or another operation holds the version. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/retire' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/retire', { method: 'POST', credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/retire', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() data = response.json() ``` **Retire a version; its bundle stops being served (staff only)** Retiring the live version takes the game off play: the catalog answers `bundleUrl: null` and `version: null` until another version goes live. A game is deleted only with no live version. Clears `reviewRequestedAt`. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `versionId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [GameVersion](/docs/api/reference/schemas/#schema-gameversion) | Version. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/review' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/review', { method: 'POST', credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/review', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() data = response.json() ``` **Ask staff to review a draft and make it live** Any member of the game. Sets `reviewRequestedAt` and emails staff. The version shows in the Approvals screen. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `versionId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [GameVersion](/docs/api/reference/schemas/#schema-gameversion) | Version. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 409 | [Error](/docs/api/reference/schemas/#schema-error) | The version is not a draft, or the game is pending approval. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/review/reject' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "reason": "string" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/review/reject', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ reason: 'string', }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/review/reject', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'reason': 'string', }, ) response.raise_for_status() data = response.json() ``` **Staff only. Decline a review request and send the owners the reason** Clears `reviewRequestedAt`; the version stays a draft. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `versionId` | path | string (uuid) | yes | | ### Request body `application/json`, required. The body is a [RejectInput](/docs/api/reference/schemas/#schema-rejectinput). | Property | Type | Required | Description | | --- | --- | --- | --- | | `reason` | string | yes | Sent to the owners by email; 1 to 1000 characters | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [GameVersion](/docs/api/reference/schemas/#schema-gameversion) | Version. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 409 | none | No review was requested. | ```sh curl 'https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/preview' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/preview', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/preview', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Signed URL to play a draft version for 1 hour** Built on `PREVIEW_URL` when it is set, else on the API origin, and never on the CDN, so Bunny cannot strip the preview cookie the bundle's other files need. Open it in a new tab (a preview answers `frame-ancestors 'none'`). Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `versionId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | object | URL. | **Body of 200** | Property | Type | Required | Description | | --- | --- | --- | --- | | `url` | string (uri) | yes | | | `expiresAt` | string (date-time) | yes | | ```sh curl 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/leaderboards' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/leaderboards', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/leaderboards', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Boards of a game** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Leaderboard](/docs/api/reference/schemas/#schema-leaderboard)[] | Boards. | ```sh curl -X PUT 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/leaderboards' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '[ { "name": "string", "sort": "desc", "min": 0, "max": 0, "keep": "best", "ratePerMinute": 10 } ]' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/leaderboards', { method: 'PUT', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify([{ name: 'string', sort: 'desc', min: 0, max: 0, keep: 'best', ratePerMinute: 10, }]), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.put( 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/leaderboards', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json=[{ 'name': 'string', 'sort': 'desc', 'min': 0, 'max': 0, 'keep': 'best', 'ratePerMinute': 10, }], ) response.raise_for_status() data = response.json() ``` **Replace the boards of a game** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string (uuid) | yes | | ### Request body `application/json`, required. An array of [LeaderboardInput](/docs/api/reference/schemas/#schema-leaderboardinput). ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Leaderboard](/docs/api/reference/schemas/#schema-leaderboard)[] | Boards. | ```sh curl 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/leaderboards/height/scores' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/leaderboards/height/scores', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/leaderboards/height/scores', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Scores for moderation** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string (uuid) | yes | | | `board` | path | string | yes | pattern `^[a-z][a-z0-9_]{0,31}$` | | `period` | query | one of `"day"`, `"week"`, `"all"` | no | default `"all"` | | `site` | query | string | no | 403 unless the caller is an admin or an editor of that site | | `hidden` | query | boolean | no | | | `limit` | query | integer | no | at most 500; default `100` | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [ScoreAdmin](/docs/api/reference/schemas/#schema-scoreadmin)[] | Scores with player ids. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/scores/string' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/scores/string', { method: 'POST', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/scores/string', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Unhide a score** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `scoreId` | path | string | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Visible again. | ```sh curl -X DELETE 'https://api.filudi.com/v1/admin/scores/string' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "reason": "string" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/scores/string', { method: 'DELETE', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ reason: 'string', }), }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/admin/scores/string', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'reason': 'string', }, ) response.raise_for_status() # No body: the status says it all. ``` **Hide a score** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `scoreId` | path | string | yes | | ### Request body `application/json`, optional. | Property | Type | Required | Description | | --- | --- | --- | --- | | `reason` | string | no | Stored in the audit row; at most 200 characters | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Hidden and removed from Redis. | ```sh curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/players' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/players', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/players', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **List the players of a site, with an optional search** Without `q`, every player that matches the filters. With `q`, a player id (a UUID), an email (a value with `@`, matched by its hash) or a nickname substring, case-insensitive. By default the list shows account players only: `guests=true` adds guests, and `merged=true` also adds guests already merged into an account. An id or an email ignores `guests` and `merged`: the one match shows, whatever it is. `lastSeen` and `created` sort newest first, `nickname` alphabetically; ties break on id. `total` counts every row that matches the filters, across pages. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | | `q` | query | string | no | at least 2 characters | | `guests` | query | boolean | no | False hides players without an email; default `false` | | `merged` | query | boolean | no | False hides guests merged into an account; only matters with `guests=true`; default `false` | | `sort` | query | one of `"lastSeen"`, `"created"`, `"nickname"` | no | default `"lastSeen"` | | `cursor` | query | string | no | Opaque; the `nextCursor` of the previous page, with the same filters and sort; at most 200 characters | | `limit` | query | integer | no | 1 to 500; default `100` | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [PlayerPage](/docs/api/reference/schemas/#schema-playerpage) | Players, emails masked. | | 400 | none | `q` shorter than 2 characters, or a cursor of another sort. | ```sh curl 'https://api.filudi.com/v1/admin/players/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/players/00000000-0000-0000-0000-000000000000', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/players/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Player detail with saves metadata and best scores** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `playerId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [PlayerAdmin](/docs/api/reference/schemas/#schema-playeradmin) | Player. | ```sh curl -X DELETE 'https://api.filudi.com/v1/admin/players/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/players/00000000-0000-0000-0000-000000000000', { method: 'DELETE', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/admin/players/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Schedule deletion (GDPR request handled by support)** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `playerId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 202 | none | Scheduled. | ```sh curl 'https://api.filudi.com/v1/admin/players/00000000-0000-0000-0000-000000000000/export' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/players/00000000-0000-0000-0000-000000000000/export', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/players/00000000-0000-0000-0000-000000000000/export', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Export a player's data** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `playerId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [PlayerExport](/docs/api/reference/schemas/#schema-playerexport) | JSON file. | ```sh curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/comments' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/comments', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/comments', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Comments of a site for moderation, newest first** Editor of the site or admin. `q` is a case-insensitive substring of the body or of the author's current nickname. `total` counts every row that matches the filters, across pages. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | | `gameId` | query | string (uuid) | no | | | `hidden` | query | one of `"all"`, `"visible"`, `"hidden"` | no | default `"all"` | | `q` | query | string | no | 2 to 100 characters | | `cursor` | query | string | no | Opaque; the `nextCursor` of the previous page, with the same filters and sort; at most 200 characters | | `limit` | query | integer | no | 1 to 500; default `100` | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [CommentPage](/docs/api/reference/schemas/#schema-commentpage) | Comments. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | none | Unknown site. | ```sh curl -X DELETE 'https://api.filudi.com/v1/admin/comments/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/comments/00000000-0000-0000-0000-000000000000', { method: 'DELETE', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/admin/comments/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Delete a comment for good** Editor of the comment's site or admin. Audit row `comment.delete` with the body in `before`. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `commentId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Deleted. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/comments/00000000-0000-0000-0000-000000000000/hide' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/comments/00000000-0000-0000-0000-000000000000/hide', { method: 'POST', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/comments/00000000-0000-0000-0000-000000000000/hide', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Hide a comment from every player route** Editor of the comment's site or admin. Sets `hiddenBy` and `hiddenAt`. Audit row `comment.hide`. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `commentId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Hidden, or already hidden. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X POST 'https://api.filudi.com/v1/admin/comments/00000000-0000-0000-0000-000000000000/unhide' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/comments/00000000-0000-0000-0000-000000000000/unhide', { method: 'POST', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/admin/comments/00000000-0000-0000-0000-000000000000/unhide', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Show a hidden comment again** Editor of the comment's site or admin. Clears `hiddenBy` and `hiddenAt`. Audit row `comment.unhide`. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `commentId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Visible, or already visible. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/ratings' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/ratings', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/ratings', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Rating of every rated game of a site** Games with at least one rating, listed in the catalog or not, most ratings first. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [GameRatingAdmin](/docs/api/reference/schemas/#schema-gameratingadmin)[] | Ratings. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | none | Unknown site. | ```sh curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/feedback' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/feedback', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/feedback', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Feedback of a site, newest first** Editor of the site or admin. `q` is a case-insensitive substring of the message. `total` counts every row that matches the filters, across pages. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | | `status` | query | one of `"new"`, `"seen"`, `"done"` | no | | | `kind` | query | one of `"bug"`, `"suggestion"`, `"other"` | no | | | `gameId` | query | string (uuid) | no | | | `q` | query | string | no | 2 to 100 characters | | `cursor` | query | string | no | Opaque; the `nextCursor` of the previous page, with the same filters and sort; at most 200 characters | | `limit` | query | integer | no | 1 to 500; default `100` | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [FeedbackPage](/docs/api/reference/schemas/#schema-feedbackpage) | Feedback. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | none | Unknown site. | ```sh curl 'https://api.filudi.com/v1/admin/feedback/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/feedback/00000000-0000-0000-0000-000000000000', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/feedback/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **One feedback message** Editor of its site or admin. The reply email is decrypted. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `feedbackId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [FeedbackAdmin](/docs/api/reference/schemas/#schema-feedbackadmin) | Feedback. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X PATCH 'https://api.filudi.com/v1/admin/feedback/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "status": "new", "note": "string" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/feedback/00000000-0000-0000-0000-000000000000', { method: 'PATCH', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ status: 'new', note: 'string', }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.patch( 'https://api.filudi.com/v1/admin/feedback/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'status': 'new', 'note': 'string', }, ) response.raise_for_status() data = response.json() ``` **Change the status or the note** Editor of its site or admin. Audit row `feedback.update` with the old and new status and note. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `feedbackId` | path | string (uuid) | yes | | ### Request body `application/json`, required. The body is a [FeedbackUpdate](/docs/api/reference/schemas/#schema-feedbackupdate). At least one field; an empty body is a 400. | Property | Type | Required | Description | | --- | --- | --- | --- | | `status` | [FeedbackStatus](/docs/api/reference/schemas/#schema-feedbackstatus) | no | | | `note` | string or null | no | Null clears the note; "" is stored as null; at most 2000 characters | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [FeedbackAdmin](/docs/api/reference/schemas/#schema-feedbackadmin) | Updated. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X DELETE 'https://api.filudi.com/v1/admin/feedback/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/feedback/00000000-0000-0000-0000-000000000000', { method: 'DELETE', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/admin/feedback/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Delete a feedback message for good** Editor of its site or admin. Audit row `feedback.delete`; the row's email is not copied into it. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `feedbackId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Deleted. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/events/today' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/events/today', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/events/today', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Counts since midnight UTC, live from the events table** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | object | Counts. | **Body of 200** | Property | Type | Required | Description | | --- | --- | --- | --- | | `since` | string (date-time) | no | | | `rows` | [EventCount](/docs/api/reference/schemas/#schema-eventcount)[] | no | | ```sh curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/events/daily?from=2026-10-30&to=2026-10-30' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/events/daily?from=2026-10-30&to=2026-10-30', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/events/daily?from=2026-10-30&to=2026-10-30', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Daily aggregates** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | | `from` | query | string (date) | yes | | | `to` | query | string (date) | yes | | | `game` | query | string | no | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [EventDaily](/docs/api/reference/schemas/#schema-eventdaily)[] | Rows. | ```sh curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/stats/overview?from=2026-10-30&to=2026-10-30' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/stats/overview?from=2026-10-30&to=2026-10-30', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/stats/overview?from=2026-10-30&to=2026-10-30', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **The site dashboard numbers for a range of UTC days** Read from Postgres (players, play_sessions, scores, saves, magic_links, event_daily) and cached in Redis for 60 s per site and range. `from` and `to` are included; at most 400 days. `previous` covers the range of the same length that ends the day before `from`. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | | `from` | query | string (date) | yes | First UTC day, included | | `to` | query | string (date) | yes | Last UTC day, included | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [StatsOverview](/docs/api/reference/schemas/#schema-statsoverview) | Overview. | ```sh curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/stats/games/00000000-0000-0000-0000-000000000000?from=2026-10-30&to=2026-10-30' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/stats/games/00000000-0000-0000-0000-000000000000?from=2026-10-30&to=2026-10-30', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/stats/games/00000000-0000-0000-0000-000000000000?from=2026-10-30&to=2026-10-30', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **One game's numbers on this site for a range of UTC days** Same range rules and cache as the overview. 404 when the game is not in the site catalog. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | | `gameId` | path | string (uuid) | yes | | | `from` | query | string (date) | yes | First UTC day, included | | `to` | query | string (date) | yes | Last UTC day, included | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [GameStats](/docs/api/reference/schemas/#schema-gamestats) | Game stats. | ```sh curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/stats/web' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/stats/web', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/stats/web', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Web audience from CausalIT (ADR 0009)** Calls the CausalIT read API with the site's read key and answers `configured: false` without one. Cached in Redis for 120 s per site, period and path. `path` narrows every number to one page path, for the game screens. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `siteId` | path | string (uuid) | yes | | | `period` | query | one of `"day"`, `"yesterday"`, `"7d"`, `"14d"`, `"30d"`, `"90d"`, `"6m"`, `"12m"`, `"custom"` | no | default `"7d"` | | `from` | query | string (date) | no | With `period=custom` | | `to` | query | string (date) | no | With `period=custom` | | `path` | query | string | no | A page path such as `/game/gloop/`; at most 200 characters | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [WebStats](/docs/api/reference/schemas/#schema-webstats) | Web stats. | | 503 | none | CausalIT did not answer. | ```sh curl 'https://api.filudi.com/v1/admin/audit' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/admin/audit', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/admin/audit', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **Audit log** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `site` | query | string | no | Site slug | | `actor` | query | string | no | | | `from` | query | string (date-time) | no | | | `cursor` | query | string | no | | | `limit` | query | integer | no | at most 500; default `100` | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | object | Entries, newest first. | **Body of 200** | Property | Type | Required | Description | | --- | --- | --- | --- | | `items` | [AuditEntry](/docs/api/reference/schemas/#schema-auditentry)[] | no | | | `nextCursor` | string or null | no | | Source: https://developers.filudi.com/docs/api/reference/admin/ --- # Bundles routes The bundles routes of games-api, with parameters, bodies, responses and request samples. Generated from `openapi.yaml` of games-api 0.7.0: 2 routes. - [`GET /v1/bundles/{slug}/{version}/{path}`](#get-v1-bundles-slug-version-path): Bundle file, CDN origin for the game hosts - [`GET /v1/bundles/{slug}/media/{file}`](#get-v1-bundles-slug-media-file): Game media file, CDN origin for play.filudi.com (ADR 0007) ```sh curl 'https://api.filudi.com/v1/bundles/gloop/1.0.0/index.html' ``` ```js const response = await fetch('https://api.filudi.com/v1/bundles/gloop/1.0.0/index.html'); // No body: check response.ok. ``` ```py import requests response = requests.get('https://api.filudi.com/v1/bundles/gloop/1.0.0/index.html') response.raise_for_status() # No body: the status says it all. ``` **Bundle file, CDN origin for the game hosts** With `PLAY_GAME_HOST_SUFFIX` set, a request without `X-Play-Host: .` answers the same 404 as any other miss. A request with any `X-Play-Host` ignores `preview`; a preview is a direct request with no such header (ADR 0013). The CDN sets that header on the path players use. With `PREVIEW_URL` set, a preview is served only on that host, which always needs a valid token, refuses `X-Play-Host`, and answers 404 outside `/v1/bundles/*`. `_upload.zip` is always 404. Credential: none. The route is public. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `slug` | path | string | yes | | | `version` | path | string | yes | | | `path` | path | string | yes | | | `preview` | query | string | no | Signed, time-limited parameter that allows a draft version | | `X-Play-Host` | header | string | no | Hostname the player asked the CDN for | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | none | File. Headers: `Cache-Control`, `Content-Security-Policy`, `Cross-Origin-Resource-Policy`. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl 'https://api.filudi.com/v1/bundles/gloop/media/string' ``` ```js const response = await fetch('https://api.filudi.com/v1/bundles/gloop/media/string'); // No body: check response.ok. ``` ```py import requests response = requests.get('https://api.filudi.com/v1/bundles/gloop/media/string') response.raise_for_status() # No body: the status says it all. ``` **Game media file, CDN origin for play.filudi.com (ADR 0007)** Serves only final names (`--.`). Answers a single `Range` with 206. Credential: none. The route is public. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `slug` | path | string | yes | | | `file` | path | string | yes | pattern `^(image-(square\|landscape\|portrait)-[0-9a-f]{16}\.(avif\|webp\|jpg\|png)\|video-(landscape\|portrait)-[0-9a-f]{16}\.mp4)$` | | `Range` | header | string | no | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | none | File. Headers: `Cache-Control`, `Accept-Ranges`, `Cross-Origin-Resource-Policy`. | | 206 | none | Requested byte range. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 416 | none | Range outside the file. | Source: https://developers.filudi.com/docs/api/reference/bundles/ --- # Catalog routes The catalog routes of games-api, with parameters, bodies, responses and request samples. Generated from `openapi.yaml` of games-api 0.7.0: 2 routes. - [`GET /v1/catalog/{site}`](#get-v1-catalog-site): Full catalog of a site - [`GET /v1/catalog/{site}/games/{slug}`](#get-v1-catalog-site-games-slug): One game of a site ```sh curl 'https://api.filudi.com/v1/catalog/filudi' ``` ```js const response = await fetch('https://api.filudi.com/v1/catalog/filudi'); const data = await response.json(); ``` ```py import requests response = requests.get('https://api.filudi.com/v1/catalog/filudi') response.raise_for_status() data = response.json() ``` **Full catalog of a site** Credential: none. The route is public. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `site` | path | string | yes | Site slug | | `locale` | query | string | no | Returns texts for one locale only; default returns every locale of the site | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Catalog](/docs/api/reference/schemas/#schema-catalog) | Catalog. Headers: `Cache-Control`, `ETag`. | | 304 | none | Not modified. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl 'https://api.filudi.com/v1/catalog/filudi/games/gloop' ``` ```js const response = await fetch('https://api.filudi.com/v1/catalog/filudi/games/gloop'); const data = await response.json(); ``` ```py import requests response = requests.get('https://api.filudi.com/v1/catalog/filudi/games/gloop') response.raise_for_status() data = response.json() ``` **One game of a site** Credential: none. The route is public. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `site` | path | string | yes | Site slug | | `slug` | path | string | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [CatalogGame](/docs/api/reference/schemas/#schema-cataloggame) | Game. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | Source: https://developers.filudi.com/docs/api/reference/catalog/ --- # Community routes Star ratings and comments on games (ADR 0012) Star ratings and comments on games (ADR 0012). Generated from `openapi.yaml` of games-api 0.7.0: 7 routes. - [`GET /v1/sdk/games/{gameId}/review`](#get-v1-sdk-games-gameid-review): The player's own rating and comments on a game - [`PUT /v1/sdk/games/{gameId}/rating`](#put-v1-sdk-games-gameid-rating): Rate a game from 1 to 5 stars, or change the rating - [`DELETE /v1/sdk/games/{gameId}/rating`](#delete-v1-sdk-games-gameid-rating): Remove the player's rating - [`POST /v1/sdk/games/{gameId}/comments`](#post-v1-sdk-games-gameid-comments): Post a comment on a game - [`PATCH /v1/sdk/comments/{commentId}`](#patch-v1-sdk-comments-commentid): Change the body of one of the player's comments - [`DELETE /v1/sdk/comments/{commentId}`](#delete-v1-sdk-comments-commentid): Delete one of the player's comments - [`GET /v1/catalog/{site}/games/{slug}/reviews`](#get-v1-catalog-site-games-slug-reviews): Rating and visible comments of a game, newest first Also tagged `community`, documented with their first tag: - [`GET /v1/admin/sites/{siteId}/comments`](/docs/api/reference/admin/#get-v1-admin-sites-siteid-comments): Comments of a site for moderation, newest first - [`DELETE /v1/admin/comments/{commentId}`](/docs/api/reference/admin/#delete-v1-admin-comments-commentid): Delete a comment for good - [`POST /v1/admin/comments/{commentId}/hide`](/docs/api/reference/admin/#post-v1-admin-comments-commentid-hide): Hide a comment from every player route - [`POST /v1/admin/comments/{commentId}/unhide`](/docs/api/reference/admin/#post-v1-admin-comments-commentid-unhide): Show a hidden comment again - [`GET /v1/admin/sites/{siteId}/ratings`](/docs/api/reference/admin/#get-v1-admin-sites-siteid-ratings): Rating of every rated game of a site ```sh curl 'https://api.filudi.com/v1/sdk/games/gloop/review' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players const response = await fetch('https://api.filudi.com/v1/sdk/games/gloop/review', { headers: { authorization: `Bearer ${playerToken}` }, }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players response = requests.get( 'https://api.filudi.com/v1/sdk/games/gloop/review', headers={'authorization': f'Bearer {player_token}'}, ) response.raise_for_status() data = response.json() ``` **The player's own rating and comments on a game** `gameId` is the game slug, as in `/v1/sdk/saves/{gameId}`. Hidden comments are left out. Comments are newest first, all of them (a player posts 5 per hour at most). Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) answers 401 here. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string | yes | Game slug | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [PlayerReview](/docs/api/reference/schemas/#schema-playerreview) | Rating and comments. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X PUT 'https://api.filudi.com/v1/sdk/games/gloop/rating' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \ -H 'content-type: application/json' \ -d '{ "stars": 1 }' ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players const response = await fetch('https://api.filudi.com/v1/sdk/games/gloop/rating', { method: 'PUT', headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' }, body: JSON.stringify({ stars: 1, }), }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players response = requests.put( 'https://api.filudi.com/v1/sdk/games/gloop/rating', headers={'authorization': f'Bearer {player_token}'}, json={ 'stars': 1, }, ) response.raise_for_status() data = response.json() ``` **Rate a game from 1 to 5 stars, or change the rating** Guests and accounts. One rating per player and game on the token's site (ADR 0012). The game must be in the site catalog and visible. 60 ratings per player per hour. The catalog shows the new average within its 60-second cache. Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) answers 401 here. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string | yes | Game slug | ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `stars` | integer | yes | 1 to 5 | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [RatingSummary](/docs/api/reference/schemas/#schema-ratingsummary) | The game's rating after this one. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | | 404 | none | Game not in this site's catalog. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | | 503 | none | Redis is down; the rate limit cannot be checked. | ```sh curl -X DELETE 'https://api.filudi.com/v1/sdk/games/gloop/rating' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players const response = await fetch('https://api.filudi.com/v1/sdk/games/gloop/rating', { method: 'DELETE', headers: { authorization: `Bearer ${playerToken}` }, }); // No body: check response.ok. ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players response = requests.delete( 'https://api.filudi.com/v1/sdk/games/gloop/rating', headers={'authorization': f'Bearer {player_token}'}, ) response.raise_for_status() # No body: the status says it all. ``` **Remove the player's rating** Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) answers 401 here. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string | yes | Game slug | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Removed. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | ```sh curl -X POST 'https://api.filudi.com/v1/sdk/games/gloop/comments' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \ -H 'content-type: application/json' \ -d '{ "body": "string" }' ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players const response = await fetch('https://api.filudi.com/v1/sdk/games/gloop/comments', { method: 'POST', headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' }, body: JSON.stringify({ body: 'string', }), }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players response = requests.post( 'https://api.filudi.com/v1/sdk/games/gloop/comments', headers={'authorization': f'Bearer {player_token}'}, json={ 'body': 'string', }, ) response.raise_for_status() data = response.json() ``` **Post a comment on a game** Account players only (ADR 0012). The comment shows at once. The API converts CRLF to LF, removes control characters other than newline and tab and the Unicode format characters (category Cf: bidi controls, zero-width characters) except U+200D, the zero-width joiner combined emoji need, trims, and turns three or more consecutive newlines into two; the result must have 2 to 1000 characters. The comment's locale is the player's locale. 5 comments per player per hour. Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) answers 401 here. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string | yes | Game slug | ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `body` | string | yes | Plain text; 2 to 1000 characters | ### Responses | Status | Body | Description | | --- | --- | --- | | 201 | [Comment](/docs/api/reference/schemas/#schema-comment) | The comment. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | | 403 | none | A guest: "Sign in to comment.". | | 404 | none | Game not in this site's catalog. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | | 503 | none | Redis is down; the rate limit cannot be checked. | ```sh curl -X PATCH 'https://api.filudi.com/v1/sdk/comments/00000000-0000-0000-0000-000000000000' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \ -H 'content-type: application/json' \ -d '{ "body": "string" }' ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players const response = await fetch('https://api.filudi.com/v1/sdk/comments/00000000-0000-0000-0000-000000000000', { method: 'PATCH', headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' }, body: JSON.stringify({ body: 'string', }), }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players response = requests.patch( 'https://api.filudi.com/v1/sdk/comments/00000000-0000-0000-0000-000000000000', headers={'authorization': f'Bearer {player_token}'}, json={ 'body': 'string', }, ) response.raise_for_status() data = response.json() ``` **Change the body of one of the player's comments** Same body rules as a new comment. Sets `editedAt`. 30 edits per player per hour. Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) answers 401 here. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `commentId` | path | string (uuid) | yes | | ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `body` | string | yes | Plain text; 2 to 1000 characters | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Comment](/docs/api/reference/schemas/#schema-comment) | The comment. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | | 404 | none | Unknown, hidden, or written by another player. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | | 503 | none | Redis is down; the rate limit cannot be checked. | ```sh curl -X DELETE 'https://api.filudi.com/v1/sdk/comments/00000000-0000-0000-0000-000000000000' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players const response = await fetch('https://api.filudi.com/v1/sdk/comments/00000000-0000-0000-0000-000000000000', { method: 'DELETE', headers: { authorization: `Bearer ${playerToken}` }, }); // No body: check response.ok. ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players response = requests.delete( 'https://api.filudi.com/v1/sdk/comments/00000000-0000-0000-0000-000000000000', headers={'authorization': f'Bearer {player_token}'}, ) response.raise_for_status() # No body: the status says it all. ``` **Delete one of the player's comments** Works on a hidden comment too. Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) answers 401 here. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `commentId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Deleted. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | | 404 | none | Unknown. | ```sh curl 'https://api.filudi.com/v1/catalog/filudi/games/gloop/reviews' ``` ```js const response = await fetch('https://api.filudi.com/v1/catalog/filudi/games/gloop/reviews'); const data = await response.json(); ``` ```py import requests response = requests.get('https://api.filudi.com/v1/catalog/filudi/games/gloop/reviews') response.raise_for_status() data = response.json() ``` **Rating and visible comments of a game, newest first** Public (ADR 0012). Leaves out hidden comments and the comments of players scheduled for deletion. The nickname is the author's at read time. Every page repeats `rating`. Credential: none. The route is public. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `site` | path | string | yes | Site slug | | `slug` | path | string | yes | | | `cursor` | query | string | no | Opaque; the `nextCursor` of the previous page, with the same filters and sort; at most 200 characters | | `limit` | query | integer | no | 1 to 50; default `20` | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [GameReviews](/docs/api/reference/schemas/#schema-gamereviews) | Reviews. Headers: `Cache-Control`, `ETag`. | | 304 | none | Not modified: `If-None-Match` matches the current ETag. | | 400 | none | Cursor not issued by this route. | | 404 | none | Unknown site. | Source: https://developers.filudi.com/docs/api/reference/community/ --- # Console routes Sign-in, account, invitations and sessions of the console and the back office (ADR 0015) Sign-in, account, invitations and sessions of the console and the back office (ADR 0015). Generated from `openapi.yaml` of games-api 0.7.0: 19 routes. - [`POST /v1/console/sign-in`](#post-v1-console-sign-in): Sign in with email and password - [`POST /v1/console/sign-in/totp`](#post-v1-console-sign-in-totp): Finish a sign-in with the 6-digit code of the authenticator - [`POST /v1/console/sign-out`](#post-v1-console-sign-out): Sign out - [`GET /v1/console/me`](#get-v1-console-me): The signed-in account - [`PATCH /v1/console/me`](#patch-v1-console-me): Change the display name - [`GET /v1/console/invitations/{token}`](#get-v1-console-invitations-token): What an invitation grants, for the accept page - [`POST /v1/console/invitations/{token}/accept`](#post-v1-console-invitations-token-accept): Accept an invitation; this creates the account and signs it in - [`POST /v1/console/password/forgot`](#post-v1-console-password-forgot): Email a password reset link - [`POST /v1/console/password/reset`](#post-v1-console-password-reset): Set a new password from a reset link - [`PUT /v1/console/password`](#put-v1-console-password): Change the password - [`POST /v1/console/email`](#post-v1-console-email): Ask to change the email address - [`POST /v1/console/email/confirm`](#post-v1-console-email-confirm): Confirm a new email address from its link - [`POST /v1/console/totp`](#post-v1-console-totp): Start a TOTP enrolment - [`DELETE /v1/console/totp`](#delete-v1-console-totp): Remove TOTP, with a current code - [`POST /v1/console/totp/confirm`](#post-v1-console-totp-confirm): Confirm the enrolment with a first code; TOTP is on from here - [`GET /v1/console/sessions`](#get-v1-console-sessions): The account's sessions, newest first - [`DELETE /v1/console/sessions/{sessionId}`](#delete-v1-console-sessions-sessionid): Revoke one session; revoking the current one signs this browser out - [`POST /v1/console/impersonation/end`](#post-v1-console-impersonation-end): End an impersonation - [`POST /v1/console/invitation-requests`](#post-v1-console-invitation-requests): Ask for an invitation, from the form on developers.filudi.com ```sh curl -X POST 'https://api.filudi.com/v1/console/sign-in' \ -H 'content-type: application/json' \ -d '{ "email": "you@example.com", "password": "string" }' ``` ```js const response = await fetch('https://api.filudi.com/v1/console/sign-in', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: 'you@example.com', password: 'string', }), }); const data = await response.json(); ``` ```py import requests response = requests.post( 'https://api.filudi.com/v1/console/sign-in', json={ 'email': 'you@example.com', 'password': 'string', }, ) response.raise_for_status() data = response.json() ``` **Sign in with email and password** Sets the `filudi_console` cookie (ADR 0015). When the account has TOTP, `totpRequired` is true, `me` is absent and the cookie holds a pending sign-in for 5 minutes: finish with `POST /v1/console/sign-in/totp`. An unknown email and a wrong password get the same 401 in the same time. Attempts are limited per email and per IP in Redis, as `POST /v1/account/sign-in` does. Credential: none. The route is public. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `email` | string (email) | yes | | | `password` | string | yes | 1 to 1024 characters; write only | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [SignInResult](/docs/api/reference/schemas/#schema-signinresult) | Signed in, or a TOTP code is due. Headers: `Set-Cookie` (`filudi_console`). | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | "Email or password is wrong.". | | 403 | none | The account is disabled. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | | 503 | none | Redis is down; the rate limit cannot be checked. | ```sh curl -X POST 'https://api.filudi.com/v1/console/sign-in/totp' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "code": "string" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/console/sign-in/totp', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ code: 'string', }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/console/sign-in/totp', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'code': 'string', }, ) response.raise_for_status() data = response.json() ``` **Finish a sign-in with the 6-digit code of the authenticator** Needs the pending cookie of `POST /v1/console/sign-in`. The code is RFC 6238, 30-second step, one step of drift either way; a code is accepted once. The fifth wrong code ends the pending sign-in. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `code` | [TotpCode](/docs/api/reference/schemas/#schema-totpcode) | yes | Six digits, "123456" or "123 456"; pattern `^[0-9 ]{6,7}$` | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [ConsoleMe](/docs/api/reference/schemas/#schema-consoleme) | Signed in; the cookie now holds a 14-day session. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Wrong code. On the fifth wrong code the pending sign-in is gone, the cookie is cleared and `details.signInEnded` is true. | | 401 | none | No pending sign-in. | ```sh curl -X POST 'https://api.filudi.com/v1/console/sign-out' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/console/sign-out', { method: 'POST', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/console/sign-out', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Sign out** Deletes the session row and clears the cookie. Answers 204 without a session too. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Signed out. | ```sh curl 'https://api.filudi.com/v1/console/me' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/console/me', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/console/me', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **The signed-in account** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [ConsoleMe](/docs/api/reference/schemas/#schema-consoleme) | Account. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | ```sh curl -X PATCH 'https://api.filudi.com/v1/console/me' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "name": "string" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/console/me', { method: 'PATCH', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: 'string', }), }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.patch( 'https://api.filudi.com/v1/console/me', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'name': 'string', }, ) response.raise_for_status() data = response.json() ``` **Change the display name** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `name` | [AccountName](/docs/api/reference/schemas/#schema-accountname) | yes | Display name, shown to other members and in the audit log; 1 to 80 characters | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [ConsoleMe](/docs/api/reference/schemas/#schema-consoleme) | Account. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | ```sh curl 'https://api.filudi.com/v1/console/invitations/string' ``` ```js const response = await fetch('https://api.filudi.com/v1/console/invitations/string'); const data = await response.json(); ``` ```py import requests response = requests.get('https://api.filudi.com/v1/console/invitations/string') response.raise_for_status() data = response.json() ``` **What an invitation grants, for the accept page** Credential: none. The route is public. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `token` | path | string | yes | The token of the invitation link | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [InvitationPreview](/docs/api/reference/schemas/#schema-invitationpreview) | Invitation. | | 404 | none | Unknown, expired, accepted or revoked: one answer for the four. | ```sh curl -X POST 'https://api.filudi.com/v1/console/invitations/string/accept' \ -H 'content-type: application/json' \ -d '{ "name": "string", "password": "string" }' ``` ```js const response = await fetch('https://api.filudi.com/v1/console/invitations/string/accept', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: 'string', password: 'string', }), }); const data = await response.json(); ``` ```py import requests response = requests.post( 'https://api.filudi.com/v1/console/invitations/string/accept', json={ 'name': 'string', 'password': 'string', }, ) response.raise_for_status() data = response.json() ``` **Accept an invitation; this creates the account and signs it in** The password follows the policy of `PUT /v1/account/password`: 8 to 128 characters after NFC normalization, not equal to the email. The token is single use. Sets the `filudi_console` cookie. Credential: none. The route is public. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `token` | path | string | yes | The token of the invitation link | ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `name` | [AccountName](/docs/api/reference/schemas/#schema-accountname) | yes | Display name, shown to other members and in the audit log; 1 to 80 characters | | `password` | [NewPassword](/docs/api/reference/schemas/#schema-newpassword) | yes | The policy of `PUT /v1/account/password`: 8 to 128 characters after NFC normalization, not equal to the email; 8 to 128 characters; write only | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [ConsoleMe](/docs/api/reference/schemas/#schema-consoleme) | Account created and signed in. Headers: `Set-Cookie` (`filudi_console`). | | 400 | none | The password breaks the policy. | | 404 | none | Unknown, expired, accepted or revoked. | ```sh curl -X POST 'https://api.filudi.com/v1/console/password/forgot' \ -H 'content-type: application/json' \ -d '{ "email": "you@example.com" }' ``` ```js const response = await fetch('https://api.filudi.com/v1/console/password/forgot', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: 'you@example.com', }), }); // No body: check response.ok. ``` ```py import requests response = requests.post( 'https://api.filudi.com/v1/console/password/forgot', json={ 'email': 'you@example.com', }, ) response.raise_for_status() # No body: the status says it all. ``` **Email a password reset link** Answers 202 whether the email is on an account or not. Limited per email and per IP in Redis. Credential: none. The route is public. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `email` | string (email) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 202 | none | Sent, or silently not sent. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | ```sh curl -X POST 'https://api.filudi.com/v1/console/password/reset' \ -H 'content-type: application/json' \ -d '{ "token": "string", "password": "string" }' ``` ```js const response = await fetch('https://api.filudi.com/v1/console/password/reset', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ token: 'string', password: 'string', }), }); // No body: check response.ok. ``` ```py import requests response = requests.post( 'https://api.filudi.com/v1/console/password/reset', json={ 'token': 'string', 'password': 'string', }, ) response.raise_for_status() # No body: the status says it all. ``` **Set a new password from a reset link** Same policy as `PUT /v1/account/password`. The token is single use. Other sessions of the account stay signed in. Credential: none. The route is public. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `token` | string | yes | | | `password` | [NewPassword](/docs/api/reference/schemas/#schema-newpassword) | yes | The policy of `PUT /v1/account/password`: 8 to 128 characters after NFC normalization, not equal to the email; 8 to 128 characters; write only | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Saved. | | 400 | none | The password breaks the policy. | | 404 | none | Unknown, expired or used token. | ```sh curl -X PUT 'https://api.filudi.com/v1/console/password' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "current": "string", "password": "string" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/console/password', { method: 'PUT', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ current: 'string', password: 'string', }), }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.put( 'https://api.filudi.com/v1/console/password', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'current': 'string', 'password': 'string', }, ) response.raise_for_status() # No body: the status says it all. ``` **Change the password** Same policy as `PUT /v1/account/password`. Other sessions stay signed in. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `current` | string | yes | 1 to 128 characters; write only | | `password` | [NewPassword](/docs/api/reference/schemas/#schema-newpassword) | yes | The policy of `PUT /v1/account/password`: 8 to 128 characters after NFC normalization, not equal to the email; 8 to 128 characters; write only | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Saved. | | 400 | none | The password breaks the policy. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing session, or `current` is wrong; then the body carries `details: { field: "current" }`. | ```sh curl -X POST 'https://api.filudi.com/v1/console/email' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "password": "string", "email": "you@example.com" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/console/email', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ password: 'string', email: 'you@example.com', }), }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/console/email', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'password': 'string', 'email': 'you@example.com', }, ) response.raise_for_status() # No body: the status says it all. ``` **Ask to change the email address** A confirmation link goes to the new address. The account keeps its current email until `POST /v1/console/email/confirm`. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `password` | string | yes | 1 to 128 characters; write only | | `email` | string (email) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 202 | none | Confirmation link sent to the new address. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing session, or the password is wrong. | | 409 | none | The email is already on an account. | ```sh curl -X POST 'https://api.filudi.com/v1/console/email/confirm' \ -H 'content-type: application/json' \ -d '{ "token": "string" }' ``` ```js const response = await fetch('https://api.filudi.com/v1/console/email/confirm', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ token: 'string', }), }); // No body: check response.ok. ``` ```py import requests response = requests.post( 'https://api.filudi.com/v1/console/email/confirm', json={ 'token': 'string', }, ) response.raise_for_status() # No body: the status says it all. ``` **Confirm a new email address from its link** Credential: none. The route is public. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `token` | string | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Email changed. | | 404 | none | Unknown, expired or used token. | | 409 | none | The email went on another account in the meantime. | ```sh curl -X POST 'https://api.filudi.com/v1/console/totp' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/console/totp', { method: 'POST', credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/console/totp', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() data = response.json() ``` **Start a TOTP enrolment** Answers a new secret and its `otpauth://` URL for the authenticator app. The secret is not active until `POST /v1/console/totp/confirm` accepts a code; starting again replaces an unconfirmed secret. Staff without TOTP gets 403 on every `/v1/admin/*` route except `GET /v1/admin/me`. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [TotpEnrolment](/docs/api/reference/schemas/#schema-totpenrolment) | Secret to show once. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | | 409 | none | TOTP is already enabled; remove it first. | ```sh curl -X DELETE 'https://api.filudi.com/v1/console/totp' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "code": "string" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/console/totp', { method: 'DELETE', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ code: 'string', }), }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/console/totp', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'code': 'string', }, ) response.raise_for_status() # No body: the status says it all. ``` **Remove TOTP, with a current code** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `code` | [TotpCode](/docs/api/reference/schemas/#schema-totpcode) | yes | Six digits, "123456" or "123 456"; pattern `^[0-9 ]{6,7}$` | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Removed. | | 400 | none | Wrong code. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | ```sh curl -X POST 'https://api.filudi.com/v1/console/totp/confirm' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' \ -H 'content-type: application/json' \ -d '{ "code": "string" }' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/console/totp/confirm', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ code: 'string', }), }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/console/totp/confirm', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, json={ 'code': 'string', }, ) response.raise_for_status() # No body: the status says it all. ``` **Confirm the enrolment with a first code; TOTP is on from here** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `code` | [TotpCode](/docs/api/reference/schemas/#schema-totpcode) | yes | Six digits, "123456" or "123 456"; pattern `^[0-9 ]{6,7}$` | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | TOTP enabled. | | 400 | none | Wrong code, or no enrolment started. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | ```sh curl 'https://api.filudi.com/v1/console/sessions' \ -b "filudi_console=$FILUDI_SESSION" ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/console/sessions', { credentials: 'include', }); const data = await response.json(); ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.get( 'https://api.filudi.com/v1/console/sessions', cookies={'filudi_console': session}, ) response.raise_for_status() data = response.json() ``` **The account's sessions, newest first** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [ConsoleSessionInfo](/docs/api/reference/schemas/#schema-consolesessioninfo)[] | Sessions. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | ```sh curl -X DELETE 'https://api.filudi.com/v1/console/sessions/00000000-0000-0000-0000-000000000000' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/console/sessions/00000000-0000-0000-0000-000000000000', { method: 'DELETE', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.delete( 'https://api.filudi.com/v1/console/sessions/00000000-0000-0000-0000-000000000000', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **Revoke one session; revoking the current one signs this browser out** Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `sessionId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Revoked. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | | 404 | none | Not a session of this account. | ```sh curl -X POST 'https://api.filudi.com/v1/console/impersonation/end' \ -b "filudi_console=$FILUDI_SESSION" \ -H 'origin: https://developers.filudi.com' ``` ```js // The browser sends the filudi_console cookie and the Origin header itself. const response = await fetch('https://api.filudi.com/v1/console/impersonation/end', { method: 'POST', credentials: 'include', }); // No body: check response.ok. ``` ```py import requests # session: the value of the filudi_console cookie set by POST /v1/console/sign-in response = requests.post( 'https://api.filudi.com/v1/console/impersonation/end', cookies={'filudi_console': session}, headers={'origin': 'https://developers.filudi.com'}, ) response.raise_for_status() # No body: the status says it all. ``` **End an impersonation** Send it with `X-Filudi-Impersonation: 1`. Deletes the impersonation session and clears the `filudi_impersonation` cookie. Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead. ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Ended. | | 401 | none | No impersonation session. | ```sh curl -X POST 'https://api.filudi.com/v1/console/invitation-requests' \ -H 'content-type: application/json' \ -d '{ "email": "you@example.com", "wants": [ "games" ], "message": "string" }' ``` ```js const response = await fetch('https://api.filudi.com/v1/console/invitation-requests', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: 'you@example.com', wants: ['games'], message: 'string', }), }); // No body: check response.ok. ``` ```py import requests response = requests.post( 'https://api.filudi.com/v1/console/invitation-requests', json={ 'email': 'you@example.com', 'wants': ['games'], 'message': 'string', }, ) response.raise_for_status() # No body: the status says it all. ``` **Ask for an invitation, from the form on developers.filudi.com** Stored for staff, who invites or dismisses. No email is sent. 5 requests per IP per day. Credential: none. The route is public. ### Request body `application/json`, required. The body is a [InvitationRequestInput](/docs/api/reference/schemas/#schema-invitationrequestinput). | Property | Type | Required | Description | | --- | --- | --- | --- | | `email` | string (email) | yes | at most 254 characters | | `wants` | array of one of `"games"`, `"portals"` | yes | What they build; 1 to 2 items; unique items | | `message` | string | yes | Counted after trim; 1 to 500 characters | ### Responses | Status | Body | Description | | --- | --- | --- | | 202 | none | Stored. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | | 503 | none | Redis is down; the rate limit cannot be checked. | Source: https://developers.filudi.com/docs/api/reference/console/ --- # Feedback routes Bug reports and suggestions, read in the admin (ADR 0012) Bug reports and suggestions, read in the admin (ADR 0012). Generated from `openapi.yaml` of games-api 0.7.0: 1 route. - [`POST /v1/feedback`](#post-v1-feedback): Send a bug report, a suggestion or another message about the site or a game Also tagged `feedback`, documented with their first tag: - [`GET /v1/admin/sites/{siteId}/feedback`](/docs/api/reference/admin/#get-v1-admin-sites-siteid-feedback): Feedback of a site, newest first - [`GET /v1/admin/feedback/{feedbackId}`](/docs/api/reference/admin/#get-v1-admin-feedback-feedbackid): One feedback message - [`PATCH /v1/admin/feedback/{feedbackId}`](/docs/api/reference/admin/#patch-v1-admin-feedback-feedbackid): Change the status or the note - [`DELETE /v1/admin/feedback/{feedbackId}`](/docs/api/reference/admin/#delete-v1-admin-feedback-feedbackid): Delete a feedback message for good ```sh curl -X POST 'https://api.filudi.com/v1/feedback' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \ -H 'content-type: application/json' \ -d '{ "site": "filudi", "game": "gloop", "kind": "bug", "message": "string", "email": "you@example.com", "page": "string", "locale": "en" }' ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players const response = await fetch('https://api.filudi.com/v1/feedback', { method: 'POST', headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' }, body: JSON.stringify({ site: 'filudi', game: 'gloop', kind: 'bug', message: 'string', email: 'you@example.com', page: 'string', locale: 'en', }), }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players response = requests.post( 'https://api.filudi.com/v1/feedback', headers={'authorization': f'Bearer {player_token}'}, json={ 'site': 'filudi', 'game': 'gloop', 'kind': 'bug', 'message': 'string', 'email': 'you@example.com', 'page': 'string', 'locale': 'en', }, ) response.raise_for_status() data = response.json() ``` **Send a bug report, a suggestion or another message about the site or a game** Anyone (ADR 0012). With a player token, the row records the player; a token of another site than `site` answers 400. An invalid or dead bearer counts as no token, as on sign-in: the route never answers 401. The API stores the user agent (first 300 characters) and an IP hash, encrypts `email` at rest, and sends no email. `locale` is kept when it is a locale of the site. `game` must be in the site catalog; a hidden entry is accepted, since a player may report a game the site just hid. 5 per IP per hour and 10 per player per day. Credential, optional: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) answers 401 here. Without a token the answer is the anonymous one. ### Request body `application/json`, required. The body is a [FeedbackInput](/docs/api/reference/schemas/#schema-feedbackinput). | Property | Type | Required | Description | | --- | --- | --- | --- | | `site` | string | yes | Site slug | | `game` | string | no | Game slug; absent when the message is about the site | | `kind` | [FeedbackKind](/docs/api/reference/schemas/#schema-feedbackkind) | yes | | | `message` | string | yes | Counted after trim; 10 to 2000 characters | | `email` | string (email) or string | no | Where the team may reply; "" counts as no email | | `page` | string | no | Path of the page, without the query string; at most 512 characters; pattern `^/` | | `locale` | string | no | The page locale; kept when it is a locale of the site | ### Responses | Status | Body | Description | | --- | --- | --- | | 202 | [FeedbackAccepted](/docs/api/reference/schemas/#schema-feedbackaccepted) | Stored for the team. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 404 | none | Unknown site. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | | 503 | none | Redis is down; the rate limit cannot be checked. | Source: https://developers.filudi.com/docs/api/reference/feedback/ --- # Health routes The health routes of games-api, with parameters, bodies, responses and request samples. Generated from `openapi.yaml` of games-api 0.7.0: 3 routes. - [`GET /healthz`](#get-healthz): Liveness, no dependency - [`GET /api/health`](#get-api-health): Readiness, SELECT 1 and Redis PING - [`GET /metrics`](#get-metrics): Prometheus metrics ```sh curl 'https://api.filudi.com/healthz' ``` ```js const response = await fetch('https://api.filudi.com/healthz'); const text = await response.text(); ``` ```py import requests response = requests.get('https://api.filudi.com/healthz') response.raise_for_status() text = response.text ``` **Liveness, no dependency** Credential: none. The route is public. ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | `"ok"` (`text/plain`) | Process alive. | ```sh curl 'https://api.filudi.com/api/health' ``` ```js const response = await fetch('https://api.filudi.com/api/health'); const data = await response.json(); ``` ```py import requests response = requests.get('https://api.filudi.com/api/health') response.raise_for_status() data = response.json() ``` **Readiness, SELECT 1 and Redis PING** Credential: none. The route is public. ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | object | Ready. | | 503 | none | Database unreachable. | **Body of 200** | Property | Type | Required | Description | | --- | --- | --- | --- | | `db` | `"ok"` | no | | | `redis` | one of `"ok"`, `"degraded"` | no | | ```sh curl 'https://api.filudi.com/metrics' ``` ```js const response = await fetch('https://api.filudi.com/metrics'); const text = await response.text(); ``` ```py import requests response = requests.get('https://api.filudi.com/metrics') response.raise_for_status() text = response.text ``` **Prometheus metrics** Credential: none. The route is public. ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | `text/plain` | Text exposition format. | Source: https://developers.filudi.com/docs/api/reference/health/ --- # Schemas Every named object of the games-api API: the fields each route reads or answers. The named schemas of `openapi.yaml`, games-api 0.7.0. A route's body or response links here; a field that is itself a named schema links to its own section. - [`Error`](#schema-error) - [`Period`](#schema-period) - [`AgeBand`](#schema-ageband) - [`Player`](#schema-player) - [`PlayerWithToken`](#schema-playerwithtoken) - [`ConsumeResponse`](#schema-consumeresponse) - [`SaveMeta`](#schema-savemeta) - [`SaveServerCopy`](#schema-saveservercopy) - [`Save`](#schema-save) - [`LeaderboardEntry`](#schema-leaderboardentry) - [`LeaderboardTop`](#schema-leaderboardtop) - [`EventBatch`](#schema-eventbatch) - [`EventIn`](#schema-eventin) - [`Catalog`](#schema-catalog) - [`CatalogGame`](#schema-cataloggame) - [`CatalogRating`](#schema-catalograting) - [`RatingSummary`](#schema-ratingsummary) - [`Comment`](#schema-comment) - [`GameReviews`](#schema-gamereviews) - [`PlayerReview`](#schema-playerreview) - [`CommentAdmin`](#schema-commentadmin) - [`CommentPage`](#schema-commentpage) - [`GameRatingAdmin`](#schema-gameratingadmin) - [`FeedbackKind`](#schema-feedbackkind) - [`FeedbackStatus`](#schema-feedbackstatus) - [`FeedbackInput`](#schema-feedbackinput) - [`FeedbackAccepted`](#schema-feedbackaccepted) - [`FeedbackAdmin`](#schema-feedbackadmin) - [`FeedbackUpdate`](#schema-feedbackupdate) - [`FeedbackPage`](#schema-feedbackpage) - [`MediaSlot`](#schema-mediaslot) - [`MediaFile`](#schema-mediafile) - [`GameMedia`](#schema-gamemedia) - [`GameTexts`](#schema-gametexts) - [`CategoryIcon`](#schema-categoryicon) - [`Category`](#schema-category) - [`CategoryInput`](#schema-categoryinput) - [`Tag`](#schema-tag) - [`TagInput`](#schema-taginput) - [`OtherSite`](#schema-othersite) - [`Site`](#schema-site) - [`SiteInput`](#schema-siteinput) - [`PublishResult`](#schema-publishresult) - [`Distribution`](#schema-distribution) - [`Game`](#schema-game) - [`GameInput`](#schema-gameinput) - [`GameVersion`](#schema-gameversion) - [`SiteGame`](#schema-sitegame) - [`SiteGameInput`](#schema-sitegameinput) - [`Leaderboard`](#schema-leaderboard) - [`LeaderboardInput`](#schema-leaderboardinput) - [`ScoreAdmin`](#schema-scoreadmin) - [`PlayerAdmin`](#schema-playeradmin) - [`PlayerPage`](#schema-playerpage) - [`PlayerExport`](#schema-playerexport) - [`EventCount`](#schema-eventcount) - [`EventDaily`](#schema-eventdaily) - [`StatsTotals`](#schema-statstotals) - [`StatsDay`](#schema-statsday) - [`StatsGameRow`](#schema-statsgamerow) - [`StatsKeyCount`](#schema-statskeycount) - [`StatsOverview`](#schema-statsoverview) - [`GameStatsDay`](#schema-gamestatsday) - [`GameStatsTotals`](#schema-gamestatstotals) - [`GameStatsBoard`](#schema-gamestatsboard) - [`GameStats`](#schema-gamestats) - [`WebStatsRow`](#schema-webstatsrow) - [`WebStats`](#schema-webstats) - [`AuditEntry`](#schema-auditentry) - [`GameToken`](#schema-gametoken) - [`AccountName`](#schema-accountname) - [`NewPassword`](#schema-newpassword) - [`TotpCode`](#schema-totpcode) - [`MembershipRole`](#schema-membershiprole) - [`Scope`](#schema-scope) - [`ConsoleMe`](#schema-consoleme) - [`SignInResult`](#schema-signinresult) - [`TotpEnrolment`](#schema-totpenrolment) - [`ConsoleSessionInfo`](#schema-consolesessioninfo) - [`InvitationPreview`](#schema-invitationpreview) - [`InvitationRequestInput`](#schema-invitationrequestinput) - [`InvitationRequestStatus`](#schema-invitationrequeststatus) - [`InvitationRequest`](#schema-invitationrequest) - [`InvitationRequestPage`](#schema-invitationrequestpage) - [`Membership`](#schema-membership) - [`AccountAdmin`](#schema-accountadmin) - [`AccountPage`](#schema-accountpage) - [`AccountPatch`](#schema-accountpatch) - [`ImpersonationStarted`](#schema-impersonationstarted) - [`InvitationGrant`](#schema-invitationgrant) - [`Invitation`](#schema-invitation) - [`InvitationInput`](#schema-invitationinput) - [`ApprovalOwner`](#schema-approvalowner) - [`ApprovalGame`](#schema-approvalgame) - [`ApprovalSite`](#schema-approvalsite) - [`ApprovalDomains`](#schema-approvaldomains) - [`ApprovalVersion`](#schema-approvalversion) - [`Approvals`](#schema-approvals) - [`RejectInput`](#schema-rejectinput) - [`Member`](#schema-member) - [`MemberInput`](#schema-memberinput) - [`SiteKeyScope`](#schema-sitekeyscope) - [`SiteKey`](#schema-sitekey) - [`SiteKeyCreated`](#schema-sitekeycreated) - [`SiteKeyInput`](#schema-sitekeyinput) - [`RetentionShare`](#schema-retentionshare) - [`StatsCohort`](#schema-statscohort) - [`StatsFunnel`](#schema-statsfunnel) - [`ConsoleStatsDay`](#schema-consolestatsday) - [`ConsoleStatsTotals`](#schema-consolestatstotals) - [`ConsoleGameStats`](#schema-consolegamestats) - [`ServerPlayerInput`](#schema-serverplayerinput) - [`ServerPlayer`](#schema-serverplayer)

Error

| Property | Type | Required | Description | | --- | --- | --- | --- | | `error` | one of `"unauthorized"`, `"forbidden"`, `"not_found"`, `"invalid"`, `"conflict"`, `"too_large"`, `"rate_limited"`, `"unavailable"`, `"internal"` | yes | | | `message` | string | yes | | | `details` | object | no | |

Period

Type: one of `"day"`, `"week"`, `"all"`; default `"all"`.

AgeBand

Type: one of `"all"`, `"kids"`, `"teen"`, `"adult"`.

Player

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | | | `nickname` | string | yes | | | `guest` | boolean | yes | | | `site` | string | yes | | | `locale` | string | yes | | | `emailMasked` | string or null | no | a***@example.com for the account page | | `hasPassword` | boolean | yes | True when the account set a password (ADR 0011); false for a guest | | `createdAt` | string (date-time) | no | |

PlayerWithToken

| Property | Type | Required | Description | | --- | --- | --- | --- | | `player` | [Player](/docs/api/reference/schemas/#schema-player) | yes | | | `token` | string | yes | |

ConsumeResponse

What magic-link consume, consume-code and sign-in answer (ADR 0011) | Property | Type | Required | Description | | --- | --- | --- | --- | | `player` | [Player](/docs/api/reference/schemas/#schema-player) | yes | | | `token` | string | yes | | | `merged` | boolean | yes | True when a guest was merged into the account by this call | | `redirect` | string | no | Path on the site to go to; magic-link consume only, when the link carried one |

SaveMeta

| Property | Type | Required | Description | | --- | --- | --- | --- | | `gameId` | string | yes | | | `version` | integer | yes | | | `etag` | string | yes | | | `bytes` | integer | yes | | | `updatedAt` | string (date-time) | yes | |

SaveServerCopy

The 409 body. When nothing is stored yet, version is 0, etag is empty and updatedAt and data are null. | Property | Type | Required | Description | | --- | --- | --- | --- | | `gameId` | string | yes | | | `version` | integer | yes | | | `etag` | string | yes | | | `bytes` | integer | yes | | | `updatedAt` | string (date-time) or null | yes | | | `data` | any or null | yes | |

Save

Type: [SaveMeta](/docs/api/reference/schemas/#schema-savemeta) + object. | Property | Type | Required | Description | | --- | --- | --- | --- | | `gameId` | string | yes | | | `version` | integer | yes | | | `etag` | string | yes | | | `bytes` | integer | yes | | | `updatedAt` | string (date-time) | yes | | | `data` | any | yes | |

LeaderboardEntry

| Property | Type | Required | Description | | --- | --- | --- | --- | | `rank` | integer | yes | | | `nickname` | string | yes | | | `score` | integer | yes | | | `meta` | object or null | no | | | `isMe` | boolean | yes | |

LeaderboardTop

| Property | Type | Required | Description | | --- | --- | --- | --- | | `period` | [Period](/docs/api/reference/schemas/#schema-period) | yes | default `"all"` | | `entries` | [LeaderboardEntry](/docs/api/reference/schemas/#schema-leaderboardentry)[] | yes | | | `total` | integer | yes | | | `me` | [LeaderboardEntry](/docs/api/reference/schemas/#schema-leaderboardentry) or null | no | |

EventBatch

| Property | Type | Required | Description | | --- | --- | --- | --- | | `events` | [EventIn](/docs/api/reference/schemas/#schema-eventin)[] | yes | at most 50 items |

EventIn

| Property | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | `frame_loaded` is reserved: accepted with a player token from the hub, refused with a game token (ADR 0015); pattern `^[a-z][a-z0-9_]{0,39}$` | | `props` | object | no | | | `props.*` | string or number or boolean | no | | | `ts` | integer | yes | Unix milliseconds on the client | | `gameId` | string | no | | | `gameVersion` | string | no | | | `sessionId` | string (uuid) | no | | | `host` | string | no | | | `locale` | string | no | |

Catalog

| Property | Type | Required | Description | | --- | --- | --- | --- | | `site` | object | yes | | | `site.slug` | string | yes | | | `site.name` | string | yes | | | `site.domains` | string[] | yes | | | `site.locales` | string[] | yes | | | `site.policy` | object | yes | | | `site.policy.accountsAllowed` | boolean | no | | | `site.policy.personalizedAdsAllowed` | boolean | no | | | `site.policy.ageBand` | [AgeBand](/docs/api/reference/schemas/#schema-ageband) | no | | | `site.navCategories` | string[] | yes | Category slugs of the top menu, in menu order; only slugs that exist | | `site.otherSites` | [OtherSite](/docs/api/reference/schemas/#schema-othersite)[] | yes | | | `site.analyticsDomain` | string | yes | The domain registered at CausalIT (causalitDomain, else the first domain); the front's tag reports to it | | `generatedAt` | string (date-time) | yes | | | `categories` | [Category](/docs/api/reference/schemas/#schema-category)[] | yes | | | `tags` | [Tag](/docs/api/reference/schemas/#schema-tag)[] | yes | Every tag of the site, in site order; game tags are slugs of this list | | `games` | [CatalogGame](/docs/api/reference/schemas/#schema-cataloggame)[] | yes | |

CatalogGame

| Property | Type | Required | Description | | --- | --- | --- | --- | | `slug` | string | yes | | | `order` | integer | yes | | | `featured` | boolean | yes | | | `categories` | string[] | yes | | | `tags` | string[] | yes | | | `orientation` | one of `"portrait"`, `"landscape"`, `"any"` | yes | | | `minutes` | integer or null | yes | Typical run length in minutes; null when not set | | `mobile` | boolean | yes | | | `daily` | boolean | yes | | | `coverColor` | string or null | no | | | `publishedAt` | string (date-time) or null | no | When the live version was activated; null without one | | `bundleUrl` | string (uri) or null | yes | `https://play.filudi.com///index.html`; null when no live version | | `version` | string or null | yes | | | `boards` | object[] | yes | | | `boards[].name` | string | no | | | `boards[].sort` | one of `"desc"`, `"asc"` | no | | | `texts` | object | yes | Keyed by locale | | `texts.*` | [GameTexts](/docs/api/reference/schemas/#schema-gametexts) | no | | | `media` | [GameMedia](/docs/api/reference/schemas/#schema-gamemedia) | yes | Filled slots only; a missing key is an empty slot | | `rating` | [CatalogRating](/docs/api/reference/schemas/#schema-catalograting) | yes | Stars of a game on the site (ADR 0012). Follows ratings within the catalog's 60-second cache. |

CatalogRating

Stars of a game on the site (ADR 0012). Follows ratings within the catalog's 60-second cache. | Property | Type | Required | Description | | --- | --- | --- | --- | | `average` | number or null | yes | Mean of the stars, rounded to one decimal; null when count is 0 | | `count` | integer | yes | at least 0 |

RatingSummary

| Property | Type | Required | Description | | --- | --- | --- | --- | | `average` | number or null | yes | Mean of the stars, rounded to one decimal; null when count is 0 | | `count` | integer | yes | at least 0 | | `histogram` | integer[] | yes | Ratings per star count; index 0 is one star, index 4 is five stars; 5 to 5 items |

Comment

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | | | `nickname` | string | yes | The author's current nickname, read when the list is served | | `body` | string | yes | Plain text with newlines; render as text | | `createdAt` | string (date-time) | yes | | | `editedAt` | string (date-time) or null | yes | |

GameReviews

| Property | Type | Required | Description | | --- | --- | --- | --- | | `rating` | [RatingSummary](/docs/api/reference/schemas/#schema-ratingsummary) | yes | | | `comments` | [Comment](/docs/api/reference/schemas/#schema-comment)[] | yes | | | `nextCursor` | string or null | yes | Null on the last page |

PlayerReview

| Property | Type | Required | Description | | --- | --- | --- | --- | | `stars` | integer or null | yes | Null when the player has not rated the game; 1 to 5 | | `comments` | [Comment](/docs/api/reference/schemas/#schema-comment)[] | yes | The player's visible comments on the game, newest first |

CommentAdmin

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | | | `gameId` | string (uuid) | yes | | | `game` | string | yes | Game slug | | `player` | object | yes | | | `player.id` | string (uuid) | yes | | | `player.nickname` | string | yes | | | `player.emailMasked` | string or null | yes | | | `body` | string | yes | | | `locale` | string | yes | | | `hidden` | boolean | yes | | | `hiddenBy` | string or null | yes | Username of the moderator | | `hiddenAt` | string (date-time) or null | yes | | | `createdAt` | string (date-time) | yes | | | `editedAt` | string (date-time) or null | yes | |

CommentPage

| Property | Type | Required | Description | | --- | --- | --- | --- | | `items` | [CommentAdmin](/docs/api/reference/schemas/#schema-commentadmin)[] | yes | | | `nextCursor` | string or null | yes | | | `total` | integer | yes | |

GameRatingAdmin

| Property | Type | Required | Description | | --- | --- | --- | --- | | `gameId` | string (uuid) | yes | | | `game` | string | yes | Game slug | | `average` | number or null | yes | | | `count` | integer | yes | | | `histogram` | integer[] | yes | Index 0 is one star; 5 to 5 items |

FeedbackKind

Type: one of `"bug"`, `"suggestion"`, `"other"`.

FeedbackStatus

Type: one of `"new"`, `"seen"`, `"done"`.

FeedbackInput

| Property | Type | Required | Description | | --- | --- | --- | --- | | `site` | string | yes | Site slug | | `game` | string | no | Game slug; absent when the message is about the site | | `kind` | [FeedbackKind](/docs/api/reference/schemas/#schema-feedbackkind) | yes | | | `message` | string | yes | Counted after trim; 10 to 2000 characters | | `email` | string (email) or string | no | Where the team may reply; "" counts as no email | | `page` | string | no | Path of the page, without the query string; at most 512 characters; pattern `^/` | | `locale` | string | no | The page locale; kept when it is a locale of the site |

FeedbackAccepted

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | |

FeedbackAdmin

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | | | `kind` | [FeedbackKind](/docs/api/reference/schemas/#schema-feedbackkind) | yes | | | `status` | [FeedbackStatus](/docs/api/reference/schemas/#schema-feedbackstatus) | yes | | | `message` | string | yes | | | `email` | string or null | yes | In clear: the reporter gave it to be contacted | | `gameId` | string (uuid) or null | yes | | | `game` | string or null | yes | Game slug; null for the site or a deleted game | | `player` | object or null | yes | | | `page` | string or null | yes | | | `userAgent` | string or null | yes | | | `locale` | string or null | yes | | | `note` | string or null | yes | | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | |

FeedbackUpdate

At least one field; an empty body is a 400. | Property | Type | Required | Description | | --- | --- | --- | --- | | `status` | [FeedbackStatus](/docs/api/reference/schemas/#schema-feedbackstatus) | no | | | `note` | string or null | no | Null clears the note; "" is stored as null; at most 2000 characters |

FeedbackPage

| Property | Type | Required | Description | | --- | --- | --- | --- | | `items` | [FeedbackAdmin](/docs/api/reference/schemas/#schema-feedbackadmin)[] | yes | | | `nextCursor` | string or null | yes | | | `total` | integer | yes | |

MediaSlot

ADR 0007 gives the ratio, pixel range, file types and size limit of each slot Type: one of `"image-square"`, `"image-landscape"`, `"image-portrait"`, `"video-landscape"`, `"video-portrait"`.

MediaFile

| Property | Type | Required | Description | | --- | --- | --- | --- | | `url` | string (uri) | yes | `https://play.filudi.com//media/--.`; immutable | | `type` | one of `"image/avif"`, `"image/webp"`, `"image/jpeg"`, `"image/png"`, `"video/mp4"` | yes | | | `bytes` | integer | yes | | | `width` | integer | yes | | | `height` | integer | yes | | | `durationMs` | integer | no | Videos only |

GameMedia

Filled slots only; a missing key is an empty slot | Property | Type | Required | Description | | --- | --- | --- | --- | | `images` | object | yes | | | `images.square` | [MediaFile](/docs/api/reference/schemas/#schema-mediafile) | no | | | `images.landscape` | [MediaFile](/docs/api/reference/schemas/#schema-mediafile) | no | | | `images.portrait` | [MediaFile](/docs/api/reference/schemas/#schema-mediafile) | no | | | `videos` | object | yes | | | `videos.landscape` | [MediaFile](/docs/api/reference/schemas/#schema-mediafile) | no | | | `videos.portrait` | [MediaFile](/docs/api/reference/schemas/#schema-mediafile) | no | |

GameTexts

| Property | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | yes | | | `tagline` | string | yes | One line under the title | | `description` | string | yes | | | `howToPlay` | string[] | no | | | `controls` | object[] | no | | | `controls[].keys` | string[] | no | | | `controls[].does` | string | no | | | `seoTitle` | string | no | | | `seoDescription` | string | no | |

CategoryIcon

Stroke icons drawn by hub-core (`categoryIconSvg`), the same list in the admin picker Type: one of `"puzzle"`, `"joystick"`, `"calendar"`, `"keyboard"`, `"letters"`, `"brain"`, `"cards"`, `"dice"`, `"target"`, `"rocket"`, `"timer"`, `"trophy"`, `"heart"`, `"star"`, `"sparkle"`, `"grid"`, `"map"`, `"music"`, `"bolt"`, `"leaf"`.

Category

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | no | | | `slug` | string | yes | | | `order` | integer | yes | | | `icon` | [CategoryIcon](/docs/api/reference/schemas/#schema-categoryicon) or null | no | | | `texts` | object | yes | | | `texts.*.title` | string | no | | | `texts.*.navTitle` | string | no | Short form for the menu, chips and card meta: "Puzzle" for "Puzzle games" | | `texts.*.description` | string | no | | | `gameCount` | integer | no | Admin routes only; games of the site in this category | | `createdAt` | string (date-time) | no | | | `updatedAt` | string (date-time) | no | |

CategoryInput

| Property | Type | Required | Description | | --- | --- | --- | --- | | `slug` | string | no | On create only; ignored on PATCH; pattern `^[a-z][a-z0-9-]{0,39}$` | | `icon` | [CategoryIcon](/docs/api/reference/schemas/#schema-categoryicon) or null | no | | | `texts` | object | no | Keyed by locale; the site's default locale needs a title | | `texts.*.title` | string | no | at most 120 characters | | `texts.*.navTitle` | string | no | at most 24 characters | | `texts.*.description` | string | no | at most 1000 characters |

Tag

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | no | | | `slug` | string | yes | | | `order` | integer | yes | | | `texts` | object | yes | Keyed by locale | | `texts.*.title` | string | no | | | `gameCount` | integer | no | Admin routes only; games of the site carrying this tag | | `createdAt` | string (date-time) | no | | | `updatedAt` | string (date-time) | no | |

TagInput

| Property | Type | Required | Description | | --- | --- | --- | --- | | `slug` | string | no | On create only; ignored on PATCH; pattern `^[a-z][a-z0-9-]{0,39}$` | | `texts` | object | no | Keyed by locale; the site's default locale needs a title | | `texts.*.title` | string | no | at most 60 characters |

OtherSite

A sister site of the footer | Property | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | 1 to 60 characters | | `url` | string (uri) | yes | https only | | `description` | string | no | at most 120 characters |

Site

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | | | `slug` | string | yes | | | `name` | string | yes | | | `domains` | string[] | yes | Approved domains; allowed origins once the site is approved | | `locales` | string[] | yes | | | `approvedAt` | string (date-time) or null | yes | Null while the site waits for staff (ADR 0015); then its domains are not allowed origins | | `pendingDomains` | string[] | yes | Domains the owner proposed; approval moves them into `domains` | | `firstParty` | boolean | yes | The owner's own site: lists any approved game, whatever its distribution | | `accountsAllowed` | boolean | yes | | | `personalizedAdsAllowed` | boolean | yes | | | `ageBand` | [AgeBand](/docs/api/reference/schemas/#schema-ageband) | yes | | | `cdnPullZoneId` | string or null | no | Bunny pull zone in front of the site; publish purges it | | `navCategories` | string[] | yes | Category slugs of the top menu, in order | | `otherSites` | [OtherSite](/docs/api/reference/schemas/#schema-othersite)[] | yes | | | `causalitDomain` | string or null | no | Domain registered at CausalIT; null means the first domain | | `causalitKeys` | object | yes | Whether each key is stored. The keys themselves are never returned. | | `causalitKeys.read` | boolean | yes | | | `causalitKeys.write` | boolean | yes | | | `createdAt` | string (date-time) | no | | | `updatedAt` | string (date-time) | no | |

SiteInput

On PATCH the slug may be repeated unchanged; the API refuses a different slug. `slug`, `domains`, `firstParty` and `cdnPullZoneId` are for staff only; owners and members write `pendingDomains` and the CausalIT fields (ADR 0015). A key field set to null clears the stored key; absent leaves it. | Property | Type | Required | Description | | --- | --- | --- | --- | | `slug` | string | no | pattern `^[a-z][a-z0-9-]{1,39}$` | | `name` | string | no | 1 to 80 characters | | `domains` | string[] | no | Lowercase hostnames, the first is canonical. Staff only.; at least 1 item | | `pendingDomains` | string[] | no | Lowercase hostnames the owner proposes; each works once staff approves it. Replaces the whole list.; at most 10 items | | `firstParty` | boolean | no | Staff only | | `locales` | string[] | no | Lowercase BCP 47, unique, the first is the default; at least 1 item | | `accountsAllowed` | boolean | no | | | `personalizedAdsAllowed` | boolean | no | | | `ageBand` | [AgeBand](/docs/api/reference/schemas/#schema-ageband) | no | | | `cdnPullZoneId` | string or null | no | pattern `^[0-9]{1,12}$` | | `navCategories` | string[] | no | Slugs of existing categories of the site, unique, in menu order; at most 12 items | | `otherSites` | [OtherSite](/docs/api/reference/schemas/#schema-othersite)[] | no | at most 10 items | | `causalitDomain` | string or null | no | pattern `^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$` | | `causalitReadKey` | string or null | no | 8 to 200 characters; write only | | `causalitWriteKey` | string or null | no | 8 to 200 characters; write only |

PublishResult

| Property | Type | Required | Description | | --- | --- | --- | --- | | `purged` | boolean | yes | | | `reason` | one of `"no_zone"`, `"no_key"` | no | Why nothing was purged; absent when purged is true | | `at` | string (date-time) | yes | |

Distribution

Where a game may be listed (ADR 0015). `first_party`: sites with `firstParty` only. `any_portal`: any approved site. Type: one of `"first_party"`, `"any_portal"`.

Game

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | | | `slug` | string | yes | | | `name` | string | yes | | | `orientation` | one of `"portrait"`, `"landscape"`, `"any"` | yes | | | `minutes` | integer or null | yes | Typical run length in minutes; null when not set | | `mobile` | boolean | yes | | | `daily` | boolean | yes | | | `coverColor` | string or null | no | | | `approvedAt` | string (date-time) or null | yes | Null while the game waits for staff (ADR 0015): no live version, no listing on an approved site | | `distribution` | [Distribution](/docs/api/reference/schemas/#schema-distribution) | yes | Where a game may be listed (ADR 0015). `first_party`: sites with `firstParty` only. `any_portal`: any approved site. | | `listedOn` | object[] | yes | Sites whose catalog carries the game | | `listedOn[].siteId` | string (uuid) | yes | | | `listedOn[].slug` | string | yes | | | `listedOn[].name` | string | yes | | | `versions` | [GameVersion](/docs/api/reference/schemas/#schema-gameversion)[] | yes | | | `media` | [GameMedia](/docs/api/reference/schemas/#schema-gamemedia) | yes | Filled slots only; a missing key is an empty slot | | `sites` | string[] | no | Slugs of sites listing this game | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | |

GameInput

| Property | Type | Required | Description | | --- | --- | --- | --- | | `slug` | string | no | pattern `^[a-z][a-z0-9-]{1,39}$` | | `name` | string | no | | | `orientation` | one of `"portrait"`, `"landscape"`, `"any"` | no | | | `minutes` | integer or null | no | 1 to 600 | | `mobile` | boolean | no | | | `daily` | boolean | no | | | `coverColor` | string or null | no | pattern `^#[0-9A-Fa-f]{6}$` | | `distribution` | [Distribution](/docs/api/reference/schemas/#schema-distribution) | no | Where a game may be listed (ADR 0015). `first_party`: sites with `firstParty` only. `any_portal`: any approved site. |

GameVersion

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | | | `version` | string | yes | | | `state` | one of `"draft"`, `"live"`, `"retired"` | yes | | | `bundlePath` | string | yes | | | `entry` | string | yes | | | `sizeBytes` | integer | yes | | | `fileCount` | integer | yes | | | `notes` | string or null | no | | | `createdBy` | string | yes | | | `createdAt` | string (date-time) | yes | | | `activatedAt` | string (date-time) or null | no | | | `retiredAt` | string (date-time) or null | no | | | `reviewRequestedAt` | string (date-time) or null | yes | Set by `POST /v1/admin/versions/{versionId}/review`; cleared on activate, retire and review reject |

SiteGame

| Property | Type | Required | Description | | --- | --- | --- | --- | | `gameId` | string (uuid) | yes | | | `slug` | string | yes | | | `order` | integer | yes | | | `featured` | boolean | yes | | | `visible` | boolean | yes | | | `categories` | string[] | yes | | | `tags` | string[] | yes | | | `texts` | object | yes | | | `texts.*` | [GameTexts](/docs/api/reference/schemas/#schema-gametexts) | no | | | `liveVersion` | string or null | no | | | `updatedAt` | string (date-time) | no | |

SiteGameInput

| Property | Type | Required | Description | | --- | --- | --- | --- | | `featured` | boolean | no | | | `visible` | boolean | no | | | `categories` | string[] | no | | | `tags` | string[] | no | | | `texts` | object | no | | | `texts.*` | [GameTexts](/docs/api/reference/schemas/#schema-gametexts) | no | |

Leaderboard

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | | | `name` | string | yes | | | `sort` | one of `"desc"`, `"asc"` | yes | | | `min` | integer | yes | | | `max` | integer | yes | | | `keep` | one of `"best"`, `"latest"` | yes | | | `ratePerMinute` | integer | yes | |

LeaderboardInput

| Property | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | pattern `^[a-z][a-z0-9_]{0,31}$` | | `sort` | one of `"desc"`, `"asc"` | yes | | | `min` | integer | yes | | | `max` | integer | yes | | | `keep` | one of `"best"`, `"latest"` | no | default `"best"` | | `ratePerMinute` | integer | no | default `10` |

ScoreAdmin

A developer who does not edit the player's site gets no `playerId`, `site`, `hiddenBy` or `sessionId` (ADR 0014). | Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `playerId` | string (uuid) | no | | | `nickname` | string | yes | | | `site` | string | no | | | `score` | integer | yes | | | `meta` | object or null | no | | | `hidden` | boolean | yes | | | `hiddenBy` | string or null | no | | | `sessionId` | string (uuid) | no | | | `createdAt` | string (date-time) | yes | |

PlayerAdmin

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | | | `site` | string | yes | | | `nickname` | string | yes | | | `guest` | boolean | yes | | | `emailMasked` | string or null | no | | | `externalId` | string or null | no | A portal backend's own user id, set through `POST /v1/server/players`; null otherwise | | `hasPassword` | boolean | yes | Whether the account set a password; the admin cannot set one | | `locale` | string | no | | | `createdAt` | string (date-time) | yes | | | `lastSeenAt` | string (date-time) | yes | | | `deleteAt` | string (date-time) or null | no | | | `mergedIntoId` | string (uuid) or null | no | | | `saves` | [SaveMeta](/docs/api/reference/schemas/#schema-savemeta)[] | no | | | `bestScores` | object[] | no | | | `bestScores[].game` | string | yes | | | `bestScores[].board` | string | yes | | | `bestScores[].score` | integer | yes | | | `bestScores[].createdAt` | string (date-time) | yes | |

PlayerPage

| Property | Type | Required | Description | | --- | --- | --- | --- | | `items` | [PlayerAdmin](/docs/api/reference/schemas/#schema-playeradmin)[] | yes | | | `nextCursor` | string or null | yes | Null on the last page | | `total` | integer | yes | Rows matching the filters |

PlayerExport

| Property | Type | Required | Description | | --- | --- | --- | --- | | `exportedAt` | string (date-time) | yes | | | `player` | [Player](/docs/api/reference/schemas/#schema-player) | yes | | | `email` | string or null | no | | | `saves` | [Save](/docs/api/reference/schemas/#schema-save)[] | yes | | | `scores` | object[] | yes | | | `events` | object[] | yes | Most recent 20 000 of the last 13 months | | `eventsTruncated` | boolean | no | True when older events were left out | | `ratings` | object[] | yes | The player's ratings on the site (ADR 0012) | | `ratings[].game` | string | yes | Game slug | | `ratings[].stars` | integer | yes | 1 to 5 | | `ratings[].createdAt` | string (date-time) | yes | | | `ratings[].updatedAt` | string (date-time) | yes | | | `comments` | object[] | yes | Every comment of the player, hidden ones included | | `comments[].id` | string (uuid) | yes | | | `comments[].game` | string | yes | Game slug | | `comments[].body` | string | yes | | | `comments[].hidden` | boolean | yes | | | `comments[].createdAt` | string (date-time) | yes | | | `comments[].editedAt` | string (date-time) or null | yes | | | `feedback` | object[] | yes | Feedback the player sent with a token | | `feedback[].id` | string (uuid) | yes | | | `feedback[].kind` | [FeedbackKind](/docs/api/reference/schemas/#schema-feedbackkind) | yes | | | `feedback[].status` | [FeedbackStatus](/docs/api/reference/schemas/#schema-feedbackstatus) | yes | | | `feedback[].message` | string | yes | | | `feedback[].game` | string or null | yes | Game slug; null for the site or a deleted game | | `feedback[].page` | string or null | yes | | | `feedback[].createdAt` | string (date-time) | yes | |

EventCount

| Property | Type | Required | Description | | --- | --- | --- | --- | | `game` | string or null | no | | | `name` | string | yes | | | `host` | string | yes | | | `count` | integer | yes | | | `players` | integer | yes | |

EventDaily

Type: [EventCount](/docs/api/reference/schemas/#schema-eventcount) + object. | Property | Type | Required | Description | | --- | --- | --- | --- | | `game` | string or null | no | | | `name` | string | yes | | | `host` | string | yes | | | `count` | integer | yes | | | `players` | integer | yes | | | `day` | string (date) | yes | |

StatsTotals

Counts over a range of days. Players and accounts count rows created in the range. | Property | Type | Required | Description | | --- | --- | --- | --- | | `players` | integer | yes | Players created | | `accounts` | integer | yes | Players that linked an email | | `activePlayers` | integer | yes | Distinct players with a play session | | `sessions` | integer | yes | | | `scores` | integer | yes | | | `saves` | integer | yes | Saves written or updated | | `linksSent` | integer | yes | Magic links created | | `linksConsumed` | integer | yes | | | `events` | integer | yes | Rows of event_daily summed | | `avgSessionSec` | number or null | yes | Mean of last_seen_at minus started_at | | `medianSessionSec` | number or null | yes | Median of the same lengths | | `returningPlayers` | integer | yes | Players of the range with a session on at least two distinct days | | `retentionD1` | [RetentionShare](/docs/api/reference/schemas/#schema-retentionshare) or null | yes | Share of new players who played again in the window; null below 10 players old enough to be counted; 0 to 1 | | `retentionD7` | [RetentionShare](/docs/api/reference/schemas/#schema-retentionshare) or null | yes | Share of new players who played again in the window; null below 10 players old enough to be counted; 0 to 1 | | `retentionD30` | [RetentionShare](/docs/api/reference/schemas/#schema-retentionshare) or null | yes | Share of new players who played again in the window; null below 10 players old enough to be counted; 0 to 1 |

StatsDay

| Property | Type | Required | Description | | --- | --- | --- | --- | | `day` | string (date) | yes | | | `players` | integer | yes | | | `accounts` | integer | yes | | | `activePlayers` | integer | yes | | | `wau` | integer | yes | Distinct players with a session in the 7 days ending that day | | `mau` | integer | yes | Distinct players with a session in the 30 days ending that day | | `returningPlayers` | integer | yes | Active players whose first session is before that day | | `sessions` | integer | yes | | | `scores` | integer | yes | | | `saves` | integer | yes | | | `linksSent` | integer | yes | | | `linksConsumed` | integer | yes | | | `events` | integer | yes | | | `medianSessionSec` | number or null | yes | |

StatsGameRow

| Property | Type | Required | Description | | --- | --- | --- | --- | | `gameId` | string (uuid) | yes | | | `slug` | string | yes | | | `name` | string | yes | The game's internal name | | `sessions` | integer | yes | | | `players` | integer | yes | Distinct players | | `scores` | integer | yes | | | `avgSessionSec` | number or null | no | |

StatsKeyCount

| Property | Type | Required | Description | | --- | --- | --- | --- | | `key` | string | yes | | | `count` | integer | yes | | | `players` | integer | no | Players, when the dimension has them. For event names: distinct players per day, summed over the range |

StatsOverview

| Property | Type | Required | Description | | --- | --- | --- | --- | | `from` | string (date) | yes | | | `to` | string (date) | yes | | | `days` | [StatsDay](/docs/api/reference/schemas/#schema-statsday)[] | yes | One per day of the range | | `totals` | [StatsTotals](/docs/api/reference/schemas/#schema-statstotals) | yes | Counts over a range of days. Players and accounts count rows created in the range. | | `previous` | [StatsTotals](/docs/api/reference/schemas/#schema-statstotals) | yes | Counts over a range of days. Players and accounts count rows created in the range. | | `cohorts` | [StatsCohort](/docs/api/reference/schemas/#schema-statscohort)[] | yes | | | `funnel` | [StatsFunnel](/docs/api/reference/schemas/#schema-statsfunnel) | yes | Distinct players of the range at each step | | `devices` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per device: phone, tablet, desktop, unknown | | `games` | [StatsGameRow](/docs/api/reference/schemas/#schema-statsgamerow)[] | yes | Every game of the site catalog, most sessions first | | `hosts` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per host | | `locales` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Players per locale, all time | | `events` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | event_daily counts per event name, top 20 | | `catalog` | object | yes | | | `catalog.games` | integer | yes | Games in the site catalog | | `catalog.visible` | integer | yes | | | `catalog.live` | integer | yes | Visible games with a live version | | `catalog.categories` | integer | yes | | | `catalog.tags` | integer | yes | | | `generatedAt` | string (date-time) | yes | |

GameStatsDay

| Property | Type | Required | Description | | --- | --- | --- | --- | | `day` | string (date) | yes | | | `sessions` | integer | yes | | | `players` | integer | yes | Distinct players with a session | | `wau` | integer | yes | Distinct players with a session in the 7 days ending that day | | `mau` | integer | yes | Distinct players with a session in the 30 days ending that day | | `newPlayers` | integer | yes | Players whose first session on this game is that day | | `returningPlayers` | integer | yes | Players with a session that day and a first session before it | | `scores` | integer | yes | | | `saves` | integer | yes | Saves written or updated | | `events` | integer | yes | | | `avgSessionSec` | number or null | no | | | `medianSessionSec` | number or null | yes | |

GameStatsTotals

| Property | Type | Required | Description | | --- | --- | --- | --- | | `sessions` | integer | yes | | | `players` | integer | yes | | | `newPlayers` | integer | yes | | | `returningPlayers` | integer | yes | Players of the range with a session on at least two distinct days | | `scores` | integer | yes | | | `saves` | integer | yes | | | `events` | integer | yes | | | `avgSessionSec` | number or null | no | | | `medianSessionSec` | number or null | yes | | | `retentionD1` | number or null | no | Share (0 to 1) of the range's new players who played again the next day, counting only players whose first day is at least two days before the current UTC day; null below 10 such players | | `retentionD7` | number or null | no | Share of the range's new players who played again between day 2 and day 7, counting only players whose first day is at least eight days before the current UTC day; null below 10 such players | | `retentionD30` | number or null | yes | Share of the range's new players who played again between day 8 and day 30, counting only players whose first day is at least 31 days before the current UTC day; null below 10 such players |

GameStatsBoard

| Property | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | | | `sort` | one of `"desc"`, `"asc"` | yes | | | `scores` | integer | yes | | | `players` | integer | yes | | | `best` | integer or null | no | Best score of the range by the board's sort | | `median` | number or null | no | |

GameStats

| Property | Type | Required | Description | | --- | --- | --- | --- | | `gameId` | string (uuid) | yes | | | `slug` | string | yes | | | `from` | string (date) | yes | | | `to` | string (date) | yes | | | `days` | [GameStatsDay](/docs/api/reference/schemas/#schema-gamestatsday)[] | yes | | | `totals` | [GameStatsTotals](/docs/api/reference/schemas/#schema-gamestatstotals) | yes | | | `previous` | [GameStatsTotals](/docs/api/reference/schemas/#schema-gamestatstotals) | yes | | | `cohorts` | [StatsCohort](/docs/api/reference/schemas/#schema-statscohort)[] | yes | | | `funnel` | [StatsFunnel](/docs/api/reference/schemas/#schema-statsfunnel) | yes | Distinct players of the range at each step | | `hosts` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per host | | `locales` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per player locale | | `devices` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per device: phone, tablet, desktop, unknown | | `boards` | [GameStatsBoard](/docs/api/reference/schemas/#schema-gamestatsboard)[] | yes | | | `events` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Per event name, top 20 | | `versions` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per bundle version | | `generatedAt` | string (date-time) | yes | |

WebStatsRow

| Property | Type | Required | Description | | --- | --- | --- | --- | | `key` | string | yes | | | `visitors` | integer | yes | | | `pageviews` | integer | yes | |

WebStats

When `configured` is false every other field is absent. | Property | Type | Required | Description | | --- | --- | --- | --- | | `configured` | boolean | yes | False when the site has no CausalIT read key | | `domain` | string | no | | | `period` | string | no | | | `start` | string (date-time) | no | | | `end` | string (date-time) | no | | | `summary` | object | no | | | `summary.visitors` | integer | yes | | | `summary.pageviews` | integer | yes | | | `summary.sessions` | integer | yes | | | `summary.bounceRate` | number or null | no | Percentage, 0 to 100 | | `summary.avgDurationSec` | number or null | no | | | `series` | object[] | no | | | `series[].bucket` | string | yes | | | `series[].visitors` | integer | yes | | | `series[].pageviews` | integer | yes | | | `pages` | [WebStatsRow](/docs/api/reference/schemas/#schema-webstatsrow)[] | no | | | `sources` | [WebStatsRow](/docs/api/reference/schemas/#schema-webstatsrow)[] | no | | | `countries` | [WebStatsRow](/docs/api/reference/schemas/#schema-webstatsrow)[] | no | | | `devices` | [WebStatsRow](/docs/api/reference/schemas/#schema-webstatsrow)[] | no | | | `goals` | object[] | no | | | `goals[].name` | string | yes | | | `goals[].kind` | one of `"path"`, `"event"` | yes | | | `goals[].pattern` | string | no | | | `goals[].conversions` | integer | yes | | | `goals[].visitors` | integer | yes | | | `goals[].rate` | number or null | no | Conversion rate, percentage, 0 to 100 |

AuditEntry

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `actor` | string | yes | The account's email (ADR 0015) | | `actorEmail` | string or null | no | | | `impersonator` | string or null | no | The staff member's email when the row was written under impersonation; null otherwise | | `site` | string or null | no | Site slug | | `action` | string | yes | | | `targetType` | string | yes | | | `targetId` | string | yes | | | `before` | object or null | no | | | `after` | object or null | no | | | `createdAt` | string (date-time) | yes | |

GameToken

What `POST /v1/sdk/game-tokens` and `POST /v1/server/game-tokens` answer (ADR 0013) | Property | Type | Required | Description | | --- | --- | --- | --- | | `token` | string | yes | `fg1..`, valid 24 hours | | `expiresAt` | string (date-time) | yes | |

AccountName

Display name, shown to other members and in the audit log Type: string; 1 to 80 characters.

NewPassword

The policy of `PUT /v1/account/password`: 8 to 128 characters after NFC normalization, not equal to the email Type: string; 8 to 128 characters; write only.

TotpCode

Six digits, "123456" or "123 456" Type: string; pattern `^[0-9 ]{6,7}$`.

MembershipRole

A member has every right of the owner except membership changes Type: one of `"owner"`, `"member"`.

Scope

One game or site of the account, for the console's scope switcher | Property | Type | Required | Description | | --- | --- | --- | --- | | `kind` | one of `"game"`, `"site"` | yes | | | `id` | string (uuid) | yes | | | `slug` | string | yes | | | `name` | string | yes | | | `role` | [MembershipRole](/docs/api/reference/schemas/#schema-membershiprole) | yes | A member has every right of the owner except membership changes | | `approved` | boolean | yes | False while the game or site waits for staff |

ConsoleMe

The signed-in account, as `GET /v1/admin/me` and `GET /v1/console/me` answer it (ADR 0015) | Property | Type | Required | Description | | --- | --- | --- | --- | | `accountId` | string (uuid) | yes | | | `name` | string | yes | | | `email` | string (email) | yes | | | `staff` | boolean | yes | | | `totpEnabled` | boolean | yes | | | `canCreateGames` | boolean | yes | | | `canCreatePortals` | boolean | yes | | | `roles` | object | yes | The identity shape of ADR 0014, computed from `staff` and the memberships | | `roles.admin` | boolean | yes | True for staff with TOTP enabled | | `roles.editorOf` | string[] | yes | Slugs of the sites the account is a member of | | `roles.developerOf` | string[] | yes | Slugs of the games the account is a member of | | `scopes` | [Scope](/docs/api/reference/schemas/#schema-scope)[] | yes | | | `impersonatedBy` | object or null | yes | The staff member behind this session; null outside impersonation |

SignInResult

| Property | Type | Required | Description | | --- | --- | --- | --- | | `totpRequired` | boolean | yes | True when a code is due on `POST /v1/console/sign-in/totp`; `me` is then absent | | `me` | [ConsoleMe](/docs/api/reference/schemas/#schema-consoleme) | no | The signed-in account, as `GET /v1/admin/me` and `GET /v1/console/me` answer it (ADR 0015) |

TotpEnrolment

| Property | Type | Required | Description | | --- | --- | --- | --- | | `secret` | string | yes | Base32, for manual entry; shown once, not active until confirmed | | `otpauthUrl` | string (uri) | yes | `otpauth://totp/...` for the QR code |

ConsoleSessionInfo

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | | | `current` | boolean | yes | The session that made this request | | `impersonation` | boolean | yes | Opened by a staff member on this account | | `userAgent` | string or null | yes | First 300 characters of the browser's User-Agent | | `createdAt` | string (date-time) | yes | | | `lastSeenAt` | string (date-time) | yes | Written at most once a minute | | `expiresAt` | string (date-time) | yes | |

InvitationPreview

What the accept page shows before the invitee sets a name and a password | Property | Type | Required | Description | | --- | --- | --- | --- | | `emailMasked` | string | yes | a***@example.com | | `inviterName` | string or null | yes | Null when the inviter's account is gone | | `staff` | boolean | yes | | | `canCreateGames` | boolean | yes | | | `canCreatePortals` | boolean | yes | | | `grants` | object[] | yes | | | `grants[].kind` | one of `"game"`, `"site"` | yes | | | `grants[].slug` | string | yes | | | `grants[].name` | string | yes | | | `grants[].role` | [MembershipRole](/docs/api/reference/schemas/#schema-membershiprole) | yes | A member has every right of the owner except membership changes | | `expiresAt` | string (date-time) | yes | |

InvitationRequestInput

| Property | Type | Required | Description | | --- | --- | --- | --- | | `email` | string (email) | yes | at most 254 characters | | `wants` | array of one of `"games"`, `"portals"` | yes | What they build; 1 to 2 items; unique items | | `message` | string | yes | Counted after trim; 1 to 500 characters |

InvitationRequestStatus

Type: one of `"new"`, `"invited"`, `"dismissed"`.

InvitationRequest

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | | | `emailMasked` | string | yes | | | `email` | string (email) | yes | In clear | | `wants` | array of one of `"games"`, `"portals"` | yes | | | `message` | string | yes | | | `status` | [InvitationRequestStatus](/docs/api/reference/schemas/#schema-invitationrequeststatus) | yes | | | `handledBy` | string or null | yes | Name of the staff member who invited or dismissed | | `handledAt` | string (date-time) or null | yes | | | `createdAt` | string (date-time) | yes | |

InvitationRequestPage

| Property | Type | Required | Description | | --- | --- | --- | --- | | `items` | [InvitationRequest](/docs/api/reference/schemas/#schema-invitationrequest)[] | yes | | | `nextCursor` | string or null | yes | Null on the last page | | `total` | integer | yes | Rows matching the filter |

Membership

One right of an account, as the staff account page lists them | Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | | | `kind` | one of `"game"`, `"site"` | yes | | | `targetId` | string (uuid) | yes | | | `slug` | string | yes | | | `name` | string | yes | | | `role` | [MembershipRole](/docs/api/reference/schemas/#schema-membershiprole) | yes | A member has every right of the owner except membership changes | | `approved` | boolean | yes | False while the game or site waits for staff |

AccountAdmin

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | | | `emailMasked` | string | yes | | | `email` | string (email) | yes | In clear; staff only | | `name` | string | yes | | | `staff` | boolean | yes | | | `totpEnabled` | boolean | yes | | | `canCreateGames` | boolean | yes | | | `canCreatePortals` | boolean | yes | | | `disabledAt` | string (date-time) or null | yes | | | `lastSignInAt` | string (date-time) or null | yes | | | `createdAt` | string (date-time) | yes | | | `memberships` | [Membership](/docs/api/reference/schemas/#schema-membership)[] | yes | | | `sessions` | integer | yes | Open sessions |

AccountPage

| Property | Type | Required | Description | | --- | --- | --- | --- | | `items` | [AccountAdmin](/docs/api/reference/schemas/#schema-accountadmin)[] | yes | | | `nextCursor` | string or null | yes | Null on the last page | | `total` | integer | yes | Rows matching the search |

AccountPatch

At least one field; an empty body is a 400. | Property | Type | Required | Description | | --- | --- | --- | --- | | `name` | [AccountName](/docs/api/reference/schemas/#schema-accountname) | no | Display name, shown to other members and in the audit log; 1 to 80 characters | | `email` | string (email) | no | Changed at once | | `staff` | boolean | no | A new staff member gets admin rights once TOTP is enabled | | `canCreateGames` | boolean | no | | | `canCreatePortals` | boolean | no | | | `disabled` | boolean | no | True revokes every session and refuses sign-in; false enables again |

ImpersonationStarted

| Property | Type | Required | Description | | --- | --- | --- | --- | | `consoleUrl` | string (uri) | yes | The console (`CONSOLE_URL`), to open in a new tab | | `expiresAt` | string (date-time) | yes | One hour after the start |

InvitationGrant

| Property | Type | Required | Description | | --- | --- | --- | --- | | `kind` | one of `"game"`, `"site"` | yes | | | `targetId` | string (uuid) | yes | | | `slug` | string | yes | | | `name` | string | yes | | | `role` | [MembershipRole](/docs/api/reference/schemas/#schema-membershiprole) | yes | A member has every right of the owner except membership changes |

Invitation

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | | | `emailMasked` | string | yes | | | `staff` | boolean | yes | | | `canCreateGames` | boolean | yes | | | `canCreatePortals` | boolean | yes | | | `grants` | [InvitationGrant](/docs/api/reference/schemas/#schema-invitationgrant)[] | yes | | | `createdBy` | string or null | yes | Name of the inviter; null when the account is gone | | `expiresAt` | string (date-time) | yes | | | `acceptedAt` | string (date-time) or null | yes | | | `revokedAt` | string (date-time) or null | yes | | | `createdAt` | string (date-time) | yes | |

InvitationInput

| Property | Type | Required | Description | | --- | --- | --- | --- | | `email` | string (email) | yes | at most 254 characters | | `staff` | boolean | no | default `false` | | `canCreateGames` | boolean | no | default `false` | | `canCreatePortals` | boolean | no | default `false` | | `grants` | object[] | no | Memberships created on accept. Each item names exactly one of `game` and `site`.; at most 50 items | | `grants[].game` | string (uuid) | no | | | `grants[].site` | string (uuid) | no | | | `grants[].role` | [MembershipRole](/docs/api/reference/schemas/#schema-membershiprole) | yes | A member has every right of the owner except membership changes |

ApprovalOwner

| Property | Type | Required | Description | | --- | --- | --- | --- | | `accountId` | string (uuid) | yes | | | `name` | string | yes | | | `email` | string (email) | yes | |

ApprovalGame

| Property | Type | Required | Description | | --- | --- | --- | --- | | `gameId` | string (uuid) | yes | | | `slug` | string | yes | | | `name` | string | yes | | | `owners` | [ApprovalOwner](/docs/api/reference/schemas/#schema-approvalowner)[] | yes | | | `createdAt` | string (date-time) | yes | |

ApprovalSite

| Property | Type | Required | Description | | --- | --- | --- | --- | | `siteId` | string (uuid) | yes | | | `slug` | string | yes | | | `name` | string | yes | | | `owners` | [ApprovalOwner](/docs/api/reference/schemas/#schema-approvalowner)[] | yes | | | `pendingDomains` | string[] | yes | The domains the owner proposed. Approving the site does not approve them; they move to the `domains` queue, decided one by one | | `createdAt` | string (date-time) | yes | |

ApprovalDomains

The proposed domains of one approved site | Property | Type | Required | Description | | --- | --- | --- | --- | | `siteId` | string (uuid) | yes | | | `slug` | string | yes | | | `name` | string | yes | | | `domains` | string[] | yes | |

ApprovalVersion

| Property | Type | Required | Description | | --- | --- | --- | --- | | `versionId` | string (uuid) | yes | | | `gameId` | string (uuid) | yes | | | `slug` | string | yes | | | `version` | string | yes | | | `reviewRequestedAt` | string (date-time) | yes | | | `createdBy` | string | yes | |

Approvals

Every list is oldest first | Property | Type | Required | Description | | --- | --- | --- | --- | | `games` | [ApprovalGame](/docs/api/reference/schemas/#schema-approvalgame)[] | yes | | | `sites` | [ApprovalSite](/docs/api/reference/schemas/#schema-approvalsite)[] | yes | | | `domains` | [ApprovalDomains](/docs/api/reference/schemas/#schema-approvaldomains)[] | yes | | | `versions` | [ApprovalVersion](/docs/api/reference/schemas/#schema-approvalversion)[] | yes | |

RejectInput

| Property | Type | Required | Description | | --- | --- | --- | --- | | `reason` | string | yes | Sent to the owners by email; 1 to 1000 characters |

Member

| Property | Type | Required | Description | | --- | --- | --- | --- | | `accountId` | string (uuid) or null | yes | Null while the invitation is not accepted | | `invitationId` | string (uuid) or null | yes | Set while the invitation is not accepted; `DELETE .../members/invitations/{invitationId}` withdraws it | | `emailMasked` | string | yes | | | `name` | string or null | yes | Null while the invitation is not accepted | | `role` | [MembershipRole](/docs/api/reference/schemas/#schema-membershiprole) | yes | A member has every right of the owner except membership changes | | `invited` | boolean | yes | True for an invitation not accepted yet | | `createdAt` | string (date-time) | yes | |

MemberInput

| Property | Type | Required | Description | | --- | --- | --- | --- | | `email` | string (email) | yes | at most 254 characters | | `role` | [MembershipRole](/docs/api/reference/schemas/#schema-membershiprole) | yes | A member has every right of the owner except membership changes |

SiteKeyScope

Each `/v1/server/*` route says which scope it needs Type: one of `"catalog"`, `"players"`, `"tokens"`, `"leaderboards"`, `"stats"`.

SiteKey

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | | | `name` | string | yes | | | `prefix` | string | yes | The first 12 characters of the key, to tell keys apart | | `scopes` | [SiteKeyScope](/docs/api/reference/schemas/#schema-sitekeyscope)[] | yes | | | `createdBy` | string or null | yes | Name of the account that created it | | `createdAt` | string (date-time) | yes | | | `lastUsedAt` | string (date-time) or null | yes | Written at most once a minute | | `revokedAt` | string (date-time) or null | yes | |

SiteKeyCreated

Type: [SiteKey](/docs/api/reference/schemas/#schema-sitekey) + object. | Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | | | `name` | string | yes | | | `prefix` | string | yes | The first 12 characters of the key, to tell keys apart | | `scopes` | [SiteKeyScope](/docs/api/reference/schemas/#schema-sitekeyscope)[] | yes | | | `createdBy` | string or null | yes | Name of the account that created it | | `createdAt` | string (date-time) | yes | | | `lastUsedAt` | string (date-time) or null | yes | Written at most once a minute | | `revokedAt` | string (date-time) or null | yes | | | `key` | string | yes | `fsk__<32 base62>`. Shown once; the API stores a hash |

SiteKeyInput

| Property | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | 1 to 60 characters | | `scopes` | [SiteKeyScope](/docs/api/reference/schemas/#schema-sitekeyscope)[] | yes | at least 1 item; unique items |

RetentionShare

Share of new players who played again in the window; null below 10 players old enough to be counted Type: number or null; 0 to 1.

StatsCohort

Players by the ISO week of their first play | Property | Type | Required | Description | | --- | --- | --- | --- | | `week` | string | yes | ISO week; pattern `^\d{4}-W\d{2}$` | | `players` | integer | yes | Players whose first session is in that week | | `d1` | [RetentionShare](/docs/api/reference/schemas/#schema-retentionshare) or null | yes | Share of new players who played again in the window; null below 10 players old enough to be counted; 0 to 1 | | `d7` | [RetentionShare](/docs/api/reference/schemas/#schema-retentionshare) or null | yes | Share of new players who played again in the window; null below 10 players old enough to be counted; 0 to 1 | | `d30` | [RetentionShare](/docs/api/reference/schemas/#schema-retentionshare) or null | yes | Share of new players who played again in the window; null below 10 players old enough to be counted; 0 to 1 |

StatsFunnel

Distinct players of the range at each step | Property | Type | Required | Description | | --- | --- | --- | --- | | `loaded` | integer | yes | Sent `frame_loaded` from the hub | | `started` | integer | yes | Opened a play session | | `saved` | integer | yes | Wrote a save | | `scored` | integer | yes | Sent a score | | `linked` | integer | yes | Linked an email |

ConsoleStatsDay

| Property | Type | Required | Description | | --- | --- | --- | --- | | `day` | string (date) | yes | | | `dau` | integer | yes | Distinct players with a session that day | | `wau` | integer | yes | Distinct players with a session in the 7 days ending that day | | `mau` | integer | yes | Distinct players with a session in the 30 days ending that day | | `newPlayers` | integer | yes | Players whose first session on this game is that day | | `returningPlayers` | integer | yes | Players with a session that day and a first session before it | | `sessions` | integer | yes | | | `scores` | integer | yes | | | `saves` | integer | yes | Saves written or updated | | `events` | integer | yes | | | `meanSessionSec` | number or null | yes | | | `medianSessionSec` | number or null | yes | |

ConsoleStatsTotals

| Property | Type | Required | Description | | --- | --- | --- | --- | | `players` | integer | yes | Distinct players with a session | | `newPlayers` | integer | yes | | | `returningPlayers` | integer | yes | Players of the range with a session on at least two distinct days | | `sessions` | integer | yes | | | `scores` | integer | yes | | | `saves` | integer | yes | | | `events` | integer | yes | | | `meanSessionSec` | number or null | yes | | | `medianSessionSec` | number or null | yes | | | `retentionD1` | [RetentionShare](/docs/api/reference/schemas/#schema-retentionshare) or null | yes | Share of new players who played again in the window; null below 10 players old enough to be counted; 0 to 1 | | `retentionD7` | [RetentionShare](/docs/api/reference/schemas/#schema-retentionshare) or null | yes | Share of new players who played again in the window; null below 10 players old enough to be counted; 0 to 1 | | `retentionD30` | [RetentionShare](/docs/api/reference/schemas/#schema-retentionshare) or null | yes | Share of new players who played again in the window; null below 10 players old enough to be counted; 0 to 1 |

ConsoleGameStats

A game's numbers across every site that lists it (ADR 0015) | Property | Type | Required | Description | | --- | --- | --- | --- | | `gameId` | string (uuid) | yes | | | `slug` | string | yes | | | `from` | string (date) | yes | | | `to` | string (date) | yes | | | `days` | [ConsoleStatsDay](/docs/api/reference/schemas/#schema-consolestatsday)[] | yes | | | `totals` | [ConsoleStatsTotals](/docs/api/reference/schemas/#schema-consolestatstotals) | yes | | | `previous` | [ConsoleStatsTotals](/docs/api/reference/schemas/#schema-consolestatstotals) | yes | | | `cohorts` | [StatsCohort](/docs/api/reference/schemas/#schema-statscohort)[] | yes | | | `funnel` | [StatsFunnel](/docs/api/reference/schemas/#schema-statsfunnel) | yes | Distinct players of the range at each step | | `sites` | object[] | yes | Every site that lists the game, most sessions first | | `sites[].siteId` | string (uuid) | yes | | | `sites[].slug` | string | yes | | | `sites[].name` | string | yes | | | `sites[].sessions` | integer | yes | | | `sites[].players` | integer | yes | Distinct players | | `hosts` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per host | | `versions` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per bundle version | | `locales` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per player locale | | `devices` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per device: phone, tablet, desktop, unknown | | `boards` | [GameStatsBoard](/docs/api/reference/schemas/#schema-gamestatsboard)[] | yes | | | `events` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Per event name with distinct players, top 20 | | `generatedAt` | string (date-time) | yes | |

ServerPlayerInput

| Property | Type | Required | Description | | --- | --- | --- | --- | | `externalId` | string | yes | Your own user id; 1 to 128 characters | | `nickname` | string | no | 3 to 20 characters; pattern `^[\p{L}\p{N} _.-]+$` | | `locale` | string | no | Defaults to the site's first locale |

ServerPlayer

| Property | Type | Required | Description | | --- | --- | --- | --- | | `id` | string (uuid) | yes | | | `externalId` | string | yes | | | `nickname` | string | yes | | | `locale` | string | yes | | | `hasAccount` | boolean | yes | True once the player linked an email on your site | | `createdAt` | string (date-time) | yes | | | `lastSeenAt` | string (date-time) | yes | | Source: https://developers.filudi.com/docs/api/reference/schemas/ --- # SDK routes The sdk routes of games-api, with parameters, bodies, responses and request samples. Generated from `openapi.yaml` of games-api 0.7.0: 13 routes. - [`POST /v1/sdk/players`](#post-v1-sdk-players): Create an anonymous player and its token - [`POST /v1/sdk/game-tokens`](#post-v1-sdk-game-tokens): Exchange a player token for a token limited to one game (ADR 0013) - [`GET /v1/sdk/players/me`](#get-v1-sdk-players-me): Current player - [`PATCH /v1/sdk/players/me`](#patch-v1-sdk-players-me): Change nickname or locale - [`POST /v1/sdk/sessions`](#post-v1-sdk-sessions): Open a play session and get the score-signing key - [`GET /v1/sdk/saves`](#get-v1-sdk-saves): List the player's saves (metadata only) - [`GET /v1/sdk/saves/{gameId}`](#get-v1-sdk-saves-gameid): Read a save - [`PUT /v1/sdk/saves/{gameId}`](#put-v1-sdk-saves-gameid): Write a save - [`DELETE /v1/sdk/saves/{gameId}`](#delete-v1-sdk-saves-gameid): Delete a save - [`GET /v1/sdk/leaderboards/{gameId}/{board}`](#get-v1-sdk-leaderboards-gameid-board): Top entries - [`GET /v1/sdk/leaderboards/{gameId}/{board}/me`](#get-v1-sdk-leaderboards-gameid-board-me): The player's own entry - [`POST /v1/sdk/leaderboards/{gameId}/{board}/scores`](#post-v1-sdk-leaderboards-gameid-board-scores): Submit a signed score - [`POST /v1/sdk/events`](#post-v1-sdk-events): Batch of events ```sh curl -X POST 'https://api.filudi.com/v1/sdk/players' \ -H 'content-type: application/json' \ -d '{ "site": "filudi", "locale": "en" }' ``` ```js const response = await fetch('https://api.filudi.com/v1/sdk/players', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ site: 'filudi', locale: 'en', }), }); const data = await response.json(); ``` ```py import requests response = requests.post( 'https://api.filudi.com/v1/sdk/players', json={ 'site': 'filudi', 'locale': 'en', }, ) response.raise_for_status() data = response.json() ``` **Create an anonymous player and its token** Credential: none. The route is public. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `site` | string | yes | Site slug | | `locale` | string | no | | ### Responses | Status | Body | Description | | --- | --- | --- | | 201 | [PlayerWithToken](/docs/api/reference/schemas/#schema-playerwithtoken) | Player and token. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | ```sh curl -X POST 'https://api.filudi.com/v1/sdk/game-tokens' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \ -H 'content-type: application/json' \ -d '{ "game": "gloop" }' ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players const response = await fetch('https://api.filudi.com/v1/sdk/game-tokens', { method: 'POST', headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' }, body: JSON.stringify({ game: 'gloop', }), }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players response = requests.post( 'https://api.filudi.com/v1/sdk/game-tokens', headers={'authorization': f'Bearer {player_token}'}, json={ 'game': 'gloop', }, ) response.raise_for_status() data = response.json() ``` **Exchange a player token for a token limited to one game (ADR 0013)** Called by the hub, never by a game. Refuses a game token. Limited to 240 per player per hour. A game token (`fg1`) is valid 24 hours on these routes, for its own game: `GET /v1/sdk/players/me` (`emailMasked` null, `hasPassword` false), `/v1/sdk/saves/{gameId}`, `POST /v1/sdk/sessions`, `/v1/sdk/leaderboards/{gameId}/*`, `POST /v1/sdk/events` (403 when an item names another game). Those routes answer 403 for another game. Every other route that needs a player answers 401. Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) answers 401 here. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `game` | string | yes | Game slug | ### Responses | Status | Body | Description | | --- | --- | --- | | 201 | [GameToken](/docs/api/reference/schemas/#schema-gametoken) | Game token. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | ```sh curl 'https://api.filudi.com/v1/sdk/players/me' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game const response = await fetch('https://api.filudi.com/v1/sdk/players/me', { headers: { authorization: `Bearer ${playerToken}` }, }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game response = requests.get( 'https://api.filudi.com/v1/sdk/players/me', headers={'authorization': f'Bearer {player_token}'}, ) response.raise_for_status() data = response.json() ``` **Current player** Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) also opens this route, for its own game only. ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Player](/docs/api/reference/schemas/#schema-player) | Player. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | ```sh curl -X PATCH 'https://api.filudi.com/v1/sdk/players/me' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \ -H 'content-type: application/json' \ -d '{ "nickname": "string", "locale": "en" }' ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players const response = await fetch('https://api.filudi.com/v1/sdk/players/me', { method: 'PATCH', headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' }, body: JSON.stringify({ nickname: 'string', locale: 'en', }), }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players response = requests.patch( 'https://api.filudi.com/v1/sdk/players/me', headers={'authorization': f'Bearer {player_token}'}, json={ 'nickname': 'string', 'locale': 'en', }, ) response.raise_for_status() data = response.json() ``` **Change nickname or locale** Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) answers 401 here. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `nickname` | string | no | 3 to 20 characters; pattern `^[\p{L}\p{N} _.-]+$` | | `locale` | string | no | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Player](/docs/api/reference/schemas/#schema-player) | Updated player. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | ```sh curl -X POST 'https://api.filudi.com/v1/sdk/sessions' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \ -H 'content-type: application/json' \ -d '{ "gameId": "gloop", "gameVersion": "1.0.0", "host": "filudi" }' ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game const response = await fetch('https://api.filudi.com/v1/sdk/sessions', { method: 'POST', headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' }, body: JSON.stringify({ gameId: 'gloop', gameVersion: '1.0.0', host: 'filudi', }), }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game response = requests.post( 'https://api.filudi.com/v1/sdk/sessions', headers={'authorization': f'Bearer {player_token}'}, json={ 'gameId': 'gloop', 'gameVersion': '1.0.0', 'host': 'filudi', }, ) response.raise_for_status() data = response.json() ``` **Open a play session and get the score-signing key** Called by `gameplay.start()`. The key is HMAC-SHA256(version secret, session id), base64url. Valid 6 hours. The API rejects scores submitted outside a session. Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) also opens this route, for its own game only. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `gameId` | string | yes | Game slug | | `gameVersion` | string | no | Bundle version; defaults to the live one | | `host` | string | no | default `"filudi"` | ### Responses | Status | Body | Description | | --- | --- | --- | | 201 | object | Session. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | | 503 | none | Redis is down, no session can open. | **Body of 201** | Property | Type | Required | Description | | --- | --- | --- | --- | | `sessionId` | string (uuid) | yes | | | `sessionKey` | string | yes | | | `expiresAt` | string (date-time) | yes | | ```sh curl 'https://api.filudi.com/v1/sdk/saves' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players const response = await fetch('https://api.filudi.com/v1/sdk/saves', { headers: { authorization: `Bearer ${playerToken}` }, }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players response = requests.get( 'https://api.filudi.com/v1/sdk/saves', headers={'authorization': f'Bearer {player_token}'}, ) response.raise_for_status() data = response.json() ``` **List the player's saves (metadata only)** Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) answers 401 here. ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [SaveMeta](/docs/api/reference/schemas/#schema-savemeta)[] | Saves. | ```sh curl 'https://api.filudi.com/v1/sdk/saves/gloop' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game const response = await fetch('https://api.filudi.com/v1/sdk/saves/gloop', { headers: { authorization: `Bearer ${playerToken}` }, }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game response = requests.get( 'https://api.filudi.com/v1/sdk/saves/gloop', headers={'authorization': f'Bearer {player_token}'}, ) response.raise_for_status() data = response.json() ``` **Read a save** Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) also opens this route, for its own game only. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string | yes | Game slug | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Save](/docs/api/reference/schemas/#schema-save) | Save. Headers: `ETag`. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl -X PUT 'https://api.filudi.com/v1/sdk/saves/gloop' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \ -H 'if-match: string' \ -H 'content-type: application/json' \ -d '{ "version": 1, "data": {} }' ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game const response = await fetch('https://api.filudi.com/v1/sdk/saves/gloop', { method: 'PUT', headers: { authorization: `Bearer ${playerToken}`, 'if-match': 'string', 'content-type': 'application/json' }, body: JSON.stringify({ version: 1, data: {}, }), }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game response = requests.put( 'https://api.filudi.com/v1/sdk/saves/gloop', headers={'authorization': f'Bearer {player_token}', 'if-match': 'string'}, json={ 'version': 1, 'data': {}, }, ) response.raise_for_status() data = response.json() ``` **Write a save** `If-Match` carries the version the client last saw (0 for none). The server accepts when the new version is greater than the stored one and `If-Match` equals the stored version. Otherwise 409 with the server copy. Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) also opens this route, for its own game only. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string | yes | Game slug | | `If-Match` | header | string | yes | Stored version as a quoted integer, e.g. "12" | ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `version` | integer | yes | at least 1 | | `data` | any | yes | Any JSON value, 64 kB max serialized | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [SaveMeta](/docs/api/reference/schemas/#schema-savemeta) | Saved. Headers: `ETag`. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 409 | [SaveServerCopy](/docs/api/reference/schemas/#schema-saveservercopy) | Server has another version, or none yet (version 0). | | 413 | [Error](/docs/api/reference/schemas/#schema-error) | Body over the limit. | ```sh curl -X DELETE 'https://api.filudi.com/v1/sdk/saves/gloop' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game const response = await fetch('https://api.filudi.com/v1/sdk/saves/gloop', { method: 'DELETE', headers: { authorization: `Bearer ${playerToken}` }, }); // No body: check response.ok. ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game response = requests.delete( 'https://api.filudi.com/v1/sdk/saves/gloop', headers={'authorization': f'Bearer {player_token}'}, ) response.raise_for_status() # No body: the status says it all. ``` **Delete a save** Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) also opens this route, for its own game only. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string | yes | Game slug | ### Responses | Status | Body | Description | | --- | --- | --- | | 204 | none | Deleted or absent. | ```sh curl 'https://api.filudi.com/v1/sdk/leaderboards/gloop/height' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game const response = await fetch('https://api.filudi.com/v1/sdk/leaderboards/gloop/height', { headers: { authorization: `Bearer ${playerToken}` }, }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game response = requests.get( 'https://api.filudi.com/v1/sdk/leaderboards/gloop/height', headers={'authorization': f'Bearer {player_token}'}, ) response.raise_for_status() data = response.json() ``` **Top entries** Credential, optional: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) also opens this route, for its own game only. Without a token the answer is the anonymous one. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string | yes | Game slug | | `board` | path | string | yes | pattern `^[a-z][a-z0-9_]{0,31}$` | | `period` | query | one of `"day"`, `"week"`, `"all"` | no | default `"all"` | | `limit` | query | integer | no | 1 to 50; default `10` | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [LeaderboardTop](/docs/api/reference/schemas/#schema-leaderboardtop) | Top entries; `isMe` is set when a token is sent. Headers: `Cache-Control`. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | ```sh curl 'https://api.filudi.com/v1/sdk/leaderboards/gloop/height/me' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game const response = await fetch('https://api.filudi.com/v1/sdk/leaderboards/gloop/height/me', { headers: { authorization: `Bearer ${playerToken}` }, }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game response = requests.get( 'https://api.filudi.com/v1/sdk/leaderboards/gloop/height/me', headers={'authorization': f'Bearer {player_token}'}, ) response.raise_for_status() data = response.json() ``` **The player's own entry** Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) also opens this route, for its own game only. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string | yes | Game slug | | `board` | path | string | yes | pattern `^[a-z][a-z0-9_]{0,31}$` | | `period` | query | one of `"day"`, `"week"`, `"all"` | no | default `"all"` | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | object | Entry or null. | **Body of 200** | Property | Type | Required | Description | | --- | --- | --- | --- | | `period` | [Period](/docs/api/reference/schemas/#schema-period) | yes | default `"all"` | | `entry` | [LeaderboardEntry](/docs/api/reference/schemas/#schema-leaderboardentry) or null | yes | | ```sh curl -X POST 'https://api.filudi.com/v1/sdk/leaderboards/gloop/height/scores' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \ -H 'content-type: application/json' \ -d '{ "score": 0, "meta": { "en": "string" }, "sessionId": "00000000-0000-0000-0000-000000000000", "nonce": "string", "ts": 0, "sig": "string" }' ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game const response = await fetch('https://api.filudi.com/v1/sdk/leaderboards/gloop/height/scores', { method: 'POST', headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' }, body: JSON.stringify({ score: 0, meta: { en: 'string', }, sessionId: '00000000-0000-0000-0000-000000000000', nonce: 'string', ts: 0, sig: 'string', }), }); const data = await response.json(); ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game response = requests.post( 'https://api.filudi.com/v1/sdk/leaderboards/gloop/height/scores', headers={'authorization': f'Bearer {player_token}'}, json={ 'score': 0, 'meta': { 'en': 'string', }, 'sessionId': '00000000-0000-0000-0000-000000000000', 'nonce': 'string', 'ts': 0, 'sig': 'string', }, ) response.raise_for_status() data = response.json() ``` **Submit a signed score** `sig` = base64url(HMAC-SHA256(sessionKey, `${playerId}|${gameId}|${board}|${score}|${nonce}|${ts}`)). `ts` is unix milliseconds, within 5 minutes of server time. The API accepts a nonce once per session. Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) also opens this route, for its own game only. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string | yes | Game slug | | `board` | path | string | yes | pattern `^[a-z][a-z0-9_]{0,31}$` | ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `score` | integer | yes | | | `meta` | object | no | 512 bytes max | | `meta.*` | string or number | no | | | `sessionId` | string (uuid) | yes | | | `nonce` | string | yes | 16 to 64 characters | | `ts` | integer | yes | | | `sig` | string | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 201 | object | Accepted. | | 400 | object | Rejected. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | **Body of 201** | Property | Type | Required | Description | | --- | --- | --- | --- | | `rank` | object | yes | 0 when Redis could not rank the score; the next rebuild fixes it | | `rank.day` | integer | yes | | | `rank.week` | integer | yes | | | `rank.all` | integer | yes | | | `best` | boolean | yes | New personal best on this board | **Body of 400** | Property | Type | Required | Description | | --- | --- | --- | --- | | `error` | `"invalid"` | yes | | | `reason` | one of `"bounds"`, `"signature"`, `"session"`, `"board"`, `"nonce"`, `"clock"`, `"meta"` | yes | | | `message` | string | no | | ```sh curl -X POST 'https://api.filudi.com/v1/sdk/events' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \ -H 'content-type: application/json' \ -d '{ "events": [ { "name": "string", "props": { "en": "string" }, "ts": 0, "gameId": "gloop", "gameVersion": "1.0.0", "sessionId": "00000000-0000-0000-0000-000000000000", "host": "string", "locale": "en" } ] }' ``` ```js // playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game const response = await fetch('https://api.filudi.com/v1/sdk/events', { method: 'POST', headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' }, body: JSON.stringify({ events: [{ name: 'string', props: { en: 'string', }, ts: 0, gameId: 'gloop', gameVersion: '1.0.0', sessionId: '00000000-0000-0000-0000-000000000000', host: 'string', locale: 'en', }], }), }); // No body: check response.ok. ``` ```py import requests # player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game response = requests.post( 'https://api.filudi.com/v1/sdk/events', headers={'authorization': f'Bearer {player_token}'}, json={ 'events': [{ 'name': 'string', 'props': { 'en': 'string', }, 'ts': 0, 'gameId': 'gloop', 'gameVersion': '1.0.0', 'sessionId': '00000000-0000-0000-0000-000000000000', 'host': 'string', 'locale': 'en', }], }, ) response.raise_for_status() # No body: the status says it all. ``` **Batch of events** The SDK sends `application/json` normally and the same JSON body as `text/plain` from a keepalive fetch on page hide (`sendBeacon` only on portals, without a token). The API parses both as JSON. 64 kB max. With a game token, an item that names another game refuses the whole batch with 403, and an item that names none takes the token's game. Credential: a player token (`fp1`), sent as `Authorization: Bearer `. A game token (`fg1`) also opens this route, for its own game only. ### Request body `application/json`, required, also accepted as `text/plain`. The body is a [EventBatch](/docs/api/reference/schemas/#schema-eventbatch). | Property | Type | Required | Description | | --- | --- | --- | --- | | `events` | [EventIn](/docs/api/reference/schemas/#schema-eventin)[] | yes | at most 50 items | ### Responses | Status | Body | Description | | --- | --- | --- | | 202 | none | Queued for write; the API drops invalid items and counts them in metrics. | | 400 | none | More than 50 items, or a body that is not a batch. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. | | 413 | [Error](/docs/api/reference/schemas/#schema-error) | Body over the limit. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | Source: https://developers.filudi.com/docs/api/reference/sdk/ --- # Server routes Routes for portal backends, authenticated by a site key (ADR 0015) Routes for portal backends, authenticated by a site key (ADR 0015). Generated from `openapi.yaml` of games-api 0.7.0: 8 routes. - [`GET /v1/server/catalog`](#get-v1-server-catalog): The site's catalog - [`POST /v1/server/players`](#post-v1-server-players): Create or find a player by your own user id - [`GET /v1/server/players/{playerId}`](#get-v1-server-players-playerid): Profile, saves metadata and best scores of one of the site's players - [`POST /v1/server/players/{playerId}/tokens`](#post-v1-server-players-playerid-tokens): A player token for that player's browser - [`POST /v1/server/game-tokens`](#post-v1-server-game-tokens): A game token for a player and a game - [`GET /v1/server/leaderboards/{gameId}/{board}`](#get-v1-server-leaderboards-gameid-board): Top entries with nicknames - [`GET /v1/server/stats/overview`](#get-v1-server-stats-overview): The site's numbers for a range of UTC days - [`GET /v1/server/stats/games/{gameId}`](#get-v1-server-stats-games-gameid): One game's numbers on the site for a range of UTC days ```sh curl 'https://api.filudi.com/v1/server/catalog' \ -H "authorization: Bearer $FILUDI_SITE_KEY" ``` ```js // siteKey: an fsk_ key of the site, created in the console; server side only const response = await fetch('https://api.filudi.com/v1/server/catalog', { headers: { authorization: `Bearer ${siteKey}` }, }); const data = await response.json(); ``` ```py import requests # site_key: an fsk_ key of the site, created in the console response = requests.get( 'https://api.filudi.com/v1/server/catalog', headers={'authorization': f'Bearer {site_key}'}, ) response.raise_for_status() data = response.json() ``` **The site's catalog** Scope `catalog`. Same shape as `GET /v1/catalog/{site}` for the key's site, with `Cache-Control: private, max-age=60`. Credential: a server key (`fsk_…`), sent as `Authorization: Bearer ` from your backend, never from a browser. Scope `catalog`. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `locale` | query | string | no | Returns texts for one locale only; default returns every locale of the site | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [Catalog](/docs/api/reference/schemas/#schema-catalog) | Catalog. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | No bearer, or a key the API does not know. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | The key lacks the scope of this route, is revoked, or its site is not approved; `message` says which. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | ```sh curl -X POST 'https://api.filudi.com/v1/server/players' \ -H "authorization: Bearer $FILUDI_SITE_KEY" \ -H 'content-type: application/json' \ -d '{ "externalId": "string", "nickname": "string", "locale": "en" }' ``` ```js // siteKey: an fsk_ key of the site, created in the console; server side only const response = await fetch('https://api.filudi.com/v1/server/players', { method: 'POST', headers: { authorization: `Bearer ${siteKey}`, 'content-type': 'application/json' }, body: JSON.stringify({ externalId: 'string', nickname: 'string', locale: 'en', }), }); const data = await response.json(); ``` ```py import requests # site_key: an fsk_ key of the site, created in the console response = requests.post( 'https://api.filudi.com/v1/server/players', headers={'authorization': f'Bearer {site_key}'}, json={ 'externalId': 'string', 'nickname': 'string', 'locale': 'en', }, ) response.raise_for_status() data = response.json() ``` **Create or find a player by your own user id** Scope `players`. `externalId` is unique per site: an existing player answers 200, a new one 201. A player created here is a guest with no token until `POST /v1/server/players/{playerId}/tokens`. `nickname` and `locale` are applied on creation only; change them later with the player token on `PATCH /v1/sdk/players/me`. Credential: a server key (`fsk_…`), sent as `Authorization: Bearer ` from your backend, never from a browser. Scope `players`. ### Request body `application/json`, required. The body is a [ServerPlayerInput](/docs/api/reference/schemas/#schema-serverplayerinput). | Property | Type | Required | Description | | --- | --- | --- | --- | | `externalId` | string | yes | Your own user id; 1 to 128 characters | | `nickname` | string | no | 3 to 20 characters; pattern `^[\p{L}\p{N} _.-]+$` | | `locale` | string | no | Defaults to the site's first locale | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [ServerPlayer](/docs/api/reference/schemas/#schema-serverplayer) | Existing player. | | 201 | [ServerPlayer](/docs/api/reference/schemas/#schema-serverplayer) | Created. | | 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | No bearer, or a key the API does not know. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | The key lacks the scope of this route, is revoked, or its site is not approved; `message` says which. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | ```sh curl 'https://api.filudi.com/v1/server/players/00000000-0000-0000-0000-000000000000' \ -H "authorization: Bearer $FILUDI_SITE_KEY" ``` ```js // siteKey: an fsk_ key of the site, created in the console; server side only const response = await fetch('https://api.filudi.com/v1/server/players/00000000-0000-0000-0000-000000000000', { headers: { authorization: `Bearer ${siteKey}` }, }); const data = await response.json(); ``` ```py import requests # site_key: an fsk_ key of the site, created in the console response = requests.get( 'https://api.filudi.com/v1/server/players/00000000-0000-0000-0000-000000000000', headers={'authorization': f'Bearer {site_key}'}, ) response.raise_for_status() data = response.json() ``` **Profile, saves metadata and best scores of one of the site's players** Scope `players`. 404 for a player of another site. Credential: a server key (`fsk_…`), sent as `Authorization: Bearer ` from your backend, never from a browser. Scope `players`. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `playerId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [PlayerAdmin](/docs/api/reference/schemas/#schema-playeradmin) | Player. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | No bearer, or a key the API does not know. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | The key lacks the scope of this route, is revoked, or its site is not approved; `message` says which. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | ```sh curl -X POST 'https://api.filudi.com/v1/server/players/00000000-0000-0000-0000-000000000000/tokens' \ -H "authorization: Bearer $FILUDI_SITE_KEY" ``` ```js // siteKey: an fsk_ key of the site, created in the console; server side only const response = await fetch('https://api.filudi.com/v1/server/players/00000000-0000-0000-0000-000000000000/tokens', { method: 'POST', headers: { authorization: `Bearer ${siteKey}` }, }); const data = await response.json(); ``` ```py import requests # site_key: an fsk_ key of the site, created in the console response = requests.post( 'https://api.filudi.com/v1/server/players/00000000-0000-0000-0000-000000000000/tokens', headers={'authorization': f'Bearer {site_key}'}, ) response.raise_for_status() data = response.json() ``` **A player token for that player's browser** Scope `tokens`. An `fp1` token, as `POST /v1/sdk/players` answers, so your page runs `GameFrame` or the SDK as filudi.com does. Hand it to the browser over your own authenticated channel. Credential: a server key (`fsk_…`), sent as `Authorization: Bearer ` from your backend, never from a browser. Scope `tokens`. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `playerId` | path | string (uuid) | yes | | ### Responses | Status | Body | Description | | --- | --- | --- | | 201 | object | Token. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | No bearer, or a key the API does not know. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | The key lacks the scope of this route, is revoked, or its site is not approved; `message` says which. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | **Body of 201** | Property | Type | Required | Description | | --- | --- | --- | --- | | `token` | string | yes | | ```sh curl -X POST 'https://api.filudi.com/v1/server/game-tokens' \ -H "authorization: Bearer $FILUDI_SITE_KEY" \ -H 'content-type: application/json' \ -d '{ "playerId": "00000000-0000-0000-0000-000000000000", "game": "gloop" }' ``` ```js // siteKey: an fsk_ key of the site, created in the console; server side only const response = await fetch('https://api.filudi.com/v1/server/game-tokens', { method: 'POST', headers: { authorization: `Bearer ${siteKey}`, 'content-type': 'application/json' }, body: JSON.stringify({ playerId: '00000000-0000-0000-0000-000000000000', game: 'gloop', }), }); const data = await response.json(); ``` ```py import requests # site_key: an fsk_ key of the site, created in the console response = requests.post( 'https://api.filudi.com/v1/server/game-tokens', headers={'authorization': f'Bearer {site_key}'}, json={ 'playerId': '00000000-0000-0000-0000-000000000000', 'game': 'gloop', }, ) response.raise_for_status() data = response.json() ``` **A game token for a player and a game** Scope `tokens`. The same token as `POST /v1/sdk/game-tokens` answers (ADR 0013), without the player's own token. Credential: a server key (`fsk_…`), sent as `Authorization: Bearer ` from your backend, never from a browser. Scope `tokens`. ### Request body `application/json`, required. | Property | Type | Required | Description | | --- | --- | --- | --- | | `playerId` | string (uuid) | yes | | | `game` | string | yes | Game slug | ### Responses | Status | Body | Description | | --- | --- | --- | | 201 | [GameToken](/docs/api/reference/schemas/#schema-gametoken) | Game token. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | No bearer, or a key the API does not know. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | The key lacks the scope of this route, is revoked, or its site is not approved; `message` says which. | | 404 | none | Unknown player on this site, or the game is not on the site catalog. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | ```sh curl 'https://api.filudi.com/v1/server/leaderboards/gloop/height' \ -H "authorization: Bearer $FILUDI_SITE_KEY" ``` ```js // siteKey: an fsk_ key of the site, created in the console; server side only const response = await fetch('https://api.filudi.com/v1/server/leaderboards/gloop/height', { headers: { authorization: `Bearer ${siteKey}` }, }); const data = await response.json(); ``` ```py import requests # site_key: an fsk_ key of the site, created in the console response = requests.get( 'https://api.filudi.com/v1/server/leaderboards/gloop/height', headers={'authorization': f'Bearer {site_key}'}, ) response.raise_for_status() data = response.json() ``` **Top entries with nicknames** Scope `leaderboards`. Same answer as `GET /v1/sdk/leaderboards/{gameId}/{board}`; `isMe` is always false. Credential: a server key (`fsk_…`), sent as `Authorization: Bearer ` from your backend, never from a browser. Scope `leaderboards`. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string | yes | Game slug | | `board` | path | string | yes | pattern `^[a-z][a-z0-9_]{0,31}$` | | `period` | query | one of `"day"`, `"week"`, `"all"` | no | default `"all"` | | `limit` | query | integer | no | 1 to 50; default `10` | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [LeaderboardTop](/docs/api/reference/schemas/#schema-leaderboardtop) | Top entries. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | No bearer, or a key the API does not know. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | The key lacks the scope of this route, is revoked, or its site is not approved; `message` says which. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | ```sh curl 'https://api.filudi.com/v1/server/stats/overview?from=2026-10-30&to=2026-10-30' \ -H "authorization: Bearer $FILUDI_SITE_KEY" ``` ```js // siteKey: an fsk_ key of the site, created in the console; server side only const response = await fetch('https://api.filudi.com/v1/server/stats/overview?from=2026-10-30&to=2026-10-30', { headers: { authorization: `Bearer ${siteKey}` }, }); const data = await response.json(); ``` ```py import requests # site_key: an fsk_ key of the site, created in the console response = requests.get( 'https://api.filudi.com/v1/server/stats/overview?from=2026-10-30&to=2026-10-30', headers={'authorization': f'Bearer {site_key}'}, ) response.raise_for_status() data = response.json() ``` **The site's numbers for a range of UTC days** Scope `stats`. Same answer and range rules as `GET /v1/admin/sites/{siteId}/stats/overview`. Credential: a server key (`fsk_…`), sent as `Authorization: Bearer ` from your backend, never from a browser. Scope `stats`. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `from` | query | string (date) | yes | First UTC day, included | | `to` | query | string (date) | yes | Last UTC day, included | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [StatsOverview](/docs/api/reference/schemas/#schema-statsoverview) | Overview. | | 400 | none | Range over 400 days, or `to` before `from`. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | No bearer, or a key the API does not know. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | The key lacks the scope of this route, is revoked, or its site is not approved; `message` says which. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | ```sh curl 'https://api.filudi.com/v1/server/stats/games/00000000-0000-0000-0000-000000000000?from=2026-10-30&to=2026-10-30' \ -H "authorization: Bearer $FILUDI_SITE_KEY" ``` ```js // siteKey: an fsk_ key of the site, created in the console; server side only const response = await fetch('https://api.filudi.com/v1/server/stats/games/00000000-0000-0000-0000-000000000000?from=2026-10-30&to=2026-10-30', { headers: { authorization: `Bearer ${siteKey}` }, }); const data = await response.json(); ``` ```py import requests # site_key: an fsk_ key of the site, created in the console response = requests.get( 'https://api.filudi.com/v1/server/stats/games/00000000-0000-0000-0000-000000000000?from=2026-10-30&to=2026-10-30', headers={'authorization': f'Bearer {site_key}'}, ) response.raise_for_status() data = response.json() ``` **One game's numbers on the site for a range of UTC days** Scope `stats`. Same answer as `GET /v1/admin/sites/{siteId}/stats/games/{gameId}`. 404 when the game is not on the site catalog. Credential: a server key (`fsk_…`), sent as `Authorization: Bearer ` from your backend, never from a browser. Scope `stats`. ### Parameters | Name | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `gameId` | path | string (uuid) | yes | | | `from` | query | string (date) | yes | First UTC day, included | | `to` | query | string (date) | yes | Last UTC day, included | ### Responses | Status | Body | Description | | --- | --- | --- | | 200 | [GameStats](/docs/api/reference/schemas/#schema-gamestats) | Game stats. | | 400 | none | Range over 400 days, or `to` before `from`. | | 401 | [Error](/docs/api/reference/schemas/#schema-error) | No bearer, or a key the API does not know. | | 403 | [Error](/docs/api/reference/schemas/#schema-error) | The key lacks the scope of this route, is revoked, or its site is not approved; `message` says which. | | 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. | | 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. | Source: https://developers.filudi.com/docs/api/reference/server/ --- # Versioning The /v1 prefix, how the Filudi API changes without breaking a client, and where the version of this reference comes from. Every route starts with `/v1`. The prefix changes only when a route stops answering what it answered before. Within `/v1` the API grows by addition: a new route, a new optional field in a body, a new field in an answer. A client that ignores fields it does not know keeps working. ## What a client may rely on - A field listed as required in a response schema is present. A field marked nullable may be null. - An enum may gain a value. Treat an unknown value as "other", never as an error. `CategoryIcon` is the usual case: hub-core draws nothing for an icon name it does not know yet. - A route that disappears is announced in the release notes of the platform first and answers 404 afterwards. - The error body keeps its shape and its codes. ## Where the version comes from The API is released as `games-api `, the version in `info.version` of `openapi.yaml`. Each reference page states the version it was generated from, in its first paragraph. The published packages have versions of their own: `@filudi/game-sdk` and `@filudi/hub-core` on npmjs.com follow semver, with release notes written by Changesets. Pin them exactly (`pnpm add -E`). ## Development only `GET`, `PUT` and `HEAD /v1/_local-files/{path}` stand in for the files service on a local API without `FILES_URL`. They are not part of the API and are not in this reference. Source: https://developers.filudi.com/docs/api/versioning/ --- # Games How a game gets on filudi.com and on portals, from the invitation to a version under review. From the invitation to a version under review, and everything a game can do on filudi.com and on portals. Start with the account and the console, then build your first bundle. | Page | What it covers | | ----------------------------------------------------------- | -------------------------------------------------------------------------------------- | | [Start here](/docs/games/start-here/) | The invitation, the account, the console, the review step | | [Your first bundle](/docs/games/first-bundle/) | The playground and its mock host, the zip, the upload, the preview, the review request | | [Game lifecycle on filudi.com](/docs/games/lifecycle/) | The handshake, the game token, the origin per game, the CSP, what a game cannot do | | [Saves](/docs/games/saves/) | One blob, a version the game increments, conflicts, the migration of an old save | | [Leaderboards](/docs/games/leaderboards/) | Declaring boards, picking bounds, signed scores, what the hub draws | | [Events and statistics](/docs/games/events-and-statistics/) | What to track and what the console shows for it | | [Ads on portals](/docs/games/ads/) | Where ads come from, the three rules, filudi.com today | | [Localization](/docs/games/localization/) | Locales on each host, what to translate, what the hub translates | | [Release checklist](/docs/games/release-checklist/) | The checks to run on a build before zipping it | | [Portals](/docs/games/portals/) | CrazyGames, Poki and Playgama specifics | The [SDK reference](/docs/sdk/) has one page per module. A coding agent can load the [`filudi-game-sdk` skill](/skills/) and do the integration with you. Source: https://developers.filudi.com/docs/games/ --- # Ads on portals Where ads come from on each host, the three rules a game follows, and the fact that filudi.com has none yet. filudi.com has no ad network today. Both ad kinds answer `unavailable` with reason `host` there, and the console shows nothing about revenue. On CrazyGames, Poki and Playgama the same calls show the portal's ads. Write the game so that it is complete without them. ## Where ads come from | Host | Rewarded | Interstitial | Pacing | | ---------- | --------------------------------------------------------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------- | | filudi.com | `unavailable: host` | `unavailable: host` | The SDK keeps a 120 s interval so the day an ad network exists, nothing changes in your game | | CrazyGames | `requestAd('rewarded')` | `requestAd('midgame')` | The portal's | | Poki | `rewardedBreak`: `completed` on `true`, `skipped` on `false` | `commercialBreak`: always `completed` | The portal's | | Playgama | `showRewarded`: `rewarded` then `closed` is `completed`, `closed` alone `skipped` | `showInterstitial` | The portal's | | mock | What `mock.ads` says: `fill`, `no-fill`, `adblock`, `error` | Same | 120 s | On filudi.com an ad request goes to the hub, which answers with a result. No ad script runs inside your bundle on any host: the SDK loads the portal's script on the portal's own page. ## The three rules 1. Bracket play with `gameplay.start()` and `stop()`, never around an ad. The SDK stops play before an ad and starts it again after. 2. Mute on `sdk.ads.on('start')`, unmute on `sdk.ads.on('end')`. Not when you request the ad: a refused ad never starts and the game would stay silent. 3. Grant the reward on `status === 'completed'` only. Hide the button when `sdk.ads.available().rewarded` is false, and read that each time you draw the button. ```js sdk.ads.on('start', () => audio.mute()); sdk.ads.on('end', () => audio.unmute()); function showGameOver() { lifeButton.hidden = usedLife || !sdk.ads.available().rewarded; } lifeButton.addEventListener('click', async () => { lifeButton.hidden = true; if ((await sdk.ads.rewarded()).status === 'completed') secondLife(); }); ``` ```ts sdk.ads.on('start', () => audio.mute()); sdk.ads.on('end', () => audio.unmute()); function showGameOver(): void { lifeButton.hidden = usedLife || !sdk.ads.available().rewarded; } lifeButton.addEventListener('click', async () => { lifeButton.hidden = true; if ((await sdk.ads.rewarded()).status === 'completed') secondLife(); }); ``` ## Where to put an interstitial At a natural break, after `gameplay.stop()`: every few game overs, between levels, never in the middle of play and never before the first frame. The SDK refuses one inside the host's interval with `too-soon`; a game that asks too often sees that status and nothing else. Keep your own counter (every third game over) so the game feels the same on every host. ## Policy and consent `policy.personalizedAds` from `init()` is false on a kids site or without consent; the portal or the hub applies it, your game does not have to. Without ad consent on filudi.com the answer is `unavailable: policy`. `sdk.on('consent', ...)` tells you when the player changes it, and `sdk.ads.available()` follows. ## Not offered yet No ad network on filudi.com, no revenue share, no payment. When that changes, the SDK calls stay the same; this page and the console will say what is new. Source: https://developers.filudi.com/docs/games/ads/ --- # Events and statistics What the console shows about your game, where each number comes from, and what to track yourself. The console's Statistics screen covers every site that lists your game, filterable by site, host, version and locale, over a range of up to 400 days. Most of it comes from the SDK calls your game already makes. `sdk.track()` adds what only your game knows. ## What you see | Block | What it counts | Where it comes from | | -------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | Active players | Per day, rolling 7 days, rolling 30 days; new and returning | A play session: `gameplay.start()` | | Sessions | Count, mean and median length | `gameplay.start()` to the end of the page or the last `stop()` | | Retention | D1, D7, D30 as cohorts by first-play week; blank under 10 players | Sessions per player over time | | Funnel | Frame loaded, play started, save written, score sent, account linked | `ready()`, `gameplay.start()`, `save.set()`, `leaderboard.submit()`, the hub's sign-in | | Events | By name, with the number of players | `sdk.track()` | | Splits | Host, version, locale, device | What the SDK sends with each session and event | | Boards | Scores, best, median per board | `leaderboard.submit()` | | Sites | Sessions and players per site that lists the game | The site of each session | Every table exports as CSV from the console. Nothing here is about money: filudi.com shows usage only. ## What to track Track the decisions the funnel cannot see: a level cleared, a difficulty chosen, a tutorial skipped, a shop opened, a run abandoned. Name events in `snake_case`, keep the set small, and keep the names across versions; a renamed event starts a new line. ```js sdk.track('level_complete', { level, moves, perfect: moves === par }); sdk.track('level_fail', { level, reason: 'time' }); sdk.track('tutorial_skip'); sdk.track('difficulty_set', { difficulty: 'hard' }); ``` ```ts sdk.track('level_complete', { level, moves, perfect: moves === par }); sdk.track('level_fail', { level, reason: 'time' }); sdk.track('tutorial_skip'); sdk.track('difficulty_set', { difficulty: 'hard' }); ``` Limits: a name of at most 40 characters, 20 properties, each value at most 200 characters. The SDK sends 50 events or 5 s of them at a time, and the last batch leaves with the page. `frame_loaded` is reserved for the hub. ## Version and device Pass the release version to `init({ version })`. Every session and event carries it, and the version split is how you compare a release with the previous one. The device is read by the API from the user agent at session open, as `phone`, `tablet` or `desktop`; nothing else about the browser is kept. ## From portals Sessions and events on CrazyGames, Poki and Playgama do not reach Filudi by default: no player token exists there. `init({ events: 'always', eventsUrl })` sends events to the API anonymously, counted for the game's site under the portal's host. Sessions, saves and scores on portals stay with the portal. ## When numbers appear Today's events are read live; past days come from a daily rollup. Statistics answers are cached for 60 s. A new version shows in the version filter after its first session. Source: https://developers.filudi.com/docs/games/events-and-statistics/ --- # Your first bundle From the sample game in the playground to a zip uploaded in the console, previewed, and sent for review. A bundle is a folder with `index.html` at its root, zipped. This page takes the playground's sample game through every step; your own game follows the same path. ## The playground The playground loads a bundle in an iframe, plays each host around it and logs every SDK call and host message. Open [developers.filudi.com/playground/](https://developers.filudi.com/playground/): nothing to install. If you have the Filudi repository, the same app runs locally from `apps/sdk-playground`. ```bash pnpm install pnpm --filter @filudi/game-sdk build # the sample game loads dist/filudi-sdk.iife.js pnpm --filter @filudi/sdk-playground dev # http://localhost:5173 ``` ```bash pnpm install pnpm --filter @filudi/game-sdk build # the sample game loads dist/filudi-sdk.iife.js pnpm --filter @filudi/sdk-playground dev # http://localhost:5173 ``` Pick "Sample game" and the `filudi` host, press Load. The log shows the handshake: `filudi:ready` from the bundle, `filudi:init` from the host with a game token that starts with `fg1.`, then `filudi:loaded` when the game calls `ready()`. Tap the canvas: `filudi:gameplay { running: true }`. Fall: the score goes out, the board comes back, every third fall asks for an interstitial and the host answers with the result you set. Tick "Mock API" so saves and scores hit an in-memory API and show as requests with their bearer. Untick it to use a real API: type its origin in "API URL", or open the playground with `?api=`: `/playground/?host=filudi&mockApi=0&api=https://api.filudi.com` for production, `api=http://localhost:8080` for the API you run. That API must list the playground's origin in its `HUB_URLS`, and the game slug must exist on the site, else the log says `game token refused`. The other hosts install the portal's global in the frame before the bundle runs: `CrazyGames.SDK`, `PokiSDK`, Playgama's `bridge`. Each call the game makes is a `game→host` line, each answer a `host→game` line. Change the ad outcome, the data module, the leaderboard type, and watch what the game does. ## The sample game The sample game is two files, [`index.html`](https://developers.filudi.com/playground/sample-game/index.html) and [`main.js`](https://developers.filudi.com/playground/sample-game/main.js) (`apps/sdk-playground/public/sample-game/` in the repository). `main.js` calls every SDK method at least once; it is the shortest complete integration. Read it next to the [SDK overview](/docs/sdk/). ```js const sdk = window.FiludiSDK; async function boot() { await sdk.init({ gameId: 'infinijump', version: '1.0.0' }); corner = { nickname: sdk.player.get().nickname, locale: sdk.i18n.locale() }; applySave(await sdk.save.get()); sdk.save.on('replaced', applySave); sdk.ads.on('start', () => (muted = true)); sdk.ads.on('end', () => (muted = false)); requestAnimationFrame(frame); // the first frame calls sdk.ready() } ``` ```ts import { sdk } from '@filudi/game-sdk'; async function boot(): Promise { await sdk.init({ gameId: 'infinijump', version: '1.0.0' }); corner = { nickname: sdk.player.get().nickname, locale: sdk.i18n.locale() }; applySave(await sdk.save.get<{ best: number }>()); sdk.save.on('replaced', applySave); sdk.ads.on('start', () => { muted = true; }); sdk.ads.on('end', () => { muted = false; }); requestAnimationFrame(frame); // the first frame calls sdk.ready() } ``` ## Your own game in the playground Run your dev server, choose "Bundle URL", paste `http://localhost:5174/index.html`, pick a host, press Load. The hosted playground frames a `localhost` URL from its https page: browsers treat `localhost` as secure. Your server must let the playground's origin frame it, `https://developers.filudi.com` (or `http://localhost:5173` when you run the playground from the repository): no `X-Frame-Options: DENY`, and `frame-ancestors` must list it if you send a CSP. The portal hosts fetch your `index.html`, so the server answers that origin with CORS. Vite's default CORS allows `localhost` origins only: add `server: { cors: { origin: ['https://developers.filudi.com'] } }` to its config while you test. Add `` after the SDK script while you work (`http://localhost:5173/sdk-trace.js` for the local playground): the log then shows each SDK call. Remove it before the build. Tick "Production CSP" on the `filudi` host. The playground's server then sends the policy filudi.com applies to bundles it serves itself, and the log gives you the header to send from your own server. Each violation shows as a `CSP violation` line. An inline script or a call to another host works without it and fails in production. ## The zip Build, then zip the content of the build folder, not the folder itself. `index.html` must be at the root of the archive. Leave the source maps out unless you want them public. ```bash npm run build cd dist && zip -r ../gloop-1.4.0.zip . -x '*.map' && cd .. unzip -l gloop-1.4.0.zip | head -5 # index.html first, at the root ``` ```bash pnpm build cd dist && zip -r ../gloop-1.4.0.zip . -x '*.map' && cd .. unzip -l gloop-1.4.0.zip | head -5 # index.html first, at the root ``` Limits: 200 MB for the zip, 500 MB unpacked, 10,000 files, 64 MB per file, no `..` in a path, no symbolic link, no encrypted entry. The upload refuses a zip that breaks one and names the entry. ## Upload In the console, open the game, Versions, "Upload a version". The version string follows semantic versioning (`1.4.0`, `2.0.0-beta.1`) and is unique for the game. Pass the same string to `init({ version })` in the build so events carry it. The browser uploads the zip straight to the files service; the console then finalizes: the API unpacks the zip under `games///`, checks `index.html`, counts files and bytes. The version is a draft. ## Preview "Preview" opens a signed link, valid one hour, in a new tab. It serves the draft from the preview host under the same CSP as production, at the top level. The SDK is on its mock there: check loading, layout and play. Saves and scores need the frame on filudi.com; the playground covers them. ## Request review When the draft is right, press "Request review" on the version. Staff gets an email, plays the preview and activates it. A pending game needs its approval first; the overview says which step is next. Once the version is live, open the game on filudi.com and play it once: saves, the leaderboard and your statistics now run against production. ## Next [Game lifecycle on filudi.com](/docs/games/lifecycle/) explains what happened in that frame. The [release checklist](/docs/games/release-checklist/) is the list to run before every zip. Source: https://developers.filudi.com/docs/games/first-bundle/ --- # Leaderboards Declare boards in the console with bounds the API enforces, submit one signed score per run, and draw what the host gives you. A board is declared in the console before any score reaches it. The API enforces the bounds and the rate; the SDK signs each score with the play session on filudi.com. The reference is [leaderboard](/docs/sdk/leaderboard/). ## Declaring a board In the console, the game, Leaderboards: add a board, then "Save boards". | Field | What it is | | --------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `name` | `^[a-z][a-z0-9_]{0,31}$`, the string you pass to `submit()`. `height`, `crowns`, `time` | | `sort` | `desc` for a score to beat, `asc` for a time to beat | | `min`, `max` | The API rejects a score outside. Pick the real range of your game, not the integer range: a bound is what stops a forged score | | `keep` | `best` keeps the player's best entry, `latest` their last one | | `ratePerMinute` | Scores accepted per player per minute, 10 by default | A board or a meta key named `time` is in seconds and the hub formats it as `m:ss`. Scores are integers: send tenths of a second as an integer if you need them, and say so in the board's name. ## One score per run Submit once, when the run ends, after `gameplay.stop()`, in a run that called `gameplay.start()`. The session opened by `start()` gives the key that signs the score; a score outside a session is rejected with reason `session`. ```js async function endRun() { sdk.gameplay.stop(); const result = await sdk.leaderboard.submit('height', metres, { tries: run.tries, route: run.route }); if (result.status === 'accepted' && result.best) celebrateBest(result.rank.all); } ``` ```ts async function endRun(): Promise { sdk.gameplay.stop(); const result = await sdk.leaderboard.submit('height', metres, { tries: run.tries, route: run.route }); if (result.status === 'accepted' && result.best) celebrateBest(result.rank.all); } ``` `meta` is up to 512 bytes of JSON with string and number values, shown next to the score as its detail. Use it for what explains the score (tries, time, the route taken), not for anything the score already says. ## Drawing a board `top()` gives up to 10 entries and `me()` the player's own; both take a period. The hub draws the top 5 and the player's row on the game's page, so an in-game board is for what the hub does not show: a per-level board, a weekly race, a board under the game's own art. Handle `unavailable`: CrazyGames and Poki draw their own boards and give you nothing to read. ```js async function drawBoard(period) { const [top, me] = await Promise.all([ sdk.leaderboard.top('height', period), sdk.leaderboard.me('height', period), ]); if (top.status !== 'ok') return hideBoard(); drawRows(top.entries, me.status === 'ok' ? me.entry : null); } ``` ```ts import type { Period } from '@filudi/game-sdk'; async function drawBoard(period: Period): Promise { const [top, me] = await Promise.all([ sdk.leaderboard.top('height', period), sdk.leaderboard.me('height', period), ]); if (top.status !== 'ok') return hideBoard(); drawRows(top.entries, me.status === 'ok' ? me.entry : null); } ``` ## Signed scores On filudi.com the SDK signs `pid|gameId|board|score|nonce|ts` with HMAC-SHA256 and the session key. The API checks the signature, the nonce (once), the clock (within a window) and the bounds. The game does nothing for this and cannot weaken it: a bundle that posts a score by hand gets `rejected`. What a forged client can still do is play badly on purpose; the bounds and the rate are your limits on that, and the console lets you hide a score. ## On portals CrazyGames takes scores through `user.submitScore` once you pass the key from its developer portal as `init({ crazygames: { leaderboardKey } })`. Keep the key in a build variable, never in the repository. Poki has no leaderboard. Playgama takes scores and, when its board type is `in_game`, gives them back. `rank` values are 0 where the portal reports none. ## Scores in the console The game's Leaderboards screen lists the boards and their scores with the nickname, the meta and the date. Hide a score that should not be there; the entry goes out of `top()` and the player keeps their session. Statistics give each board's count, best and median. Source: https://developers.filudi.com/docs/games/leaderboards/ --- # Game lifecycle on filudi.com The frame, the handshake, the game token, the origin per game, the CSP, and what a game cannot do, in plain words. A game on filudi.com is a static bundle in a sandboxed frame on a page of the hub. The SDK runs inside the frame; the hub runs outside. Everything between them is a message. This page says what happens, in order, and what it means for the code you write. ## Where the bundle lives Every version is unpacked under `games///` and served through the CDN from `https://.///index.html`. One origin per game: your bundle shares nothing with another game, not `localStorage`, not a cookie, not a cache. The host must match the path; the API answers 404 for game A on the origin of game B. Until the play domain is switched on per game, the same path is served from `https://play.filudi.com`. The frame is `sandbox="allow-scripts allow-same-origin allow-pointer-lock"` with `allow="fullscreen; autoplay"`. The page keeps the top-level navigation, the forms, the popups. ## The handshake 1. The hub opens the frame on the version's URL. 2. Your bundle calls `sdk.init({ gameId })`. The SDK posts `filudi:ready` to the parent and waits 1.5 s (`handshakeTimeoutMs`) for an answer. 3. The hub answers `filudi:init`: the API URL, a game token, the slug of the game, the player, the locale, the site, the age policy, the consent state, the version string. The SDK accepts it only from `window.parent`. 4. `init()` resolves. From here the SDK calls the API itself with the game token: saves, scores, events. 5. Your game draws its first frame and calls `sdk.ready()`. The SDK posts `filudi:loaded`; the hub hides its loader and counts the first step of your funnel. No answer within 1.5 s means the bundle is not in a Filudi frame: the SDK runs on the mock and `result.host` says so. That is what happens in the preview, which runs at the top level. ```js // filudi:init, posted by the hub. The SDK reads it; the game never does. ({ type: 'filudi:init', apiUrl: 'https://api.filudi.com', token: 'fg1.…', // game token, opaque, 24 hours gameId: 'gloop', player: { id: '…', nickname: 'Guest-Fox', guest: true }, locale: 'fr', site: 'filudi', policy: { personalizedAds: false, ageBand: 'all', accounts: true }, consent: { ads: false, analytics: true }, gameVersion: '1.4.0', }); ``` ```ts import type { FiludiInit } from '@filudi/game-sdk'; // filudi:init, posted by the hub. The SDK reads it; the game never does. const message: FiludiInit = { type: 'filudi:init', apiUrl: 'https://api.filudi.com', token: 'fg1.…', // game token, opaque, 24 hours gameId: 'gloop', player: { id: '…', nickname: 'Guest-Fox', guest: true }, locale: 'fr', site: 'filudi', policy: { personalizedAds: false, ageBand: 'all', accounts: true }, consent: { ads: false, analytics: true }, gameVersion: '1.4.0', }; ``` ## The game token The player signs in to filudi.com and has a token of their own. That token never enters your frame. The hub exchanges it for a game token: limited to your game, to that player, for 24 hours. It opens your saves, your boards and your events and nothing else. The hub sends a fresh one every 12 hours (`filudi:token`) and the SDK uses it from the next request, without a reload. When the API refuses the token, the SDK posts `filudi:unauthorized`, keeps saves in `localStorage` and stops calling the API; the hub fetches a new token and reloads the frame. Your game sees a reload, nothing else. ## While the game runs The SDK posts to the hub: `filudi:gameplay { running }` on `gameplay.start()` and `stop()`, `filudi:save { version }` after a write, `filudi:score { board, score, rank }` after a submit, `filudi:track { event }` after an event, `filudi:fullscreen { request: true }` on `sdk.fullscreen()`, `filudi:ad` to request an ad. The hub treats each as a claim from an untrusted frame: it may update what the page shows for your game, and nothing else. The hub posts to the SDK: `filudi:consent`, `filudi:locale`, `filudi:pause`, `filudi:resume`, `filudi:token`, and the answers to fullscreen and ad requests. They reach your game as the events of [host events](/docs/sdk/events/), [i18n](/docs/sdk/i18n/) and [ads](/docs/sdk/ads/). ## The CSP The bundle route sends a Content Security Policy. Scripts, styles, images, media, fonts and workers load from the bundle's own origin only, with `blob:` and `data:` allowed for assets and `wasm-unsafe-eval` for WebAssembly. `fetch`, XHR, WebSocket and EventSource reach the bundle's origin and the API only. No frame inside the frame, no form post. An inline `