Guides
Free currencies and items
Hand out currency and items from your server, let players buy and use items, and know the limits that apply.
On this page
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. 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.
/players/{player}/wallet/grantcurl -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"}}'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 }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)$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}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}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 }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.
Give an item
An item can also be given. This adds units to the player's inventory.
/players/{player}/inventory/grantcurl -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}'const potions = await gamecoin.inventory.grant(
ext("user-42"),
{ sku: "potion", quantity: 3 },
{ idempotencyKey: "welcome-potions-user-42" },
);
console.log(potions.quantity); // 3potions = gamecoin.inventory.grant(
ext("user-42"),
sku="potion",
quantity=3,
idempotency_key="welcome-potions-user-42",
)
print(potions.quantity) # 3$potions = $gamecoin->inventory->grant(
PlayerRef::ext('user-42'),
['sku' => 'potion', 'quantity' => 3],
['idempotency_key' => 'welcome-potions-user-42'],
);
echo $potions->quantity, "\n"; // 3potions, 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) // 3var 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{ "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.
/players/{player}/purchasescurl -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}'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 potionpurchase = 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$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 potionpurchase, 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 potionvar 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// 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);{
"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.
/players/{player}/inventory/consumecurl -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"}'const used = await gamecoin.inventory.consume(ext("user-42"), { sku: "potion" }, { idempotencyKey: "drink-user-42-1" });
console.log(used.quantity); // 1used = gamecoin.inventory.consume(ext("user-42"), sku="potion", idempotency_key="drink-user-42-1")
print(used.quantity) # 1$used = $gamecoin->inventory->consume(PlayerRef::ext('user-42'), ['sku' => 'potion'], ['idempotency_key' => 'drink-user-42-1']);
echo $used->quantity, "\n"; // 1used, 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) // 1var used = await gamecoin.Inventory.ConsumeAsync(PlayerRef.Ext("user-42"), new InventoryConsumeRequest { Sku = "potion" }, new RequestOptions { IdempotencyKey = "drink-user-42-1" });
Console.WriteLine(used.Quantity); // 1await gc.consume("potion"); // the "change" event fires with the new quantity
console.log(gc.owned("potion"));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:
curl "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/inventory" \
-H "Authorization: Bearer $GAMECOIN_SECRET_KEY"const items = await gamecoin.inventory.list(ext("user-42"));
console.log(items.map((item) => `${item.sku} x${item.quantity}`)); // [ 'fire-sword x1', 'potion x1' ]items = gamecoin.inventory.list(ext("user-42"))
print([f"{item.sku} x{item.quantity}" for item in items]) # ['fire-sword x1', 'potion x1']$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"]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 x1var 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 x1The 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:
{
"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.
{ "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:
spend,buyItemandconsumein the browser. - Wallets and ledger
- Errors