# 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

<RouteHeading id="get-v1-admin-me" method="GET" path="/v1/admin/me" />

<CodePanel title="GET /v1/admin/me">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/me' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-accounts" method="GET" path="/v1/admin/accounts" />

<CodePanel title="GET /v1/admin/accounts">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/accounts' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-accounts-accountid" method="GET" path="/v1/admin/accounts/{accountId}" />

<CodePanel title="GET /v1/admin/accounts/{accountId}">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/accounts/00000000-0000-0000-0000-000000000000' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="patch-v1-admin-accounts-accountid" method="PATCH" path="/v1/admin/accounts/{accountId}" />

<CodePanel title="PATCH /v1/admin/accounts/{accountId}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-accounts-accountid-reset-link" method="POST" path="/v1/admin/accounts/{accountId}/reset-link" />

<CodePanel title="POST /v1/admin/accounts/{accountId}/reset-link">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="delete-v1-admin-accounts-accountid-totp" method="DELETE" path="/v1/admin/accounts/{accountId}/totp" />

<CodePanel title="DELETE /v1/admin/accounts/{accountId}/totp">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="delete-v1-admin-accounts-accountid-sessions" method="DELETE" path="/v1/admin/accounts/{accountId}/sessions" />

<CodePanel title="DELETE /v1/admin/accounts/{accountId}/sessions">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-accounts-accountid-impersonate" method="POST" path="/v1/admin/accounts/{accountId}/impersonate" />

<CodePanel title="POST /v1/admin/accounts/{accountId}/impersonate">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-invitations" method="GET" path="/v1/admin/invitations" />

<CodePanel title="GET /v1/admin/invitations">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/invitations' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-invitations" method="POST" path="/v1/admin/invitations" />

<CodePanel title="POST /v1/admin/invitations">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="delete-v1-admin-invitations-invitationid" method="DELETE" path="/v1/admin/invitations/{invitationId}" />

<CodePanel title="DELETE /v1/admin/invitations/{invitationId}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-invitations-invitationid-resend" method="POST" path="/v1/admin/invitations/{invitationId}/resend" />

<CodePanel title="POST /v1/admin/invitations/{invitationId}/resend">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-invitation-requests" method="GET" path="/v1/admin/invitation-requests" />

<CodePanel title="GET /v1/admin/invitation-requests">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/invitation-requests' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="patch-v1-admin-invitation-requests-requestid" method="PATCH" path="/v1/admin/invitation-requests/{requestId}" />

<CodePanel title="PATCH /v1/admin/invitation-requests/{requestId}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-approvals" method="GET" path="/v1/admin/approvals" />

<CodePanel title="GET /v1/admin/approvals">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/approvals' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-sites" method="GET" path="/v1/admin/sites" />

<CodePanel title="GET /v1/admin/sites">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/sites' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-sites" method="POST" path="/v1/admin/sites" />

<CodePanel title="POST /v1/admin/sites">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-sites-siteid" method="GET" path="/v1/admin/sites/{siteId}" />

<CodePanel title="GET /v1/admin/sites/{siteId}">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="patch-v1-admin-sites-siteid" method="PATCH" path="/v1/admin/sites/{siteId}" />

<CodePanel title="PATCH /v1/admin/sites/{siteId}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-sites-siteid-approve" method="POST" path="/v1/admin/sites/{siteId}/approve" />

<CodePanel title="POST /v1/admin/sites/{siteId}/approve">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-sites-siteid-reject" method="POST" path="/v1/admin/sites/{siteId}/reject" />

<CodePanel title="POST /v1/admin/sites/{siteId}/reject">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-sites-siteid-domains-approve" method="POST" path="/v1/admin/sites/{siteId}/domains/approve" />

<CodePanel title="POST /v1/admin/sites/{siteId}/domains/approve">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-sites-siteid-domains-reject" method="POST" path="/v1/admin/sites/{siteId}/domains/reject" />

<CodePanel title="POST /v1/admin/sites/{siteId}/domains/reject">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-sites-siteid-members" method="GET" path="/v1/admin/sites/{siteId}/members" />

<CodePanel title="GET /v1/admin/sites/{siteId}/members">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/members' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-sites-siteid-members" method="POST" path="/v1/admin/sites/{siteId}/members" />

