# 1GP Leaderboard API v1

Create a leaderboard from **Studio → Leaderboard settings** (or the game page). Keys are generated automatically in the creation form. Use “Copy integration prompt” for the selected board to give your AI assistant its exact key and scoring rules. Each game has at most 10 boards. Configuration key, sorting direction, units, bounds and minimum play time are immutable after creation; create a new board for incompatible scoring rules. Publishers can rename, disable, hide or restore entries. Hidden players stay hidden even after a better score.

This first version accepts client-reported scores for casual play, not verified competition. Tokens identify the player and game; they do not prove a score is legitimate. Do not put developer secrets or platform login tokens in game code.

## REST-only integration

Do not install packages or load a platform script. Copy the selected board's integration prompt and use native `fetch` with its **boardId**. The human-readable key is not required when calling REST by ID.

1. Obtain a game-scoped player token through the native browser message handshake below.
2. At actual round start, POST `/api/v1/leaderboards/{boardId}/runs` with `Authorization: Bearer <token>` and a new UUID `Idempotency-Key`. Retain the returned `runId`.
3. At final settlement, POST `/api/v1/runs/{runId}/score` with the token, a separate UUID `Idempotency-Key`, `Content-Type: application/json`, and `{"score":12800}`.
4. GET `/api/v1/leaderboards/{boardId}/entries` or `/me` to show standings or personal rank.

Use `credentials: 'omit'` for cross-origin fetch. Check `response.ok` and the JSON error code. Keep keys and final score unchanged when retrying a request; refresh expired credentials via the handshake. A new round needs new keys. Keep gameplay running when ranking fails. The backend handles all scoring, ranking, authorization and rate limits. CORS allows Authorization, Content-Type and Idempotency-Key, and exposes Retry-After on rate-limit responses.

A complete native JavaScript example is on [the integration guide](/guide/leaderboards).

## Identity and sessions

The platform parent page calls `POST /api/v1/games/{gameId}/sessions` with its own same-origin cookies and `{"origin":"https://your-game-host.example"}`. The server checks the origin against the game's published URL (including platform-reviewed localized builds) and checks restricted-game access. A game iframe cannot mint a session for an arbitrary account or game.

The game sends `window.parent.postMessage({source:'aimadegames', type:'leaderboard-session', requestId:crypto.randomUUID()}, platformOrigin)`. Use the exact platform origin from your copied prompt as targetOrigin. Register the message listener before sending and remove it after completion or timeout (15 seconds).

The parent verifies BOTH the mounted frame window and registered game origin, calls the session REST endpoint using platform cookies, and replies to that exact game origin. The reply is `{source:'aimadegames-platform', type:'leaderboard-session', requestId, apiOrigin, session}` or `{source:'aimadegames-platform', type:'leaderboard-session', requestId, error}`. Accept it only when `event.source === window.parent`, `event.origin === platformOrigin`, source/type/requestId match, and successful `apiOrigin === platformOrigin`. The `session` contains the response below. This handshake is only for securely obtaining player identity; all gameplay operations use REST directly.

A cross-origin game cannot call the session endpoint with platform cookies or choose an arbitrary player. Test inside `/games/{slug}/playing`. Standalone games should skip ranking when the handshake is unavailable. HTTP clients can call REST with an already-issued game token. No developer secret is needed.

Response:
```json
{"token":"...","expiresAt":"2026-10-10T01:00:00Z","gameId":"uuid","player":{"displayName":"Player","isGuest":false}}
```
Tokens last one hour and are scoped to one game and its origin. Only keep the game token, never the platform account session. Runs belong to the game-specific player, so refreshing an expired token preserves the ability to settle an existing run. Guests use the platform's HttpOnly browser visitor cookie. Clearing it loses access to that guest identity. Guest and account scores are separate in v1; logging in does not automatically merge them.

## Player endpoints

All URLs are relative to the platform origin. Send `Authorization: Bearer <game token>` for game-side REST calls. CORS supports the registered game origin. Cookie-based requests on the platform itself can read boards, entries and personal rank. Restricted games require permission on every request; removing access also prevents pending runs from settling.

### GET /api/v1/games/{gameId}/leaderboards
Read enabled boards at initialization. Response: `{"leaderboards":[{"id":"uuid","key":"main","name":"High score","order":"desc","unit":"points","enabled":true,"minScore":0,"maxScore":1000000000,"minPlayMs":0,"description":"Coins from one run"}]}`.

### POST /api/v1/leaderboards/{boardId}/runs
Call at the actual start of each new round. Requires game token and UUID `Idempotency-Key` header. No body. A retry with the same key returns the original run; a new round needs a new key.

