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

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](/docs/api): 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](/docs/sdk/browser) 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](/docs/guides/codes) 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](/docs/sdk/node) |
| Python | `apilow-gamecoin` (import `gamecoin`) | Python 3.9 or later | None (standard library) | [Python SDK](/docs/sdk/python) |
| PHP | `apilow/gamecoin` | PHP 8.1 or later, with `curl` and `json` | None beyond those two extensions | [PHP SDK](/docs/sdk/php) |
| Go | `github.com/apilow/gamecoin-go` | Go 1.21 or later | None (standard library) | [Go SDK](/docs/sdk/go) |
| 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](/docs/sdk/dotnet) |

> [!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](/docs/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.

<!-- tabs:start -->
```javascript tab="Node.js"
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 checkoutUrl
```
```python tab="Python"
import 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 tab="PHP"
<?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 checkoutUrl
```
```go tab="Go"
package 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
}
```
```csharp tab="C#"
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 CheckoutUrl
```
<!-- tabs:end -->

`https://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](/docs/guides/webhooks) 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.

<!-- tabs:start -->
```javascript tab="Node.js"
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);
```
```python tab="Python"
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)
```
```php tab="PHP"
use ApilowGameCoinWebhooks;

$event = Webhooks::constructEvent(file_get_contents('php://input'), $_SERVER['HTTP_GAMECOIN_SIGNATURE'] ?? null, getenv('GAMECOIN_WEBHOOK_SECRET'));
echo $event->type, ' ', $event->id, "
";
```
```go tab="Go"
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)
```
```csharp tab="C#"
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}");
```
<!-- tabs:end -->

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](/docs/concepts/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 `5xx` and a `429`. Reads, `players.upsert` and `players.createToken` follow the same rules as the writes.
- **What is not**: any other `4xx`, an answer that is malformed although its status was `200`, 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. A `Retry-After` header wins, on a `5xx` as well as on a `429`, up to 30 seconds. Beyond that the SDK does not wait: it raises the error and `retryAfter` tells 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](/docs/concepts/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](/docs/api) 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](/docs/concepts/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.
