API reference
Client API
The API a browser or a game client calls with a publishable key and a player token, to read, spend, buy and pay without ever crediting a player.
On this page
The client API is the part of GameCoin that your game client calls directly, from a browser or from the game itself. It uses a publishable key, which is safe to ship in a client, and for everything about one player it uses a player token. It can read the catalog, read the player's wallet and inventory, spend, buy an item with a currency, consume an item and pay for a pack. It can never credit anything, with one exception: the player can redeem a gift code, which gives free coins and items from a code that your studio issued. All its routes live under /client.
In a browser you will usually not call these routes yourself: the browser SDK does. This page documents the routes, for the SDK, for game clients that are not browsers, and for debugging. The guide for a game without a backend and the one for a game with a backend show the two usual setups.
Setup for the examples
Every example has two tabs: curl, for the HTTP request, and Browser, for the same thing with the browser SDK. In curl you need your publishable key, and a player token for the routes that act on a player. In the Browser tab, gc is the client returned by GameCoin.init; playerToken is a token your backend got from Issue a player token. Without a playerToken, the SDK creates an anonymous player on its own: see Create an anonymous player.
export GAMECOIN_PUBLISHABLE_KEY="gc_pk_test_…" # a publishable test key: safe in a browser
export GAMECOIN_PLAYER_TOKEN="gc_pt_…" # a player token, issued by your server for ext:user-42<script src="https://gamecoin.apilow.com/sdk/gamecoin.js"></script>
<script type="module">
const gc = await GameCoin.init({ publishableKey: "gc_pk_test_…", playerToken }); // playerToken: from your backend
// ... the example you want to run goes here
</script>The player of these examples is the ext:user-42 of the server API pages, so that you find the same wallet and inventory.
Keys and tokens
| Header | Value | Needed on |
|---|---|---|
X-GameCoin-Key | The publishable key, gc_pk_test_… or gc_pk_live_… | Every client route |
Authorization | Bearer gc_pt_…, a player token | The routes that act on a player: /client/me/** |
A player token lasts one hour and is tied to one player, one game and one environment. A secret key sent to a client route is refused with 403 FORBIDDEN, and a missing or invalid token with 401 UNAUTHENTICATED. When a request answers 401 and you hold a token, ask for a new one (your backend for a player it created, Get a new player token for an anonymous player) and send the same request again, with the same Idempotency-Key: if the first attempt did go through, you get its answer back instead of a second change.
A blocked player is refused on every client route with 403 PLAYER_BLOCKED.
CORS
Each game has a list of allowed origins (game settings in the dashboard, one origin per line, e.g. https://play.example.com). A client API request whose Origin is on the list is answered with that origin in Access-Control-Allow-Origin (and Vary: Origin). A request from another origin is refused with 403 FORBIDDEN (fieldErrors.Origin = ORIGIN_NOT_ALLOWED). With an empty list the test environment accepts any origin (*) and live accepts none. Requests without an Origin header (game engines, servers) are never affected. A preflight OPTIONS request is answered 204 without any authentication. The response lists the headers a browser may send (Authorization, Content-Type, X-GameCoin-Key, Idempotency-Key), and exposes Idempotent-Replayed and Retry-After to your code.
curl -i -X OPTIONS "https://gamecoin.apilow.com/api/v1/client/me/wallet/spend" \
-H "Origin: https://example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: authorization,x-gamecoin-key,idempotency-key,content-type"HTTP/1.1 204 No Content
Access-Control-Allow-Headers: Authorization, Content-Type, X-GameCoin-Key, Idempotency-Key
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Origin: *
Access-Control-Max-Age: 600The server API sends no CORS header: a browser cannot call it, and it must not, because it needs your secret key.
Return URLs of a checkout
A browser can start a checkout, but it cannot choose where the player goes next. successUrl and cancelUrl must have the same origin as the page that calls: the Origin header the browser adds to the request. Any other address is refused with 400 VALIDATION_FAILED and ORIGIN_MISMATCH in details.fieldErrors. A client that does not send an Origin header, such as curl, cannot pass return URLs at all. country is not accepted: the buyer picks it on the payment page.
The browser SDK handles this: it opens the payment page in a window and needs no return URL, or, in redirect mode, it returns to the current page. See Order a currency pack.
A client never credits, except with a gift code
There is no route to grant currency, to grant an item, to adjust a balance or to refund an order in the client API. This is the point of it: the publishable key is public, so anyone can call these routes with it, and nothing they can do creates value. A player can spend what they have and pay with real money; coins appear in a wallet only through your server (a grant), through a paid order, or from a gift code that you created.
The gift code is the one exception, and it is safe because the value is decided by your studio, not by the client: a code is random, limited in uses, works once per player, gives free coins only (never paid coins) and cannot be passed to another player. See Redeem a gift code and Security model.
Get the catalog
Returns the active currencies, items and packs of the game. It needs only the publishable key, so you can show a shop before a player is known.
/client/catalogAuthentication: publishable key. Idempotency: not needed. Parameters: none.
curl "https://gamecoin.apilow.com/api/v1/client/catalog" \
-H "X-GameCoin-Key: $GAMECOIN_PUBLISHABLE_KEY"const catalog = await gc.getCatalog();
for (const item of catalog.items) console.log(item.sku, item.type, item.price?.amount);
for (const pack of catalog.packs) console.log(pack.sku, pack.priceCents);Response 200 OK: a Catalog, the same as the server API returns.
{
"currencies": [
{ "code": "gems", "name": "Gems", "purchasable": true },
{ "code": "gold", "name": "Gold", "purchasable": false }
],
"items": [
{
"sku": "potion",
"name": "Potion",
"description": null,
"type": "consumable",
"price": { "currency": "gems", "amount": 20 },
"maxOwned": null,
"clientPurchasable": true,
"metadata": {}
},
{
"sku": "fire-sword",
"name": "Fire sword",
"description": null,
"type": "durable",
"price": { "currency": "gems", "amount": 300 },
"maxOwned": 1,
"clientPurchasable": true,
"metadata": {}
}
],
"packs": [
{
"sku": "starter",
"name": "Starter pack",
"description": null,
"priceCents": 99,
"currency": "EUR",
"grants": [{ "currency": "gems", "amount": 100, "bonus": 20 }],
"items": [{ "sku": "potion", "quantity": 1 }],
"badge": null,
"maxPerPlayer": 1,
"availability": null
},
{
"sku": "gems-500",
"name": "Bag of gems",
"description": null,
"priceCents": 499,
"currency": "EUR",
"grants": [{ "currency": "gems", "amount": 500, "bonus": 50 }],
"items": [],
"badge": null,
"maxPerPlayer": null,
"availability": null
}
],
"prelaunch": false,
"launchAt": null
}Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 401 | UNAUTHENTICATED | The X-GameCoin-Key header is missing, or the key is unknown or revoked | Send the publishable key of your game. |
| 403 | FORBIDDEN | A secret key was sent | Never use a secret key in a client: use the publishable key. |
| 429 | RATE_LIMITED | More than 120 requests a minute from one IP address without a token | Wait Retry-After seconds. |
Notes
items[].clientPurchasabletells which items Buy an item with currency accepts from a client.
Create an anonymous player
Creates a player for a game that has no backend, and returns its credentials. Use it the first time a device opens the game. The browser SDK does it for you.
/client/playersAuthentication: publishable key. Idempotency: none: every call creates a new player. Call it once per device.
Body (JSON; optional, an empty body means {})
| Field | Type | Required | Rules |
|---|---|---|---|
displayName | string | no | Name shown in your game: 1 to 80 characters once trimmed. |
curl -X POST "https://gamecoin.apilow.com/api/v1/client/players" \
-H "X-GameCoin-Key: $GAMECOIN_PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{"displayName": "Guest"}'// With no playerToken, init() creates the anonymous player the first time and remembers it in localStorage.
const guest = await GameCoin.init({ publishableKey: "gc_pk_test_…", displayName: "Guest" });
console.log(guest.player.id, guest.player.kind); // hostedResponse 201 Created: the player and their credentials.
| Field | Type | Description |
|---|---|---|
player | Player | The new player: kind is hosted and externalId is null. |
playerSecret | string | The secret of the player, returned only here. Store it on the device: it is the only way to get new tokens. |
token | string | A first player token. |
expiresAt | string | Expiry of token, one hour after issue. |
{
"player": {
"id": "665f1c2e8a3b4d5e6f7081c6",
"externalId": null,
"kind": "hosted",
"displayName": "Guest",
"country": null,
"blocked": false,
"createdAt": "2026-10-05T22:14:41.141Z"
},
"playerSecret": "bD7QMp…",
"token": "gc_pt_…",
"expiresAt": "2026-10-05T23:14:41.000Z"
}Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 400 | VALIDATION_FAILED | An unknown field, or a displayName that is empty or longer than 80 characters | Read details.fieldErrors. |
| 401 | UNAUTHENTICATED | The publishable key is missing, unknown or revoked | Send the publishable key of your game. |
| 403 | FORBIDDEN | The game does not allow anonymous players, or a secret key was sent | Issue tokens from your backend instead. |
| 429 | RATE_LIMITED | More than 30 anonymous players an hour from one IP address | Wait Retry-After seconds. |
Notes
- Keep
player.idandplayerSecreton the device, in a place that survives a restart. A player whose secret is lost cannot be recovered. Never ship them to another player. - The player is anonymous: no
ext:id, no way to find them again from your backend. If your game has accounts, use the server API and issue tokens for your own users. - The browser SDK stores the player in
localStorageand renews the token itself.
Get a new player token
Exchanges the secret of an anonymous player for a new token, when the previous one has expired. The browser SDK calls it for you when a request answers 401.
/client/players/tokenAuthentication: publishable key. Idempotency: not needed.
Body
| Field | Type | Required | Rules |
|---|---|---|---|
playerId | string | yes | The player.id returned by Create an anonymous player: 1 to 64 characters. |
playerSecret | string | yes | The playerSecret returned with it: 1 to 200 characters. |
curl -X POST "https://gamecoin.apilow.com/api/v1/client/players/token" \
-H "X-GameCoin-Key: $GAMECOIN_PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{"playerId": "665f1c2e8a3b4d5e6f7081c6", "playerSecret": "<player secret>"}'// The SDK keeps the player id and secret of an anonymous player and calls this route when a request answers 401:
// there is nothing to call. A game whose tokens come from a backend gives init() an onTokenExpired callback instead.
const gc = await GameCoin.init({ publishableKey: "gc_pk_test_…" });
await gc.refresh(); // an expired token is renewed on the wayResponse 200 OK: a new token.
| Field | Type | Description |
|---|---|---|
token | string | The player token, gc_pt_…. |
expiresAt | string | Expiry, one hour after issue. |
{ "token": "gc_pt_…", "expiresAt": "2026-10-05T23:14:41.000Z" }Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 400 | VALIDATION_FAILED | A field is missing, too long or unknown | Read details.fieldErrors. |
| 401 | UNAUTHENTICATED | The secret is wrong, the player is unknown, or the player is not anonymous: the three cases look the same | The player cannot be recovered: create a new anonymous player. |
{ "error": { "code": "UNAUTHENTICATED", "message": "Invalid player credentials" } }Notes
- It works for anonymous players only. A player created by your backend (
ext:) has no secret: ask your server for a token with Issue a player token.
Get the token player
Returns the player of the token, with their wallet and their inventory, in one call. Use it when your game starts and after anything that may have changed the wallet.
/client/meAuthentication: publishable key and player token. Idempotency: not needed. Parameters: none.
curl "https://gamecoin.apilow.com/api/v1/client/me" \
-H "X-GameCoin-Key: $GAMECOIN_PUBLISHABLE_KEY" \
-H "Authorization: Bearer $GAMECOIN_PLAYER_TOKEN"const { wallet, inventory } = await gc.refresh(); // reads GET /client/me and updates the cache
console.log(gc.player.id, wallet.balances.map((b) => `${b.currency} ${b.total}`), inventory);
console.log(gc.balance("gems"), gc.owned("potion")); // cached copies: no requestResponse 200 OK: the player, their wallet and their owned items.
| Field | Type | Description |
|---|---|---|
player | Player | The player of the token. |
wallet | Wallet | Their balances, one per active currency, zeros included. |
inventory | array of InventoryItem | The items they own (quantity above 0). |
{
"player": {
"id": "665f1c2e8a3b4d5e6f708192",
"externalId": "user-42",
"kind": "external",
"displayName": "Alice",
"country": "BE",
"blocked": false,
"createdAt": "2026-10-05T22:14:34.808Z"
},
"wallet": {
"playerId": "665f1c2e8a3b4d5e6f708192",
"balances": [
{ "currency": "gems", "paid": 500, "bonus": 100, "total": 600 },
{ "currency": "gold", "paid": 0, "bonus": 0, "total": 0 }
]
},
"inventory": [{ "sku": "potion", "quantity": 3, "updatedAt": "2026-10-05T22:14:38.396Z" }]
}Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 401 | UNAUTHENTICATED | The token is missing, invalid or expired (the cases look the same), or it was issued for another game or environment | Get a new token. |
| 403 | PLAYER_BLOCKED | The player is blocked | Tell the player; nothing can be done from the client. |
| 429 | RATE_LIMITED | More than 120 requests a minute for this player | Wait Retry-After seconds. |
Notes
- The browser SDK reads this route at
init()and keeps a copy:gc.player,gc.walletandgc.inventoryhold it,gc.balance("gems")andgc.owned("potion")answer without a request, and achangeevent fires when the copy moves.gc.refresh()reads the route again and returns{ wallet, inventory }.
Spend currency
Debits a currency from the wallet of the token player. Use it when the player pays coins for something in your game.
/client/me/wallet/spendAuthentication: publishable key and player token. Idempotency: required (Idempotency-Key header).
Body
| Field | Type | Required | Rules |
|---|---|---|---|
currency | string | yes | A currency code of the catalog (2 to 24 lowercase characters). |
amount | integer | yes | A whole number from 1 to 1,000,000,000,000. |
reason | string | no | Free text, at most 500 characters, recorded in the ledger. There is no metadata on the client API. |
curl -X POST "https://gamecoin.apilow.com/api/v1/client/me/wallet/spend" \
-H "X-GameCoin-Key: $GAMECOIN_PUBLISHABLE_KEY" \
-H "Authorization: Bearer $GAMECOIN_PLAYER_TOKEN" \
-H "Idempotency-Key: client-revive-user-42" \
-H "Content-Type: application/json" \
-d '{"currency": "gems", "amount": 10, "reason": "revive"}'const { entry, balance } = await gc.spend("gems", 10, "revive", { idempotencyKey: "client-revive-user-42" });
console.log(entry.bonusDelta, balance.total, gc.balance("gems"));Response 200 OK: the ledger entry and the new balance, as for the server API.
{
"entry": {
"id": "6ac4215160ea7eba011aaca3",
"type": "spend",
"currency": "gems",
"paidDelta": 0,
"bonusDelta": -10,
"balanceAfter": { "paid": 500, "bonus": 90, "total": 590 },
"reason": "revive",
"metadata": {},
"ref": {},
"createdAt": "2026-10-05T22:14:41.733Z"
},
"balance": { "currency": "gems", "paid": 500, "bonus": 90, "total": 590 }
}Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 400 | IDEMPOTENCY_KEY_REQUIRED | The Idempotency-Key header is missing or empty | Send one. The browser SDK always does. |
| 400 | VALIDATION_FAILED | An unknown field (metadata included); amount not an integer between 1 and 10¹²; currency malformed | Read details.fieldErrors. |
| 401 | UNAUTHENTICATED | The token is missing, invalid or expired | Get a new token and send the request again with the same key. |
| 403 | PLAYER_BLOCKED | The player is blocked | Nothing can be done from the client. |
| 404 | NOT_FOUND | The currency is not in the catalog | Check the code against Get the catalog. |
| 409 | INSUFFICIENT_FUNDS | The balance is lower than amount: details.required and details.available | Offer a pack. Nothing was spent. |
| 409 | IDEMPOTENCY_CONFLICT | The key was already used with a different request | Use a new key for a new spend. |
Notes
- Same rules as the server route: bonus coins first, all or nothing. A key you choose makes a spend safe against a double click or a page reload:
{ idempotencyKey }is the last argument of every changing method of the SDK. Without it, the SDK uses a random key per call and reuses it when it replays the request.
Buy an item with currency
Buys an item for the token player: the price is debited and the item delivered in one step, as for the server API. Only the items flagged clientPurchasable can be bought from a client.
/client/me/purchasesAuthentication: publishable key and player token. Idempotency: required (Idempotency-Key header).
Body
| Field | Type | Required | Rules |
|---|---|---|---|
sku | string | yes | The sku of an item that has a price and is clientPurchasable: 1 to 48 characters. |
quantity | integer | no | A whole number from 1 to 1,000,000,000,000. Default 1. |
curl -X POST "https://gamecoin.apilow.com/api/v1/client/me/purchases" \
-H "X-GameCoin-Key: $GAMECOIN_PUBLISHABLE_KEY" \
-H "Authorization: Bearer $GAMECOIN_PLAYER_TOKEN" \
-H "Idempotency-Key: client-buy-potion-user-42" \
-H "Content-Type: application/json" \
-d '{"sku": "potion", "quantity": 1}'const { entry, balance, item } = await gc.buyItem("potion", 1, { idempotencyKey: "client-buy-potion-user-42" });
console.log(balance.total, item.quantity, gc.owned("potion"));Response 200 OK: the debit, the new balance and the item, as for the server API.
{
"entry": {
"id": "6ac42151c3d98b9534c5ac2d",
"type": "spend",
"currency": "gems",
"paidDelta": 0,
"bonusDelta": -20,
"balanceAfter": { "paid": 500, "bonus": 70, "total": 570 },
"reason": null,
"metadata": {},
"ref": { "itemSku": "potion" },
"createdAt": "2026-10-05T22:14:41.944Z"
},
"balance": { "currency": "gems", "paid": 500, "bonus": 70, "total": 570 },
"item": { "sku": "potion", "quantity": 4, "updatedAt": "2026-10-05T22:14:41.958Z" }
}Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 400 | IDEMPOTENCY_KEY_REQUIRED | The Idempotency-Key header is missing or empty | Send one. The browser SDK always does. |
| 400 | VALIDATION_FAILED | An unknown field; sku empty or too long; quantity not an integer between 1 and 10¹² | Read details.fieldErrors. |
| 401 | UNAUTHENTICATED | The token is missing, invalid or expired | Get a new token and send the request again with the same key. |
| 403 | FORBIDDEN | The item is not clientPurchasable | Sell it from your backend with Buy an item with currency. |
| 403 | PLAYER_BLOCKED | The player is blocked | Nothing can be done from the client. |
| 404 | NOT_FOUND | The item is not in the catalog | Check the sku against Get the catalog. |
| 409 | INSUFFICIENT_FUNDS | The wallet is lower than the price times quantity | Offer a pack. Nothing was debited. |
| 409 | LIMIT_REACHED | The purchase would take the player over the item's maxOwned | The player already has the item. |
| 409 | CONFLICT | The item has no price in currency | Sell the item another way. |
| 409 | IDEMPOTENCY_CONFLICT | The key was already used with a different request | Use a new key for a new purchase. |
Notes
- Tick « Achetable depuis le jeu (API cliente) » on an item in the dashboard (in French for now) only if a player may buy it without your server in the loop. Anything you want to check first (a level, a quest) belongs to your backend.
Consume an item
Removes units of a consumable item the token player owns.
/client/me/inventory/consumeAuthentication: publishable key and player token. Idempotency: required (Idempotency-Key header).
Body
| Field | Type | Required | Rules |
|---|---|---|---|
sku | string | yes | The sku of an item of the catalog: 1 to 48 characters. |
quantity | integer | no | A whole number from 1 to 1,000,000,000,000. Default 1. |
curl -X POST "https://gamecoin.apilow.com/api/v1/client/me/inventory/consume" \
-H "X-GameCoin-Key: $GAMECOIN_PUBLISHABLE_KEY" \
-H "Authorization: Bearer $GAMECOIN_PLAYER_TOKEN" \
-H "Idempotency-Key: client-use-potion-user-42" \
-H "Content-Type: application/json" \
-d '{"sku": "potion"}'const { item } = await gc.consume("potion", 1, { idempotencyKey: "client-use-potion-user-42" });
console.log(item.sku, item.quantity, gc.owned("potion"));Response 200 OK: the item with its remaining quantity, even when it reaches 0.
{ "item": { "sku": "potion", "quantity": 3, "updatedAt": "2026-10-05T22:14:42.152Z" } }Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 400 | IDEMPOTENCY_KEY_REQUIRED | The Idempotency-Key header is missing or empty | Send one. The browser SDK always does. |
| 400 | VALIDATION_FAILED | An unknown field; sku empty or too long; quantity not an integer between 1 and 10¹² | Read details.fieldErrors. |
| 401 | UNAUTHENTICATED | The token is missing, invalid or expired | Get a new token and send the request again with the same key. |
| 403 | PLAYER_BLOCKED | The player is blocked | Nothing can be done from the client. |
| 404 | NOT_FOUND | The item is not in the catalog | Check the sku against Get the catalog. |
| 409 | INSUFFICIENT_FUNDS | The player owns fewer units than quantity: details.required and details.available | Nothing was consumed. |
| 409 | CONFLICT | The item is durable and cannot be consumed | Only consume consumable items. |
| 409 | IDEMPOTENCY_CONFLICT | The key was already used with a different request | Use a new key for a new consumption. |
Notes
- Same rules as the server route. A client can consume what the player owns; it cannot give them anything.
Order a currency pack
Creates an order for a pack, for the token player, and returns the hosted payment page. In a browser, the SDK opens it in a window and tells you when the order is over. Payments are simulated in the test environment.
/client/me/checkoutsAuthentication: publishable key and player token. Idempotency: required (Idempotency-Key header).
Body
| Field | Type | Required | Rules |
|---|---|---|---|
packSku | string | yes | The sku of an active pack: 1 to 48 characters. |
successUrl | string | no | Where to send the player after a successful payment. Same origin as the request's Origin header: see Return URLs. |
cancelUrl | string | no | Where to send the player when the payment fails. Same rule. |
locale | string | no | Language of the hosted payment page, the receipt and the e-mails of this order: fr, en or nl. Any other value is ignored (no error). Without it, the payment page follows the player's browser language (Accept-Language), then French. |
creatorCode | string | no | A creator code typed by the buyer. A refused code answers 400 VALIDATION_FAILED with fieldErrors.creatorCode = ["CODE_INVALID"] and no order is created. The attempts are limited per player and per IP address. |
country is not accepted here: it exists on the server route only.
curl -X POST "https://gamecoin.apilow.com/api/v1/client/me/checkouts" \
-H "X-GameCoin-Key: $GAMECOIN_PUBLISHABLE_KEY" \
-H "Authorization: Bearer $GAMECOIN_PLAYER_TOKEN" \
-H "Idempotency-Key: client-checkout-gems-500-user-42" \
-H "Content-Type: application/json" \
-d '{"packSku": "gems-500"}'// Call it from a click: the SDK opens the payment window before any request, or browsers block it.
document.querySelector("#buy-gems").addEventListener("click", async () => {
const order = await gc.checkout("gems-500"); // resolves when the order is over
console.log(order.status); // "fulfilled" once the player has paid
});Response 201 Created for a new order, 200 OK for a replay: the order and the payment page, as for the server route.
{
"order": {
"id": "665f1c2e8a3b4d5e6f7081b4",
"playerId": "665f1c2e8a3b4d5e6f708192",
"status": "pending",
"pack": { "sku": "gems-500", "name": "Bag of gems" },
"amountCents": 499,
"currency": "EUR",
"country": "BE",
"vatRateBp": 2100,
"vatCents": 87,
"netCents": 412,
"createdAt": "2026-10-05T22:14:42.336Z",
"paidAt": null,
"fulfilledAt": null,
"refundedAt": null,
"creatorCode": null
},
"checkoutUrl": "https://gamecoin.apilow.com/pay/665f1c2e8a3b4d5e6f7081b4?t=…"
}Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 400 | IDEMPOTENCY_KEY_REQUIRED | The Idempotency-Key header is missing or empty | Send one. The browser SDK always does. |
| 400 | VALIDATION_FAILED | An unknown field (country included); packSku empty or too long; a return URL that is invalid, not https, or whose origin is not the one of the request (ORIGIN_MISMATCH) | Read details.fieldErrors. |
| 401 | UNAUTHENTICATED | The token is missing, invalid or expired | Get a new token and send the request again with the same key. |
| 402 | PAYMENT_FAILED | No payment provider is available for the environment | Use a test key: payments are simulated there. |
| 403 | PLAYER_BLOCKED | The player is blocked | Nothing can be done from the client. |
| 404 | NOT_FOUND | The pack is not in the catalog | Check the sku against Get the catalog. |
| 409 | LIMIT_REACHED | The player already bought the pack as many times as its maxPerPlayer allows | Hide the pack for this player. |
| 409 | PACK_NOT_AVAILABLE | The pack is outside its sale window: details.startsAt and details.endsAt | Show « coming soon » or « ended » with these dates. |
| 409 | PACK_SOLD_OUT | The total stock of the pack is used up | Show « sold out ». The stock can come back if a pending order expires. |
| 409 | IDEMPOTENCY_CONFLICT | The key was already used with a different request | Use a new key for a new order. |
{
"error": {
"code": "VALIDATION_FAILED",
"message": "successUrl and cancelUrl must have the same origin as the Origin header of the request",
"details": { "fieldErrors": { "successUrl": ["ORIGIN_MISMATCH"] } }
}
}Notes
gc.checkout(packSku, options)resolves with the order read from the API once it is over (fulfilled,failed,expired…), or with its current status if the player closes the window without paying. Afulfilledorder refreshes the wallet before the promise resolves. When the browser blocks the window, or with{ mode: "redirect" }, the SDK sends the current tab to the payment page and back to the current URL:init()then reads the order and exposes it asgc.returnedOrder.- As on the server, an order is created
pending; the coins arrive when it isfulfilled. Read the order, not the query string of the return URL. - Return URLs are optional: the payment page has a built-in confirmation page that tells the window that opened it with
postMessage({ type: "gamecoin:order", orderId, status }).
Redeem a gift code
The token player types a gift code and receives its free coins and items, once. The route is POST /client/me/codes/redeem, with the body { "code": "…" } and an Idempotency-Key. It is described, with its errors, in Redeem a gift code: any refused code answers 400 VALIDATION_FAILED with fieldErrors.code = ["CODE_INVALID"], and attempts are limited per player and per IP address.
Get an order of the token player
Returns an order of the token player, to follow a payment from the client.
/client/me/orders/{orderId}Authentication: publishable key and player token. Idempotency: not needed.
Path parameters
| Parameter | Type | Description |
|---|---|---|
orderId | string | The id of an order of this player: 24 hexadecimal characters. |
curl "https://gamecoin.apilow.com/api/v1/client/me/orders/665f1c2e8a3b4d5e6f7081b4" \
-H "X-GameCoin-Key: $GAMECOIN_PUBLISHABLE_KEY" \
-H "Authorization: Bearer $GAMECOIN_PLAYER_TOKEN"// Back from a redirect checkout (?order=…&status=… in the URL), init() has already read the order for you.
if (gc.returnedOrder) console.log(gc.returnedOrder.id, gc.returnedOrder.status);Response 200 OK: an Order.
{
"id": "665f1c2e8a3b4d5e6f7081b4",
"playerId": "665f1c2e8a3b4d5e6f708192",
"status": "pending",
"pack": { "sku": "gems-500", "name": "Bag of gems" },
"amountCents": 499,
"currency": "EUR",
"country": "BE",
"vatRateBp": 2100,
"vatCents": 87,
"netCents": 412,
"createdAt": "2026-10-05T22:14:42.336Z",
"paidAt": null,
"fulfilledAt": null,
"refundedAt": null,
"creatorCode": null
}Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 401 | UNAUTHENTICATED | The token is missing, invalid or expired | Get a new token. |
| 404 | NOT_FOUND | No order has this id for this player. An order of another player answers 404 too | Check the id. |
Notes
- The browser SDK has no method to read an order by id:
gc.checkout()follows the order it creates, andgc.returnedOrderis the order of a redirect checkout. - Your server reads any order of the game with Get an order.