API reference
Ledger API
Read the immutable history of every change to a player's balances, page by page, with the server API.
On this page
The ledger is the history of a wallet: one entry for every grant, spend, pack delivery and refund, never edited and never deleted. Read it to show a player their history, to answer a support request, or to reconcile your own records. The examples on this page continue from the setup.
List ledger entries
Returns a page of a player's ledger entries, newest first. Use currency to keep one currency, and limit and cursor to walk back in time.
/players/{player}/ledgerAuthentication: secret key. Idempotency: not needed.
Path parameters
| Parameter | Type | Description |
|---|---|---|
player | string | A GameCoin player id, or ext:<your id> URL-encoded once. |
Query parameters
| Parameter | Type | Required | Rules |
|---|---|---|---|
currency | string | no | Only the entries of this currency (2 to 24 lowercase characters). A well-formed code that has no entries gives an empty list. |
limit | integer | no | Page size, from 1 to 100. Default 20. |
cursor | string | no | The nextCursor of the previous page. Opaque: never build one. |
curl "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/ledger?currency=gems&limit=2" \
-H "Authorization: Bearer $GAMECOIN_SECRET_KEY"const page = await gamecoin.ledger.list(player, { currency: "gems", limit: 2 });
for (const entry of page.entries) {
console.log(entry.createdAt.toISOString(), entry.type, entry.paidDelta + entry.bonusDelta, entry.balanceAfter.total);
}
console.log(page.nextCursor); // null on the last pagepage = gamecoin.ledger.list(player, currency="gems", limit=2)
for entry in page.entries:
print(entry.created_at.isoformat(), entry.type, entry.paid_delta + entry.bonus_delta, entry.balance_after.total)
print(page.next_cursor) # None on the last page$page = $gamecoin->ledger->list($player, ['currency' => 'gems', 'limit' => 2]);
foreach ($page->entries as $entry) {
echo $entry->createdAt->format(DATE_ATOM), ' ', $entry->type, ' ', $entry->paidDelta + $entry->bonusDelta, ' ', $entry->balanceAfter->total, "\n";
}
echo var_export($page->nextCursor, true), "\n"; // NULL on the last pagepage, err := client.Ledger.List(ctx, player, &gamecoin.LedgerListParams{Currency: "gems", Limit: 2})
check(err)
for _, entry := range page.Entries {
fmt.Println(entry.CreatedAt, entry.Type, entry.PaidDelta+entry.BonusDelta, entry.BalanceAfter.Total)
}
fmt.Println(page.NextCursor != nil) // false on the last pagevar page = await gamecoin.Ledger.ListAsync(player, new LedgerQuery { Currency = "gems", Limit = 2 });
foreach (var entry in page.Entries)
{
Console.WriteLine($"{entry.CreatedAt:u} {entry.Type} {entry.PaidDelta + entry.BonusDelta} {entry.BalanceAfter.Total}");
}
Console.WriteLine(page.NextCursor ?? "last page");Response 200 OK: a page of LedgerEntry objects and the cursor of the next page.
| Field | Type | Description |
|---|---|---|
entries | array | The entries of the page, newest first. |
nextCursor | string or null | Pass it as cursor to get the next page. null on the last page. |
{
"entries": [
{
"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"
},
{
"id": "6ac4214ceab6d4425553aff6",
"type": "spend",
"currency": "gems",
"paidDelta": 0,
"bonusDelta": -30,
"balanceAfter": { "paid": 0, "bonus": 70, "total": 70 },
"reason": "revive",
"metadata": {},
"ref": {},
"createdAt": "2026-10-05T22:14:36.491Z"
}
],
"nextCursor": "MTc5MTIzODQ3NjQ5MTo2YWM0MjE0Y2VhYjZkNDQyNTU1M2FmZjY"
}To get the next page, pass the cursor back; the other parameters stay the same:
curl "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/ledger?currency=gems&limit=2&cursor=<nextCursor>" \
-H "Authorization: Bearer $GAMECOIN_SECRET_KEY"{
"entries": [
{
"id": "6ac4214bae33663917ec0ca6",
"type": "grant",
"currency": "gems",
"paidDelta": 0,
"bonusDelta": 100,
"balanceAfter": { "paid": 0, "bonus": 100, "total": 100 },
"reason": "level_up",
"metadata": { "level": "5" },
"ref": {},
"createdAt": "2026-10-05T22:14:35.965Z"
}
],
"nextCursor": null
}The SDKs can follow the cursors for you. iterate fetches a page only when your loop reaches it, so you can stop whenever you want; limit is the page size:
for await (const entry of gamecoin.ledger.iterate(player, { currency: "gems", limit: 50 })) {
console.log(entry.id, entry.type, entry.reason);
}for entry in gamecoin.ledger.iterate(player, currency="gems", limit=50):
print(entry.id, entry.type, entry.reason)foreach ($gamecoin->ledger->iterate($player, ['currency' => 'gems', 'limit' => 50]) as $entry) {
echo $entry->id, ' ', $entry->type, ' ', $entry->reason ?? '-', "\n";
}it := client.Ledger.Iterate(ctx, player, &gamecoin.LedgerListParams{Currency: "gems", Limit: 50})
for it.Next() {
entry := it.Entry()
fmt.Println(entry.ID, entry.Type)
}
check(it.Err())await foreach (var entry in gamecoin.Ledger.IterateAsync(player, new LedgerQuery { Currency = "gems", Limit = 50 }))
{
Console.WriteLine($"{entry.Id} {entry.Type} {entry.Reason}");
}Errors
| Status | Code | When | What to do |
|---|---|---|---|
| 400 | VALIDATION_FAILED | limit is not a whole number from 1 to 100 (details.fieldErrors.limit); currency is malformed; cursor is not one this API issued (CURSOR_INVALID); the player reference is malformed (PLAYER_REF_INVALID) | Fix the parameter. Never edit a cursor: pass back the one you received. |
| 404 | NOT_FOUND | The player does not exist | An ext: player exists after its first write; before that it has no history. |
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Invalid cursor",
"details": { "fieldErrors": { "cursor": ["CURSOR_INVALID"] } }
}
}Notes
- Every entry carries
balanceAfter, the balance right after it, so you can display a running balance without adding anything up.paidDeltaandbonusDeltaare negative for a spend. ref.itemSkuis set on a purchase of an item andref.orderIdon an entry caused by an order (pack delivery, refund). The entry types are listed in Objects.- Entries are never edited: an error is corrected by a new entry, not by changing an old one.
- A parameter this API does not know is ignored, not refused. A
currencythat is well-formed but unknown to the game is not an error either: you get an empty page. - Ids sort by creation time. Paging goes back in time from the newest entry, so entries written while you page do not shift the pages behind you.