API reference
API reference
The GameCoin API v1 in one place, with the rules every call shares and an example in six languages for each operation.
On this page
GameCoin keeps the virtual currency, the inventory and the sales of your game behind one HTTP API. This page gives the rules that every operation shares; the pages that follow describe each resource, one section per operation. For each operation you will find what to send, what you get back, what can fail, and an example you can copy in curl, Node.js, Python, PHP, Go and C#.
Base URL
Every route lives under /api/v1. For this service the base URL is:
https://gamecoin.apilow.com/api/v1Requests and responses are JSON (Content-Type: application/json). Paths in this reference are relative to the base URL: GET /catalog means GET https://gamecoin.apilow.com/api/v1/catalog.
Two APIs
| Server API | Client API | |
|---|---|---|
| Called from | Your game backend | A browser or a game client |
| Routes | Everything except /client/** | /client/** |
| Credential | Secret key, Authorization: Bearer gc_sk_test_… | Publishable key, X-GameCoin-Key: gc_pk_test_…, plus a player token on player routes |
| Can credit a player | Yes: grants, pack deliveries, refunds | Never |
| CORS | None: never call it from a browser | Restricted to the game's allowed origins (open in test while the list is empty) |
| Reference | Catalog, Players, Wallet, Ledger, Inventory, Purchases, Checkouts, Orders, Codes | Client API |
A browser can read, spend, buy an item with a currency, consume and pay for a pack. Crediting needs your secret key, on your server (the only exception: a gift code that you issued adds free coins, once per player). Security model explains why this split exists.
Authentication
Server API. Send your secret key as a bearer token:
Authorization: Bearer gc_sk_test_…Client API. Send your publishable key in X-GameCoin-Key. Routes that act on a player (/client/me/**) also need a player token, which lasts one hour:
X-GameCoin-Key: gc_pk_test_…
Authorization: Bearer gc_pt_…You get a player token from your backend with Issue a player token, or for an anonymous player from the Client API.
The key decides the game and the environment (test or live). Nothing in a URL or a body can change that. The live environment is reserved for verified studios and answers 403 ENV_NOT_ENABLED to the others. See Keys and environments.
Warning
A secret key must never leave your server. Keep it in an environment variable or a secret manager, and never put it in a game client, a web page or a repository. The examples read it from GAMECOIN_SECRET_KEY.
Wrong credentials are refused in a way that does not leak anything. A missing, malformed, unknown or revoked key is 401 UNAUTHENTICATED. A key of the wrong kind for the route (a publishable key sent as the bearer of a server route, a secret key sent in X-GameCoin-Key to a client route) is 403 FORBIDDEN. An invalid or expired player token is 401 UNAUTHENTICATED, and the two cases look the same.
Referencing a player
The {player} segment of a path is either:
- a GameCoin player id: 24 hexadecimal characters, such as
665f1c2e8a3b4d5e6f708192; - or your own id, prefixed with
ext:, such asext:user-42.
Your own id is opaque and case-sensitive: 1 to 128 characters, with no leading or trailing whitespace. Anything else is allowed (spaces, slashes, accents).
The whole reference is URL-encoded exactly once, with encodeURIComponent("ext:" + id) or its equivalent. The SDKs do it for you.
| Your id | In the path |
|---|---|
user-42 | ext%3Auser-42 |
Zoë Müller/1% | ext%3AZo%C3%AB%20M%C3%BCller%2F1%25 |
Encoding twice, or sending a reference that is neither a 24-character hexadecimal id nor an ext: reference, is 400 VALIDATION_FAILED with details.fieldErrors.player set to PLAYER_REF_INVALID.
With a secret key, the first write on an unknown ext: player creates it: the PUT /players/{player} and every POST /players/{player}/… (tokens, grants, spends, purchases, checkouts) need no registration step. The player is created even when the write then fails, for instance a spend that answers INSUFFICIENT_FUNDS. A read (GET) on an unknown player answers 404 NOT_FOUND. A GameCoin id that does not exist is always 404, because only ext: players are created on the fly.
Requests and responses
- Strict bodies. An unknown field is a
400 VALIDATION_FAILED, with the field named indetails.fieldErrors. This is deliberate:{ "paid": 100 }on a grant fails instead of being ignored. Malformed JSON, and a body that is not an object, fail the same way. - Size. A body is at most 64 KiB (65,536 bytes), otherwise
400 VALIDATION_FAILED. - Empty body. Where every field is optional (
PUT /players/{player}, refund,POST /client/players), an empty body means{}. - Dates are ISO 8601 strings in UTC, with milliseconds:
2026-10-05T21:37:21.993Z. - Amounts are integers, with no decimals: virtual currencies in their smallest unit, euro prices in cents. A movement is between 1 and 1,000,000,000,000 (10¹²), and a balance can never exceed 1,000,000,000,000,000 (10¹⁵), otherwise
409 LIMIT_REACHED. - Caching. Every response carries
Cache-Control: no-store, except the OpenAPI document. - Query strings. Unknown query parameters are ignored.
Idempotency
Every POST that changes a wallet or an inventory, or creates a checkout, requires an Idempotency-Key header: 1 to 100 printable ASCII characters. It makes a retry safe: if your request times out, send it again with the same key and the change happens once.
Idempotency-Key: grant-level-5-user-42| You send | You get |
|---|---|
| A key never seen | The request runs; the response is stored under the key |
| The same key and the same request | The original response, with the header Idempotent-Replayed: true. Nothing runs twice. |
| The same key and a different request | 409 IDEMPOTENCY_CONFLICT |
| No key, or an empty one | 400 IDEMPOTENCY_KEY_REQUIRED |
| A key that is too long or has characters outside printable ASCII | 400 VALIDATION_FAILED |
- The "same request" means the same route, the same player and the same body. The key is shared by every operation of the game and the environment: reusing it on another route or another player is a conflict.
- Keys are kept 30 days.
- Only successful responses are stored. A request that failed (
INSUFFICIENT_FUNDS,VALIDATION_FAILED…) did nothing and can be retried with the same key. - A replayed checkout returns the order in its current state, not the one it had at creation (see Order a currency pack).
POST /orders/{orderId}/refundhas no idempotency key: check the order withGET /orders/{orderId}before retrying it.
The operations that take a key are marked Idempotency: required in this reference. The SDKs generate a key for you, once per call, and reuse it for every automatic retry. See Idempotency.
Pagination
Lists use a cursor. Send ?limit= (1 to 100, default 20) and ?cursor=. The response carries nextCursor: pass it as cursor to get the next page, until it is null. A cursor is opaque: do not build or parse one. An invalid cursor is 400 VALIDATION_FAILED with CURSOR_INVALID in details.fieldErrors.cursor. Only the ledger is paginated today.
Status codes
| Status | Meaning |
|---|---|
200 | Success. This includes the replay of a request that was already processed. |
201 | A new anonymous player (POST /client/players), or a new checkout (not a replay). |
204 | A CORS preflight (OPTIONS) on a client route. |
400 | Invalid request: VALIDATION_FAILED or IDEMPOTENCY_KEY_REQUIRED. |
401 | UNAUTHENTICATED. |
402 | PAYMENT_FAILED. |
403 | FORBIDDEN, PLAYER_BLOCKED or ENV_NOT_ENABLED. |
404 | NOT_FOUND. |
409 | INSUFFICIENT_FUNDS, LIMIT_REACHED, IDEMPOTENCY_CONFLICT or CONFLICT. |
429 | RATE_LIMITED, with Retry-After. |
500 | INTERNAL. |
Requests are processed in this order: authentication, rate limit, idempotency key, body validation. So a request with a bad key never reaches validation, and a missing Idempotency-Key is reported before a malformed body.
Errors common to every operation
Every error has the same shape:
{
"error": {
"code": "INSUFFICIENT_FUNDS",
"message": "Insufficient funds",
"details": { "required": 5000, "available": 70 }
}
}code is stable and is what your code should test. message is English text for developers, not for your players. details is optional and only guaranteed for required and available on INSUFFICIENT_FUNDS; details.fieldErrors on VALIDATION_FAILED maps a field to messages or codes, and is meant for you to read, not to parse.
Any operation can fail with these errors, so the tables of the next pages do not repeat them:
| Status | Code | When | What to do |
|---|---|---|---|
| 401 | UNAUTHENTICATED | Missing, malformed, unknown or revoked key; invalid or expired player token | Check the key and its environment. For a player token, issue a new one. |
| 403 | FORBIDDEN | The key is of the wrong kind for the route | Use the secret key on the server API and the publishable key on the client API. |
| 403 | ENV_NOT_ENABLED | A live key used by a studio that is not verified | Use a test key, or get the studio verified. |
| 429 | RATE_LIMITED | Too many requests | Wait Retry-After seconds, then retry. |
| 500 | INTERNAL | Unexpected error on our side | Retry later; a request with an idempotency key is safe to repeat. |
The complete list of codes is in Errors.
Rate limits
| Scope | Limit |
|---|---|
| Server API, per secret key | 600 requests per minute |
| Client API with a player token, per player | 120 requests per minute |
| Client API without a token, per IP address | 120 requests per minute |
| Anonymous player creation, per IP address | 30 per hour |
A 429 answer carries a Retry-After header: the number of seconds to wait. The SDKs wait and retry for you. See Rate limits.
OpenAPI document
The same contract is published as an OpenAPI 3.1 document, with schemas, examples and the errors of every operation: 23 paths and 24 operations. Use it to generate a client, to import the API into Postman or Insomnia, or to feed a tool.
GET https://gamecoin.apilow.com/api/v1/openapi.jsonIt is public: no key, no rate limit, Access-Control-Allow-Origin: *, and Cache-Control: public, max-age=300. Its servers entry is this service. Open it here.
Setup for the examples
The examples of the next pages share one setup: a client built with your secret key, and a player whose own id is user-42. Copy it once; each example then continues from it. The SDKs are the server SDKs of Node.js, Python, PHP, Go and C#: their pages tell you how to install them.
The curl examples read your key from GAMECOIN_SECRET_KEY (the client API ones also use GAMECOIN_PUBLISHABLE_KEY and GAMECOIN_PLAYER_TOKEN). Use a test key: the test environment is where payments are simulated.
export GAMECOIN_SECRET_KEY="gc_sk_test_…" # a secret test key: « Clés d'API » in the dashboard
# The player "ext:user-42" is written ext%3Auser-42 in a path (URL-encoded once)import { GameCoin, ext } from "@apilow/gamecoin";
const gamecoin = new GameCoin(process.env.GAMECOIN_SECRET_KEY, { baseUrl: "https://gamecoin.apilow.com" });
const player = ext("user-42"); // your own id for the player: the SDK sends ext:user-42, URL-encoded onceimport os
from gamecoin import GameCoin, ext
gamecoin = GameCoin(os.environ["GAMECOIN_SECRET_KEY"], base_url="https://gamecoin.apilow.com")
player = ext("user-42") # your own id for the player: the SDK sends ext:user-42, URL-encoded once<?php
require __DIR__ . '/vendor/autoload.php';
use Apilow\GameCoin\GameCoin;
use Apilow\GameCoin\PlayerRef;
$gamecoin = new GameCoin(getenv('GAMECOIN_SECRET_KEY'), ['base_url' => 'https://gamecoin.apilow.com']);
$player = PlayerRef::ext('user-42'); // your own id for the player: the SDK sends ext:user-42, URL-encoded oncepackage main
import (
"context"
"errors"
"fmt"
"log"
"os"
gamecoin "github.com/apilow/gamecoin-go"
)
func check(err error) {
if err != nil {
log.Fatal(err)
}
}
func main() {
// "errors" is used by the error-handling example: drop the imports yours do not need.
ctx := context.Background()
client, err := gamecoin.New(os.Getenv("GAMECOIN_SECRET_KEY"), gamecoin.WithBaseURL("https://gamecoin.apilow.com"))
check(err)
player := gamecoin.Ext("user-42") // your own id for the player: the SDK sends ext:user-42, URL-encoded once
// ... the example you want to run goes here
}using Apilow.GameCoin;
using var gamecoin = new GameCoinClient(
Environment.GetEnvironmentVariable("GAMECOIN_SECRET_KEY")!,
new GameCoinOptions { BaseUrl = "https://gamecoin.apilow.com" });
var player = PlayerRef.Ext("user-42"); // your own id for the player: the SDK sends ext:user-42, URL-encoded onceNote
The SDKs are pre-release, and the package names above are the planned ones. The examples run as written against the test environment; on your own project, use the gc_sk_test_… key of your game (the dashboard is in French for now: « Clés d'API »), and the currencies, items and packs of your own catalog in place of the gems, potion and gems-500 of the examples.
The examples use a game with two currencies, gems (sold for real money) and gold (free); two items, potion (consumable, 20 gems) and fire-sword (durable, 300 gems, at most one per player); and two packs, starter (once per player) and gems-500.
All operations
Server API, with a secret key:
| Operation | Idempotency |
|---|---|
GET /catalog reads the catalog | |
PUT /players/{player} creates or updates a player | |
GET /players/{player} reads a player | |
POST /players/{player}/tokens issues a player token | |
GET /players/{player}/wallet reads a wallet | |
POST /players/{player}/wallet/grant grants currency | required |
POST /players/{player}/wallet/spend spends currency | required |
GET /players/{player}/ledger lists ledger entries | |
GET /players/{player}/inventory lists the inventory | |
POST /players/{player}/inventory/grant grants an item | required |
POST /players/{player}/inventory/consume consumes an item | required |
POST /players/{player}/purchases buys an item with currency | required |
POST /players/{player}/checkouts orders a currency pack | required |
POST /players/{player}/codes/redeem redeems a gift code | required |
GET /orders/{orderId} reads an order | |
POST /orders/{orderId}/refund refunds an order | none: see the page |
Client API, with a publishable key (and a player token where marked):
| Operation | Player token | Idempotency |
|---|---|---|
GET /client/catalog reads the catalog | ||
POST /client/players creates an anonymous player | ||
POST /client/players/token renews an anonymous player's token | ||
GET /client/me reads the player, wallet and inventory | yes | |
POST /client/me/wallet/spend spends currency | yes | required |
POST /client/me/purchases buys an item with currency | yes | required |
POST /client/me/inventory/consume consumes an item | yes | required |
POST /client/me/checkouts orders a currency pack | yes | required |
POST /client/me/codes/redeem redeems a gift code | yes | required |
GET /client/me/orders/{orderId} reads an order | yes |
The objects these operations return are described in Objects.