<CodePanel title="POST /v1/admin/sites/{siteId}/members">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="delete-v1-admin-sites-siteid-members-accountid" method="DELETE" path="/v1/admin/sites/{siteId}/members/{accountId}" />

<CodePanel title="DELETE /v1/admin/sites/{siteId}/members/{accountId}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

**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.". |

<RouteHeading id="delete-v1-admin-sites-siteid-members-invitations-invitationid" method="DELETE" path="/v1/admin/sites/{siteId}/members/invitations/{invitationId}" />

<CodePanel title="DELETE /v1/admin/sites/{siteId}/members/invitations/{invitationId}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-sites-siteid-keys" method="GET" path="/v1/admin/sites/{siteId}/keys" />

<CodePanel title="GET /v1/admin/sites/{siteId}/keys">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/keys' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-sites-siteid-keys" method="POST" path="/v1/admin/sites/{siteId}/keys" />

<CodePanel title="POST /v1/admin/sites/{siteId}/keys">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="delete-v1-admin-sites-siteid-keys-keyid" method="DELETE" path="/v1/admin/sites/{siteId}/keys/{keyId}" />

<CodePanel title="DELETE /v1/admin/sites/{siteId}/keys/{keyId}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-sites-siteid-categories" method="GET" path="/v1/admin/sites/{siteId}/categories" />

<CodePanel title="GET /v1/admin/sites/{siteId}/categories">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-sites-siteid-categories" method="POST" path="/v1/admin/sites/{siteId}/categories" />

<CodePanel title="POST /v1/admin/sites/{siteId}/categories">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="put-v1-admin-sites-siteid-categories-order" method="PUT" path="/v1/admin/sites/{siteId}/categories/order" />

<CodePanel title="PUT /v1/admin/sites/{siteId}/categories/order">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-sites-siteid-categories-slug" method="GET" path="/v1/admin/sites/{siteId}/categories/{slug}" />

<CodePanel title="GET /v1/admin/sites/{siteId}/categories/{slug}">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/categories/gloop' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="patch-v1-admin-sites-siteid-categories-slug" method="PATCH" path="/v1/admin/sites/{siteId}/categories/{slug}" />

<CodePanel title="PATCH /v1/admin/sites/{siteId}/categories/{slug}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="delete-v1-admin-sites-siteid-categories-slug" method="DELETE" path="/v1/admin/sites/{siteId}/categories/{slug}" />

<CodePanel title="DELETE /v1/admin/sites/{siteId}/categories/{slug}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-sites-siteid-tags" method="GET" path="/v1/admin/sites/{siteId}/tags" />

<CodePanel title="GET /v1/admin/sites/{siteId}/tags">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-sites-siteid-tags" method="POST" path="/v1/admin/sites/{siteId}/tags" />

<CodePanel title="POST /v1/admin/sites/{siteId}/tags">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="put-v1-admin-sites-siteid-tags-order" method="PUT" path="/v1/admin/sites/{siteId}/tags/order" />

<CodePanel title="PUT /v1/admin/sites/{siteId}/tags/order">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-sites-siteid-tags-slug" method="GET" path="/v1/admin/sites/{siteId}/tags/{slug}" />

<CodePanel title="GET /v1/admin/sites/{siteId}/tags/{slug}">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/tags/gloop' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="patch-v1-admin-sites-siteid-tags-slug" method="PATCH" path="/v1/admin/sites/{siteId}/tags/{slug}" />

<CodePanel title="PATCH /v1/admin/sites/{siteId}/tags/{slug}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="delete-v1-admin-sites-siteid-tags-slug" method="DELETE" path="/v1/admin/sites/{siteId}/tags/{slug}" />

<CodePanel title="DELETE /v1/admin/sites/{siteId}/tags/{slug}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-sites-siteid-catalog" method="GET" path="/v1/admin/sites/{siteId}/catalog" />

<CodePanel title="GET /v1/admin/sites/{siteId}/catalog">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/catalog' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="put-v1-admin-sites-siteid-catalog-order" method="PUT" path="/v1/admin/sites/{siteId}/catalog/order" />

<CodePanel title="PUT /v1/admin/sites/{siteId}/catalog/order">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="put-v1-admin-sites-siteid-catalog-gameid" method="PUT" path="/v1/admin/sites/{siteId}/catalog/{gameId}" />

