API reference
Purchases API
Sell an item to a player for virtual currency, debiting the wallet and delivering the item in one step, with the server API.
On this page
A purchase turns coins into an item: the player pays the price of the item in its currency and receives the item. It happens in one step: either both the debit and the delivery are done, or nothing is. This is not a sale for real money: for that, see Checkouts. The examples on this page continue from the setup.
Buy an item with currency
Debits the price of an item, multiplied by the quantity, from the player's wallet, and adds the item to their inventory. Use it for every in-game shop where coins buy items.
/players/{player}/purchasesAuthentication: secret key. Idempotency: required (Idempotency-Key header).
Path parameters
| Parameter | Type | Description |
|---|---|---|
player | string | A GameCoin player id, or ext:<your id> URL-encoded once. An unknown ext: player is created, with an empty wallet. |
Body
| Field | Type | Required | Rules |
|---|---|---|---|
sku | string | yes | The sku of an active item that has a price: 1 to 48 characters. |
quantity | integer | no | A whole number from 1 to 1,000,000,000,000. Default 1. The player pays the price times quantity. |
curl -X POST "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/purchases" \
-H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
-H "Idempotency-Key: buy-potion-user-42" \
-H "Content-Type: application/json" \
-d '{"sku": "potion", "quantity": 1}'const purchase = await gamecoin.purchases.create(
player,
{ sku: "potion", quantity: 1 },
{ idempotencyKey: "buy-potion-user-42" },
);
console.log(purchase.balance.total, purchase.item.quantity, purchase.entry.ref.itemSku);purchase = gamecoin.purchases.create(player, sku="potion", quantity=1, idempotency_key="buy-potion-user-42")
print(purchase.balance.total, purchase.item.quantity, purchase.entry.ref.item_sku)$purchase = $gamecoin->purchases->create(
$player,
['sku' => 'potion', 'quantity' => 1],
['idempotency_key' => 'buy-potion-user-42'],
);
echo $purchase->balance->total, ' ', $purchase->item->quantity, ' ', $purchase->entry->ref->itemSku, "\n";purchase, err := client.Purchases.Create(ctx, player,
gamecoin.PurchaseParams{SKU: "potion", Quantity: 1},
gamecoin.WithIdempotencyKey("buy-potion-user-42"))
check(err)
fmt.Println(purchase.Balance.Total, purchase.Item.Quantity, *purchase.Entry.Ref.ItemSKU)var purchase = await gamecoin.Purchases.CreateAsync(
player,
new PurchaseRequest { Sku = "potion", Quantity = 1 },
new RequestOptions { IdempotencyKey = "buy-potion-user-42" });
Console.WriteLine($"{purchase.Balance.Total} {purchase.Item.Quantity} {purchase.Entry.Ref.ItemSku}");Response 200 OK: the debit, the new balance and the item.
| Field | Type | Description |
|---|---|---|
entry | LedgerEntry | The debit: type spend, with ref.itemSku set to the item. |
balance | Balance | The balance of the item's currency after the purchase. |
item | InventoryItem | The item and the quantity the player owns after the purchase. |
{
"entry": {
"id": "6ac4214e255a2e692cd84196",
"type": "spend",
"currency": "gems",
"paidDelta": 0,
"bonusDelta": -20,
"balanceAfter": { "paid": 0, "bonus": 50, "total": 50 },
"reason": null,
"metadata": {},
"ref": { "itemSku": "potion" },
"createdAt": "2026-10-05T22:14:38.350Z"
},
"balance": { "currency": "gems", "paid": 0, "bonus": 50, "total": 50 },
"item": { "sku": "potion", "quantity": 3, "updatedAt": "2026-10-05T22:14:38.396Z" }
}Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 400 | IDEMPOTENCY_KEY_REQUIRED | The Idempotency-Key header is missing or empty | Send one: see Idempotency. |
| 400 | VALIDATION_FAILED | An unknown field; sku empty or longer than 48 characters; quantity not an integer between 1 and 10¹²; a key that is not 1 to 100 printable ASCII characters | Read details.fieldErrors. |
| 403 | PLAYER_BLOCKED | The player is blocked | Nothing can be written for this player until the player is unblocked. |
| 404 | NOT_FOUND | The item is not in the catalog (or is archived), or a GameCoin id does not exist | Check the sku against Get the catalog. |
| 409 | INSUFFICIENT_FUNDS | The wallet is lower than the price times quantity: details.required and details.available | Nothing was debited and nothing delivered. Offer a pack, or a smaller quantity. |
| 409 | LIMIT_REACHED | The purchase would take the player over the item's maxOwned: details.maxOwned and details.owned | Nothing was debited. The player already has the item. |
| 409 | CONFLICT | The item has no price in currency | Sell the item another way, or grant it. |
| 409 | IDEMPOTENCY_CONFLICT | The key was already used with a different request | Use a new key for a new purchase. |
{
"error": {
"code": "INSUFFICIENT_FUNDS",
"message": "Insufficient funds",
"details": { "required": 2000, "available": 935 }
}
}Notes
- Replay. The same key with the same request returns the original response again, with the header
Idempotent-Replayed: true, and changes nothing. - The debit and the delivery are one transaction: if the player cannot afford the item, or is at the item's limit, nothing changes. You never have to undo half a purchase.
- The price is the one of the item in the catalog, in its own currency (
gemsforpotion). Coins are taken the usual way:bonusfirst, thenpaid. - On this server route any item with a price can be sold. The client API only sells the items flagged
clientPurchasable. - The ledger entry has no
reason: it is identified by itsref.itemSku. - The first write on an unknown
ext:player creates it; its purchase then fails withINSUFFICIENT_FUNDS.