A backend with server keys
A server key lets your backend act for your site on /v1/server/*: create a Filudi player for each of your
users, give the browser a token for that player, read leaderboards with nicknames, read the site’s numbers.
The key never reaches a browser and nothing else of the API takes it.
Keys and scopes
Section titled “Keys and scopes”Create a key in the console, under the site, Keys: a name and the scopes it needs. The answer shows the key
once, fsk_<site slug>_<32 base62 characters>; the API keeps a hash and the console shows the first 12
characters from then on. Store it in your backend’s environment. Only an approved site has keys, 20 at most,
revoked ones included.
| Scope | Routes |
|---|---|
catalog | GET /v1/server/catalog |
players | POST /v1/server/players, GET /v1/server/players/{playerId} |
tokens | POST /v1/server/players/{playerId}/tokens, POST /v1/server/game-tokens |
leaderboards | GET /v1/server/leaderboards/{gameId}/{board} |
stats | GET /v1/server/stats/overview, GET /v1/server/stats/games/{gameId} |
A key without the scope of a route answers 403, as does a revoked key or a key of a site that lost its
approval; message says which. 600 requests a minute per key, then 429 with Retry-After. No IP limit, no CORS.
Revoke a key from the console: the row stays with revokedAt and every call with it answers 403 from then on.
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": ["players", "tokens", "leaderboards"] }'// From the console, in the browser. The answer carries `key` once.const response = await fetch(`https://api.filudi.com/v1/admin/sites/${siteId}/keys`, { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: 'string', scopes: ['players', 'tokens', 'leaderboards'] }),});const { key, prefix } = await response.json();# The console does this for you. Keep the key in your backend's environment:# FILUDI_SITE_KEY=fsk_...import os
site_key = os.environ['FILUDI_SITE_KEY']Players by your own user id
Section titled “Players by your own user id”POST /v1/server/players { externalId, nickname?, locale? } creates a player for one of your users, or finds
the one created before: externalId is unique per site, so the call is safe to repeat. A new player answers
201, an existing one 200. A player created here is a guest with no token until you ask for one. nickname and
locale apply on creation only; the player changes them later with their own token on
PATCH /v1/sdk/players/me.
GET /v1/server/players/{playerId} reads the profile, the saves metadata and the best scores. A player of
another site answers 404.
curl -X POST 'https://api.filudi.com/v1/server/players' \ -H "authorization: Bearer $FILUDI_SITE_KEY" \ -H 'content-type: application/json' \ -d '{ "externalId": "string", "nickname": "Pebble", "locale": "en" }'// Node, in your backend, on each sign-in of one of your users.async function filudiPlayer(user) { const response = await fetch('https://api.filudi.com/v1/server/players', { method: 'POST', headers: { authorization: `Bearer ${process.env.FILUDI_SITE_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ externalId: user.id, nickname: user.displayName, locale: user.locale }), }); if (!response.ok) throw new Error(`filudi: ${response.status}`); return response.json(); // { id, externalId, nickname, locale, hasAccount, createdAt, lastSeenAt }}import osimport requests
def filudi_player(user): response = requests.post( 'https://api.filudi.com/v1/server/players', headers={'authorization': f"Bearer {os.environ['FILUDI_SITE_KEY']}"}, json={'externalId': user.id, 'nickname': user.display_name, 'locale': user.locale}, ) response.raise_for_status() return response.json()Handing a token to the browser
Section titled “Handing a token to the browser”The page needs a player token to run a game as filudi.com does. POST /v1/server/players/{playerId}/tokens
answers an fp1 token, the same kind POST /v1/sdk/players answers in the browser. Give it to the page over
your own authenticated channel: the answer of your sign-in, a page you render for a signed-in user, a short
fetch on your origin. Never in a URL, never in a cookie readable by scripts of other origins.
In the page, the token goes where a guest token would: localStorage under filudi:player:<site>, then
PlayerClient or your own code uses it. The page exchanges it for a game token before each frame, as any
front does. A backend that renders the whole page can skip that step too: POST /v1/server/game-tokens { playerId, game } answers the fg1 token for the frame, and the page posts it in filudi:init without
holding the player token at all.
curl -X POST 'https://api.filudi.com/v1/server/players/00000000-0000-0000-0000-000000000000/tokens' \ -H "authorization: Bearer $FILUDI_SITE_KEY"// Your route, called by your page after sign-in. The token goes in the response body, not in a URL.app.post('/api/filudi-token', requireSession, async (request, reply) => { const player = await filudiPlayer(request.user); const response = await fetch(`https://api.filudi.com/v1/server/players/${player.id}/tokens`, { method: 'POST', headers: { authorization: `Bearer ${process.env.FILUDI_SITE_KEY}` }, }); const { token } = await response.json(); reply.header('cache-control', 'private, no-store').send({ token });});@app.post('/api/filudi-token')def filudi_token(): player = filudi_player(current_user()) response = requests.post( f"https://api.filudi.com/v1/server/players/{player['id']}/tokens", headers={'authorization': f"Bearer {os.environ['FILUDI_SITE_KEY']}"}, ) response.raise_for_status() return {'token': response.json()['token']}, 200, {'cache-control': 'private, no-store'}The whole exchange, from your user’s sign-in to the game calling the API:
Dashed arrows are answers. Solid ones are calls. The server key travels only on the arrows that leave your backend; the player token never leaves the browser; the frame sees the game token alone.
Leaderboards with nicknames
Section titled “Leaderboards with nicknames”GET /v1/server/leaderboards/{gameId}/{board}?period=week&limit=10 answers the same shape as the browser
route, with the nickname of each player. isMe is always false: a server has no “me”. Use it for a page
rendered on the server, a Discord bot, a weekly email. The browser route stays the one to use inside a page,
where the player’s own entry matters.
curl 'https://api.filudi.com/v1/server/leaderboards/gloop/height?period=week&limit=10' \ -H "authorization: Bearer $FILUDI_SITE_KEY"const response = await fetch( 'https://api.filudi.com/v1/server/leaderboards/gloop/height?period=week&limit=10', { headers: { authorization: `Bearer ${process.env.FILUDI_SITE_KEY}` } },);const { entries, total } = await response.json();response = requests.get( 'https://api.filudi.com/v1/server/leaderboards/gloop/height', params={'period': 'week', 'limit': 10}, headers={'authorization': f"Bearer {os.environ['FILUDI_SITE_KEY']}"},)entries = response.json()['entries']Statistics
Section titled “Statistics”GET /v1/server/stats/overview?from&to and GET /v1/server/stats/games/{gameId}?from&to answer what the
console shows for the site: the same shapes as the admin routes, cached 60 seconds, 400 days at most.
Statistics lists the fields.
What a backend cannot do
Section titled “What a backend cannot do”- Store a player token. It has no expiry and opens the player’s account; it belongs in the player’s browser. Ask for a new one on each sign-in instead.
- Act on a player’s saves or scores. Saves and scores are written by the game, in the browser, with a game token; the API signs scores per play session and a server cannot forge that signature.
- Call
/v1/admin/*. The console API takes a session cookie, not a key. Automation of the console is not offered in this release. - Reach players of another site, or of filudi.com. A player id from your site means nothing elsewhere.