SDK routes
Generated from openapi.yaml of games-api 0.7.0: 13 routes.
POST /v1/sdk/players: Create an anonymous player and its tokenPOST /v1/sdk/game-tokens: Exchange a player token for a token limited to one game (ADR 0013)GET /v1/sdk/players/me: Current playerPATCH /v1/sdk/players/me: Change nickname or localePOST /v1/sdk/sessions: Open a play session and get the score-signing keyGET /v1/sdk/saves: List the player’s saves (metadata only)GET /v1/sdk/saves/{gameId}: Read a savePUT /v1/sdk/saves/{gameId}: Write a saveDELETE /v1/sdk/saves/{gameId}: Delete a saveGET /v1/sdk/leaderboards/{gameId}/{board}: Top entriesGET /v1/sdk/leaderboards/{gameId}/{board}/me: The player’s own entryPOST /v1/sdk/leaderboards/{gameId}/{board}/scores: Submit a signed scorePOST /v1/sdk/events: Batch of events
POST /v1/sdk/players
curl -X POST 'https://api.filudi.com/v1/sdk/players' \ -H 'content-type: application/json' \ -d '{ "site": "filudi", "locale": "en"}'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();import requests
response = requests.post( 'https://api.filudi.com/v1/sdk/players', json={ 'site': 'filudi', 'locale': 'en', },)response.raise_for_status()data = response.json()Create an anonymous player and its token
Credential: none. The route is public.
Request body
Section titled “Request body”application/json, required.
| Property | Type | Required | Description |
|---|---|---|---|
site | string | yes | Site slug |
locale | string | no |
Responses
Section titled “Responses”| Status | Body | Description |
|---|---|---|
| 201 | PlayerWithToken | Player and token. |
| 404 | Error | Unknown resource. |
| 429 | Error | Too many requests. Headers: Retry-After. |
POST /v1/sdk/game-tokens
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"}'// playerToken: the fp1 token of POST /v1/sdk/playersconst 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();import requests
# player_token: the fp1 token of POST /v1/sdk/playersresponse = requests.post( 'https://api.filudi.com/v1/sdk/game-tokens', headers={'authorization': f'Bearer {player_token}'}, json={ 'game': 'gloop', },)response.raise_for_status()data = response.json()Exchange a player token for a token limited to one game (ADR 0013)
Called by the hub, never by a game. Refuses a game token. Limited to 240 per player per hour.
A game token (fg1) is valid 24 hours on these routes, for its own game: GET /v1/sdk/players/me
(emailMasked null, hasPassword false), /v1/sdk/saves/{gameId}, POST /v1/sdk/sessions,
/v1/sdk/leaderboards/{gameId}/*, POST /v1/sdk/events (403 when an item names another game).
Those routes answer 403 for another game. Every other route that needs a player answers 401.
Credential: a player token (fp1), sent as Authorization: Bearer <token>. A game token (fg1) answers 401 here.
Request body
Section titled “Request body”application/json, required.
| Property | Type | Required | Description |
|---|---|---|---|
game | string | yes | Game slug |
Responses
Section titled “Responses”| Status | Body | Description |
|---|---|---|
| 201 | GameToken | Game token. |
| 401 | Error | Missing or invalid token. |
| 404 | Error | Unknown resource. |
| 429 | Error | Too many requests. Headers: Retry-After. |
GET /v1/sdk/players/me
curl 'https://api.filudi.com/v1/sdk/players/me' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN"// playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this gameconst response = await fetch('https://api.filudi.com/v1/sdk/players/me', { headers: { authorization: `Bearer ${playerToken}` },});const data = await response.json();import requests
# player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this gameresponse = requests.get( 'https://api.filudi.com/v1/sdk/players/me', headers={'authorization': f'Bearer {player_token}'},)response.raise_for_status()data = response.json()Current player
Credential: a player token (fp1), sent as Authorization: Bearer <token>. A game token (fg1) also opens this route, for its own game only.
Responses
Section titled “Responses”| Status | Body | Description |
|---|---|---|
| 200 | Player | Player. |
| 401 | Error | Missing or invalid token. |
PATCH /v1/sdk/players/me
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"}'// playerToken: the fp1 token of POST /v1/sdk/playersconst 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();import requests
# player_token: the fp1 token of POST /v1/sdk/playersresponse = requests.patch( 'https://api.filudi.com/v1/sdk/players/me', headers={'authorization': f'Bearer {player_token}'}, json={ 'nickname': 'string', 'locale': 'en', },)response.raise_for_status()data = response.json()Change nickname or locale
Credential: a player token (fp1), sent as Authorization: Bearer <token>. A game token (fg1) answers 401 here.
Request body
Section titled “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
Section titled “Responses”| Status | Body | Description |
|---|---|---|
| 200 | Player | Updated player. |
| 400 | Error | Validation failed. |
| 401 | Error | Missing or invalid token. |
POST /v1/sdk/sessions
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"}'// playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this gameconst 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();import requests
# player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this gameresponse = requests.post( 'https://api.filudi.com/v1/sdk/sessions', headers={'authorization': f'Bearer {player_token}'}, json={ 'gameId': 'gloop', 'gameVersion': '1.0.0', 'host': 'filudi', },)response.raise_for_status()data = response.json()Open a play session and get the score-signing key
Called by gameplay.start(). The key is HMAC-SHA256(version secret, session id),
base64url. Valid 6 hours. The API rejects scores submitted outside a session.
Credential: a player token (fp1), sent as Authorization: Bearer <token>. A game token (fg1) also opens this route, for its own game only.
Request body
Section titled “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
Section titled “Responses”| Status | Body | Description |
|---|---|---|
| 201 | object | Session. |
| 401 | Error | Missing or invalid token. |
| 404 | Error | Unknown resource. |
| 429 | 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 |
GET /v1/sdk/saves
curl 'https://api.filudi.com/v1/sdk/saves' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN"// playerToken: the fp1 token of POST /v1/sdk/playersconst response = await fetch('https://api.filudi.com/v1/sdk/saves', { headers: { authorization: `Bearer ${playerToken}` },});const data = await response.json();import requests
# player_token: the fp1 token of POST /v1/sdk/playersresponse = requests.get( 'https://api.filudi.com/v1/sdk/saves', headers={'authorization': f'Bearer {player_token}'},)response.raise_for_status()data = response.json()List the player’s saves (metadata only)
Credential: a player token (fp1), sent as Authorization: Bearer <token>. A game token (fg1) answers 401 here.
Responses
Section titled “Responses”| Status | Body | Description |
|---|---|---|
| 200 | SaveMeta[] | Saves. |
GET /v1/sdk/saves/{gameId}
curl 'https://api.filudi.com/v1/sdk/saves/gloop' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN"// playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this gameconst response = await fetch('https://api.filudi.com/v1/sdk/saves/gloop', { headers: { authorization: `Bearer ${playerToken}` },});const data = await response.json();import requests
# player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this gameresponse = requests.get( 'https://api.filudi.com/v1/sdk/saves/gloop', headers={'authorization': f'Bearer {player_token}'},)response.raise_for_status()data = response.json()Read a save
Credential: a player token (fp1), sent as Authorization: Bearer <token>. A game token (fg1) also opens this route, for its own game only.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
gameId | path | string | yes | Game slug |
Responses
Section titled “Responses”| Status | Body | Description |
|---|---|---|
| 200 | Save | Save. Headers: ETag. |
| 404 | Error | Unknown resource. |
PUT /v1/sdk/saves/{gameId}
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": {}}'// playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this gameconst 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();import requests
# player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this gameresponse = requests.put( 'https://api.filudi.com/v1/sdk/saves/gloop', headers={'authorization': f'Bearer {player_token}', 'if-match': 'string'}, json={ 'version': 1, 'data': {}, },)response.raise_for_status()data = response.json()Write a save
If-Match carries the version the client last saw (0 for none). The server accepts when
the new version is greater than the stored one and If-Match equals the stored version.
Otherwise 409 with the server copy.
Credential: a player token (fp1), sent as Authorization: Bearer <token>. A game token (fg1) also opens this route, for its own game only.
Parameters
Section titled “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
Section titled “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
Section titled “Responses”| Status | Body | Description |
|---|---|---|
| 200 | SaveMeta | Saved. Headers: ETag. |
| 400 | Error | Validation failed. |
| 404 | Error | Unknown resource. |
| 409 | SaveServerCopy | Server has another version, or none yet (version 0). |
| 413 | Error | Body over the limit. |
DELETE /v1/sdk/saves/{gameId}
curl -X DELETE 'https://api.filudi.com/v1/sdk/saves/gloop' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN"// playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this gameconst response = await fetch('https://api.filudi.com/v1/sdk/saves/gloop', { method: 'DELETE', headers: { authorization: `Bearer ${playerToken}` },});// No body: check response.ok.import requests
# player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this gameresponse = requests.delete( 'https://api.filudi.com/v1/sdk/saves/gloop', headers={'authorization': f'Bearer {player_token}'},)response.raise_for_status()# No body: the status says it all.Delete a save
Credential: a player token (fp1), sent as Authorization: Bearer <token>. A game token (fg1) also opens this route, for its own game only.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
gameId | path | string | yes | Game slug |
Responses
Section titled “Responses”| Status | Body | Description |
|---|---|---|
| 204 | none | Deleted or absent. |
GET /v1/sdk/leaderboards/{gameId}/{board}
curl 'https://api.filudi.com/v1/sdk/leaderboards/gloop/height' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN"// playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this gameconst response = await fetch('https://api.filudi.com/v1/sdk/leaderboards/gloop/height', { headers: { authorization: `Bearer ${playerToken}` },});const data = await response.json();import requests
# player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this gameresponse = requests.get( 'https://api.filudi.com/v1/sdk/leaderboards/gloop/height', headers={'authorization': f'Bearer {player_token}'},)response.raise_for_status()data = response.json()Top entries
Credential, optional: a player token (fp1), sent as Authorization: Bearer <token>. A game token (fg1) also opens this route, for its own game only. Without a token the answer is the anonymous one.
Parameters
Section titled “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
Section titled “Responses”| Status | Body | Description |
|---|---|---|
| 200 | LeaderboardTop | Top entries; isMe is set when a token is sent. Headers: Cache-Control. |
| 404 | Error | Unknown resource. |
GET /v1/sdk/leaderboards/{gameId}/{board}/me
curl 'https://api.filudi.com/v1/sdk/leaderboards/gloop/height/me' \ -H "authorization: Bearer $FILUDI_PLAYER_TOKEN"// playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this gameconst response = await fetch('https://api.filudi.com/v1/sdk/leaderboards/gloop/height/me', { headers: { authorization: `Bearer ${playerToken}` },});const data = await response.json();import requests
# player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this gameresponse = requests.get( 'https://api.filudi.com/v1/sdk/leaderboards/gloop/height/me', headers={'authorization': f'Bearer {player_token}'},)response.raise_for_status()data = response.json()The player’s own entry
Credential: a player token (fp1), sent as Authorization: Bearer <token>. A game token (fg1) also opens this route, for its own game only.
Parameters
Section titled “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
Section titled “Responses”| Status | Body | Description |
|---|---|---|
| 200 | object | Entry or null. |
Body of 200
| Property | Type | Required | Description |
|---|---|---|---|
period | Period | yes | default "all" |
entry | LeaderboardEntry or null | yes |
POST /v1/sdk/leaderboards/{gameId}/{board}/scores
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"}'// playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this gameconst 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();import requests
# player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this gameresponse = requests.post( 'https://api.filudi.com/v1/sdk/leaderboards/gloop/height/scores', headers={'authorization': f'Bearer {player_token}'}, json={ 'score': 0, 'meta': { 'en': 'string', }, 'sessionId': '00000000-0000-0000-0000-000000000000', 'nonce': 'string', 'ts': 0, 'sig': 'string', },)response.raise_for_status()data = response.json()Submit a signed score
sig = base64url(HMAC-SHA256(sessionKey, ${playerId}|${gameId}|${board}|${score}|${nonce}|${ts})).
ts is unix milliseconds, within 5 minutes of server time. The API accepts a nonce once per session.
Credential: a player token (fp1), sent as Authorization: Bearer <token>. A game token (fg1) also opens this route, for its own game only.
Parameters
Section titled “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
Section titled “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
Section titled “Responses”| Status | Body | Description |
|---|---|---|
| 201 | object | Accepted. |
| 400 | object | Rejected. |
| 429 | 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 |
POST /v1/sdk/events
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" } ]}'// playerToken: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this gameconst 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.import requests
# player_token: the fp1 token of POST /v1/sdk/players, or the fg1 game token of this gameresponse = requests.post( 'https://api.filudi.com/v1/sdk/events', headers={'authorization': f'Bearer {player_token}'}, json={ 'events': [{ 'name': 'string', 'props': { 'en': 'string', }, 'ts': 0, 'gameId': 'gloop', 'gameVersion': '1.0.0', 'sessionId': '00000000-0000-0000-0000-000000000000', 'host': 'string', 'locale': 'en', }], },)response.raise_for_status()# No body: the status says it all.Batch of events
The SDK sends application/json normally and the same JSON body as text/plain from a
keepalive fetch on page hide (sendBeacon only on portals, without a token). The API parses both as JSON. 64 kB max.
With a game token, an item that names another game refuses the whole batch with 403, and an item that names none takes the token’s game.
Credential: a player token (fp1), sent as Authorization: Bearer <token>. A game token (fg1) also opens this route, for its own game only.
Request body
Section titled “Request body”application/json, required, also accepted as text/plain. The body is a EventBatch.
| Property | Type | Required | Description |
|---|---|---|---|
events | EventIn[] | yes | at most 50 items |
Responses
Section titled “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 | Missing or invalid token. |
| 403 | Error | Not allowed for this role or site. |
| 413 | Error | Body over the limit. |
| 429 | Error | Too many requests. Headers: Retry-After. |