# 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 |
| 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 |
| 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 |
| 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 | |
| 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"`.
| 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 | |
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"`.
| 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 |
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 | |
| 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 | |
| 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 | |
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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 |
| 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 | |
| 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 `