Skip to content
Skip the menu

Getting started

Quickstart

Sell your game's virtual currency and items in minutes with the browser SDK or the server API, in two paths of three steps each.

View as Markdown

On this page

Before you start

GameCoin keeps the ledger, the inventory and the orders of your game. You call it from the browser, from your server, or both. You need a game, a currency and a pack, and your keys.

  1. Create an account and a game in the dashboard (« Nouveau jeu »). Your test keys are created with the game.
  2. Create a paid currency, for example gems (« Monnaies », type « Payante »), then a pack that sells it, for example gems-500: 500 gems and 50 free, 4.99 € (« Packs »). A new pack is a draft: use « Publier ». For items, create one with a price and tick « Achetable depuis le jeu (API cliente) », so that the browser may buy it.
  3. Copy your keys from the game's overview or from « Clés d'API »: the publishable key gc_pk_test_… (safe in a browser) and the secret key gc_sk_test_… (shown once, kept on your server).

The dashboard is in French for now; the labels above are the ones you will see.

Pick the path that matches your game. You can start with A and move to B later: both end with the same SDK calls.

PathForHow it works
A. No backendA browser-only game: itch.io, a game jam, a no-code engineGameCoin creates an anonymous player and remembers it
B. With a backendA game whose server knows its playersYour server declares them, credits and spends, and gives the browser a short-lived token

Path A: no backend

Everything runs in the browser, with your publishable key. Three steps.

Step 1: Load the SDK

One file, no dependency, no build step. It adds a global GameCoin and talks to the host it was loaded from.

Load the SDK
<script src="https://gamecoin.apilow.com/sdk/gamecoin.js"></script>

Step 2: Start it

GameCoin.init creates an anonymous player on the first visit and remembers it in localStorage. On the next visits it asks for a fresh token with the remembered secret. Same browser, same player, same balance.

Start the SDK
<script type="module">
  const gc = await GameCoin.init({ publishableKey: "gc_pk_test_…" });

  console.log(gc.player.id);              // the anonymous player, remembered in localStorage
  console.log(gc.balance("gems"));        // 0 until the player buys something
  const catalog = await gc.getCatalog();  // { currencies, items, packs }
</script>

Clearing the browser's data makes a new player. Anonymous players can be turned off in the game's settings (« Réglages »).

Step 3: Sell a pack and an item

A complete page. checkout pays for a pack, buyItem pays for an item with the game's currency, and consume uses one up. The balance and the inventory in the SDK are updated after every call.

A page that sells a pack and an item
<button id="buy-pack">Buy 500 gems</button>
<button id="buy-potion">Buy a potion (20 gems)</button>
<button id="use-potion">Drink a potion</button>
<p>Gems: <span id="gems">0</span></p>

<script src="https://gamecoin.apilow.com/sdk/gamecoin.js"></script>
<script type="module">
  const gc = await GameCoin.init({ publishableKey: "gc_pk_test_…" });

  // "change" fires after every purchase, spend, consume or refresh.
  const render = () => (document.getElementById("gems").textContent = gc.balance("gems"));
  gc.on("change", render);
  render();

  // A pack is paid in euros (simulated in test): checkout() opens the payment page in a window.
  // Call it straight from a click, or the browser blocks the window.
  document.getElementById("buy-pack").onclick = async () => {
    const order = await gc.checkout("gems-500");
    console.log(order.status); // "fulfilled" | "failed" | "pending" (window closed before paying)
  };

  // An item is paid with the game's currency: no window.
  document.getElementById("buy-potion").onclick = () => gc.buyItem("potion", 1).catch(console.error);
  document.getElementById("use-potion").onclick = () => gc.consume("potion", 1).catch(console.error);
</script>

Spending without an item ("continue?", "skip the timer", an entry fee):

JavaScript
await gc.spend("gems", 5, "continue");

Note

checkout() opens the payment window synchronously and resolves when the order is over: fulfilled (the coins are in, gc.balance() is already up to date), failed (declined), or pending (the window was closed without paying). If the browser blocks the window, the SDK sends the current tab to the payment page and brings the player back to the same URL. init then reads the order, cleans ?order=&status= from the address and exposes it as gc.returnedOrder. Force it with gc.checkout(sku, { mode: "redirect" }).

Warning

A player can never credit themselves. The SDK has no grant: coins come from a paid order or from your server (path B). The one exception is a gift code that you created in the dashboard: gc.redeemCode("…") gives free coins and items, once per player, and the value is decided by you, not by the browser. See Gift codes and creator codes. Points your game computes locally are yours to keep locally.

The calls of the SDK:

