# Launching games

## 4.1 Modes

A click on a game in a widget is handled the way agreed for your installation (form,
`widgets.game_launch`; manifest `launch.mode`):

| Mode | What happens |
| :--- | :--- |
| `sdk-modal` | the SDK launches the game itself and shows it in a modal iframe over your page (§2.3, §4.3) |
| `redirect` | the page goes to your URL template with the game's values in place, e.g. `/game/{gameId}`; with target `_blank` the URL opens in a new tab |
| `popup` | the same URL opens in a new window |
| `host-handled` | the widget does nothing itself and tells your page (§4.2); your page launches the game, on GA as in §4.3 |

The URL template takes three placeholders, each filled URL-encoded, in every place it appears:

| Placeholder | Value |
| :--- | :--- |
| `{gameId}` | the game's id in the catalogue — unique across all providers |
| `{gameSlug}` | the game's code at its provider; when the game has none, `{gameId}` |
| `{providerId}` | the provider's id in GA's catalogue; for the catalogue you send us (§8.1), its `provider` |

A game code is unique only within its provider: two providers can offer a game under the same code.
Route on `{gameId}`, or on `{providerId}` and `{gameSlug}` together — never on `{gameSlug}` alone.

## 4.2 The `ig:game-launch` event

```json
{
  "gameId": "019edf56-a69e-75ed-bf7f-06706720a9d6",
  "gameSlug": "10869",
  "providerId": "00000000-0000-0000-0000-000000000de3",
  "providerName": "Yggdrasil",
  "gameTitle": "Hercules Fortune Quest",
  "mode": "redirect",
  "url": "/game/019edf56-a69e-75ed-bf7f-06706720a9d6"
}
```

`gameSlug` is absent when the game has no code; `url` is present for `redirect` and `popup` only.
With `sdk-modal` the event comes after the modal has opened, carries `"mode": "sdk-modal"` and no `url`:
the SDK has launched the game itself (§2.3).

With `host-handled` your page launches the game. A minimal handler:

```js
document.addEventListener('ig:game-launch', (e) => {
  const { gameId, gameSlug, providerId, system, page } = e.detail;
  // launch the game on GA as in §4.3; with `system` and `page` — by that pair
});
```

`gameId` is the game's UUID in GA's catalogue; `gameSlug` is the game's code at its provider (absent
when the game has none); `providerId` is the provider's id in GA's catalogue. `gameSlug` is unique only
within one provider, so route on `gameId`, or on `providerId` with `gameSlug` — never on `gameSlug` alone.

`system`, `page` are optional strings: the System/Page pair from Game/List for operators on the SoftGaming protocol; launch the game by it if it is present. A game without the pair has no such keys.

## 4.3 Launching on GA

If your games run through GA's aggregation, the chosen game is launched through GA. With `sdk-modal`
the SDK sends this request; with `host-handled` your page does. The player is proven by an operator
token (§3.2): on the click, a fresh one is taken (the SDK takes it from `tokenProvider`, §3.2.1) and
sent in the body. The GA player token of §3.1 stays inside the widgets; your page does not need it.

A `real` launch plays with the player's money: the game's provider asks GA for the balance and
every bet and win, and GA forwards each of them to **your wallet** (Operator API v1 or v2). Until your
wallet endpoint is connected, GA refuses `real` launches with `403` `wallet_not_configured`; `demo`
launches need no wallet.

### One token for the whole launch

The operator token your page already holds is the only token of a real-money launch. Pass it as
`operatorToken` when launching. GA verifies its signature, takes the player from it, and calls your
wallet once before the game provider is called, with this exact string as `launch_token`
(`Authenticate` if your wallet declares it, otherwise `GetBalance`).

- If your wallet refuses (invalid token, blocked or unknown player), the launch is refused with
  `401` `login_required` (in `sdk-modal` mode: `ig:login-required`) and no session stays open.
- If your wallet cannot be reached, the launch is refused with a retryable `503`; retry with a fresh
  token.
