# Players API

> Create and update players, read them back, and issue the tokens your game client needs, with the server API.

A player is someone who plays your game. Most games identify players by their own user id: GameCoin accepts it as `ext:<your id>` and creates the player the first time you write to it, so there is nothing to register in advance. A player id is the `{player}` of every path in this reference: see [Referencing a player](/docs/api#referencing-a-player). The examples on this page continue from the [setup](/docs/api#setup-for-the-examples).

## Create or update a player

Creates the `ext:` player if it does not exist, otherwise updates the profile fields you send. Use it when a player signs up or changes their profile; you do not need it before a grant, a spend or a checkout, which create the player themselves.

```endpoint
PUT /players/{player}
```

**Authentication:** secret key. **Idempotency:** not needed: sending the same profile twice gives the same result.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `player` | string | A GameCoin player id, or `ext:<your id>` URL-encoded once. A GameCoin id that does not exist answers `404`. |

**Body** (JSON; every field is optional, and an empty body means `{}`)

| Field | Type | Required | Rules |
|---|---|---|---|
| `displayName` | string or `null` | no | Name shown in your game: 1 to 80 characters once trimmed. `null` clears it. |
| `email` | string or `null` | no | A valid e-mail address of at most 254 characters. Stored, never returned by the API. `null` clears it. |
| `country` | string or `null` | no | ISO 3166-1 alpha-2 code such as `BE`: two letters, upper-cased for you. It is the default country of the player's checkouts. `null` clears it. |
| `birthYear` | integer or `null` | no | A whole year between 1900 and the current year. Stored, never returned by the API. `null` clears it. |

A field you leave out keeps its value; `null` erases it. Any other field is refused.

<!-- 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": "Alice", "country": "BE"}'
```
```javascript tab="Node.js"
const profile = await gamecoin.players.upsert(player, { displayName: "Alice", country: "BE" });
console.log(profile.id, profile.externalId, profile.displayName);
```
```python tab="Python"
profile = gamecoin.players.upsert(player, display_name="Alice", country="BE")
print(profile.id, profile.external_id, profile.display_name)
```
```php tab="PHP"
$profile = $gamecoin->players->upsert($player, ['display_name' => 'Alice', 'country' => 'BE']);
echo $profile->id, ' ', $profile->externalId, ' ', $profile->displayName, "\n";
```
```go tab="Go"
name, country := "Alice", "BE"
profile, err := client.Players.Upsert(ctx, player, &gamecoin.PlayerProfile{DisplayName: &name, Country: &country})
check(err)
fmt.Println(profile.ID, *profile.ExternalID, *profile.DisplayName)
```
```csharp tab="C#"
var profile = await gamecoin.Players.UpsertAsync(player, new PlayerProfile { DisplayName = "Alice", Country = "BE" });
Console.WriteLine($"{profile.Id} {profile.ExternalId} {profile.DisplayName}");
```
<!-- tabs:end -->

**Response** `200 OK`: a [Player](/docs/api/objects#player). The status is `200` whether the player was created or updated.

```json
{
  "id": "665f1c2e8a3b4d5e6f708192",
  "externalId": "user-42",
  "kind": "external",
  "displayName": "Alice",
  "country": "BE",
  "blocked": false,
  "createdAt": "2026-10-05T22:14:34.808Z"
}
```

**Errors**

| Status | Code | When | What to do |
|---|---|---|---|
| 400 | `VALIDATION_FAILED` | An unknown field; `displayName` empty or longer than 80 characters; `email` not an address; `country` not two letters; `birthYear` not a whole year in range; a player reference that is neither a GameCoin id nor an `ext:` reference (`PLAYER_REF_INVALID`); your own id empty, longer than 128 characters or with leading or trailing whitespace | Read `details.fieldErrors`, fix the field. |
| 404 | `NOT_FOUND` | A GameCoin id that does not exist | Use `ext:<your id>`, which is created on the fly, or an id you got from GameCoin. |

```json
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Invalid player profile",
    "details": { "fieldErrors": { "birthYear": ["Too small: expected number to be >=1900"] } }
  }
}
```

**Notes**

- It is an upsert: the first call creates the player, later calls change the fields you send. To create a player with no profile, send `{}` or nothing.
- To erase a field, send it as `null`: `{"displayName": null}`. An empty string is refused, not treated as `null`.
- `country` is not checked against the list of countries. Checkouts only sell to European Union countries; a player with another country gets the default country `BE` unless you pass `country` to [Order a currency pack](/docs/api/checkouts#order-a-currency-pack).
- The `ext:` id of a player is permanent: it is the key you will use in every later call.

## Get a player

Returns one player. Use it to look a player up by your own id or by GameCoin id, and to check whether they are blocked.

```endpoint
GET /players/{player}
```

**Authentication:** secret key. **Idempotency:** not needed.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `player` | string | A GameCoin player id, or `ext:<your id>` URL-encoded once. |

<!-- tabs:start -->
```bash tab="curl"
curl "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY"
```
```javascript tab="Node.js"
const found = await gamecoin.players.get(player);
console.log(found.id, found.kind, found.blocked);
```
```python tab="Python"
found = gamecoin.players.get(player)
print(found.id, found.kind, found.blocked)
```
```php tab="PHP"
$found = $gamecoin->players->get($player);
echo $found->id, ' ', $found->kind, ' ', var_export($found->blocked, true), "\n";
```
```go tab="Go"
found, err := client.Players.Get(ctx, player)
check(err)
fmt.Println(found.ID, found.Kind, found.Blocked)
```
```csharp tab="C#"
var found = await gamecoin.Players.GetAsync(player);
Console.WriteLine($"{found.Id} {found.Kind} {found.Blocked}");
```
<!-- tabs:end -->

**Response** `200 OK`: a [Player](/docs/api/objects#player).

```json
{
  "id": "665f1c2e8a3b4d5e6f708192",
  "externalId": "user-42",
  "kind": "external",
  "displayName": "Alice",
  "country": "BE",
  "blocked": false,
  "createdAt": "2026-10-05T22:14:34.808Z"
}
```

**Errors**

| Status | Code | When | What to do |
|---|---|---|---|
| 400 | `VALIDATION_FAILED` | The reference is neither a 24-character hexadecimal id nor an `ext:` reference, or it was URL-encoded twice (`PLAYER_REF_INVALID`) | Encode the `ext:` reference exactly once. |
| 404 | `NOT_FOUND` | The player does not exist | For an `ext:` player this is normal until your first write: create it with [Create or update a player](#create-or-update-a-player). |

```json
{ "error": { "code": "NOT_FOUND", "message": "Player not found" } }
```

**Notes**

- A read never creates a player. Only writes do.
- The same player by its GameCoin id (the `id` of the response) gives the same answer:

```bash
curl "https://gamecoin.apilow.com/api/v1/players/665f1c2e8a3b4d5e6f708192" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY"
```

## Issue a player token

Issues a token that lets one player act on their own wallet and inventory from a browser or a game client, through the [client API](/docs/api/client). Your backend calls it after it has authenticated the player, and hands the token to the client.

```endpoint
POST /players/{player}/tokens
```

**Authentication:** secret key. **Idempotency:** not needed: every call issues a new token. **Body:** none.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `player` | string | A GameCoin player id, or `ext:<your id>` URL-encoded once. An unknown `ext:` player is created. |

<!-- 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(player);
console.log(expiresAt.toISOString()); // give `token` to the player's browser
```
```python tab="Python"
token = gamecoin.players.create_token(player)
print(token.expires_at.isoformat())  # give token.token to the player's browser
```
```php tab="PHP"
$token = $gamecoin->players->createToken($player);
echo $token->expiresAt->format(DATE_ATOM), "\n"; // give $token->token to the player's browser
```
```go tab="Go"
token, err := client.Players.CreateToken(ctx, player)
check(err)
fmt.Println(token.ExpiresAt) // give token.Token to the player's browser
```
```csharp tab="C#"
var token = await gamecoin.Players.CreateTokenAsync(player);
Console.WriteLine(token.ExpiresAt); // give token.Token to the player's browser
```
<!-- tabs:end -->

