Server SDKs
Server SDKs
Pick the GameCoin SDK for your backend language (Node.js, Python, PHP, Go or C#) and learn what the five have in common and where they differ.
On this page
A server SDK is the quickest way to call the GameCoin server API from your game backend. It signs every request with your secret key, keeps writes safe to retry, and turns errors into typed values, so that you write wallet.grant(...) instead of building HTTP requests by hand. Each SDK is a thin layer over the API reference: everything it does, you could do with curl.
Warning
A secret key can credit coins, grant items and refund orders for every player of your game. It belongs on your server, never in a browser, a mobile app or a game client. For code that runs on the player's device, use the browser SDK with a publishable key: it can read, spend and pay for packs, and it can never credit anything by itself. The one exception is a gift code that your studio created: it gives free coins and items, once per player, and the browser SDK can redeem it. Your server hands it a short-lived player token, which the SDKs create with players.createToken.
Pick your SDK
| Language | Package | Needs | Dependencies | Page |
|---|---|---|---|---|
| Node.js and TypeScript | @apilow/gamecoin | Node.js 18 or later | None (built-in fetch) | Node.js SDK |
| Python | apilow-gamecoin (import gamecoin) | Python 3.9 or later | None (standard library) | Python SDK |
| PHP | apilow/gamecoin | PHP 8.1 or later, with curl and json | None beyond those two extensions | PHP SDK |
| Go | github.com/apilow/gamecoin-go | Go 1.21 or later | None (standard library) | Go SDK |
| C# and .NET | Apilow.GameCoin | .NET 8, or a runtime that supports .NET Standard 2.1 | None on .NET 8, System.Text.Json on .NET Standard 2.1 | C# SDK |
Note
The SDKs are a pre-release (version 0.1.0) and are not published yet: they are not on npm, PyPI, Packagist, the Go module proxy or NuGet. The package names above are the planned ones and may change before the first release. Each page gives the install command that will work once the package exists.
Your language is not on the list? Call the API directly. The SDKs add no hidden behaviour: what they do beyond the raw API is listed on this page.
The same start in five languages
Grant a new player 100 gems, spend 30, and start the sale of a pack. The player is ext("user-42"): your own id for the player, which GameCoin creates on first use. The key is read from the GAMECOIN_SECRET_KEY environment variable.
import { GameCoin, ext } from "@apilow/gamecoin";
const gamecoin = new GameCoin(process.env.GAMECOIN_SECRET_KEY, { baseUrl: "https://gamecoin.apilow.com" });
const player = ext("user-42");
await gamecoin.wallet.grant(player, { currency: "gems", amount: 100, reason: "welcome_gift" });
const { balance } = await gamecoin.wallet.spend(player, { currency: "gems", amount: 30, reason: "revive" });
console.log(balance.total); // 70
const { order, checkoutUrl } = await gamecoin.checkouts.create(player, { packSku: "gems-500" });
console.log(order.status, checkoutUrl); // pending, then send the player to checkoutUrlimport os
from gamecoin import GameCoin, ext
gamecoin = GameCoin(os.environ["GAMECOIN_SECRET_KEY"], base_url="https://gamecoin.apilow.com")
player = ext("user-42")
gamecoin.wallet.grant(player, currency="gems", amount=100, reason="welcome_gift")
movement = gamecoin.wallet.spend(player, currency="gems", amount=30, reason="revive")
print(movement.balance.total) # 70
checkout = gamecoin.checkouts.create(player, pack_sku="gems-500")
print(checkout.order.status, checkout.checkout_url) # pending, then send the player to checkout_url<?php
require __DIR__ . '/vendor/autoload.php';
use Apilow\GameCoin\GameCoin;
use Apilow\GameCoin\PlayerRef;
$gamecoin = new GameCoin(getenv('GAMECOIN_SECRET_KEY'), ['base_url' => 'https://gamecoin.apilow.com']);
$player = PlayerRef::ext('user-42');
$gamecoin->wallet->grant($player, ['currency' => 'gems', 'amount' => 100, 'reason' => 'welcome_gift']);
$movement = $gamecoin->wallet->spend($player, ['currency' => 'gems', 'amount' => 30, 'reason' => 'revive']);
echo $movement->balance->total, "\n"; // 70
$checkout = $gamecoin->checkouts->create($player, ['pack_sku' => 'gems-500']);
echo $checkout->order->status, ' ', $checkout->checkoutUrl, "\n"; // pending, then send the player to checkoutUrlpackage main
import (
"context"
"fmt"
"log"
"os"
gamecoin "github.com/apilow/gamecoin-go"
)
func main() {
ctx := context.Background()
client, err := gamecoin.New(os.Getenv("GAMECOIN_SECRET_KEY"), gamecoin.WithBaseURL("https://gamecoin.apilow.com"))
if err != nil {
log.Fatal(err)
}
player := gamecoin.Ext("user-42")
if _, err = client.Wallet.Grant(ctx, player, gamecoin.GrantParams{Currency: "gems", Amount: 100, Reason: "welcome_gift"}); err != nil {
log.Fatal(err)
}
spent, err := client.Wallet.Spend(ctx, player, gamecoin.SpendParams{Currency: "gems", Amount: 30, Reason: "revive"})
if err != nil {
log.Fatal(err)
}
fmt.Println(spent.Balance.Total) // 70
checkout, err := client.Checkouts.Create(ctx, player, gamecoin.CheckoutParams{PackSKU: "gems-500"})
if err != nil {
log.Fatal(err)
}
fmt.Println(checkout.Order.Status, checkout.CheckoutURL) // pending, then send the player to CheckoutURL
}using Apilow.GameCoin;
using var gamecoin = new GameCoinClient(
Environment.GetEnvironmentVariable("GAMECOIN_SECRET_KEY")!,
new GameCoinOptions { BaseUrl = "https://gamecoin.apilow.com" });
var player = PlayerRef.Ext("user-42");
await gamecoin.Wallet.GrantAsync(player, new GrantRequest { Currency = "gems", Amount = 100, Reason = "welcome_gift" });
var spent = await gamecoin.Wallet.SpendAsync(player, new SpendRequest { Currency = "gems", Amount = 30, Reason = "revive" });
Console.WriteLine(spent.Balance.Total); // 70
var checkout = await gamecoin.Checkouts.CreateAsync(player, new CheckoutRequest { PackSku = "gems-500" });
Console.WriteLine($"{checkout.Order.Status} {checkout.CheckoutUrl}"); // pending, then send the player to CheckoutUrlhttps://gamecoin.apilow.com is the address of this service, which is also the default of every SDK: you only need to pass a base URL to point an SDK at another address. The test environment simulates the payment: the player opens checkoutUrl, and the hosted page, in French for now, offers « Payer » and « Refuser » buttons. The Go version is longer because Go returns every error to you, and a program has to say what to do with each.
Check the signature of a webhook
Since version 0.2.0 each SDK checks the signature of a webhook for you. It is a plain function: it needs no client and makes no request. Give it the raw body of the request, the GameCoin-Signature header and the signing secret of the endpoint (whsec_gc_…). It returns the event, or fails with a signature error that has a reason: missing_header, malformed_header, no_matching_signature, timestamp_out_of_tolerance or invalid_payload. A call older than 5 minutes is refused; every SDK lets you change that.
import { webhooks, WebhookSignatureError } from "@apilow/gamecoin";
// With Express: app.post("/gamecoin", express.raw({ type: "application/json" }), (req, res) => { … })
const event = webhooks.constructEvent(req.body, req.get("GameCoin-Signature"), process.env.GAMECOIN_WEBHOOK_SECRET);
console.log(event.type, event.id);from gamecoin import webhooks
event = webhooks.construct_event(request.get_data(), request.headers.get("GameCoin-Signature"), os.environ["GAMECOIN_WEBHOOK_SECRET"])
print(event.type, event.id)use ApilowGameCoinWebhooks;
$event = Webhooks::constructEvent(file_get_contents('php://input'), $_SERVER['HTTP_GAMECOIN_SIGNATURE'] ?? null, getenv('GAMECOIN_WEBHOOK_SECRET'));
echo $event->type, ' ', $event->id, "
";body, _ := io.ReadAll(r.Body)
event, err := gamecoin.ConstructEvent(body, r.Header.Get(gamecoin.WebhookSignatureHeader), os.Getenv("GAMECOIN_WEBHOOK_SECRET"))
if err != nil {
http.Error(w, "invalid signature", http.StatusBadRequest)
return
}
fmt.Println(event.Type, event.ID)using Apilow.GameCoin;
// rawBody is the byte[] of the request body, read before any JSON parsing
var webhookEvent = Webhooks.ConstructEvent(rawBody, request.Headers["GameCoin-Signature"], Environment.GetEnvironmentVariable("GAMECOIN_WEBHOOK_SECRET")!);
Console.WriteLine($"{webhookEvent.Type} {webhookEvent.Id}");Read the body before any JSON parsing: a parsed and serialized body can differ by one character, and the check then fails. Each language page has a complete example.
Language of the payment page
checkouts.create takes an optional locale (fr, en or nl): the language of the hosted payment page, of the receipt and of the e-mails of the order. Leave it out and the page follows the browser of the player.
What the five do the same way
The five SDKs follow one specification. They cover the whole server API with the same eight resources, catalog, players, wallet, ledger, inventory, purchases, checkouts and orders, and the same sixteen calls. These behaviours do not change from one language to the next.
Writes are idempotent
Every call that changes something sends an Idempotency-Key: wallet.grant, wallet.spend, inventory.grant, inventory.consume, purchases.create and checkouts.create. When you give no key, the SDK generates a UUID v4 once per call, and every retry of that call reuses it. A retry can therefore never credit a player twice. To make a retry of your own code safe too, pass a key of your choosing, from 1 to 100 printable ASCII characters. The result of these calls carries a boolean replayed, which is true when the API answered from an earlier request with the same key. See Idempotency.
Failed calls are retried
By default an SDK retries twice, so a call makes at most three attempts. You can set this from 0, which turns retries off, to 10.
- What is retried: a network error, a timeout, a
5xxand a429. Reads,players.upsertandplayers.createTokenfollow the same rules as the writes. - What is not: any other
4xx, an answer that is malformed although its status was200, and a call that you cancelled. - How long it waits:
0.5 s × 2^attempt, multiplied by a random factor between 0.75 and 1.25. ARetry-Afterheader wins, on a5xxas well as on a429, up to 30 seconds. Beyond that the SDK does not wait: it raises the error andretryAftertells you how long the API asked for.
A refund is retried on 429 only
orders.refund has no idempotency key, so a blind retry could refund twice. The SDK retries it on 429 only. After a network error, a timeout or a 5xx, the call fails and the outcome is unknown: read the order back with orders.get, and look at its status, before you try again.
One error type, with a code
Everything that goes wrong with a request is one error type, with the same five pieces of information: a code, the HTTP status, a message, the details of the API and a retryAfter. The code is the API code (INSUFFICIENT_FUNDS, NOT_FOUND…), or one of three codes the SDK raises when no usable answer exists: NETWORK_ERROR and TIMEOUT (status 0), and INVALID_RESPONSE. A code that the SDK does not know yet is passed through as it is. For INSUFFICIENT_FUNDS, details holds required and available. The codes are listed in Errors, and each SDK page gives the ones you will meet.
Arguments are checked before any request
An empty player, a negative amount, a quantity that is not an integer, a page size outside 1 to 100, an idempotency key that is not 1 to 100 printable ASCII characters: the call fails at once, with the usual argument error of the language, and nothing is sent. This error is not the SDK error type, and its message never contains the offending value.
Your players are yours to name
Any string works as your own id for a player, through the ext helper: spaces, slashes, % and accents included. The SDK URL-encodes the reference exactly once. With a secret key, a write on an unknown ext player creates it. A read answers NOT_FOUND.
The key stays hidden
An SDK accepts only a secret key (gc_sk_test_… or gc_sk_live_…). A publishable key gets a message that points to the browser SDK. The key is trimmed of surrounding white space, and the SDK reads the environment, test or live, from its prefix. It never appears in an error message, in a log line or in the string form of the client, which shows gc_sk_test_… only.
Plain objects, native dates
The results mirror the objects of the API reference field for field. Timestamps are the date type of the language, amounts are integers, and a field that an SDK does not know yet is ignored, so a newer API does not break an older SDK. Every request carries a User-Agent such as gamecoin-node/0.1.0 (node/22.0.0), which names the SDK and its version.
What changes from one language to the next
The behaviours above are the same everywhere. What differs is the way each SDK fits its language. The first table covers the shape of the calls.
| Node.js | Python | PHP | Go | C# | |
|---|---|---|---|---|---|
| Names | camelCase (packSku) | snake_case (pack_sku) | camelCase methods and properties, snake_case array keys (pack_sku) | PascalCase, initialisms in capitals (PackSKU, CheckoutURL) | PascalCase, Async suffix (GrantAsync) |
| Parameters | An object | Keyword arguments | An array | A struct (GrantParams) | A record (GrantRequest) |
| Your own player id | ext("id") | ext("id") | PlayerRef::ext('id') | gamecoin.Ext("id") | PlayerRef.Ext("id") |
| Asynchronous | Yes, promises | No, synchronous only | No, synchronous | No, synchronous and safe to share between goroutines | Yes, async and await |
| Cancel a call | An AbortSignal | Not possible | Not possible | The context.Context | A CancellationToken |
| Read the whole ledger | for await over ledger.iterate | for over ledger.iterate | foreach over ledger->iterate() | The iterator of Ledger.Iterate: Next, Entry, Err | await foreach over Ledger.IterateAsync |
| Pass your own idempotency key | { idempotencyKey } as the last argument | idempotency_key= | ['idempotency_key' => …] as the last argument | The gamecoin.WithIdempotencyKey(…) call option | A RequestOptions |
| Replace the HTTP layer | The fetch option | A transport function | A TransportInterface | WithHTTPClient with your own RoundTripper | An HttpMessageHandler |
The second table covers values and errors.
| Node.js | Python | PHP | Go | C# | |
|---|---|---|---|---|---|
Unit of the timeout option | Milliseconds | Seconds, for each socket operation | Seconds | A time.Duration | A TimeSpan |
Type of retryAfter | A number of seconds, or undefined | Seconds, or None | Seconds, or null | A time.Duration, zero when absent | A TimeSpan? |
| Error type | GameCoinError | GameCoinError | GameCoinException | *gamecoin.Error | GameCoinException |
| Where the code is | error.code | error.code | $e->errorCode | err.Code, or gamecoin.IsCode(err, …) | ex.Code |
| Wrong argument | TypeError or RangeError | TypeError or ValueError | InvalidArgumentException | An error that wraps ErrInvalidArgument | ArgumentException |
| Clear a profile field | Set it to null | Set it to None | Set the key to null | List it in Clear | List it in Clear |
| Leave an optional value out | Omit the field | Omit the argument | Omit the key | The zero value ("", 0, nil) | Leave the property null |
| Dates | Date | datetime, with a time zone | DateTimeImmutable | time.Time | DateTimeOffset |
Two small differences are worth knowing. PHP keeps the API code in errorCode, because getCode() has to be an integer and returns the HTTP status. In Go, a zero value means "not set", so Quantity: 0 is a quantity of 1 and a ledger Limit: 0 is the default page size.
Status
The SDKs are a pre-release, version 0.2.0. The GameCoin API runs in the test environment, with simulated payments or Stripe in test mode: real payments are not available yet, and the live environment is reserved for studios that Apilow has verified. See Keys and environments. Until the first release, the API of an SDK may change between minor versions, and so may the name of its package.