<CodePanel title="PUT /v1/admin/sites/{siteId}/catalog/{gameId}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="delete-v1-admin-sites-siteid-catalog-gameid" method="DELETE" path="/v1/admin/sites/{siteId}/catalog/{gameId}" />

<CodePanel title="DELETE /v1/admin/sites/{siteId}/catalog/{gameId}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-sites-siteid-publish" method="POST" path="/v1/admin/sites/{siteId}/publish" />

<CodePanel title="POST /v1/admin/sites/{siteId}/publish">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-games" method="GET" path="/v1/admin/games" />

<CodePanel title="GET /v1/admin/games">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/games' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-games" method="POST" path="/v1/admin/games" />

<CodePanel title="POST /v1/admin/games">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

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

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

    ```sh
    curl 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="patch-v1-admin-games-gameid" method="PATCH" path="/v1/admin/games/{gameId}" />

<CodePanel title="PATCH /v1/admin/games/{gameId}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="delete-v1-admin-games-gameid" method="DELETE" path="/v1/admin/games/{gameId}" />

<CodePanel title="DELETE /v1/admin/games/{gameId}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

**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/<slug>/` 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.". |

<RouteHeading id="post-v1-admin-games-gameid-approve" method="POST" path="/v1/admin/games/{gameId}/approve" />

<CodePanel title="POST /v1/admin/games/{gameId}/approve">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-games-gameid-reject" method="POST" path="/v1/admin/games/{gameId}/reject" />

<CodePanel title="POST /v1/admin/games/{gameId}/reject">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

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

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

    ```sh
    curl 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/members' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-games-gameid-members" method="POST" path="/v1/admin/games/{gameId}/members" />

<CodePanel title="POST /v1/admin/games/{gameId}/members">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="delete-v1-admin-games-gameid-members-accountid" method="DELETE" path="/v1/admin/games/{gameId}/members/{accountId}" />

<CodePanel title="DELETE /v1/admin/games/{gameId}/members/{accountId}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

**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.". |

<RouteHeading id="delete-v1-admin-games-gameid-members-invitations-invitationid" method="DELETE" path="/v1/admin/games/{gameId}/members/invitations/{invitationId}" />

<CodePanel title="DELETE /v1/admin/games/{gameId}/members/invitations/{invitationId}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

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

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

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-games-gameid-versions" method="POST" path="/v1/admin/games/{gameId}/versions" />

<CodePanel title="POST /v1/admin/games/{gameId}/versions">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-games-gameid-media-slot" method="POST" path="/v1/admin/games/{gameId}/media/{slot}" />

<CodePanel title="POST /v1/admin/games/{gameId}/media/{slot}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="delete-v1-admin-games-gameid-media-slot" method="DELETE" path="/v1/admin/games/{gameId}/media/{slot}" />

<CodePanel title="DELETE /v1/admin/games/{gameId}/media/{slot}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-games-gameid-media-slot-finalize" method="POST" path="/v1/admin/games/{gameId}/media/{slot}/finalize" />

<CodePanel title="POST /v1/admin/games/{gameId}/media/{slot}/finalize">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-versions-versionid-finalize" method="POST" path="/v1/admin/versions/{versionId}/finalize" />

<CodePanel title="POST /v1/admin/versions/{versionId}/finalize">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-versions-versionid-activate" method="POST" path="/v1/admin/versions/{versionId}/activate" />

<CodePanel title="POST /v1/admin/versions/{versionId}/activate">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-versions-versionid-retire" method="POST" path="/v1/admin/versions/{versionId}/retire" />

<CodePanel title="POST /v1/admin/versions/{versionId}/retire">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-versions-versionid-review" method="POST" path="/v1/admin/versions/{versionId}/review" />

<CodePanel title="POST /v1/admin/versions/{versionId}/review">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-versions-versionid-review-reject" method="POST" path="/v1/admin/versions/{versionId}/review/reject" />

<CodePanel title="POST /v1/admin/versions/{versionId}/review/reject">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-versions-versionid-preview" method="GET" path="/v1/admin/versions/{versionId}/preview" />

<CodePanel title="GET /v1/admin/versions/{versionId}/preview">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/versions/00000000-0000-0000-0000-000000000000/preview' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

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

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

    ```sh
    curl 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/leaderboards' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="put-v1-admin-games-gameid-leaderboards" method="PUT" path="/v1/admin/games/{gameId}/leaderboards" />