**Response** `200 OK`: the token and its expiry.

| Field | Type | Description |
|---|---|---|
| `token` | string | The player token, `gc_pt_…`. An opaque string: do not parse it. |
| `expiresAt` | string | ISO 8601 date, one hour after issue. |

```json
{ "token": "gc_pt_…", "expiresAt": "2026-10-05T23:14:35.000Z" }
```

**Errors**

| Status | Code | When | What to do |
|---|---|---|---|
| 400 | `VALIDATION_FAILED` | The player reference is malformed (`PLAYER_REF_INVALID`) | Encode the `ext:` reference exactly once. |
| 403 | `PLAYER_BLOCKED` | The player is blocked | Do not hand out a token to this player. |
| 404 | `NOT_FOUND` | A GameCoin id that does not exist | Use an existing id, or `ext:<your id>`. |

**Notes**

- The token is tied to one player, one game and one environment: it is refused on any other. It lasts one hour: issue a new one when it expires, or when the client gets a `401`.
- The client sends it as `Authorization: Bearer gc_pt_…` next to the publishable key, or gives it to the browser SDK as `playerToken`. See [Players and tokens](/docs/concepts/players-and-tokens).
- A token is a credential for that player: do not log it, and send it only over HTTPS. Issuing a new one does not revoke the previous ones, which stay valid until they expire.
