# 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

<RouteHeading id="get-v1-server-catalog" method="GET" path="/v1/server/catalog" />

<CodePanel title="GET /v1/server/catalog">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/server/catalog' \
      -H "authorization: Bearer $FILUDI_SITE_KEY"
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```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();
    ```

  </Fragment>
  <Fragment slot="python">

    ```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()
    ```

  </Fragment>
</CodePanel>

**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 <key>` 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`. |

<RouteHeading id="post-v1-server-players" method="POST" path="/v1/server/players" />

<CodePanel title="POST /v1/server/players">
  <Fragment slot="curl">

    ```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"
    }'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```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();
    ```

  </Fragment>
  <Fragment slot="python">

    ```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()
    ```

  </Fragment>
</CodePanel>

**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 <key>` 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`. |

<RouteHeading id="get-v1-server-players-playerid" method="GET" path="/v1/server/players/{playerId}" />

<CodePanel title="GET /v1/server/players/{playerId}">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/server/players/00000000-0000-0000-0000-000000000000' \
      -H "authorization: Bearer $FILUDI_SITE_KEY"
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```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();
    ```

  </Fragment>
  <Fragment slot="python">

    ```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()
    ```

  </Fragment>
</CodePanel>

**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 <key>` 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`. |

<RouteHeading id="post-v1-server-players-playerid-tokens" method="POST" path="/v1/server/players/{playerId}/tokens" />

<CodePanel title="POST /v1/server/players/{playerId}/tokens">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/server/players/00000000-0000-0000-0000-000000000000/tokens' \
      -H "authorization: Bearer $FILUDI_SITE_KEY"
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```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();
    ```

  </Fragment>
  <Fragment slot="python">

    ```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()
    ```

  </Fragment>
</CodePanel>

**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 <key>` 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 |  |

<RouteHeading id="post-v1-server-game-tokens" method="POST" path="/v1/server/game-tokens" />

<CodePanel title="POST /v1/server/game-tokens">
  <Fragment slot="curl">

    ```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"
    }'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```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();
    ```

  </Fragment>
  <Fragment slot="python">

    ```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()
    ```

  </Fragment>
</CodePanel>

**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 <key>` 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`. |

<RouteHeading id="get-v1-server-leaderboards-gameid-board" method="GET" path="/v1/server/leaderboards/{gameId}/{board}" />

<CodePanel title="GET /v1/server/leaderboards/{gameId}/{board}">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/server/leaderboards/gloop/height' \
      -H "authorization: Bearer $FILUDI_SITE_KEY"
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```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();
    ```

  </Fragment>
  <Fragment slot="python">

    ```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()
    ```

  </Fragment>
</CodePanel>

**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 <key>` 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`. |

<RouteHeading id="get-v1-server-stats-overview" method="GET" path="/v1/server/stats/overview" />

<CodePanel title="GET /v1/server/stats/overview">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/server/stats/overview?from=2026-10-30&to=2026-10-30' \
      -H "authorization: Bearer $FILUDI_SITE_KEY"
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```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();
    ```

  </Fragment>
  <Fragment slot="python">

    ```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()
    ```

  </Fragment>
</CodePanel>

**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 <key>` 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`. |

<RouteHeading id="get-v1-server-stats-games-gameid" method="GET" path="/v1/server/stats/games/{gameId}" />

<CodePanel title="GET /v1/server/stats/games/{gameId}">
  <Fragment slot="curl">

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

  </Fragment>
  <Fragment slot="javascript">

    ```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();
    ```

  </Fragment>
  <Fragment slot="python">

    ```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()
    ```

  </Fragment>
</CodePanel>

**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 <key>` 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`. |