<CodePanel title="PUT /v1/admin/games/{gameId}/leaderboards">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

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

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

    ```sh
    curl 'https://api.filudi.com/v1/admin/games/00000000-0000-0000-0000-000000000000/leaderboards/height/scores' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-scores-scoreid" method="POST" path="/v1/admin/scores/{scoreId}" />

<CodePanel title="POST /v1/admin/scores/{scoreId}">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/admin/scores/string' \
      -b "filudi_console=$FILUDI_SESSION" \
      -H 'origin: https://developers.filudi.com'
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="delete-v1-admin-scores-scoreid" method="DELETE" path="/v1/admin/scores/{scoreId}" />

<CodePanel title="DELETE /v1/admin/scores/{scoreId}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-sites-siteid-players" method="GET" path="/v1/admin/sites/{siteId}/players" />

<CodePanel title="GET /v1/admin/sites/{siteId}/players">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/players' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

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

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

    ```sh
    curl 'https://api.filudi.com/v1/admin/players/00000000-0000-0000-0000-000000000000' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="delete-v1-admin-players-playerid" method="DELETE" path="/v1/admin/players/{playerId}" />

<CodePanel title="DELETE /v1/admin/players/{playerId}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

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

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

    ```sh
    curl 'https://api.filudi.com/v1/admin/players/00000000-0000-0000-0000-000000000000/export' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-sites-siteid-comments" method="GET" path="/v1/admin/sites/{siteId}/comments" />

<CodePanel title="GET /v1/admin/sites/{siteId}/comments">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/comments' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="delete-v1-admin-comments-commentid" method="DELETE" path="/v1/admin/comments/{commentId}" />

<CodePanel title="DELETE /v1/admin/comments/{commentId}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-comments-commentid-hide" method="POST" path="/v1/admin/comments/{commentId}/hide" />

<CodePanel title="POST /v1/admin/comments/{commentId}/hide">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="post-v1-admin-comments-commentid-unhide" method="POST" path="/v1/admin/comments/{commentId}/unhide" />

<CodePanel title="POST /v1/admin/comments/{commentId}/unhide">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-sites-siteid-ratings" method="GET" path="/v1/admin/sites/{siteId}/ratings" />

<CodePanel title="GET /v1/admin/sites/{siteId}/ratings">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/ratings' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-sites-siteid-feedback" method="GET" path="/v1/admin/sites/{siteId}/feedback" />

<CodePanel title="GET /v1/admin/sites/{siteId}/feedback">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/feedback' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-feedback-feedbackid" method="GET" path="/v1/admin/feedback/{feedbackId}" />

<CodePanel title="GET /v1/admin/feedback/{feedbackId}">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/feedback/00000000-0000-0000-0000-000000000000' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="patch-v1-admin-feedback-feedbackid" method="PATCH" path="/v1/admin/feedback/{feedbackId}" />

<CodePanel title="PATCH /v1/admin/feedback/{feedbackId}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="delete-v1-admin-feedback-feedbackid" method="DELETE" path="/v1/admin/feedback/{feedbackId}" />

<CodePanel title="DELETE /v1/admin/feedback/{feedbackId}">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-sites-siteid-events-today" method="GET" path="/v1/admin/sites/{siteId}/events/today" />

<CodePanel title="GET /v1/admin/sites/{siteId}/events/today">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/events/today' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-sites-siteid-events-daily" method="GET" path="/v1/admin/sites/{siteId}/events/daily" />

<CodePanel title="GET /v1/admin/sites/{siteId}/events/daily">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-sites-siteid-stats-overview" method="GET" path="/v1/admin/sites/{siteId}/stats/overview" />

<CodePanel title="GET /v1/admin/sites/{siteId}/stats/overview">
  <Fragment slot="curl">

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-sites-siteid-stats-games-gameid" method="GET" path="/v1/admin/sites/{siteId}/stats/games/{gameId}" />

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

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

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-sites-siteid-stats-web" method="GET" path="/v1/admin/sites/{siteId}/stats/web" />

<CodePanel title="GET /v1/admin/sites/{siteId}/stats/web">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/sites/00000000-0000-0000-0000-000000000000/stats/web' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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

<RouteHeading id="get-v1-admin-audit" method="GET" path="/v1/admin/audit" />

<CodePanel title="GET /v1/admin/audit">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/admin/audit' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

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

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

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

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

  </Fragment>
</CodePanel>

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