CallReturnsNotes
GameCoin.init(options)the clientOptions: publishableKey (required), playerToken, onTokenExpired, baseUrl, storage, displayName, debug.
gc.getCatalog(){ currencies, items, packs }Active entries only.
gc.getWallet(){ playerId, balances }Reads the server. A fresh copy is also in gc.wallet.
gc.getInventory()[{ sku, quantity, updatedAt }]Owned items only.
gc.balance(currency)a numberCached total; 0 when unknown. Synchronous.
gc.spend(currency, amount, reason?){ entry, balance }Fails with INSUFFICIENT_FUNDS when the wallet is too low.
gc.buyItem(sku, quantity = 1){ entry, balance, item }Only items marked as buyable from the game.
gc.consume(sku, quantity = 1){ item }The item is returned even when it reaches 0.
gc.checkout(packSku, options?)the orderFrom a click. options.mode: "redirect".
gc.on("change", fn)an unsubscribe functionCalled with { wallet, inventory, reason } when the cached wallet or inventory changed.
gc.refresh(){ wallet, inventory }Reads the server and updates the cache.

Every option and every error is in the Browser SDK reference. For a longer walkthrough, see A game without a backend.

Path B: with a backend

Your server is the authority: it declares the players, credits and spends, and gives each browser a token. The examples have a curl version and a Node.js version. The URL is this server's own.

Set your secret key, and install the Node.js SDK if you use it:

Set your secret key
export GAMECOIN_SECRET_KEY="gc_sk_test_…"   # your secret key: server side only
npm install @apilow/gamecoin                # Node.js 18 or later

Every Node.js example in this documentation starts from this client, gamecoin, and from the ext helper. The other server languages are in Server SDKs.

The client
import { GameCoin, ext } from "@apilow/gamecoin";

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

Step 1: Declare a player

A player is your own id behind ext:. Any PUT or POST on an unknown ext: player creates it, so there is nothing to synchronize beforehand. The id is URL-encoded exactly once: encodeURIComponent("ext:user-42") is ext%3Auser-42, and the SDK's ext("user-42") does it for you.

LangageLanguage
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"}'
Answer
{"id":"6ac4199eb5efc66d69f4d301","externalId":"user-42","kind":"external","displayName":"Player 42","country":"BE","blocked":false,"createdAt":"2026-10-05T21:41:50.328Z"}

Step 2: Credit and spend

wallet/grant always credits the free (« bonus ») part of the balance; coins bought with real money come only from paid orders. Both calls need an Idempotency-Key (see below). Build it from the event you are paying for.

LangageLanguage
curl -X POST "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/wallet/grant" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
  -H "Idempotency-Key: welcome-user-42" \
  -H "Content-Type: application/json" \
  -d '{"currency":"gems","amount":100,"reason":"welcome-bonus"}'

curl -X POST "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/wallet/spend" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
  -H "Idempotency-Key: revive-user-42-1" \
  -H "Content-Type: application/json" \
  -d '{"currency":"gems","amount":30,"reason":"revive"}'
Answer to the spend
{"entry":{"id":"6ac4199e1be9a8701b926925","type":"spend","currency":"gems","paidDelta":0,"bonusDelta":-30,
  "balanceAfter":{"paid":0,"bonus":70,"total":70},"reason":"revive","metadata":{},"ref":{},"createdAt":"2026-10-05T21:41:50.922Z"},
 "balance":{"currency":"gems","paid":0,"bonus":70,"total":70}}

Spending more than the balance answers 409 INSUFFICIENT_FUNDS and changes nothing. Read a balance with GET /players/ext%3Auser-42/wallet and the history with GET …/ledger.

Step 3: Give the browser a token, then use the SDK

The token (gc_pt_…, valid one hour) lets the browser act as that one player with your publishable key. Ask for it from your server, never from the browser with the secret key.

LangageLanguage
curl -X POST "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/tokens" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY"
Answer
{"token":"gc_pt_…","expiresAt":"2026-10-05T22:41:51.000Z"}

Put that call behind an endpoint of your own, behind your login:

A token endpoint on your server (Express-style)
app.get("/api/gamecoin-token", async (req, res) => {
  const { token } = await gamecoin.players.createToken(ext(req.user.id)); // the logged-in player only
  res.json({ token });
});

Then start the browser SDK with it:

Start the SDK with the token
<script src="https://gamecoin.apilow.com/sdk/gamecoin.js"></script>
<script type="module">
  const getToken = async () => (await fetch("/api/gamecoin-token").then((r) => r.json())).token;

  const gc = await GameCoin.init({
    publishableKey: "gc_pk_test_…",
    playerToken: await getToken(),   // the SDK creates no player: it plays as this one
    onTokenExpired: getToken,        // called once when the token (valid 1 hour) is rejected
  });

  console.log(gc.balance("gems"));   // 70: the balance your server just set
  await gc.checkout("gems-500");     // same as path A from here
</script>

With playerToken the SDK never calls the player-creation routes. When the token is rejected it calls onTokenExpired once, then replays the request with the same idempotency key.

Tip

A refund takes back the coins and items of an order. It needs the secret key: the browser cannot refund. POST /orders/{orderId}/refund with an optional reason. See Refunds.

