API reference
Checkouts API
Sell a currency pack for real money by creating an order and sending the player to the hosted payment page, with the server API.
On this page
A checkout is how a player pays real money for a pack: a bundle of currency and items that you define in the dashboard (« Packs », in French for now). You create an order, GameCoin returns the address of a hosted payment page, and you send the player there. When the payment succeeds, the coins and items are delivered to the player. You never handle card details. Payments are simulated in the test environment: the payment page offers "Pay (simulated)" and "Decline"; real payments are not available yet. The examples on this page continue from the setup.
Order a currency pack
Creates an order for a pack and returns the hosted payment page. Use it when a player taps "Buy" on a pack in your shop. The call returns at once with a pending order: the coins are delivered later, when the player pays.
/players/{player}/checkoutsAuthentication: 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. |
Body
| Field | Type | Required | Rules |
|---|---|---|---|
packSku | string | yes | The sku of an active pack: 1 to 48 characters. |
successUrl | string | no | Where to send the player after a successful payment: at most 2048 characters, https (http only on localhost, in the test environment). |
cancelUrl | string | no | Where to send the player when the payment fails or is refused. Same rules as successUrl. |
country | string | no | The buyer's country, two letters (ISO 3166-1 alpha-2), which decides the VAT. Only countries of the European Union are sold to. Defaults to the player's country if it is sold to, otherwise BE. |
locale | string | no | Language of the hosted payment page, the receipt and the e-mails of this order: fr, en or nl. Any other value is ignored (no error). Without it, the payment page follows the player's browser language (Accept-Language), then French. |
creatorCode | string | no | The creator code the buyer typed: 1 to 64 characters, case, spaces and dashes ignored. The buyer gets a bonus of free coins and the creator earns a tracked commission, both when the order is delivered. |
curl -X POST "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/checkouts" \
-H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
-H "Idempotency-Key: checkout-gems-500-user-42" \
-H "Content-Type: application/json" \
-d '{"packSku": "gems-500", "successUrl": "https://example.com/shop/thanks", "cancelUrl": "https://example.com/shop", "country": "BE"}'const { order, checkoutUrl } = await gamecoin.checkouts.create(
player,
{ packSku: "gems-500", successUrl: "https://example.com/shop/thanks", cancelUrl: "https://example.com/shop", country: "BE" },
{ idempotencyKey: "checkout-gems-500-user-42" },
);
console.log(order.id, order.status, order.amountCents, checkoutUrl); // send the player to checkoutUrlcheckout = gamecoin.checkouts.create(
player,
pack_sku="gems-500",
success_url="https://example.com/shop/thanks",
cancel_url="https://example.com/shop",
country="BE",
idempotency_key="checkout-gems-500-user-42",
)
print(checkout.order.id, checkout.order.status, checkout.order.amount_cents, checkout.checkout_url) # send the player to checkout_url$checkout = $gamecoin->checkouts->create(
$player,
[
'pack_sku' => 'gems-500',
'success_url' => 'https://example.com/shop/thanks',
'cancel_url' => 'https://example.com/shop',
'country' => 'BE',
],
['idempotency_key' => 'checkout-gems-500-user-42'],
);
echo $checkout->order->id, ' ', $checkout->order->status, ' ', $checkout->order->amountCents, ' ', $checkout->checkoutUrl, "\n"; // send the player to checkoutUrlcheckout, err := client.Checkouts.Create(ctx, player,
gamecoin.CheckoutParams{
PackSKU: "gems-500",
SuccessURL: "https://example.com/shop/thanks",
CancelURL: "https://example.com/shop",
Country: "BE",
},
gamecoin.WithIdempotencyKey("checkout-gems-500-user-42"))
check(err)
fmt.Println(checkout.Order.ID, checkout.Order.Status, checkout.Order.AmountCents, checkout.CheckoutURL) // send the player to CheckoutURLvar checkout = await gamecoin.Checkouts.CreateAsync(
player,
new CheckoutRequest
{
PackSku = "gems-500",
SuccessUrl = "https://example.com/shop/thanks",
CancelUrl = "https://example.com/shop",
Country = "BE",
},
new RequestOptions { IdempotencyKey = "checkout-gems-500-user-42" });
Console.WriteLine($"{checkout.Order.Id} {checkout.Order.Status} {checkout.Order.AmountCents} {checkout.CheckoutUrl}"); // send the player to CheckoutUrlResponse 201 Created for a new order, 200 OK for a replay.
| Field | Type | Description |
|---|---|---|
order | Order | The new order, pending. The pack, the amount and the VAT are frozen at this moment. |
checkoutUrl | string | The hosted payment page of the order, /pay/{orderId}?t=…. Send the player there. |
{
"order": {
"id": "665f1c2e8a3b4d5e6f7081b4",
"playerId": "665f1c2e8a3b4d5e6f708192",
"status": "pending",
"pack": { "sku": "gems-500", "name": "Bag of gems" },
"amountCents": 499,
"currency": "EUR",
"country": "BE",
"vatRateBp": 2100,
"vatCents": 87,
"netCents": 412,
"createdAt": "2026-10-05T22:14:39.356Z",
"paidAt": null,
"fulfilledAt": null,
"refundedAt": null,
"creatorCode": null
},
"checkoutUrl": "https://gamecoin.apilow.com/pay/665f1c2e8a3b4d5e6f7081b4?t=…"
}Send the same request again with the same key and you get the same order in its current state and the same checkoutUrl, with 200 and Idempotent-Replayed: true, and no second order. After the player has paid, the replay shows it:
curl -i -X POST "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/checkouts" \
-H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
-H "Idempotency-Key: checkout-gems-500-user-42" \
-H "Content-Type: application/json" \
-d '{"packSku": "gems-500", "successUrl": "https://example.com/shop/thanks", "cancelUrl": "https://example.com/shop", "country": "BE"}'HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Idempotent-Replayed: true
{
"order": {
"id": "665f1c2e8a3b4d5e6f7081b4",
"playerId": "665f1c2e8a3b4d5e6f708192",
"status": "fulfilled",
"pack": { "sku": "gems-500", "name": "Bag of gems" },
"amountCents": 499,
"currency": "EUR",
"country": "BE",
"vatRateBp": 2100,
"vatCents": 87,
"netCents": 412,
"createdAt": "2026-10-05T22:14:39.356Z",
"paidAt": "2026-10-05T22:14:39.826Z",
"fulfilledAt": "2026-10-05T22:14:39.826Z",
"refundedAt": null,
"creatorCode": null
},
"checkoutUrl": "https://gamecoin.apilow.com/pay/665f1c2e8a3b4d5e6f7081b4?t=…"
}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 | details.fieldErrors.creatorCode is ["CODE_INVALID"]: the creator code is unknown, disabled, not valid for this pack, or the creator's own player (always the same answer, and no order is created) | Tell the buyer the code is not valid; let them retry or buy without it. |
| 400 | VALIDATION_FAILED | An unknown field; packSku empty or longer than 48 characters; country not two letters, or a country that is not sold to (COUNTRY_NOT_SELLABLE); a return URL that is not a valid URL (URL_INVALID) or not https (URL_SCHEME_NOT_ALLOWED); a key that is not 1 to 100 printable ASCII characters | Read details.fieldErrors. |
| 402 | PAYMENT_FAILED | No payment provider is available for the environment | Use a test key: payments are simulated there. |
| 403 | PLAYER_BLOCKED | The player is blocked | No order can be created for this player until the player is unblocked. |
| 404 | NOT_FOUND | The pack is not in the catalog (or is archived), or a GameCoin id does not exist | Check the sku against Get the catalog. |
| 409 | LIMIT_REACHED | The player already bought the pack as many times as its maxPerPlayer allows: details.maxPerPlayer and details.bought | Hide the pack for this player. |
| 409 | PACK_NOT_AVAILABLE | The pack is outside its sale window: details.startsAt and details.endsAt (ISO 8601 or null) | Show « coming soon » or « ended ». See Founder packs. |
| 409 | PACK_SOLD_OUT | The total stock of the pack is used up | Show « sold out ». The stock can come back if a pending order expires. |
| 409 | IDEMPOTENCY_CONFLICT | The key was already used with a different request | Use a new key for a new order. |
{
"error": {
"code": "LIMIT_REACHED",
"message": "Pack purchase limit reached",
"details": { "maxPerPlayer": 1, "bought": 1 }
}
}Notes
- The call creates an order, not a payment. The coins and items are delivered when the order becomes
fulfilled, a moment after the player pays. Do not give anything yourself when the call returns. - After payment the player is redirected to
successUrlwith?order=<id>&status=fulfilledadded. When they decline, or the order fails or expires, they are redirected tocancelUrlwith?order=<id>&status=failed(orexpired). Without these URLs the player lands on a built-in confirmation page, which also notifies the window that opened it withpostMessage({ type: "gamecoin:order", orderId, status }). - Never trust the query string: when the player comes back, read the order with Get an order and check its
statusbefore you tell them it worked. - If an item of the pack cannot be delivered, because the player already owns the most they can of a durable item, the pack is still delivered: the currency and the other items arrive. See Selling packs.
- The buyer confirms their country on the payment page, and the VAT is recomputed if they change it. That is why
countryon the order can differ from the one you sent. - A
pendingorder that is not paid within one hour becomesexpired. The player can opencheckoutUrlas long as the order ispending. - Limited packs. A new order holds one unit of the stock of a pack that has one, from the moment it is created. The unit goes back when the order fails, expires or is refunded, and becomes a sale when the order is delivered. A second request for the same pack by the same player returns the pending order and holds nothing more. The sale window is checked when the order is created: a player who started in time can still pay after the window closes, within the hour.
maxPerPlayercounts the orders that arepaidorfulfilled, and it is checked when the order is created. An order that was refunded, never paid or declined does not count. Two orders created before the first one is paid are both accepted, and both can then be paid: when a pack is limited, create the next order only after the previous one is no longerpending.successUrlandcancelUrlare free on this server route. The browser route only accepts addresses on the origin of the page: see Order a currency pack in the client API.- The
checkoutUrlis a link for one player: do not log it, and do not share it.