REST API reference
Generated from /openapi.json, the same file you can load
into Postman, Insomnia or an OpenAPI code generator. Your game never calls
these endpoints itself: player endpoints are called by the arcade page on the
player's behalf, developer endpoints by your server with an API key from
/developer/keys.
Gems Arcadia Coin API · version 1.0.0 · OpenAPI 3.1.0 · download openapi.json
Two audiences:
- Player endpoints (
/api/play/...) are called by the Gems Arcadia host page on the player's behalf, authenticated by the Auth4 session cookie. The player's email must be verified. Games never call these directly: they run in a sandboxed iframe and use the SDK's postMessage bridge. - Developer endpoints (
/api/v1/...) are for game backends, authenticated with an API key (Authorization: Bearer ga_live_...orga_test_...). They only see the developer's own games; ids from other studios return 404.
1 coin = 25 US cents. Each game charges coinsPerCredit (1-5) coins per credit, and the developer earns 70% of that value.
Errors share one flat shape: { "error": "<CODE>", "message": "..." } plus code-specific fields.
Client-submitted scores are untrusted reports from the game iframe. For trusted leaderboards, verify gameplay on your backend and submit through POST /api/v1/insertions/{insertionId}/score with a ga_live_ API key.
Servers: https://gemsarcadia.com, http://localhost:3000
PLAYER ENDPOINTS
Session-cookie endpoints used by the host page.
post/api/play/sessions
Only PUBLISHED games can be played. The game's owner and admins get a test-mode preview session instead (always for unpublished games, or with preview: true). Test-mode sessions never debit coins.
Auth: player session cookie — Auth4 session cookie set by /auth/login (split into auth4_session.0, .1, ... when large). Same-origin only.
REQUEST BODY (REQUIRED)
| FIELD | TYPE | NOTES |
|---|---|---|
gameSlug* (required) | string | max length 64 · pattern ^[a-z0-9-]+$ |
preview | boolean | Owner/admin only: force a test-mode session. |
versionId | string | With preview: true: the stored build to preview. Defaults to the current version, else the newest non-rejected upload. |
RESPONSES
- 201Session started.
- 400INVALID_REQUEST: the body failed validation.
- 401UNAUTHENTICATED: not signed in.
- 403EMAIL_NOT_VERIFIED (verify your email to play) or ACCOUNT_NOT_LINKED.
- 404GAME_UNAVAILABLE: unknown, unpublished or unplayable game.
▸▾ 201 RESPONSE SCHEMA
| FIELD | TYPE | NOTES |
|---|---|---|
sessionId* (required) | string | |
gameId* (required) | string | |
coinsPerCredit* (required) | integer | 1–5 |
balance* (required) | integer | Player's wallet balance in coins. ≥ 0 |
testMode* (required) | boolean | Preview session: insertions are free and not recorded. |
expiresAt* (required) | string | format: date-time |
player* (required) | object | What the game receives from the SDK's init(). |
player.ref* (required) | string | Pseudonymous playerRef, stable per developer. |
player.displayName* (required) | string |
{
"sessionId": "session_123",
"gameId": "game_123",
"coinsPerCredit": 1,
"balance": 0,
"testMode": false,
"expiresAt": "2026-01-01T12:00:00.000Z",
"player": {
"ref": "string",
"displayName": "Player One"
}
}curl -X POST "https://gemsarcadia.com/api/play/sessions" \
-H "Cookie: auth4_session=..." \
-H "Content-Type: application/json" \
-d '{"gameSlug":"my-game","preview":false,"versionId":"version_123"}'post/api/play/sessions/{id}/insert-coin
Insert a coin (buy one credit)
Atomically debits coinsPerCredit from the wallet and records the CoinInsertion and the developer earning. Idempotent per idempotencyKey within the session: a retry returns the original insertion with replayed: true and is never charged twice. Rate limited per session to 1 per 500 ms and 60 per minute. Sessions idle for more than 30 minutes are rejected.
Auth: player session cookie — Auth4 session cookie set by /auth/login (split into auth4_session.0, .1, ... when large). Same-origin only.
PARAMETERS
id(path, required, string, max length 64)
REQUEST BODY (REQUIRED)
| FIELD | TYPE | NOTES |
|---|---|---|
idempotencyKey* (required) | string | Client-generated, unique per intended insertion (a UUID is ideal). Reuse it only to retry. min length 8 · max length 128 · pattern ^[A-Za-z0-9_.:-]+$ |
RESPONSES
- 200Credit granted (or replayed).
- 400INVALID_REQUEST: the body failed validation.
- 401UNAUTHENTICATED: not signed in.
- 402INSUFFICIENT_COINS: the wallet cannot cover coinsPerCredit. Nothing was debited.
- 403EMAIL_NOT_VERIFIED (verify your email to play) or ACCOUNT_NOT_LINKED.
- 404SESSION_NOT_FOUND: unknown session, or it belongs to another player.
- 409IDEMPOTENCY_KEY_CONFLICT: the key was already used for a different request.
- 410SESSION_EXPIRED: the session ended or was idle for more than 30 minutes. Start a new one.
- 429RATE_LIMITED. Headers: Retry-After.
▸▾ 200 RESPONSE SCHEMA
| FIELD | TYPE | NOTES |
|---|---|---|
insertionId* (required) | string | Verify it with GET /api/v1/insertions/{insertionId}. Test-mode ids start with test_. |
creditsGranted* (required) | 1 | |
coinsSpent* (required) | integer | ≥ 0 |
balance* (required) | integer | ≥ 0 |
testMode* (required) | boolean | |
replayed* (required) | boolean | True when this idempotencyKey had already been applied. |
{
"insertionId": "insertion_123",
"creditsGranted": 1,
"coinsSpent": 0,
"balance": 0,
"testMode": false,
"replayed": false
}curl -X POST "https://gemsarcadia.com/api/play/sessions/{id}/insert-coin" \
-H "Cookie: auth4_session=..." \
-H "Content-Type: application/json" \
-d '{"idempotencyKey":"9b2f6c1e-5a7d-4e8b-9c3f-2d1a0b4e6f70"}'post/api/play/sessions/{id}/heartbeat
Call every few minutes while the game is open. A heartbeat never revives an ended or timed-out session.
Auth: player session cookie — Auth4 session cookie set by /auth/login (split into auth4_session.0, .1, ... when large). Same-origin only.
PARAMETERS
id(path, required, string, max length 64)
RESPONSES
- 200Session extended.
- 401UNAUTHENTICATED: not signed in.
- 403EMAIL_NOT_VERIFIED (verify your email to play) or ACCOUNT_NOT_LINKED.
- 404SESSION_NOT_FOUND: unknown session, or it belongs to another player.
- 410SESSION_EXPIRED: the session ended or was idle for more than 30 minutes. Start a new one.
▸▾ 200 RESPONSE SCHEMA
| FIELD | TYPE | NOTES |
|---|---|---|
sessionId* (required) | string | |
lastSeenAt* (required) | string | format: date-time |
expiresAt* (required) | string | format: date-time |
{
"sessionId": "session_123",
"lastSeenAt": "2026-01-01T12:00:00.000Z",
"expiresAt": "2026-01-01T12:00:00.000Z"
}curl -X POST "https://gemsarcadia.com/api/play/sessions/{id}/heartbeat" \
-H "Cookie: auth4_session=..."post/api/play/sessions/{id}/end
Idempotent: ending an ended session returns the original endedAt.
Auth: player session cookie — Auth4 session cookie set by /auth/login (split into auth4_session.0, .1, ... when large). Same-origin only.
PARAMETERS
id(path, required, string, max length 64)
RESPONSES
- 200Session ended.
- 401UNAUTHENTICATED: not signed in.
- 403EMAIL_NOT_VERIFIED (verify your email to play) or ACCOUNT_NOT_LINKED.
- 404SESSION_NOT_FOUND: unknown session, or it belongs to another player.
▸▾ 200 RESPONSE SCHEMA
| FIELD | TYPE | NOTES |
|---|---|---|
sessionId* (required) | string | |
endedAt* (required) | string | format: date-time |
{
"sessionId": "session_123",
"endedAt": "2026-01-01T12:00:00.000Z"
}curl -X POST "https://gemsarcadia.com/api/play/sessions/{id}/end" \
-H "Cookie: auth4_session=..."post/api/play/sessions/{id}/score
Client-submitted scores are untrusted. Requires an active session and an unscored paid insertion. Test-mode previews are accepted but never saved.
Auth: player session cookie — Auth4 session cookie set by /auth/login (split into auth4_session.0, .1, ... when large). Same-origin only.
PARAMETERS
id(path, required, string, max length 64)
REQUEST BODY (REQUIRED)
| FIELD | TYPE | NOTES |
|---|---|---|
score* (required) | integer | 0–9007199254740991 |
idempotencyKey* (required) | string | min length 8 · max length 128 · pattern ^[A-Za-z0-9_.:-]+$ |
RESPONSES
- 200Score accepted; saved is false in test mode.
- 400INVALID_REQUEST: the body failed validation.
- 401UNAUTHENTICATED: not signed in.
- 403EMAIL_NOT_VERIFIED (verify your email to play) or ACCOUNT_NOT_LINKED.
- 404SESSION_NOT_FOUND: unknown session, or it belongs to another player.
- 409COIN_REQUIRED, INSERTION_ALREADY_SCORED or IDEMPOTENCY_KEY_CONFLICT.
- 410SESSION_EXPIRED: the session ended or was idle for more than 30 minutes. Start a new one.
- 422SCORE_OUT_OF_RANGE.
- 429RATE_LIMITED: wait before submitting again. Headers: Retry-After.
▸▾ 200 RESPONSE SCHEMA
| FIELD | TYPE | NOTES |
|---|---|---|
scoreId* (required) | string | null | |
saved* (required) | boolean | |
testMode* (required) | boolean | |
qualifies* (required) | boolean | |
replayed* (required) | boolean | |
rank* (required) | integer | null | |
initials* (required) | string | null | pattern ^[A-Z]{3}$ |
needsInitials* (required) | boolean |
{
"scoreId": "score_123",
"saved": false,
"testMode": false,
"qualifies": false,
"replayed": false,
"rank": 0,
"initials": "string",
"needsInitials": false
}curl -X POST "https://gemsarcadia.com/api/play/sessions/{id}/score" \
-H "Cookie: auth4_session=..." \
-H "Content-Type: application/json" \
-d '{"score":0,"idempotencyKey":"9b2f6c1e-5a7d-4e8b-9c3f-2d1a0b4e6f70"}'post/api/play/sessions/{id}/score/initials
Auth: player session cookie — Auth4 session cookie set by /auth/login (split into auth4_session.0, .1, ... when large). Same-origin only.
PARAMETERS
id(path, required, string, max length 64)
REQUEST BODY (REQUIRED)
| FIELD | TYPE | NOTES |
|---|---|---|
scoreId* (required) | string | max length 64 |
initials* (required) | string | pattern ^[A-Z]{3}$ |
RESPONSES
- 200Initials saved to the score and player sound preferences.
- 400INVALID_REQUEST: the body failed validation.
- 401UNAUTHENTICATED: not signed in.
- 403EMAIL_NOT_VERIFIED (verify your email to play) or ACCOUNT_NOT_LINKED.
- 404SCORE_NOT_FOUND.
- 429RATE_LIMITED: wait before submitting again. Headers: Retry-After.
▸▾ 200 RESPONSE SCHEMA
| FIELD | TYPE | NOTES |
|---|---|---|
id* (required) | string | |
initials* (required) | string | pattern ^[A-Z]{3}$ |
{
"id": "id_123",
"initials": "string"
}curl -X POST "https://gemsarcadia.com/api/play/sessions/{id}/score/initials" \
-H "Cookie: auth4_session=..." \
-H "Content-Type: application/json" \
-d '{"scoreId":"score_123","initials":"string"}'post/api/v1/sessions/{id}/achievements
Unlock one of the game's achievements
Called by the host page when a game calls GemsArcadia.unlockAchievement(key) (SDK 1.1.0+); games never call it directly. Same checks as insert-coin: a verified player, their own session (otherwise 404) and an active one (410). key must be an active achievement the developer defined for the session's game. Idempotent per player and achievement: repeats return the original unlock with newlyUnlocked: false and no rewards. Preview (test-mode) sessions store nothing but still return the celebration. rewards carries the ACHIEVEMENT UNLOCKED celebration for the host page; the bridge does not forward it to the game.
Auth: player session cookie — Auth4 session cookie set by /auth/login (split into auth4_session.0, .1, ... when large). Same-origin only.
PARAMETERS
id(path, required, string, max length 64)
REQUEST BODY (REQUIRED)
| FIELD | TYPE | NOTES |
|---|---|---|
key* (required) | string | The achievement key defined in the developer workshop. min length 1 · max length 48 · pattern ^[a-z0-9][a-z0-9_-]{0,47}$ |
RESPONSES
- 200Unlocked (or already unlocked).
- 400INVALID_REQUEST: the body failed validation.
- 401UNAUTHENTICATED: not signed in.
- 403EMAIL_NOT_VERIFIED (verify your email to play) or ACCOUNT_NOT_LINKED.
- 404SESSION_NOT_FOUND (not your session), ACHIEVEMENT_NOT_FOUND (unknown or retired key) or GAME_UNAVAILABLE.
- 410SESSION_EXPIRED: the session ended or was idle for more than 30 minutes. Start a new one.
▸▾ 200 RESPONSE SCHEMA
| FIELD | TYPE | NOTES |
|---|---|---|
achievement* (required) | object | |
achievement.key* (required) | string | |
achievement.name* (required) | string | |
achievement.description* (required) | string | |
achievement.iconKey* (required) | string | |
achievement.secret* (required) | boolean | Hidden badges show as ??? on the game page until unlocked. |
unlocked* (required) | boolean | |
newlyUnlocked* (required) | boolean | False for repeats. |
unlockedAt* (required) | string | null | format: date-time |
testMode* (required) | boolean | Preview session: nothing was stored. |
rewards | object[] | Celebrations for the host page (ACHIEVEMENT UNLOCKED, maybe LEVEL UP). |
{
"achievement": {
"key": "9b2f6c1e-5a7d-4e8b-9c3f-2d1a0b4e6f70",
"name": "Player One",
"description": "string",
"iconKey": "9b2f6c1e-5a7d-4e8b-9c3f-2d1a0b4e6f70",
"secret": false
},
"unlocked": false,
"newlyUnlocked": false,
"unlockedAt": "2026-01-01T12:00:00.000Z",
"testMode": false,
"rewards": [
{}
]
}curl -X POST "https://gemsarcadia.com/api/v1/sessions/{id}/achievements" \
-H "Cookie: auth4_session=..." \
-H "Content-Type: application/json" \
-d '{"key":"9b2f6c1e-5a7d-4e8b-9c3f-2d1a0b4e6f70"}'PUBLIC ENDPOINTS
Public endpoints for published game leaderboards.
get/api/games/{slug}/scores
Auth: none
PARAMETERS
slug(path, required, string, pattern ^[a-z0-9-]+$)period(query, string, one of "all", "week")
RESPONSES
- 200Top ten scores for a published game.
- 400INVALID_REQUEST: the body failed validation.
- 404NOT_FOUND: game is not published or does not exist.
▸▾ 200 RESPONSE SCHEMA
| FIELD | TYPE | NOTES |
|---|---|---|
slug* (required) | string | |
period* (required) | string | one of "all", "week" |
scores* (required) | LeaderboardEntry[] | |
scores[].rank* (required) | integer | 1–10 |
scores[].initials* (required) | string | pattern ^[A-Z]{3}$ |
scores[].score* (required) | string | pattern ^\d+$ |
scores[].achievedAt* (required) | string | format: date-time |
{
"slug": "string",
"period": "all",
"scores": [
{
"rank": 1,
"initials": "string",
"score": "string",
"achievedAt": "2026-01-01T12:00:00.000Z"
}
]
}curl -X GET "https://gemsarcadia.com/api/games/{slug}/scores"DEVELOPER ENDPOINTS
Server-to-server endpoints authenticated with API keys.
get/api/v1/insertions/{insertionId}
Confirm from your backend that an insertionId reported by your game was really paid for. Test-mode sessions do not create insertions, so their ids (prefixed test_) always return 404.
Auth: API key (Bearer) — Developer API key from /developer/keys: ga_live_... or ga_test_.... Keep it on your server.
PARAMETERS
insertionId(path, required, string, max length 64)
RESPONSES
- 200The insertion exists and belongs to one of your games.
- 401UNAUTHENTICATED: missing, malformed or revoked API key. Headers: WWW-Authenticate.
- 404NOT_FOUND: unknown id, or it belongs to another developer's game.
▸▾ 200 RESPONSE SCHEMA
| FIELD | TYPE | NOTES |
|---|---|---|
valid* (required) | true | |
insertionId* (required) | string | |
gameId* (required) | string | |
sessionId* (required) | string | |
playerRef* (required) | string (PlayerRef) | Stable pseudonymous player id: the same player always has the same ref for your studio, a different one for other studios. Contains no email or account id. pattern ^pr_[0-9a-f]{32}$ |
coins* (required) | integer | ≥ 1 |
createdAt* (required) | string | format: date-time |
{
"valid": true,
"insertionId": "insertion_123",
"gameId": "game_123",
"sessionId": "session_123",
"playerRef": "pr_0123456789abcdef0123456789abcdef",
"coins": 1,
"createdAt": "2026-01-01T12:00:00.000Z"
}curl -X GET "https://gemsarcadia.com/api/v1/insertions/{insertionId}" \
-H "Authorization: Bearer $GA_API_KEY"get/api/v1/sessions/{id}
Get a session summary with its insertions
Auth: API key (Bearer) — Developer API key from /developer/keys: ga_live_... or ga_test_.... Keep it on your server.
PARAMETERS
id(path, required, string, max length 64)
RESPONSES
- 200Session summary.
- 401UNAUTHENTICATED: missing, malformed or revoked API key. Headers: WWW-Authenticate.
- 404NOT_FOUND: unknown id, or it belongs to another developer's game.
▸▾ 200 RESPONSE SCHEMA
| FIELD | TYPE | NOTES |
|---|---|---|
id* (required) | string | |
gameId* (required) | string | |
gameVersionId* (required) | string | |
playerRef* (required) | string (PlayerRef) | Stable pseudonymous player id: the same player always has the same ref for your studio, a different one for other studios. Contains no email or account id. pattern ^pr_[0-9a-f]{32}$ |
testMode* (required) | boolean | |
status* (required) | string | one of "active", "idle", "ended" |
startedAt* (required) | string | format: date-time |
lastSeenAt* (required) | string | format: date-time |
endedAt* (required) | string | null | format: date-time |
expiresAt* (required) | string | null | Null unless active. format: date-time |
totalCoins* (required) | integer | ≥ 0 |
insertions* (required) | object[] | |
insertions[].id* (required) | string | |
insertions[].coins* (required) | integer | ≥ 1 |
insertions[].createdAt* (required) | string | format: date-time |
{
"id": "id_123",
"gameId": "game_123",
"gameVersionId": "gameversion_123",
"playerRef": "pr_0123456789abcdef0123456789abcdef",
"testMode": false,
"status": "active",
"startedAt": "2026-01-01T12:00:00.000Z",
"lastSeenAt": "2026-01-01T12:00:00.000Z",
"endedAt": "2026-01-01T12:00:00.000Z",
"expiresAt": "2026-01-01T12:00:00.000Z",
"totalCoins": 0,
"insertions": [
{
"id": "id_123",
"coins": 1,
"createdAt": "2026-01-01T12:00:00.000Z"
}
]
}curl -X GET "https://gemsarcadia.com/api/v1/sessions/{id}" \
-H "Authorization: Bearer $GA_API_KEY"post/api/v1/insertions/{insertionId}/score
Submit a trusted server-verified score
Requires a ga_live_ key. The score is bound to this paid insertion, and each insertion can have only one score.
Auth: API key (Bearer) — Developer API key from /developer/keys: ga_live_... or ga_test_.... Keep it on your server.
PARAMETERS
insertionId(path, required, string, max length 64)
REQUEST BODY (REQUIRED)
| FIELD | TYPE | NOTES |
|---|---|---|
score* (required) | integer | 0–9007199254740991 |
idempotencyKey* (required) | string | min length 8 · max length 128 · pattern ^[A-Za-z0-9_.:-]+$ |
RESPONSES
- 200Score accepted.
- 400INVALID_REQUEST: the body failed validation.
- 401UNAUTHENTICATED: missing, malformed or revoked API key. Headers: WWW-Authenticate.
- 403LIVE_KEY_REQUIRED.
- 404NOT_FOUND: insertion is unknown or belongs to another developer.
- 409INSERTION_ALREADY_SCORED or IDEMPOTENCY_KEY_CONFLICT.
- 422SCORE_OUT_OF_RANGE.
- 429RATE_LIMITED: wait before submitting again. Headers: Retry-After.
▸▾ 200 RESPONSE SCHEMA
| FIELD | TYPE | NOTES |
|---|---|---|
scoreId* (required) | string | null | |
saved* (required) | boolean | |
testMode* (required) | boolean | |
qualifies* (required) | boolean | |
replayed* (required) | boolean | |
rank* (required) | integer | null | |
initials* (required) | string | null | pattern ^[A-Z]{3}$ |
needsInitials* (required) | boolean |
{
"scoreId": "score_123",
"saved": false,
"testMode": false,
"qualifies": false,
"replayed": false,
"rank": 0,
"initials": "string",
"needsInitials": false
}curl -X POST "https://gemsarcadia.com/api/v1/insertions/{insertionId}/score" \
-H "Authorization: Bearer $GA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"score":0,"idempotencyKey":"9b2f6c1e-5a7d-4e8b-9c3f-2d1a0b4e6f70"}'ERROR CODES
Every error body looks like { "error": "<CODE>", "message": "..." }.
INVALID_REQUESTUNAUTHENTICATEDACCOUNT_NOT_LINKEDEMAIL_NOT_VERIFIEDFORBIDDENGAME_UNAVAILABLESESSION_NOT_FOUNDSESSION_EXPIREDINSUFFICIENT_COINSRATE_LIMITEDIDEMPOTENCY_KEY_CONFLICTNOT_FOUND