# A game with a backend

> Run GameCoin from your own server, reward players there, hand each browser a one-hour token, and use the browser SDK for the rest.

## What you will build

Your server is the authority. It knows its players, decides what they earn, and credits them. The browser gets a short-lived token and uses the SDK to show the balance, spend, sell packs and use items. In this guide you will:

1. Install the server SDK and make a client.
2. Reward a player from your server.
3. Give the browser a token.
4. Start the browser SDK with that token.
5. Let the server spend when the server must decide.

If your game has no server, read [A game without a backend](/docs/guides/no-backend-game) instead. The catalog set-up (a `gems` currency, a `potion` item, a `gems-500` pack) is described there.

## Step 1: Install and connect

You need Node.js 18 or later for the Node.js examples. Every example also has a `curl` version, and the other server languages are in [Server SDKs](/docs/sdk/server).

```bash
npm install @apilow/gamecoin
```

Put the **secret key** of your game (the `gc_sk_test_…` key from « Clés d'API » in the dashboard, which is in French) in the environment of your server. It never goes in your code or in the browser.

```bash
export GAMECOIN_SECRET_KEY="gc_sk_test_…"
```

```javascript title="gamecoin.mjs"
import { GameCoin, ext } from "@apilow/gamecoin";

export const gamecoin = new GameCoin(process.env.GAMECOIN_SECRET_KEY, { baseUrl: "https://gamecoin.apilow.com" });
export { ext };
```

`baseUrl` is where GameCoin runs. The SDK adds `/api/v1`. The other examples on this page use `gamecoin` and `ext` from this file. The `.mjs` extension makes Node.js read the files as modules without any other set-up.

## Step 2: Name your players

A player is your own id behind `ext:`. You do not need to declare players first: the first write for an id creates it. When you do want to store a name or a country, send the profile. The id is URL-encoded exactly once; the SDK's `ext()` and the `%3A` below do that.

<!-- tabs:start -->
```bash tab="curl"
curl -X PUT "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"displayName":"Player 42","country":"BE"}'
```
```javascript tab="Node.js"
const player = await gamecoin.players.upsert(ext("user-42"), { displayName: "Player 42", country: "BE" });
console.log(player.kind, player.externalId); // external user-42
```
```python tab="Python"
player = gamecoin.players.upsert(ext("user-42"), display_name="Player 42", country="BE")
print(player.kind, player.external_id)  # external user-42
```
```php tab="PHP"
$player = $gamecoin->players->upsert(PlayerRef::ext('user-42'), ['display_name' => 'Player 42', 'country' => 'BE']);
echo $player->kind, ' ', $player->externalId, "\n"; // external user-42
```
```go tab="Go"
name, country := "Player 42", "BE"
player, err := client.Players.Upsert(ctx, gamecoin.Ext("user-42"), &gamecoin.PlayerProfile{DisplayName: &name, Country: &country})
if err != nil {
	log.Fatal(err)
}
fmt.Println(player.Kind, *player.ExternalID) // external user-42
```
```csharp tab="C#"
var player = await gamecoin.Players.UpsertAsync(PlayerRef.Ext("user-42"), new PlayerProfile { DisplayName = "Player 42", Country = "BE" });
Console.WriteLine($"{player.Kind} {player.ExternalId}"); // external user-42
```
<!-- tabs:end -->

```json title="Answer"
{
  "id": "6ac4199eb5efc66d69f4d301",
  "externalId": "user-42",
  "kind": "external",
  "displayName": "Player 42",
  "country": "BE",
  "blocked": false,
  "createdAt": "2026-10-05T21:41:50.328Z"
}
```

## Step 3: Reward a player from your server

Rewards are yours to give: a level, a quest, a daily bonus. Your server checks that the event really happened, then grants. A grant always credits the free (`bonus`) part of the balance.

Build the idempotency key from the event, so that retrying the same event can never pay twice: `level-3-user-42`, not a random value.

<!-- tabs:start -->
```bash tab="curl"
curl -X POST "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/wallet/grant" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
  -H "Idempotency-Key: level-3-user-42" \
  -H "Content-Type: application/json" \
  -d '{"currency":"gems","amount":100,"reason":"level-3"}'
```
```javascript tab="Node.js"
const { balance, replayed } = await gamecoin.wallet.grant(
  ext("user-42"),
  { currency: "gems", amount: 100, reason: "level-3" },
  { idempotencyKey: "level-3-user-42" },
);
console.log(balance.total, replayed); // 100 false
```
```python tab="Python"
movement = gamecoin.wallet.grant(
    ext("user-42"),
    currency="gems",
    amount=100,
    reason="level-3",
    idempotency_key="level-3-user-42",
)
print(movement.balance.total, movement.replayed)  # 100 False
```
```php tab="PHP"
$movement = $gamecoin->wallet->grant(
    PlayerRef::ext('user-42'),
    ['currency' => 'gems', 'amount' => 100, 'reason' => 'level-3'],
    ['idempotency_key' => 'level-3-user-42'],
);
echo $movement->balance->total, ' ', var_export($movement->replayed, true), "\n"; // 100 false
```
```go tab="Go"
movement, err := client.Wallet.Grant(
	ctx,
	gamecoin.Ext("user-42"),
	gamecoin.GrantParams{Currency: "gems", Amount: 100, Reason: "level-3"},
	gamecoin.WithIdempotencyKey("level-3-user-42"),
)
if err != nil {
	log.Fatal(err)
}
fmt.Println(movement.Balance.Total, movement.Replayed) // 100 false
```
```csharp tab="C#"
var movement = await gamecoin.Wallet.GrantAsync(
    PlayerRef.Ext("user-42"),
    new GrantRequest { Currency = "gems", Amount = 100, Reason = "level-3" },
    new RequestOptions { IdempotencyKey = "level-3-user-42" });
Console.WriteLine($"{movement.Balance.Total} {movement.Replayed}"); // 100 False
```
<!-- tabs:end -->

```json title="Answer"
{
  "entry": {
    "id": "6ac4199e37c701028804249c",
    "type": "grant",
    "currency": "gems",
    "paidDelta": 0,
    "bonusDelta": 100,
    "balanceAfter": { "paid": 0, "bonus": 100, "total": 100 },
    "reason": "level-3",
    "metadata": {},
    "ref": {},
    "createdAt": "2026-10-05T21:41:50.594Z"
  },
  "balance": { "currency": "gems", "paid": 0, "bonus": 100, "total": 100 }
}
```

Send the same request again and you get the same answer, with `Idempotent-Replayed: true` (`replayed: true` in the SDK). The player still has 100 gems. See [Idempotency](/docs/concepts/idempotency).

## Step 4: Give the browser a token

The browser must act as one player. A **player token** (`gc_pt_…`) lets it do that for one hour, together with your publishable key. Only your server can ask for it, because only your server knows who is logged in.

<!-- 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"
```
```javascript tab="Node.js"
const { token, expiresAt } = await gamecoin.players.createToken(ext("user-42"));
console.log(token.startsWith("gc_pt_"), expiresAt.toISOString()); // true 2026-10-05T22:41:51.000Z
```
```python tab="Python"
token = gamecoin.players.create_token(ext("user-42"))
print(token.token.startswith("gc_pt_"), token.expires_at.isoformat())  # True 2026-10-05T22:41:51+00:00
```
```php tab="PHP"
$token = $gamecoin->players->createToken(PlayerRef::ext('user-42'));
echo var_export(str_starts_with($token->token, 'gc_pt_'), true), ' ', $token->expiresAt->format(DATE_ATOM), "\n"; // true 2026-10-05T22:41:51+00:00
```
```go tab="Go"
token, err := client.Players.CreateToken(ctx, gamecoin.Ext("user-42"))
if err != nil {
	log.Fatal(err)
}
fmt.Println(strings.HasPrefix(token.Token, "gc_pt_"), token.ExpiresAt.Format(time.RFC3339)) // true 2026-10-05T22:41:51Z
```
```csharp tab="C#"
var token = await gamecoin.Players.CreateTokenAsync(PlayerRef.Ext("user-42"));
Console.WriteLine($"{token.Token.StartsWith("gc_pt_")} {token.ExpiresAt:u}"); // True 2026-10-05 22:41:51Z
```
<!-- tabs:end -->

```json title="Answer"
{ "token": "gc_pt_…", "expiresAt": "2026-10-05T22:41:51.000Z" }
```

Wrap it in an endpoint of your own, behind your login. This server uses only Node.js's built-in `http` module, so you can run it as it is. The line `currentUser` is the one to replace with your own session lookup.

```javascript title="server.mjs"
import http from "node:http";
import { gamecoin, ext } from "./gamecoin.mjs";

// Replace with your own login: the id of the player who made this request.
const currentUser = (req) => req.headers["x-user-id"];

const server = http.createServer(async (req, res) => {
  const send = (status, body) => {
    res.writeHead(status, { "Content-Type": "application/json" });
    res.end(JSON.stringify(body));
  };

  const user = currentUser(req);
  if (!user) return send(401, { error: "not logged in" });

  try {
    // The browser asks for a token. It can only get one for the player it is logged in as.
    if (req.method === "POST" && req.url === "/api/gamecoin-token") {
      const { token } = await gamecoin.players.createToken(ext(user));
      return send(200, { token });
    }

    // The browser says a level is over. Your server decides if it is true, and what it is worth.
    if (req.method === "POST" && req.url === "/api/level-complete") {
      const level = 3;
      const { balance } = await gamecoin.wallet.grant(
        ext(user),
        { currency: "gems", amount: 10, reason: `level-${level}` },
        { idempotencyKey: `level-${level}-${user}` },
      );
      return send(200, { gems: balance.total });
    }

    send(404, { error: "not found" });
  } catch (error) {
    console.error("GameCoin:", error.code ?? error);
    send(502, { error: "GameCoin is not available" });
  }
});

server.listen(8080);
```

Never send the secret key, or a token for another player, to the browser.

## Step 5: Start the browser SDK with the token

Pass the token as `playerToken`. The SDK then creates no player of its own: it plays as the player of the token, and the balance it reads is the one your server just set.

A token lasts an hour, so also pass `onTokenExpired`. The SDK calls it once, when a request is refused because the token expired, takes the token you return, and replays the request.

```html title="index.html"
<script src="https://gamecoin.apilow.com/sdk/gamecoin.js"></script>
<script type="module">
  const getToken = async () => {
    const response = await fetch("/api/gamecoin-token", { method: "POST" });
    return (await response.json()).token;
  };

  const gc = await GameCoin.init({
    publishableKey: "gc_pk_test_…",
    playerToken: await getToken(),
    onTokenExpired: getToken, // called once when the one-hour token is rejected
  });

  console.log(gc.balance("gems")); // 100: what your server granted
  gc.on("change", () => console.log("gems:", gc.balance("gems")));
</script>
```

From here the browser does the same things as in [A game without a backend](/docs/guides/no-backend-game): `gc.spend`, `gc.buyItem`, `gc.consume`, `gc.checkout`. It can read, spend and pay. It still cannot credit anything by itself: the one exception is a [gift code](/docs/guides/codes) that you created, which the player can redeem with `gc.redeemCode(code)` for free coins and items, once. It works with your player token too; if you want your server to decide who may redeem, call the [server route](/docs/api/codes) instead.

When your server grants something while the page is open (a reward after `/api/level-complete`), tell the SDK to read again:

```javascript
await fetch("/api/level-complete", { method: "POST" });
await gc.refresh(); // the "change" event fires and your screen updates
```

## Step 6: Let the server spend when the server must decide

The browser can spend by itself, which is simple and fine for a game where nothing depends on the purchase. When your server enforces the result (an item that unlocks something the server checks, an entry fee for a match your server runs), let the **server** spend, and the browser only asks:

<!-- tabs:start -->
```bash tab="curl"
curl -X POST "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/purchases" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
  -H "Idempotency-Key: potion-user-42-order-9" \
  -H "Content-Type: application/json" \
  -d '{"sku":"potion","quantity":2}'
```
```javascript tab="Node.js"
const purchase = await gamecoin.purchases.create(
  ext("user-42"),
  { sku: "potion", quantity: 2 },
  { idempotencyKey: "potion-user-42-order-9" },
);
console.log(purchase.balance.total, purchase.item.quantity); // 60 2
```
```python tab="Python"
purchase = gamecoin.purchases.create(
    ext("user-42"),
    sku="potion",
    quantity=2,
    idempotency_key="potion-user-42-order-9",
)
print(purchase.balance.total, purchase.item.quantity)  # 60 2
```
```php tab="PHP"
$purchase = $gamecoin->purchases->create(
    PlayerRef::ext('user-42'),
    ['sku' => 'potion', 'quantity' => 2],
    ['idempotency_key' => 'potion-user-42-order-9'],
);
echo $purchase->balance->total, ' ', $purchase->item->quantity, "\n"; // 60 2
```
```go tab="Go"
purchase, err := client.Purchases.Create(
	ctx,
	gamecoin.Ext("user-42"),
	gamecoin.PurchaseParams{SKU: "potion", Quantity: 2},
	gamecoin.WithIdempotencyKey("potion-user-42-order-9"),
)
if err != nil {
	log.Fatal(err)
}
fmt.Println(purchase.Balance.Total, purchase.Item.Quantity) // 60 2
```
```csharp tab="C#"
var purchase = await gamecoin.Purchases.CreateAsync(
    PlayerRef.Ext("user-42"),
    new PurchaseRequest { Sku = "potion", Quantity = 2 },
    new RequestOptions { IdempotencyKey = "potion-user-42-order-9" });
Console.WriteLine($"{purchase.Balance.Total} {purchase.Item.Quantity}"); // 60 2
```
<!-- tabs:end -->

`purchases` buys an item with its own price: the wallet is debited and the item added, in one step. If the wallet is too low, the call answers `409 INSUFFICIENT_FUNDS` and nothing changes. The server SDK handles errors with `GameCoinError`; see [Errors](/docs/concepts/errors).

You can also read what your server needs:

<!-- tabs:start -->
```bash tab="curl"
curl "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/wallet" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY"

curl "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/inventory" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY"
```
```javascript tab="Node.js"
const wallet = await gamecoin.wallet.get(ext("user-42"));
const items = await gamecoin.inventory.list(ext("user-42"));
console.log(wallet.balances.map((b) => `${b.currency}: ${b.total}`), items);
```
```python tab="Python"
wallet = gamecoin.wallet.get(ext("user-42"))
items = gamecoin.inventory.list(ext("user-42"))
print([f"{b.currency}: {b.total}" for b in wallet.balances], items)
```
```php tab="PHP"
$wallet = $gamecoin->wallet->get(PlayerRef::ext('user-42'));
$items = $gamecoin->inventory->list(PlayerRef::ext('user-42'));
echo implode(', ', array_map(static fn ($b) => "{$b->currency}: {$b->total}", $wallet->balances)), "
";
print_r($items);
```
```go tab="Go"
wallet, err := client.Wallet.Get(ctx, gamecoin.Ext("user-42"))
if err != nil {
	log.Fatal(err)
}
items, err := client.Inventory.List(ctx, gamecoin.Ext("user-42"))
if err != nil {
	log.Fatal(err)
}
for _, b := range wallet.Balances {
	fmt.Printf("%s: %d\n", b.Currency, b.Total)
}
fmt.Printf("%+v\n", items)
```
```csharp tab="C#"
var wallet = await gamecoin.Wallet.GetAsync(PlayerRef.Ext("user-42"));
var items = await gamecoin.Inventory.ListAsync(PlayerRef.Ext("user-42"));
Console.WriteLine(string.Join(", ", wallet.Balances.Select(b => $"{b.Currency}: {b.Total}")));
foreach (var item in items)
{
    Console.WriteLine(item);
}
```
<!-- tabs:end -->

## Before you go live

- The secret key lives only in your server's environment.
- `/api/gamecoin-token` sits behind your login, and gives a token only for the logged-in player.
- Every grant has an idempotency key made from the event.
- Your browser code uses the publishable key and the token, nothing else.
- You tried a declined payment, a player without enough gems, and an expired token. See [Testing](/docs/guides/testing).

## Where next

- [Selling packs](/docs/guides/selling-packs): create the checkout from your server.
- [Free currencies and items](/docs/guides/free-currencies-and-items): rewards, items, limits.
- [Security model](/docs/concepts/security-model)
