Skip to content
GEMSARCADIA
▸ MANUAL CONTENTS(REST API reference)
GEMS ARCADIA · COIN-OP SERVICE MANUAL · FORM GA-DEV-1SEC. 2.3

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_... or ga_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

Start a play session

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)

Request body fields
FIELDTYPENOTES
gameSlug* (required)stringmax length 64 · pattern ^[a-z0-9-]+$
previewbooleanOwner/admin only: force a test-mode session.
versionIdstringWith 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
201 response fields
FIELDTYPENOTES
sessionId* (required)string
gameId* (required)string
coinsPerCredit* (required)integer1–5
balance* (required)integerPlayer's wallet balance in coins. ≥ 0
testMode* (required)booleanPreview session: insertions are free and not recorded.
expiresAt* (required)stringformat: date-time
player* (required)objectWhat the game receives from the SDK's init().
player.ref* (required)stringPseudonymous playerRef, stable per developer.
player.displayName* (required)string
Example 201 response
{
  "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 · createSession
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)

Request body fields
FIELDTYPENOTES
idempotencyKey* (required)stringClient-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
200 response fields
FIELDTYPENOTES
insertionId* (required)stringVerify 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)booleanTrue when this idempotencyKey had already been applied.
Example 200 response
{
  "insertionId": "insertion_123",
  "creditsGranted": 1,
  "coinsSpent": 0,
  "balance": 0,
  "testMode": false,
  "replayed": false
}
curl · insertCoin
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

Keep a session alive

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
200 response fields
FIELDTYPENOTES
sessionId* (required)string
lastSeenAt* (required)stringformat: date-time
expiresAt* (required)stringformat: date-time
Example 200 response
{
  "sessionId": "session_123",
  "lastSeenAt": "2026-01-01T12:00:00.000Z",
  "expiresAt": "2026-01-01T12:00:00.000Z"
}
curl · heartbeat
curl -X POST "https://gemsarcadia.com/api/play/sessions/{id}/heartbeat" \
  -H "Cookie: auth4_session=..."

post/api/play/sessions/{id}/end

End a session

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
200 response fields
FIELDTYPENOTES
sessionId* (required)string
endedAt* (required)stringformat: date-time
Example 200 response
{
  "sessionId": "session_123",
  "endedAt": "2026-01-01T12:00:00.000Z"
}
curl · endSession
curl -X POST "https://gemsarcadia.com/api/play/sessions/{id}/end" \
  -H "Cookie: auth4_session=..."

post/api/play/sessions/{id}/score

Submit a player 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)

Request body fields
FIELDTYPENOTES
score* (required)integer0–9007199254740991
idempotencyKey* (required)stringmin 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
200 response fields
FIELDTYPENOTES
scoreId* (required)string | null
saved* (required)boolean
testMode* (required)boolean
qualifies* (required)boolean
replayed* (required)boolean
rank* (required)integer | null
initials* (required)string | nullpattern ^[A-Z]{3}$
needsInitials* (required)boolean
Example 200 response
{
  "scoreId": "score_123",
  "saved": false,
  "testMode": false,
  "qualifies": false,
  "replayed": false,
  "rank": 0,
  "initials": "string",
  "needsInitials": false
}
curl · submitPlayerScore
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

Save high-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)

Request body fields
FIELDTYPENOTES
scoreId* (required)stringmax length 64
initials* (required)stringpattern ^[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
200 response fields
FIELDTYPENOTES
id* (required)string
initials* (required)stringpattern ^[A-Z]{3}$
Example 200 response
{
  "id": "id_123",
  "initials": "string"
}
curl · saveScoreInitials
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)

Request body fields
FIELDTYPENOTES
key* (required)stringThe 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
200 response fields
FIELDTYPENOTES
achievement* (required)object
achievement.key* (required)string
achievement.name* (required)string
achievement.description* (required)string
achievement.iconKey* (required)string
achievement.secret* (required)booleanHidden badges show as ??? on the game page until unlocked.
unlocked* (required)boolean
newlyUnlocked* (required)booleanFalse for repeats.
unlockedAt* (required)string | nullformat: date-time
testMode* (required)booleanPreview session: nothing was stored.
rewardsobject[]Celebrations for the host page (ACHIEVEMENT UNLOCKED, maybe LEVEL UP).
Example 200 response
{
  "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 · unlockAchievement
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

Get a public leaderboard

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
200 response fields
FIELDTYPENOTES
slug* (required)string
period* (required)stringone of "all", "week"
scores* (required)LeaderboardEntry[]
scores[].rank* (required)integer1–10
scores[].initials* (required)stringpattern ^[A-Z]{3}$
scores[].score* (required)stringpattern ^\d+$
scores[].achievedAt* (required)stringformat: date-time
Example 200 response
{
  "slug": "string",
  "period": "all",
  "scores": [
    {
      "rank": 1,
      "initials": "string",
      "score": "string",
      "achievedAt": "2026-01-01T12:00:00.000Z"
    }
  ]
}
curl · getLeaderboard
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}

Verify a coin insertion

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
200 response fields
FIELDTYPENOTES
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)stringformat: date-time
Example 200 response
{
  "valid": true,
  "insertionId": "insertion_123",
  "gameId": "game_123",
  "sessionId": "session_123",
  "playerRef": "pr_0123456789abcdef0123456789abcdef",
  "coins": 1,
  "createdAt": "2026-01-01T12:00:00.000Z"
}
curl · verifyInsertion
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
200 response fields
FIELDTYPENOTES
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)stringone of "active", "idle", "ended"
startedAt* (required)stringformat: date-time
lastSeenAt* (required)stringformat: date-time
endedAt* (required)string | nullformat: date-time
expiresAt* (required)string | nullNull unless active. format: date-time
totalCoins* (required)integer≥ 0
insertions* (required)object[]
insertions[].id* (required)string
insertions[].coins* (required)integer≥ 1
insertions[].createdAt* (required)stringformat: date-time
Example 200 response
{
  "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 · getSession
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)

Request body fields
FIELDTYPENOTES
score* (required)integer0–9007199254740991
idempotencyKey* (required)stringmin 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
200 response fields
FIELDTYPENOTES
scoreId* (required)string | null
saved* (required)boolean
testMode* (required)boolean
qualifies* (required)boolean
replayed* (required)boolean
rank* (required)integer | null
initials* (required)string | nullpattern ^[A-Z]{3}$
needsInitials* (required)boolean
Example 200 response
{
  "scoreId": "score_123",
  "saved": false,
  "testMode": false,
  "qualifies": false,
  "replayed": false,
  "rank": 0,
  "initials": "string",
  "needsInitials": false
}
curl · submitTrustedScore
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_REQUEST
  • UNAUTHENTICATED
  • ACCOUNT_NOT_LINKED
  • EMAIL_NOT_VERIFIED
  • FORBIDDEN
  • GAME_UNAVAILABLE
  • SESSION_NOT_FOUND
  • SESSION_EXPIRED
  • INSUFFICIENT_COINS
  • RATE_LIMITED
  • IDEMPOTENCY_KEY_CONFLICT
  • NOT_FOUND
REV 1.0 · KEEP THIS MANUAL WITH THE CABINET · QUESTIONS? ASK GLINT