# API reference

> The GameCoin API v1 in one place, with the rules every call shares and an example in six languages for each operation.

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:

```text
https://gamecoin.apilow.com/api/v1
```

Requests 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](/docs/api/catalog), [Players](/docs/api/players), [Wallet](/docs/api/wallet), [Ledger](/docs/api/ledger), [Inventory](/docs/api/inventory), [Purchases](/docs/api/purchases), [Checkouts](/docs/api/checkouts), [Orders](/docs/api/orders), [Codes](/docs/api/codes) | [Client API](/docs/api/client) |

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](/docs/concepts/security-model) explains why this split exists.

## Authentication

**Server API.** Send your secret key as a bearer token:

```http
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:

```http
X-GameCoin-Key: gc_pk_test_…
Authorization: Bearer gc_pt_…
```

You get a player token from your backend with [Issue a player token](/docs/api/players#issue-a-player-token), or for an anonymous player from the [Client API](/docs/api/client#create-an-anonymous-player).

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](/docs/concepts/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 as `ext: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 in `details.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.

```http
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](/docs/api/checkouts#order-a-currency-pack)).
- `POST /orders/{orderId}/refund` has no idempotency key: check the order with `GET /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](/docs/concepts/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](/docs/api/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:

```json
{
  "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](/docs/concepts/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](/docs/concepts/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.

```text
GET https://gamecoin.apilow.com/api/v1/openapi.json
```

It 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](/api/v1/openapi.json).

## 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](/docs/sdk/node), [Python](/docs/sdk/python), [PHP](/docs/sdk/php), [Go](/docs/sdk/go) and [C#](/docs/sdk/dotnet): 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.

<!-- tabs:start -->
```bash tab="curl"
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)
```
```javascript tab="Node.js"
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 once
```
```python tab="Python"
import 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 tab="PHP"
<?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 once
```
```go tab="Go"
package 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
}
```
```csharp tab="C#"
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 once
```
<!-- tabs:end -->

> [!NOTE]
> 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`](/docs/api/catalog#get-the-catalog) reads the catalog | |
| [`PUT /players/{player}`](/docs/api/players#create-or-update-a-player) creates or updates a player | |
| [`GET /players/{player}`](/docs/api/players#get-a-player) reads a player | |
| [`POST /players/{player}/tokens`](/docs/api/players#issue-a-player-token) issues a player token | |
| [`GET /players/{player}/wallet`](/docs/api/wallet#get-a-wallet) reads a wallet | |
| [`POST /players/{player}/wallet/grant`](/docs/api/wallet#grant-currency) grants currency | required |
| [`POST /players/{player}/wallet/spend`](/docs/api/wallet#spend-currency) spends currency | required |
| [`GET /players/{player}/ledger`](/docs/api/ledger#list-ledger-entries) lists ledger entries | |
| [`GET /players/{player}/inventory`](/docs/api/inventory#list-the-inventory) lists the inventory | |
| [`POST /players/{player}/inventory/grant`](/docs/api/inventory#grant-an-item) grants an item | required |
| [`POST /players/{player}/inventory/consume`](/docs/api/inventory#consume-an-item) consumes an item | required |
| [`POST /players/{player}/purchases`](/docs/api/purchases#buy-an-item-with-currency) buys an item with currency | required |
| [`POST /players/{player}/checkouts`](/docs/api/checkouts#order-a-currency-pack) orders a currency pack | required |
| [`POST /players/{player}/codes/redeem`](/docs/api/codes#redeem-a-gift-code) redeems a gift code | required |
| [`GET /orders/{orderId}`](/docs/api/orders#get-an-order) reads an order | |
| [`POST /orders/{orderId}/refund`](/docs/api/orders#refund-an-order) 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`](/docs/api/client#get-the-catalog) reads the catalog | | |
| [`POST /client/players`](/docs/api/client#create-an-anonymous-player) creates an anonymous player | | |
| [`POST /client/players/token`](/docs/api/client#get-a-new-player-token) renews an anonymous player's token | | |
| [`GET /client/me`](/docs/api/client#get-the-token-player) reads the player, wallet and inventory | yes | |
| [`POST /client/me/wallet/spend`](/docs/api/client#spend-currency) spends currency | yes | required |
| [`POST /client/me/purchases`](/docs/api/client#buy-an-item-with-currency) buys an item with currency | yes | required |
| [`POST /client/me/inventory/consume`](/docs/api/client#consume-an-item) consumes an item | yes | required |
| [`POST /client/me/checkouts`](/docs/api/client#order-a-currency-pack) orders a currency pack | yes | required |
| [`POST /client/me/codes/redeem`](/docs/api/codes#redeem-a-gift-code) redeems a gift code | yes | required |
| [`GET /client/me/orders/{orderId}`](/docs/api/client#get-an-order-of-the-token-player) reads an order | yes | |

The objects these operations return are described in [Objects](/docs/api/objects).