The two keys

Publishable keySecret key
Looks likegc_pk_test_…gc_sk_test_…
Where it livesIn your game's code, in the browserOn your server only. Never in a browser, an app bundle or a repository
How it is sentX-GameCoin-Key: gc_pk_…Authorization: Bearer gc_sk_…
CanRead the catalog, create an anonymous player. With that player's token (Authorization: Bearer gc_pt_…): read their wallet and inventory, spend, buy an item with a currency, consume, pay for a pack.The whole server API: declare players, grant and spend on any player, give or take items, start checkouts, read and refund orders.
Can neverCredit anything: no grant, no item grant, no balance correction, no refund.Nothing is off limits, which is why it stays on your server.

Warning

Code that runs in a browser can be edited by the player, so GameCoin never lets it add to a balance. Only a completed payment or your server can. That is what keeps a game without a backend safe for real money.

The key also decides the game and the environment (test or live); nothing in a request body can change that. If a secret key leaks, revoke it and create another one from the dashboard. More in Keys and environments and the Security model.

Idempotency

A request that changes a wallet or an inventory, or starts a checkout, must carry an Idempotency-Key header (1 to 100 printable ASCII characters). The same key with the same request returns the original answer, with Idempotent-Replayed: true and no second effect. The same key with a different request answers 409 IDEMPOTENCY_CONFLICT. Keys are kept 30 days. A missing key is a 400 IDEMPOTENCY_KEY_REQUIRED.

The SDK does it for you: one random key per call, reused on every automatic retry (network error, 5xx, expired token). To make a call safe across page reloads, pass your own key as the last argument: gc.spend("gems", 5, "level-3", { idempotencyKey: "level-3-entry" }).

From your server, derive the key from the event you are paying for, not from a random value: if your request times out and you send it again, the same key guarantees it counts once.

LangageLanguage
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-reward-user-42" \
  -H "Content-Type: application/json" \
  -d '{"currency":"gems","amount":10,"reason":"level-3"}'
# Run it twice: the second answer is identical and carries "Idempotent-Replayed: true". Nothing is credited twice.

Details and rules: Idempotency.

Common errors

Every error has the same shape; the SDK turns it into a GameCoinError with code, status, message and details.

Error body
{ "error": { "code": "INSUFFICIENT_FUNDS", "message": "Insufficient funds", "details": { "required": 50, "available": 20 } } }
CodeHTTPMeaning and what to do
INSUFFICIENT_FUNDS409The wallet (or the item quantity) is too low. details.required and details.available say by how much: offer a pack.
LIMIT_REACHED409An item's ownership cap or a pack's per-player limit is reached.
PLAYER_BLOCKED403The player is blocked in the dashboard: every client call and every wallet or inventory write is refused.
UNAUTHENTICATED401Unknown or revoked key, or an expired or invalid player token. The SDK renews a token once by itself; if it persists, check the key.
FORBIDDEN403Not allowed here: a secret key on a client route, anonymous players turned off, or an item that cannot be bought from the game.
VALIDATION_FAILED400The request is invalid. Bodies are strict: an unknown field is refused. details.fieldErrors says which one.
NOT_FOUND404Unknown player, currency, item, pack or order for this game and environment.
IDEMPOTENCY_CONFLICT409The same Idempotency-Key was sent with a different request.
PAYMENT_FAILED402The payment was refused, or no payment provider exists for this environment.
RATE_LIMITED429Too many requests. Retry-After says how many seconds to wait; the SDK waits and retries.
NETWORK_ERROR0SDK only: GameCoin could not be reached after three attempts. The operation may or may not have been applied: refresh the wallet, or pass your own idempotencyKey so that a retry cannot count twice.
Handling an error in the browser
try {
  await gc.spend("gems", 50, "continue");
} catch (error) {
  if (error.code === "INSUFFICIENT_FUNDS") {
    const { required, available } = error.details; // 50, 20
    showShop(required - available);                 // offer a pack
  } else {
    throw error; // GameCoinError: code, status, message, details
  }
}

The full table is on Errors.

Test and live

The key carries the environment: gc_pk_test_… and gc_sk_test_… work in test, gc_pk_live_… and gc_sk_live_… in live. The catalog (currencies, items, packs) is shared; players, balances, ledger and orders are separate in each.

  • Test is the only environment you can use today. Payments are simulated: the payment page shows « Payer » and « Refuser » buttons (it is in French for now), and no money moves. Test data is disposable.
  • Live is reserved for verified studios and real payments, which are not available yet. The environment is chosen by the key, so the code you write today does not change between the two.

Try it

The demo game is a small clicker built on the SDK: it reads your catalog, shows the balance live, sells your packs and items, and lets you consume them. Open it from your game's overview in the dashboard (it passes your test key), or paste a publishable test key on its start screen.

Next: Core concepts, then Testing.