- The same string is then the `launch_token` on every wallet call of that session.

Your side:

- Mint a fresh token for every launch (for example a unique `jti`): the token is also the launch's
  idempotency key.
- Resolve the player from the token on every wallet call.
- Do not apply the JWT `exp` to a session that is already running; `exp` limits the time to start
  the game.

Only some games have a demo. A game's `has_demo` says it: `true` — `mode: "demo"` launches it;
`false` — the answer is `400` `demo_not_supported`, offer the game signed-in only. `has_demo` is in
every game of the catalogue the widgets read,
`GET https://<api host>/public/v1/catalog/lobby?operator_id=<your GA operator id>` (no key needed;
`data[].games[]`, the game's `id` is the event's `gameId`; a game may carry the optional strings `system` and `page` — the System/Page pair from Game/List for SoftGaming operators, absent when the game has none), and of GA's catalogue for your side
(§8.2).

```
POST https://<api host>/public/v1/embed/launch
Content-Type: application/json

{"gameId": "019edf56-a69e-75ed-bf7f-06706720a9d6", "providerId": "00000000-0000-0000-0000-000000000de3", "operatorToken": "<JWT of §3.2>", "mode": "real", "returnUrl": "https://casino.example/lobby", "lang": "en"}
```

| Field | Rule |
| :--- | :--- |
| `gameId` | the `gameId` of the event (§4.2) — unique on its own; a game code works too, but it is unique only together with `providerId` |
| `providerId` | optional; the `providerId` of the event. With a game code it picks that provider's game; with a `gameId` of another provider the answer is `404` |
| `operatorToken` | the operator token of §3.2 for the signed-in player, checked the same way: signed with your key, `HS256`, with `exp`. Currency and locale come from its claims |
| `mode` | `real` (default) needs `operatorToken` and your connected wallet; `demo` works without a token, then `operatorId` — your GA operator id — is required; only for a game with `has_demo: true` |
| `returnUrl` | where the game sends the player back |
| `lang` | the game's language; by default the player's, then `en` |

`200` `{"launchUrl":"…","mode":"popup","expiresAt":"…"}` — with `sdk-modal` the SDK opens `launchUrl`
in its modal (a `200` without `launchUrl` is `ig:game-launch-error`); with `host-handled` open it in an iframe or a new window.

| Status | `error` / `message` | Meaning |
| :--- | :--- | :--- |
| `400` | `bad_request` / what is wrong | no `gameId`, a `providerId` or `operatorId` that is not a UUID, a demo launch without `operatorId` |
| `400` | `bad_request` / `demo_not_supported` | a demo launch of a game with `has_demo: false` |
| `401` | `unauthorized` / `login_required` | a `real` launch without `operatorToken`, or with one that is not signed with your key, has no `exp` or has expired; take a fresh token. With `sdk-modal` the SDK raises `ig:login-required` with `{reason: 'launch'}` (§2.3); show your sign-in |
| `403` | `forbidden` / `wallet_not_configured` | a `real` launch before your wallet (Operator API v1 or v2) is connected; ask your manager to connect it |
| `401` | `unauthorized` / `login_required` | your wallet refused the operator token (invalid token, blocked or unknown player); no session stays open |
| `503` | retryable | your wallet could not be reached; retry with a fresh token |
| `403`, `404` | `forbidden`, `not_found` / the reason, e.g. `game_not_found` | the game is unavailable to you or does not exist |
| `502` | `bad_gateway` | our side failed; try again |

With `sdk-modal` the SDK sends `gameId`, `providerId`, `mode`, `operatorToken` (`real`) and `operatorId` (your GA operator id, when known). Every
answer except `200` and `401` raises `ig:game-launch-error` with `{status, reason}` and the game's fields
of §4.2: `status` is the HTTP status, `reason` is the `message` of the answer or `launch_failed`. A network
failure gives `status` `0`, `reason` `network_error`.

If your games are not on GA, you launch them yourself: `gameId` is the id you sent in your catalogue
(§8.1).
