# The hosted shop

> Open a ready-made shop page for your game, identify the player with a signed link or an e-mail code, and bring them back to your game after a purchase.

## What you get

Every game can have a **hosted shop**: a public page at `/shop/<your game slug>` where a player sees their balance, buys packs, types a gift code or a creator code, and finds their latest purchases with the receipt. It is built for phones first, works in French, English and Dutch, and needs no code from you beyond a link.

Use it when your game runs on a phone, in an engine without a good checkout, or simply when you do not want to build a shop screen. The payment page, the VAT, the receipt and the withdrawal consent are the ones of GameCoin, as with any [pack purchase](/docs/guides/selling-packs).

What the player sees:

| Part | What it shows |
|---|---|
| Header | The shop name and tagline you chose, a link back to your game, the language switch (FR, EN, NL). |
| Balance | One card per currency, with the **paid** coins and the **free** coins told apart. |
| Code | A single field. A valid **gift code** is used at once and the balance updates. A **creator code** is kept for the next purchase and shown with its bonus. |
| Packs | Price in euros with VAT included, a badge (popular, best value, founder), the stock left and a countdown if you set an availability, and the states scheduled, sold out and ended. Before your launch the button says « pre-order ». |
| Purchases | The latest orders with their state and a link to the receipt. |

> [!NOTE]
> The shop is part of the **founder pack** and **codes** features: see [Founder packs](/docs/guides/founder-packs) and [Gift codes and creator codes](/docs/guides/codes). Anything you set there appears in the shop.

## Turn it on

In the dashboard (in French for now), open your game, then **Boutique**. Nothing is public until you tick **Activer la boutique**: until then the address answers « page not found », exactly as if it did not exist, whether the game is unknown, archived or has its shop switched off.

| Setting | Meaning |
|---|---|
| Activer la boutique | Turns the page on. |
| Nom affiché, Accroche | The name and the welcome sentence. The game name is used when the name is empty. |
| Couleur d'accent | One of eight colors. The contrast of the text on each color (white or dark, at least 4.5:1) is checked, so the shop stays readable. |
| URL de retour | Where the player goes back to after a purchase, in a web game. |
| Schémas d'application | Where the player goes back to in a phone app. |
| Indexation | Whether search engines may list the shop. Off by default. |

The page shows a live preview and a button that opens your shop in `test`. Changing the settings needs the role **developer**; the role **support** can read them.

## Open the shop from your game

The shop knows who the player is from a **player token**: the short token (one hour) your backend creates with `POST /players/{player}/tokens`, or the one the browser SDK holds for an anonymous player. Put it in the link:

```text
https://<your-gamecoin-host>/shop/<your-game-slug>?t=<player token>&locale=en
```

Create the token on your server and build the link:

<!-- tabs:start -->
```bash tab="curl"
curl -X POST "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/tokens" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
  -H "Idempotency-Key: shop-link-user-42-1" \
  -H "Content-Type: application/json" \
  -d '{}'
```
```javascript tab="Node.js"
const { token } = await gamecoin.players.createToken("ext:user-42");
const link = `https://<your-gamecoin-host>/shop/my-game?t=${encodeURIComponent(token)}&locale=en`;
```
<!-- tabs:end -->

Open the link in the browser of the player: a button, a `window.open`, the system browser of a phone, or an in-app web view.

What happens, so that the token never lingers:

1. The shop checks that the game has a shop and that the token is valid, unexpired and made for this game.
2. It swaps the token for a **shop session**: a signed cookie that is `HttpOnly`, `SameSite=Lax`, limited to the path `/shop/<your game slug>` and valid for one hour at most. The player is then sent to the shop **without the token in the address**.
3. The shop sends `Referrer-Policy: same-origin` and `X-Robots-Tag: noindex`, so the token is never sent as a `Referer` and the pages are not indexed.

The session is not an API token: it works only on the shop of that game. A link that is invalid, expired, made for another game, or for a player who is blocked, simply lands on the screen **Retrouve ton compte** with a notice. It never says which of these is the case.

### The environment

The player token carries the environment: a `test` token opens the shop in `test` (a banner says so, and the payments are simulated), a `live` token opens it in `live`. Nothing else in the address can switch to `live`.

### The language

The language is the one of the request: the `locale` parameter (`fr`, `en` or `nl`) when you give it, otherwise the `Accept-Language` header of the browser, otherwise French. Amounts and dates are written with the rules of that language.

## When the player has no link

Without a valid token the shop shows **Retrouve ton compte**: the player types an e-mail address, receives a six-digit code, and gets back the player that is linked to that address. It is the [e-mail sign-in](/docs/guides/player-email-sign-in) of the browser SDK, with the same rules: a code lasts ten minutes, five tries, and the answer is the same whether the address is known or not.

Two things to know:

- The address must already be **linked** to a player from your game. The shop does not create players.
- Signing in to the shop does **not** change the secret that the game keeps on the device: the player stays signed in in the game.

For a game in `test`, add `?env=test` to the address to open the screen in the test environment: `https://<your-gamecoin-host>/shop/<your-game-slug>?env=test`. Only `test` is accepted from the address.

