# Free currencies and items

> Hand out currency and items from your server, let players buy and use items, and know the limits that apply.

## What you will do

Not everything is sold for euros. Players also earn coins by playing, and spend them on items. This guide covers:

- granting currency from your server (daily rewards, quests, prizes);
- granting an item;
- buying an item with a currency;
- consuming an item;
- the limits that cap each of them.

The examples use a free currency `gold`, the paid currency `gems`, a consumable item `potion` (20 gems) and a durable item `fire-sword` (300 gems, one at most). Set up in the dashboard: [A game without a backend](/docs/guides/no-backend-game#before-you-start). A free currency is « Gratuite » in the dashboard; a paid one is « Payante ».

## Grant currency

Only your server grants (or the dashboard, for a manual gift). A grant always credits the free `bonus` bucket, for a free currency and for a paid one.

```endpoint
POST /players/{player}/wallet/grant
```

<!-- tabs:start -->
```bash tab="curl"
curl -X POST "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/wallet/grant" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
  -H "Idempotency-Key: daily-user-42-2026-10-05" \
  -H "Content-Type: application/json" \
  -d '{"currency":"gold","amount":50,"reason":"daily_reward","metadata":{"day":"3"}}'
```
```javascript tab="Node.js"
const { balance } = await gamecoin.wallet.grant(
  ext("user-42"),
  { currency: "gold", amount: 50, reason: "daily_reward", metadata: { day: "3" } },
  { idempotencyKey: "daily-user-42-2026-10-05" },
);
console.log(balance); // { currency: 'gold', paid: 0, bonus: 50, total: 50 }
```
```python tab="Python"
movement = gamecoin.wallet.grant(
    ext("user-42"),
    currency="gold",
    amount=50,
    reason="daily_reward",
    metadata={"day": "3"},
    idempotency_key="daily-user-42-2026-10-05",
)
print(movement.balance)  # Balance(currency='gold', paid=0, bonus=50, total=50)
```
```php tab="PHP"
$movement = $gamecoin->wallet->grant(
    PlayerRef::ext('user-42'),
    ['currency' => 'gold', 'amount' => 50, 'reason' => 'daily_reward', 'metadata' => ['day' => '3']],
    ['idempotency_key' => 'daily-user-42-2026-10-05'],
);
echo json_encode($movement->balance), "\n"; // {"currency":"gold","paid":0,"bonus":50,"total":50}
```
```go tab="Go"
movement, err := client.Wallet.Grant(
	ctx,
	gamecoin.Ext("user-42"),
	gamecoin.GrantParams{Currency: "gold", Amount: 50, Reason: "daily_reward", Metadata: map[string]string{"day": "3"}},
	gamecoin.WithIdempotencyKey("daily-user-42-2026-10-05"),
)
if err != nil {
	log.Fatal(err)
}
fmt.Printf("%+v\n", movement.Balance) // {Currency:gold Paid:0 Bonus:50 Total:50}
```
```csharp tab="C#"
var movement = await gamecoin.Wallet.GrantAsync(
    PlayerRef.Ext("user-42"),
    new GrantRequest { Currency = "gold", Amount = 50, Reason = "daily_reward", Metadata = new Dictionary<string, string> { ["day"] = "3" } },
    new RequestOptions { IdempotencyKey = "daily-user-42-2026-10-05" });
Console.WriteLine(movement.Balance); // Balance { Currency = gold, Paid = 0, Bonus = 50, Total = 50 }
```
<!-- tabs:end -->

`reason` and `metadata` come back in the ledger, so use them to say why: a quest id, a match id, a day. The **idempotency key** is what makes a daily reward daily: the key `daily-user-42-2026-10-05` can be sent as often as you like and pays once. See [Idempotency](/docs/concepts/idempotency).

## Give an item

An item can also be given. This adds units to the player's inventory.

```endpoint
POST /players/{player}/inventory/grant
```

<!-- tabs:start -->
```bash tab="curl"
curl -X POST "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/inventory/grant" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
  -H "Idempotency-Key: welcome-potions-user-42" \
  -H "Content-Type: application/json" \
  -d '{"sku":"potion","quantity":3}'
```
```javascript tab="Node.js"
const potions = await gamecoin.inventory.grant(
  ext("user-42"),
  { sku: "potion", quantity: 3 },
  { idempotencyKey: "welcome-potions-user-42" },
);
console.log(potions.quantity); // 3
```
```python tab="Python"
potions = gamecoin.inventory.grant(
    ext("user-42"),
    sku="potion",
    quantity=3,
    idempotency_key="welcome-potions-user-42",
)
print(potions.quantity)  # 3
```
```php tab="PHP"
$potions = $gamecoin->inventory->grant(
    PlayerRef::ext('user-42'),
    ['sku' => 'potion', 'quantity' => 3],
    ['idempotency_key' => 'welcome-potions-user-42'],
);
echo $potions->quantity, "\n"; // 3
```
```go tab="Go"
potions, err := client.Inventory.Grant(
	ctx,
	gamecoin.Ext("user-42"),
	gamecoin.InventoryParams{SKU: "potion", Quantity: 3},
	gamecoin.WithIdempotencyKey("welcome-potions-user-42"),
)
if err != nil {
	log.Fatal(err)
}
fmt.Println(potions.Quantity) // 3
```
```csharp tab="C#"
var potions = await gamecoin.Inventory.GrantAsync(
    PlayerRef.Ext("user-42"),
    new InventoryGrantRequest { Sku = "potion", Quantity = 3 },
    new RequestOptions { IdempotencyKey = "welcome-potions-user-42" });
Console.WriteLine(potions.Quantity); // 3
```
<!-- tabs:end -->

```json title="Answer"
{ "item": { "sku": "potion", "quantity": 3, "updatedAt": "2026-10-05T21:55:10.200Z" } }
```

`quantity` is optional and defaults to 1. The answer is the item as the player now holds it: `quantity` is the **total**, not the amount added.

## Buy an item with a currency

An item with a price is bought with its own currency. The wallet is debited and the item added in **one step**: either both happen or neither does. In the examples, a player with 1000 gems and no potion buys two potions.

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

<!-- tabs:start -->
```bash tab="curl"
curl -X POST "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/purchases" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
  -H "Idempotency-Key: potions-user-42-order-9" \
  -H "Content-Type: application/json" \
  -d '{"sku":"potion","quantity":2}'
```
```javascript tab="Node.js"
const purchase = await gamecoin.purchases.create(
  ext("user-42"),
  { sku: "potion", quantity: 2 },
  { idempotencyKey: "potions-user-42-order-9" },
);
console.log(purchase.balance.total, purchase.item.quantity, purchase.entry.ref.itemSku); // 960 2 potion
```
```python tab="Python"
purchase = gamecoin.purchases.create(
    ext("user-42"),
    sku="potion",
    quantity=2,
    idempotency_key="potions-user-42-order-9",
)
print(purchase.balance.total, purchase.item.quantity, purchase.entry.ref.item_sku)  # 960 2 potion
```
```php tab="PHP"
$purchase = $gamecoin->purchases->create(
    PlayerRef::ext('user-42'),
    ['sku' => 'potion', 'quantity' => 2],
    ['idempotency_key' => 'potions-user-42-order-9'],
);
echo $purchase->balance->total, ' ', $purchase->item->quantity, ' ', $purchase->entry->ref->itemSku, "\n"; // 960 2 potion
```
```go tab="Go"
purchase, err := client.Purchases.Create(
	ctx,
	gamecoin.Ext("user-42"),
	gamecoin.PurchaseParams{SKU: "potion", Quantity: 2},
	gamecoin.WithIdempotencyKey("potions-user-42-order-9"),
)
if err != nil {
	log.Fatal(err)
}
fmt.Println(purchase.Balance.Total, purchase.Item.Quantity, *purchase.Entry.Ref.ItemSKU) // 960 2 potion
```
```csharp tab="C#"
var purchase = await gamecoin.Purchases.CreateAsync(
    PlayerRef.Ext("user-42"),
    new PurchaseRequest { Sku = "potion", Quantity = 2 },
    new RequestOptions { IdempotencyKey = "potions-user-42-order-9" });
Console.WriteLine($"{purchase.Balance.Total} {purchase.Item.Quantity} {purchase.Entry.Ref.ItemSku}"); // 960 2 potion
```
```javascript tab="Browser"
// Only items marked « Achetable depuis le jeu (API cliente) » can be bought from the browser.
const { balance, item } = await gc.buyItem("potion", 2);
console.log(balance.total, item.quantity);
```
<!-- tabs:end -->

```json title="Answer to the server call"
{
  "entry": {
    "id": "6ac419ee0b7ba2060d7ef399",
    "type": "spend",
    "currency": "gems",
    "paidDelta": 0,
    "bonusDelta": -40,
    "balanceAfter": { "paid": 0, "bonus": 960, "total": 960 },
    "reason": null,
    "metadata": {},
    "ref": { "itemSku": "potion" },
    "createdAt": "2026-10-05T21:43:10.807Z"
  },
  "balance": { "currency": "gems", "paid": 0, "bonus": 960, "total": 960 },
  "item": { "sku": "potion", "quantity": 2, "updatedAt": "2026-10-05T21:43:10.871Z" }
}
```

The purchase is a `spend` entry whose `ref.itemSku` names the item. It follows the same rules as every spend: free coins first, and `409 INSUFFICIENT_FUNDS` if the player cannot afford it.

An item with no price (`price: null`) cannot be bought with a currency: give it, or sell it inside a pack.

## Consume an item

A consumable item is used up. This removes units from the inventory. In the examples, the player who just bought two potions drinks one.

```endpoint
POST /players/{player}/inventory/consume
```

<!-- tabs:start -->
```bash tab="curl"
curl -X POST "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/inventory/consume" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
  -H "Idempotency-Key: drink-user-42-1" \
  -H "Content-Type: application/json" \
  -d '{"sku":"potion"}'
```
```javascript tab="Node.js"
const used = await gamecoin.inventory.consume(ext("user-42"), { sku: "potion" }, { idempotencyKey: "drink-user-42-1" });
console.log(used.quantity); // 1
```
```python tab="Python"
used = gamecoin.inventory.consume(ext("user-42"), sku="potion", idempotency_key="drink-user-42-1")
print(used.quantity)  # 1
```
```php tab="PHP"
$used = $gamecoin->inventory->consume(PlayerRef::ext('user-42'), ['sku' => 'potion'], ['idempotency_key' => 'drink-user-42-1']);
echo $used->quantity, "\n"; // 1
```
```go tab="Go"
used, err := client.Inventory.Consume(ctx, gamecoin.Ext("user-42"), gamecoin.InventoryParams{SKU: "potion"}, gamecoin.WithIdempotencyKey("drink-user-42-1"))
if err != nil {
	log.Fatal(err)
}
fmt.Println(used.Quantity) // 1
```
```csharp tab="C#"
var used = await gamecoin.Inventory.ConsumeAsync(PlayerRef.Ext("user-42"), new InventoryConsumeRequest { Sku = "potion" }, new RequestOptions { IdempotencyKey = "drink-user-42-1" });
Console.WriteLine(used.Quantity); // 1
```
```javascript tab="Browser"
await gc.consume("potion"); // the "change" event fires with the new quantity
console.log(gc.owned("potion"));
```
<!-- tabs:end -->

The answer carries the item as it is now, **even when it reaches 0**. Reading the inventory lists only items the player owns (quantity above 0). Here it is the inventory of a player who owns a fire-sword and one potion:

<!-- tabs:start -->
```bash tab="curl"
curl "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/inventory" \
  -H "Authorization: Bearer $GAMECOIN_SECRET_KEY"
```
```javascript tab="Node.js"
const items = await gamecoin.inventory.list(ext("user-42"));
console.log(items.map((item) => `${item.sku} x${item.quantity}`)); // [ 'fire-sword x1', 'potion x1' ]
```
```python tab="Python"
items = gamecoin.inventory.list(ext("user-42"))
print([f"{item.sku} x{item.quantity}" for item in items])  # ['fire-sword x1', 'potion x1']
```
```php tab="PHP"
$items = $gamecoin->inventory->list(PlayerRef::ext('user-42'));
echo json_encode(array_map(static fn ($item) => "{$item->sku} x{$item->quantity}", $items)), "\n"; // ["fire-sword x1","potion x1"]
```
```go tab="Go"
items, err := client.Inventory.List(ctx, gamecoin.Ext("user-42"))
if err != nil {
	log.Fatal(err)
}
for _, item := range items {
	fmt.Printf("%s x%d\n", item.SKU, item.Quantity)
}
// fire-sword x1
// potion x1
```
```csharp tab="C#"
var items = await gamecoin.Inventory.ListAsync(PlayerRef.Ext("user-42"));
Console.WriteLine(string.Join(", ", items.Select(item => $"{item.Sku} x{item.Quantity}"))); // fire-sword x1, potion x1
```
<!-- tabs:end -->

## The limits

| Limit | Where it comes from | What you get when you hit it |
|---|---|---|
| **Items owned** | `maxOwned` on the item. A durable item has `1` by default; a consumable has no limit. | `409 LIMIT_REACHED`, with `details.maxOwned` and `details.owned` |
| **Packs bought** | `maxPerPlayer` on the pack | `409 LIMIT_REACHED`, with `details.maxPerPlayer` and `details.bought` |
| **Currency** | You cannot spend more than the balance | `409 INSUFFICIENT_FUNDS`, with `details.required` and `details.available` |
| **Items to consume** | You cannot consume more than the player owns | `409 INSUFFICIENT_FUNDS`, with `details.required` and `details.available` |
| **Amounts** | One operation moves 1 to 1 000 000 000 000 units; a balance stays under 10¹⁵ | `400 VALIDATION_FAILED`, or `409 LIMIT_REACHED` |

A second purchase of a durable item shows the first limit:

```json title="Buying the fire-sword twice"
{
  "error": {
    "code": "LIMIT_REACHED",
    "message": "Item ownership limit exceeded",
    "details": { "maxOwned": 1, "owned": 1 }
  }
}
```

A durable item **cannot be consumed**: `409 CONFLICT`. It stays. Use `consume` only on consumables.

```json title="Consuming the fire-sword"
{ "error": { "code": "CONFLICT", "message": "A durable item cannot be consumed" } }
```

Treat both as normal cases. In a shop, hide the "buy" button of an item the player already owns, and show a message instead of an error.

## Who may do what

| Action | Server (secret key) | Browser (publishable key + token) |
|---|---|---|
| Grant currency | yes | **never** |
| Grant an item | yes | **never** |
| Buy an item with a currency | yes | yes, if the item is « Achetable depuis le jeu » |
| Consume an item | yes | yes |
| Spend currency without an item | yes | yes |

An item that is not buyable from the game answers `403 FORBIDDEN` when the browser tries to buy it. Keep valuable items that way, and sell them from your server, which can check the rules of your game first.

## Where next

- [Browser SDK](/docs/sdk/browser): `spend`, `buyItem` and `consume` in the browser.
- [Wallets and ledger](/docs/concepts/wallets-and-ledger)
- [Errors](/docs/concepts/errors)
