# 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.

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](/docs/api/checkouts). The examples on this page continue from the [setup](/docs/api#setup-for-the-examples).

## 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.

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

**Authentication:** 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`. |

<!-- 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: buy-potion-user-42" \
  -H "Content-Type: application/json" \
  -d '{"sku": "potion", "quantity": 1}'
```
```javascript tab="Node.js"
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);
```
```python tab="Python"
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)
```
```php tab="PHP"
$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";
```
```go tab="Go"
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)
```
```csharp tab="C#"
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}");
```
<!-- tabs:end -->

**Response** `200 OK`: the debit, the new balance and the item.

| Field | Type | Description |
|---|---|---|
| `entry` | [LedgerEntry](/docs/api/objects#ledgerentry) | The debit: type `spend`, with `ref.itemSku` set to the item. |
| `balance` | [Balance](/docs/api/objects#balance) | The balance of the item's currency after the purchase. |
| `item` | [InventoryItem](/docs/api/objects#inventoryitem) | The item and the quantity the player owns after the purchase. |

```json
{
  "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](/docs/api#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](/docs/api/catalog#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](/docs/api/inventory#grant-an-item). |
| 409 | `IDEMPOTENCY_CONFLICT` | The key was already used with a different request | Use a new key for a new purchase. |

```json
{
  "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 (`gems` for `potion`). Coins are taken the usual way: `bonus` first, then `paid`.
- 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 its `ref.itemSku`.
- The first write on an unknown `ext:` player creates it; its purchase then fails with `INSUFFICIENT_FUNDS`.
