# A front of your own

The public catalog, the frame handshake message by message, the game token exchange, frame-ancestors and your domains. Any stack.

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.

## 1. Read the catalog

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

<CodePanel title="Catalog">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/catalog/filudi?locale=en'
    ```

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

    ```js
    const catalog = await (await fetch('https://api.filudi.com/v1/catalog/filudi?locale=en')).json();
    const gloop = catalog.games.find((game) => game.slug === 'gloop');
    // gloop.bundleUrl is the iframe address; null means no live version.
    ```

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

    ```py
    import requests

    catalog = requests.get('https://api.filudi.com/v1/catalog/filudi', params={'locale': 'en'}).json()
    gloop = next(game for game in catalog['games'] if game['slug'] == 'gloop')
    bundle_url = gloop['bundleUrl']  # None without a live version
    ```

  </Fragment>
</CodePanel>

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](/docs/api/reference/schemas/#schema-catalog) has every field.

## 2. Create the player, in the browser

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.

<CodePanel title="Guest">
  <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 KEY = 'filudi:player:filudi';
    let token = localStorage.getItem(KEY);
    if (!token) {
      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' }),
      });
      token = (await response.json()).token;
      localStorage.setItem(KEY, token);
    }
    ```

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

    ```py
    # A server does not create guests: every visitor would share its IP.
    # A backend creates players by externalId with a server key instead:
    # see "A backend with server keys".
    ```

  </Fragment>
</CodePanel>

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.

## 3. Exchange it for a game token

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

<CodePanel title="Game token">
  <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
    const exchange = fetch('https://api.filudi.com/v1/sdk/game-tokens', {
      method: 'POST',
      headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
      body: JSON.stringify({ game: 'gloop' }),
    }).then((response) => response.json());
    // exchange resolves to { token: 'fg1....', expiresAt }
    ```

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

    ```py
    # From a backend, with a server key and one of your players:
    response = requests.post(
        'https://api.filudi.com/v1/server/game-tokens',
        headers={'authorization': f'Bearer {site_key}'},
        json={'playerId': player_id, 'game': 'gloop'},
    )
    game_token = response.json()['token']
    ```

  </Fragment>
</CodePanel>

## 4. Create the frame

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

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

## 5. The handshake, message by message

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:

| Message               | Payload                          | When                                                                                                                                                |
| --------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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:

| Message                         | Payload                                                            | When                                                     |
| ------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------- |
| `filudi:init`                   | see below                                                          | In 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:

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

<CodePanel title="Parent side of the handshake">
  <Fragment slot="javascript">

    ```js
    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);
      }
    });
    ```

  </Fragment>
  <Fragment slot="typescript">

    ```ts
    type FromGame =
      | { type: 'filudi:ready'; sdk: string }
      | { type: 'filudi:loaded' }
      | { type: 'filudi:gameplay'; running: boolean }
      | { type: 'filudi:fullscreen'; request: true }
      | { type: 'filudi:ad'; id: string; kind: 'rewarded' | 'interstitial'; phase: 'request' }
      | { type: 'filudi:save'; version: number }
      | { type: 'filudi:score'; board: string; score: number; rank: { day: number; week: number; all: number } | null }
      | { type: 'filudi:track'; event: string }
      | { type: 'filudi:unauthorized' };

    function isFromGame(event: MessageEvent, frame: HTMLIFrameElement, origin: string): event is MessageEvent<FromGame> {
      return event.source === frame.contentWindow && event.origin === origin && typeof event.data?.type === 'string';
    }
    ```

  </Fragment>
</CodePanel>

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.

## 6. Keep the token fresh

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.

## 7. Your domains

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](/docs/portals/start-here/)).