Response (201): `{"runId":"uuid","startedAt":"...","submitBefore":"..."}`. Runs accept settlement for two hours. Pausing does not require another run. Abandoned runs expire automatically. Maximum 20 new runs per player per minute across the game's boards.

### POST /api/v1/runs/{runId}/score
Call once at final settlement, not every time points change. Requires game token, `Content-Type: application/json`, and a UUID `Idempotency-Key` for settlement (separate from the start key).

Body: `{"score":12800}`. Nonnegative integers only, within board bounds and at most 9,000,000,000,000. Time boards use milliseconds; percent boards use hundredths of a percent (9820 = 98.20%). Extra fields, fractional values and non-finite numbers are rejected. Minimum play duration uses the platform's start time, not a client-supplied timer; it is a basic sanity check, not anti-cheat proof.

Response (200): `{"accepted":true,"submittedScore":12800,"personalBest":12800,"isPersonalBest":true,"rank":23,"participants":1500}`.

Lower scores are still accepted but do not replace a higher-is-better personal best (reversed for ascending boards). Ties share rank; display order is first achievement, then an opaque entry ID. Hidden entries return `rank:null`. Replaying the same score with the same settlement key returns the stored receipt; changing the key or score after settlement returns 409. Receipts report rank at settlement time, not a live rank. Maximum 20 successful settlements per player per minute. Run receipts are retained for 30 days after expiry.

### GET /api/v1/leaderboards/{boardId}/entries?limit=10&cursor=...
Read ranked entries. Limit defaults to 10, maximum 50. Response: `{"entries":[{"entryId":"opaque-uuid","score":12800,"rank":23,"achievedAt":"...","isMe":false,"player":{"displayName":"Player","avatarUrl":null,"isGuest":false}}],"nextCursor":"..."}`.

Pass the opaque next cursor unchanged to load more; null means the end. Ranking is live, not a frozen snapshot. Scores may improve between pages; refresh from the first page for the latest standings, and deduplicate entries by entryId when appending. Public responses contain no emails or account IDs. Avatar URLs are relative to the platform if they begin with `/`.

### GET /api/v1/leaderboards/{boardId}/me?around=2
Returns `{"entry":<your entry or null>,"neighbors":[...]}`. `around` is 0–10, defaults to 2 (two displayed entries on each side). Not yet ranked or hidden returns null and an empty array. Game clients use a game token; platform pages can use current player cookies.

## Publisher management (platform login only)

These operations require the game's publisher, without an administrator bypass. Mutations require same-origin requests; game player tokens cannot configure or moderate boards.

- `GET /api/v1/games/{gameId}/leaderboards?manage=1`: include disabled boards.
- `POST /api/v1/games/{gameId}/leaderboards`: create with `{key,name,description,order,unit,minScore,maxScore,minPlayMs}`. `description` is an optional scoring note of up to 120 characters, shown above the board; update it later with `{id,name,enabled,description}`. `key` is optional; omission generates a unique key. Existing explicit keys remain supported. `order`: asc/desc. `unit`: points/milliseconds/count/percent. Bounds default to 0–1,000,000,000; minimum play time defaults to zero, maximum 3,600,000 ms.
- Same POST with `{id,name,enabled}`: rename or enable/disable. Rules cannot be edited.
- `GET /api/v1/leaderboards/{boardId}/entries?hidden=1`: review hidden entries, same pagination.
- `POST /api/v1/entries/{entryId}/moderation` with `{hidden:true}` or `{hidden:false}`: hide/restore. Entry ID is a leaderboard row ID, never an account ID or a mutable rank.

## Errors

All API errors use `{"error":{"code":"...","message":"..."}}`.

| Status | Code | Action |
|---|---|---|
| 400 | INVALID_INPUT / INVALID_CURSOR | Fix fields or reload the first page. |
| 401 | SESSION_EXPIRED | Reconnect through the parent player. |
| 403 | FORBIDDEN | Player lacks access, wrong game/origin, or not the publisher. |
| 404 | NOT_FOUND / BOARD_UNAVAILABLE / RUN_NOT_FOUND | Check identifiers and whether the board is enabled. |
| 409 | ALREADY_SUBMITTED / BOARD_KEY_EXISTS | Do not change a settled run; use a unique board key. |
| 410 | RUN_EXPIRED | The run's two-hour submission deadline has passed. |
| 422 | INVALID_SCORE / BOARD_LIMIT | Fix score bounds/duration, or use an existing board. |
| 429 | RATE_LIMITED | Wait for Retry-After (60 seconds). |
| 503 | SERVICE_UNAVAILABLE | Retry later; keep the game playable. |