## Buy, pay and come back

A click on a pack creates the order (like `POST /client/me/checkouts`) and sends the player to the **hosted payment page**, where the country, the VAT and the withdrawal consent are confirmed. The shop asks for the return in both cases, success and cancel, so the player always comes back.

The return goes through `/shop/<your game slug>/return`, which sends the player to **what you declared** in the settings, in this order:

1. **A phone app.** If the link of the shop carries `r=<scheme>` and that scheme is in your **Schémas d'application**, the player is sent to `<scheme>://shop/return?order=<order id>&status=<status>`. Browsers on phones often block a redirect to an app scheme, so the player lands on a small page with a real button, **Retourner dans le jeu**, and the shop also tries to open the app by itself.
2. **A web page.** Otherwise, if you set an **URL de retour**, the player is redirected to it with `order` and `status` added.
3. **The shop.** Otherwise the player goes back to the shop, which reads the order and tells the result at the top: confirmed, being confirmed (the page refreshes by itself), declined, canceled or expired.

Add the app scheme to the link when your game is an app:

```text
https://<your-gamecoin-host>/shop/<your-game-slug>?t=<player token>&r=mygame
```

The values are strict: `order` must be an order identifier and `status` one of the order states, otherwise the player is simply sent back to the shop. A scheme you did not declare is ignored, and the settings refuse `javascript:`, `data:`, `file:`, `http`, `https` and the other schemes that are not apps. The address of the return always comes from your settings, never from the request, so the shop cannot be used to redirect a player to another site. Your **URL de retour** must be `https` (`http` only for `localhost`, to test).

> [!WARNING]
> `status` in the return address is only a hint. Anyone can type it. Confirm the purchase from your backend with `GET /orders/{order}` or from the `order.fulfilled` [webhook](/docs/guides/webhooks) before you rely on it.

## Codes in the shop

The code field takes both kinds of code and **the server decides**:

- A **creator code** is kept for the next purchase (it lives in the shop session) and shown with its bonus, with the exact amount of free coins on each pack it covers. The player can remove it. It is checked again, with the pack, when the order is created.
- Otherwise the text is tried as a **gift code**: if it is valid it is used right away, the free coins and items are added and the balance updates. A gift code never gives paid coins.

A refused code always gets the same answer, whether it is unknown, expired, used up, disabled or already used by that player. The tries are limited per player and per address, whether they work or not. See [Gift codes and creator codes](/docs/guides/codes).

## Search engines

Shops are `noindex` by default. If you tick **Indexation**, only the page seen **without** signing in is open to search engines: a title and a description about your game, in the three languages, a canonical address, `hreflang` links, and a place in the sitemap for the games that are `live`. The page of a signed-in player (balance, purchases) is never indexed.

Each language has its own address, `https://<your-gamecoin-host>/shop/<your-game-slug>?locale=fr`, `?locale=en` or `?locale=nl`.

## Embedded in a web page

The same page can run in an iframe, at `/shop/<your game slug>/embed`: no header and no footer, a transparent background, a height that follows its content, a light or dark theme. The easy way is the **[shop widget](/docs/sdk/widget)**: one script, `GameCoinShop.open({ gameSlug, token })`, a modal that is accessible from the keyboard, and events for the balance and the purchases. It also explains how to add it to a GDevelop or Construct game.

What to know here:

- **Who can frame it.** Only the pages in the **allowed origins** of the game (« Réglages »). In `live` the list is mandatory; in `test` an empty list allows `http://localhost` and `http://127.0.0.1`. Every other page of GameCoin refuses to be framed.
- **The player.** An iframe of another site cannot keep a cookie, so the embedded shop swaps the player token (`?t=`) for a signed ticket of one hour that it sends back with each action. Without a valid token the shop says that the session has expired and tells the widget, which tells your game.
- **The payment.** It opens in its own window, since the payment page cannot be shown in an iframe; the shop follows the order by itself and announces the end.
- **The messages.** The shop talks to your page with `postMessage`: `gamecoin:ready`, `gamecoin:balance`, `gamecoin:purchased`, `gamecoin:height`, `gamecoin:close` (Escape inside the shop) and `gamecoin:error`. The target is always the exact origin of your page, and only if it is in the allowed origins; never `*`. Your page should use the widget rather than read them itself.

| You want to | Use |
|---|---|
| Send the player to a page of GameCoin and bring them back | The link of this page |
| Show the shop in a window over your web game, or inside a page | The [shop widget](/docs/sdk/widget) |
| Draw your own shop screen | The [browser SDK](/docs/sdk/browser) and your own buttons |

## Limits and good practice

- The shop session lasts one hour. After that the player opens the shop again from the game, or signs in with the e-mail code.
- A player sees only their own balance, orders and codes. The shop never shows an e-mail address.
- The tokens you put in links are short-lived: create one when the player taps the button, not in advance.
- Keep `test` and `live` apart: the same slug serves both, and the player token says which one.
