Skip to content

A front of your own

Everything hub-core does is a few HTTP calls and a few postMessage calls. This page lists them, in the order a page runs them, so a front in any language embeds Gloop! with the handshake alone.

GET /v1/catalog/{site} is public: no token, no header. ?locale=en narrows the texts to one locale. The answer carries Cache-Control: public, max-age=300, stale-while-revalidate=600 and an ETag; a 304 has no body.

Catalog

Terminal window
curl 'https://api.filudi.com/v1/catalog/filudi?locale=en'

Each game has slug, order, featured, categories, tags, orientation, minutes, mobile, daily, coverColor, bundleUrl, version, publishedAt, boards, texts per locale, media and rating. Categories and tags come with their own texts. site.navCategories is the top menu; site.otherSites the sister sites of the footer. The Catalog schema has every field.

A visitor is a guest from the first page. POST /v1/sdk/players { site, locale } answers { player, token }. Keep the token in localStorage, never in a cookie, a URL or a log; it has no expiry and opens the whole account. Create it from the browser: the route allows 20 guests per hour per IP, and a server would spend that on its own address.

Guest

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

A 401 on any later call means the API refused the token: forget it and create a new guest. Sign-in by email link, code or password goes through /v1/account/* with the same token; the guest’s saves and scores merge into the account.

The frame never sees the player’s token. Before the game asks, exchange it: POST /v1/sdk/game-tokens { game: '<slug>' } with the player token answers { token, expiresAt }, an fg1 token valid 24 hours for that game only. The API answers 404 unless the game is on your site’s catalog. Do the exchange when you create the iframe: the game waits 1.5 s for the answer once it says filudi:ready.

Game token

Terminal window
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" }'

Use bundleUrl from the catalog as it is. Do not build it from the slug and do not copy the bundle to your origin. The iframe needs sandbox="allow-scripts allow-same-origin allow-pointer-lock" and allow="fullscreen".

<iframe
id="game"
src="https://gloop.<play domain>/gloop/1.4.0/index.html"
title="Gloop!"
sandbox="allow-scripts allow-same-origin allow-pointer-lock"
allow="fullscreen"
></iframe>

Your page’s CSP must allow the frame and the API:

frame-src https://play.filudi.com https://*.<play domain>
connect-src 'self' https://api.filudi.com
img-src 'self' data: https://play.filudi.com
media-src https://play.filudi.com

Every message is a postMessage with a type that starts with filudi:. Post to the bundle’s exact origin, read from new URL(bundleUrl).origin, never to *. Accept a message only when event.source is the frame’s contentWindow and event.origin is that origin.

From the game to your page:

MessagePayloadWhen
filudi:ready{ sdk: '1' }At sdk.init(). Answer with filudi:init within 1.5 s. The game may repeat it after a reload
filudi:loaded{}The first frame is drawn. Hide your loader. Send frame_loaded to POST /v1/sdk/events with the player token if you want the funnel’s first step
filudi:gameplay{ running: boolean }Real play starts or stops
filudi:fullscreen{ request: true }The game asks for fullscreen. Call requestFullscreen() on the iframe if the player just clicked inside it, then answer filudi:fullscreen { ok }
filudi:ad{ id, kind, phase: 'request' }An ad request. Without an ad network, answer { id, phase: 'result', result: { status: 'unavailable', reason: 'host' } }
filudi:save{ version }A save was written. A claim: read saves from the API if you show them
filudi:score{ board, score, rank }A score was sent. A claim: the rank that counts is in the API
filudi:track{ event }A custom event. A claim; no listener needed
filudi:unauthorized{}The API refused the game token. Exchange a new one and reload the frame, once per load

From your page to the game:

MessagePayloadWhen
filudi:initsee belowIn answer to filudi:ready
filudi:token{ token }A fresh game token, every 12 hours while the frame lives
filudi:consent{ consent: { ads, analytics } }The visitor’s consent changed
filudi:locale{ locale }The page locale changed
filudi:pause, filudi:resume{}You cover or uncover the game
filudi:fullscreen{ ok: boolean }In answer to the game’s request
filudi:ad{ id, phase: 'started' }, then { id, phase: 'result', result }Around an ad you show

filudi:init carries everything the game needs:

interface FiludiInit {
type: 'filudi:init';
apiUrl: string; // https://api.filudi.com
token: string; // the fg1 game token, never the player's fp1
gameId: string; // the slug this frame was opened for
player: { id: string; nickname: string; guest: boolean };
locale: string;
site: string; // your site's slug
policy: { personalizedAds: boolean; ageBand: 'all' | 'kids' | 'teen' | 'adult'; accounts: boolean };
consent: { ads: boolean; analytics: boolean };
gameVersion: string; // the catalog's version
}

player comes from GET /v1/sdk/players/me with the player token. policy.personalizedAds is true only when the site allows it, the visitor consented and the age band is adult.

Parent side of the handshake

const frame = document.querySelector('#game');
const origin = new URL(gloop.bundleUrl).origin;
const me = await (await fetch('https://api.filudi.com/v1/sdk/players/me', {
headers: { authorization: `Bearer ${token}` },
})).json();
window.addEventListener('message', async (event) => {
if (event.source !== frame.contentWindow || event.origin !== origin) return;
const message = event.data;
if (message?.type === 'filudi:ready') {
const game = await exchange; // { token, expiresAt } from step 3
frame.contentWindow.postMessage(
{
type: 'filudi:init',
apiUrl: 'https://api.filudi.com',
token: game.token,
gameId: 'gloop',
player: { id: me.id, nickname: me.nickname, guest: me.guest },
locale: 'en',
site: 'filudi',
policy: { personalizedAds: false, ageBand: catalog.site.policy.ageBand, accounts: catalog.site.policy.accountsAllowed },
consent: { ads: false, analytics: false },
gameVersion: gloop.version,
},
origin,
);
}
if (message?.type === 'filudi:loaded') hideLoader();
if (message?.type === 'filudi:ad' && message.phase === 'request') {
frame.contentWindow.postMessage(
{ type: 'filudi:ad', id: message.id, phase: 'result', result: { status: 'unavailable', reason: 'host' } },
origin,
);
}
if (message?.type === 'filudi:fullscreen' && message.request) {
frame.contentWindow.postMessage({ type: 'filudi:fullscreen', ok: false }, origin);
}
});

Without filudi:init within 1.5 s the SDK goes standalone: local saves, no scores, no filudi:loaded. A late filudi:init is ignored. Posting the game token early, from memory, is what keeps the deadline.

A game token lasts 24 hours. While the frame lives, exchange a new one every 12 hours and post filudi:token { token }; the SDK uses it from its next request, without a reload. On filudi:unauthorized, exchange once and reload the frame; if the exchange itself answers 401, the API refused the player’s own token: forget it, create a new guest, start again.

The frame loads only when your page’s origin is in the bundle’s frame-ancestors, and the API answers your page only when that origin is allowed. Both lists are the approved domains of approved sites. Until staff approves yours, build on the development origins (Start here).