# SDK routes

The sdk routes of games-api, with parameters, bodies, responses and request samples.

Generated from `openapi.yaml` of games-api 0.7.0: 13 routes.

- [`POST /v1/sdk/players`](#post-v1-sdk-players): Create an anonymous player and its token
- [`POST /v1/sdk/game-tokens`](#post-v1-sdk-game-tokens): Exchange a player token for a token limited to one game (ADR 0013)
- [`GET /v1/sdk/players/me`](#get-v1-sdk-players-me): Current player
- [`PATCH /v1/sdk/players/me`](#patch-v1-sdk-players-me): Change nickname or locale
- [`POST /v1/sdk/sessions`](#post-v1-sdk-sessions): Open a play session and get the score-signing key
- [`GET /v1/sdk/saves`](#get-v1-sdk-saves): List the player's saves (metadata only)
- [`GET /v1/sdk/saves/{gameId}`](#get-v1-sdk-saves-gameid): Read a save
- [`PUT /v1/sdk/saves/{gameId}`](#put-v1-sdk-saves-gameid): Write a save
- [`DELETE /v1/sdk/saves/{gameId}`](#delete-v1-sdk-saves-gameid): Delete a save
- [`GET /v1/sdk/leaderboards/{gameId}/{board}`](#get-v1-sdk-leaderboards-gameid-board): Top entries
- [`GET /v1/sdk/leaderboards/{gameId}/{board}/me`](#get-v1-sdk-leaderboards-gameid-board-me): The player's own entry
- [`POST /v1/sdk/leaderboards/{gameId}/{board}/scores`](#post-v1-sdk-leaderboards-gameid-board-scores): Submit a signed score
- [`POST /v1/sdk/events`](#post-v1-sdk-events): Batch of events

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

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

    ```sh
    curl -X POST 'https://api.filudi.com/v1/sdk/players' \
      -H 'content-type: application/json' \
      -d '{
      "site": "filudi",
      "locale": "en"
    }'
    ```

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

    ```js
    const response = await fetch('https://api.filudi.com/v1/sdk/players', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        site: 'filudi',
        locale: 'en',
      }),
    });
    const data = await response.json();
    ```

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

    ```py
    import requests

    response = requests.post(
        'https://api.filudi.com/v1/sdk/players',
        json={
            'site': 'filudi',
            'locale': 'en',
        },
    )
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**Create an anonymous player and its token**

Credential: none. The route is public.

### Request body

`application/json`, required.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `site` | string | yes | Site slug |
| `locale` | string | no |  |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 201 | [PlayerWithToken](/docs/api/reference/schemas/#schema-playerwithtoken) | Player and token. |
| 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. |
| 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. |

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

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

    ```sh
    curl -X POST 'https://api.filudi.com/v1/sdk/game-tokens' \
      -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \
      -H 'content-type: application/json' \
      -d '{
      "game": "gloop"
    }'
    ```

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

    ```js
    // playerToken: the fp1 token of POST /v1/sdk/players
    const response = await fetch('https://api.filudi.com/v1/sdk/game-tokens', {
      method: 'POST',
      headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' },
      body: JSON.stringify({
        game: 'gloop',
      }),
    });
    const data = await response.json();
    ```

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

    ```py
    import requests

    # player_token: the fp1 token of POST /v1/sdk/players
    response = requests.post(
        'https://api.filudi.com/v1/sdk/game-tokens',
        headers={'authorization': f'Bearer {player_token}'},
        json={
            'game': 'gloop',
        },
    )
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**Exchange a player token for a token limited to one game (ADR 0013)**

Called by the hub, never by a game. Refuses a game token. Limited to 240 per player per hour.
A game token (`fg1`) is valid 24 hours on these routes, for its own game: `GET /v1/sdk/players/me`
(`emailMasked` null, `hasPassword` false), `/v1/sdk/saves/{gameId}`, `POST /v1/sdk/sessions`,
`/v1/sdk/leaderboards/{gameId}/*`, `POST /v1/sdk/events` (403 when an item names another game).
Those routes answer 403 for another game. Every other route that needs a player answers 401.

Credential: a player token (`fp1`), sent as `Authorization: Bearer <token>`. A game token (`fg1`) answers 401 here.

### Request body

`application/json`, required.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `game` | string | yes | Game slug |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 201 | [GameToken](/docs/api/reference/schemas/#schema-gametoken) | Game token. |
| 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. |
| 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. |
| 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. |

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

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

    ```sh
    curl 'https://api.filudi.com/v1/sdk/players/me' \
      -H "authorization: Bearer $FILUDI_PLAYER_TOKEN"
    ```

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

    ```js
    // playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game
    const response = await fetch('https://api.filudi.com/v1/sdk/players/me', {
      headers: { authorization: `Bearer ${playerToken}` },
    });
    const data = await response.json();
    ```

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

    ```py
    import requests

    # player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game
    response = requests.get(
        'https://api.filudi.com/v1/sdk/players/me',
        headers={'authorization': f'Bearer {player_token}'},
    )
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**Current player**

Credential: a player token (`fp1`), sent as `Authorization: Bearer <token>`. A game token (`fg1`) also opens this route, for its own game only.

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 200 | [Player](/docs/api/reference/schemas/#schema-player) | Player. |
| 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. |

<RouteHeading id="patch-v1-sdk-players-me" method="PATCH" path="/v1/sdk/players/me" />

<CodePanel title="PATCH /v1/sdk/players/me">
  <Fragment slot="curl">

    ```sh
    curl -X PATCH 'https://api.filudi.com/v1/sdk/players/me' \
      -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \
      -H 'content-type: application/json' \
      -d '{
      "nickname": "string",
      "locale": "en"
    }'
    ```

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

    ```js
    // playerToken: the fp1 token of POST /v1/sdk/players
    const response = await fetch('https://api.filudi.com/v1/sdk/players/me', {
      method: 'PATCH',
      headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' },
      body: JSON.stringify({
        nickname: 'string',
        locale: 'en',
      }),
    });
    const data = await response.json();
    ```

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

    ```py
    import requests

    # player_token: the fp1 token of POST /v1/sdk/players
    response = requests.patch(
        'https://api.filudi.com/v1/sdk/players/me',
        headers={'authorization': f'Bearer {player_token}'},
        json={
            'nickname': 'string',
            'locale': 'en',
        },
    )
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**Change nickname or locale**

Credential: a player token (`fp1`), sent as `Authorization: Bearer <token>`. A game token (`fg1`) answers 401 here.

### Request body

`application/json`, required.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `nickname` | string | no | 3 to 20 characters; pattern `^[\p{L}\p{N} _.-]+$` |
| `locale` | string | no |  |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 200 | [Player](/docs/api/reference/schemas/#schema-player) | Updated player. |
| 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. |
| 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. |

<RouteHeading id="post-v1-sdk-sessions" method="POST" path="/v1/sdk/sessions" />

<CodePanel title="POST /v1/sdk/sessions">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/sdk/sessions' \
      -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \
      -H 'content-type: application/json' \
      -d '{
      "gameId": "gloop",
      "gameVersion": "1.0.0",
      "host": "filudi"
    }'
    ```

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

    ```js
    // playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game
    const response = await fetch('https://api.filudi.com/v1/sdk/sessions', {
      method: 'POST',
      headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' },
      body: JSON.stringify({
        gameId: 'gloop',
        gameVersion: '1.0.0',
        host: 'filudi',
      }),
    });
    const data = await response.json();
    ```

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

    ```py
    import requests

    # player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game
    response = requests.post(
        'https://api.filudi.com/v1/sdk/sessions',
        headers={'authorization': f'Bearer {player_token}'},
        json={
            'gameId': 'gloop',
            'gameVersion': '1.0.0',
            'host': 'filudi',
        },
    )
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**Open a play session and get the score-signing key**

Called by `gameplay.start()`. The key is HMAC-SHA256(version secret, session id),
base64url. Valid 6 hours. The API rejects scores submitted outside a session.

Credential: a player token (`fp1`), sent as `Authorization: Bearer <token>`. A game token (`fg1`) also opens this route, for its own game only.

### Request body

`application/json`, required.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `gameId` | string | yes | Game slug |
| `gameVersion` | string | no | Bundle version; defaults to the live one |
| `host` | string | no | default `"filudi"` |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 201 | object | Session. |
| 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. |
| 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. |
| 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. |
| 503 | none | Redis is down, no session can open. |

**Body of 201**

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `sessionId` | string (uuid) | yes |  |
| `sessionKey` | string | yes |  |
| `expiresAt` | string (date-time) | yes |  |

<RouteHeading id="get-v1-sdk-saves" method="GET" path="/v1/sdk/saves" />

<CodePanel title="GET /v1/sdk/saves">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/sdk/saves' \
      -H "authorization: Bearer $FILUDI_PLAYER_TOKEN"
    ```

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

    ```js
    // playerToken: the fp1 token of POST /v1/sdk/players
    const response = await fetch('https://api.filudi.com/v1/sdk/saves', {
      headers: { authorization: `Bearer ${playerToken}` },
    });
    const data = await response.json();
    ```

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

    ```py
    import requests

    # player_token: the fp1 token of POST /v1/sdk/players
    response = requests.get(
        'https://api.filudi.com/v1/sdk/saves',
        headers={'authorization': f'Bearer {player_token}'},
    )
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**List the player's saves (metadata only)**

Credential: a player token (`fp1`), sent as `Authorization: Bearer <token>`. A game token (`fg1`) answers 401 here.

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 200 | [SaveMeta](/docs/api/reference/schemas/#schema-savemeta)[] | Saves. |

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

<CodePanel title="GET /v1/sdk/saves/{gameId}">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/sdk/saves/gloop' \
      -H "authorization: Bearer $FILUDI_PLAYER_TOKEN"
    ```

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

    ```js
    // playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game
    const response = await fetch('https://api.filudi.com/v1/sdk/saves/gloop', {
      headers: { authorization: `Bearer ${playerToken}` },
    });
    const data = await response.json();
    ```

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

    ```py
    import requests

    # player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game
    response = requests.get(
        'https://api.filudi.com/v1/sdk/saves/gloop',
        headers={'authorization': f'Bearer {player_token}'},
    )
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**Read a save**

Credential: a player token (`fp1`), sent as `Authorization: Bearer <token>`. A game token (`fg1`) also opens this route, for its own game only.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `gameId` | path | string | yes | Game slug |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 200 | [Save](/docs/api/reference/schemas/#schema-save) | Save. Headers: `ETag`. |
| 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. |

<RouteHeading id="put-v1-sdk-saves-gameid" method="PUT" path="/v1/sdk/saves/{gameId}" />

<CodePanel title="PUT /v1/sdk/saves/{gameId}">
  <Fragment slot="curl">

    ```sh
    curl -X PUT 'https://api.filudi.com/v1/sdk/saves/gloop' \
      -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \
      -H 'if-match: string' \
      -H 'content-type: application/json' \
      -d '{
      "version": 1,
      "data": {}
    }'
    ```

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

    ```js
    // playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game
    const response = await fetch('https://api.filudi.com/v1/sdk/saves/gloop', {
      method: 'PUT',
      headers: { authorization: `Bearer ${playerToken}`, 'if-match': 'string', 'content-type': 'application/json' },
      body: JSON.stringify({
        version: 1,
        data: {},
      }),
    });
    const data = await response.json();
    ```

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

    ```py
    import requests

    # player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game
    response = requests.put(
        'https://api.filudi.com/v1/sdk/saves/gloop',
        headers={'authorization': f'Bearer {player_token}', 'if-match': 'string'},
        json={
            'version': 1,
            'data': {},
        },
    )
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**Write a save**

`If-Match` carries the version the client last saw (0 for none). The server accepts when
the new version is greater than the stored one and `If-Match` equals the stored version.
Otherwise 409 with the server copy.

Credential: a player token (`fp1`), sent as `Authorization: Bearer <token>`. A game token (`fg1`) also opens this route, for its own game only.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `gameId` | path | string | yes | Game slug |
| `If-Match` | header | string | yes | Stored version as a quoted integer, e.g. "12" |

### Request body

`application/json`, required.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `version` | integer | yes | at least 1 |
| `data` | any | yes | Any JSON value, 64 kB max serialized |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 200 | [SaveMeta](/docs/api/reference/schemas/#schema-savemeta) | Saved. Headers: `ETag`. |
| 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. |
| 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. |
| 409 | [SaveServerCopy](/docs/api/reference/schemas/#schema-saveservercopy) | Server has another version, or none yet (version 0). |
| 413 | [Error](/docs/api/reference/schemas/#schema-error) | Body over the limit. |

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

<CodePanel title="DELETE /v1/sdk/saves/{gameId}">
  <Fragment slot="curl">

    ```sh
    curl -X DELETE 'https://api.filudi.com/v1/sdk/saves/gloop' \
      -H "authorization: Bearer $FILUDI_PLAYER_TOKEN"
    ```

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

    ```js
    // playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game
    const response = await fetch('https://api.filudi.com/v1/sdk/saves/gloop', {
      method: 'DELETE',
      headers: { authorization: `Bearer ${playerToken}` },
    });
    // No body: check response.ok.
    ```

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

    ```py
    import requests

    # player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game
    response = requests.delete(
        'https://api.filudi.com/v1/sdk/saves/gloop',
        headers={'authorization': f'Bearer {player_token}'},
    )
    response.raise_for_status()
    # No body: the status says it all.
    ```

  </Fragment>
</CodePanel>

**Delete a save**

Credential: a player token (`fp1`), sent as `Authorization: Bearer <token>`. A game token (`fg1`) also opens this route, for its own game only.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `gameId` | path | string | yes | Game slug |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 204 | none | Deleted or absent. |

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

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

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

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

    ```js
    // playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game
    const response = await fetch('https://api.filudi.com/v1/sdk/leaderboards/gloop/height', {
      headers: { authorization: `Bearer ${playerToken}` },
    });
    const data = await response.json();
    ```

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

    ```py
    import requests

    # player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game
    response = requests.get(
        'https://api.filudi.com/v1/sdk/leaderboards/gloop/height',
        headers={'authorization': f'Bearer {player_token}'},
    )
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**Top entries**

Credential, optional: a player token (`fp1`), sent as `Authorization: Bearer <token>`. A game token (`fg1`) also opens this route, for its own game only. Without a token the answer is the anonymous one.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `gameId` | path | string | yes | Game slug |
| `board` | path | string | yes | pattern `^[a-z][a-z0-9_]{0,31}$` |
| `period` | query | one of `"day"`, `"week"`, `"all"` | no | default `"all"` |
| `limit` | query | integer | no | 1 to 50; default `10` |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 200 | [LeaderboardTop](/docs/api/reference/schemas/#schema-leaderboardtop) | Top entries; `isMe` is set when a token is sent. Headers: `Cache-Control`. |
| 404 | [Error](/docs/api/reference/schemas/#schema-error) | Unknown resource. |

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

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

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

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

    ```js
    // playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game
    const response = await fetch('https://api.filudi.com/v1/sdk/leaderboards/gloop/height/me', {
      headers: { authorization: `Bearer ${playerToken}` },
    });
    const data = await response.json();
    ```

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

    ```py
    import requests

    # player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game
    response = requests.get(
        'https://api.filudi.com/v1/sdk/leaderboards/gloop/height/me',
        headers={'authorization': f'Bearer {player_token}'},
    )
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**The player's own entry**

Credential: a player token (`fp1`), sent as `Authorization: Bearer <token>`. A game token (`fg1`) also opens this route, for its own game only.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `gameId` | path | string | yes | Game slug |
| `board` | path | string | yes | pattern `^[a-z][a-z0-9_]{0,31}$` |
| `period` | query | one of `"day"`, `"week"`, `"all"` | no | default `"all"` |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 200 | object | Entry or null. |

**Body of 200**

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

<RouteHeading id="post-v1-sdk-leaderboards-gameid-board-scores" method="POST" path="/v1/sdk/leaderboards/{gameId}/{board}/scores" />

<CodePanel title="POST /v1/sdk/leaderboards/{gameId}/{board}/scores">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/sdk/leaderboards/gloop/height/scores' \
      -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \
      -H 'content-type: application/json' \
      -d '{
      "score": 0,
      "meta": {
        "en": "string"
      },
      "sessionId": "00000000-0000-0000-0000-000000000000",
      "nonce": "string",
      "ts": 0,
      "sig": "string"
    }'
    ```

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

    ```js
    // playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game
    const response = await fetch('https://api.filudi.com/v1/sdk/leaderboards/gloop/height/scores', {
      method: 'POST',
      headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' },
      body: JSON.stringify({
        score: 0,
        meta: {
          en: 'string',
        },
        sessionId: '00000000-0000-0000-0000-000000000000',
        nonce: 'string',
        ts: 0,
        sig: 'string',
      }),
    });
    const data = await response.json();
    ```

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

    ```py
    import requests

    # player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game
    response = requests.post(
        'https://api.filudi.com/v1/sdk/leaderboards/gloop/height/scores',
        headers={'authorization': f'Bearer {player_token}'},
        json={
            'score': 0,
            'meta': {
                'en': 'string',
            },
            'sessionId': '00000000-0000-0000-0000-000000000000',
            'nonce': 'string',
            'ts': 0,
            'sig': 'string',
        },
    )
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**Submit a signed score**

`sig` = base64url(HMAC-SHA256(sessionKey, `${playerId}|${gameId}|${board}|${score}|${nonce}|${ts}`)).
`ts` is unix milliseconds, within 5 minutes of server time. The API accepts a nonce once per session.

Credential: a player token (`fp1`), sent as `Authorization: Bearer <token>`. A game token (`fg1`) also opens this route, for its own game only.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `gameId` | path | string | yes | Game slug |
| `board` | path | string | yes | pattern `^[a-z][a-z0-9_]{0,31}$` |

### Request body

`application/json`, required.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `score` | integer | yes |  |
| `meta` | object | no | 512 bytes max |
| `meta.*` | string or number | no |  |
| `sessionId` | string (uuid) | yes |  |
| `nonce` | string | yes | 16 to 64 characters |
| `ts` | integer | yes |  |
| `sig` | string | yes |  |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 201 | object | Accepted. |
| 400 | object | Rejected. |
| 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. |

**Body of 201**

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `rank` | object | yes | 0 when Redis could not rank the score; the next rebuild fixes it |
| `rank.day` | integer | yes |  |
| `rank.week` | integer | yes |  |
| `rank.all` | integer | yes |  |
| `best` | boolean | yes | New personal best on this board |

**Body of 400**

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | `"invalid"` | yes |  |
| `reason` | one of `"bounds"`, `"signature"`, `"session"`, `"board"`, `"nonce"`, `"clock"`, `"meta"` | yes |  |
| `message` | string | no |  |

<RouteHeading id="post-v1-sdk-events" method="POST" path="/v1/sdk/events" />

<CodePanel title="POST /v1/sdk/events">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/sdk/events' \
      -H "authorization: Bearer $FILUDI_PLAYER_TOKEN" \
      -H 'content-type: application/json' \
      -d '{
      "events": [
        {
          "name": "string",
          "props": {
            "en": "string"
          },
          "ts": 0,
          "gameId": "gloop",
          "gameVersion": "1.0.0",
          "sessionId": "00000000-0000-0000-0000-000000000000",
          "host": "string",
          "locale": "en"
        }
      ]
    }'
    ```

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

    ```js
    // playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game
    const response = await fetch('https://api.filudi.com/v1/sdk/events', {
      method: 'POST',
      headers: { authorization: `Bearer ${playerToken}`, 'content-type': 'application/json' },
      body: JSON.stringify({
        events: [{
          name: 'string',
          props: {
            en: 'string',
          },
          ts: 0,
          gameId: 'gloop',
          gameVersion: '1.0.0',
          sessionId: '00000000-0000-0000-0000-000000000000',
          host: 'string',
          locale: 'en',
        }],
      }),
    });
    // No body: check response.ok.
    ```

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

    ```py
    import requests

    # player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this game
    response = requests.post(
        'https://api.filudi.com/v1/sdk/events',
        headers={'authorization': f'Bearer {player_token}'},
        json={
            'events': [{
                'name': 'string',
                'props': {
                    'en': 'string',
                },
                'ts': 0,
                'gameId': 'gloop',
                'gameVersion': '1.0.0',
                'sessionId': '00000000-0000-0000-0000-000000000000',
                'host': 'string',
                'locale': 'en',
            }],
        },
    )
    response.raise_for_status()
    # No body: the status says it all.
    ```

  </Fragment>
</CodePanel>

**Batch of events**

The SDK sends `application/json` normally and the same JSON body as `text/plain` from a
keepalive fetch on page hide (`sendBeacon` only on portals, without a token). The API parses both as JSON. 64 kB max.
With a game token, an item that names another game refuses the whole batch with 403, and an item that names none takes the token's game.

Credential: a player token (`fp1`), sent as `Authorization: Bearer <token>`. A game token (`fg1`) also opens this route, for its own game only.

### Request body

`application/json`, required, also accepted as `text/plain`. The body is a [EventBatch](/docs/api/reference/schemas/#schema-eventbatch).

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

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 202 | none | Queued for write; the API drops invalid items and counts them in metrics. |
| 400 | none | More than 50 items, or a body that is not a batch. |
| 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. |
| 403 | [Error](/docs/api/reference/schemas/#schema-error) | Not allowed for this role or site. |
| 413 | [Error](/docs/api/reference/schemas/#schema-error) | Body over the limit. |
| 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. |
