Skip to content
GEMSARCADIA
▸ MANUAL CONTENTS(Server verification)
GEMS ARCADIA · COIN-OP SERVICE MANUAL · FORM GA-DEV-1SEC. 3.1

Server verification

Every coin that goes in gets an insertionId. Your backend can ask the arcade whether an id is real, which game it belongs to and who paid. Check it before you store anything valuable yourself: an unlock, a cosmetic, a support refund.

1. Create an API key#

Open /developer/keys and create a key. Keys look like ga_live_... or ga_test_.... The full key is shown once; the arcade only stores a SHA-256 hash of it.

2. Verify an insertion#

Call GET /api/v1/insertions/{insertionId} with your key as a bearer token:

HTTP
GET /api/v1/insertions/ins_8f2c HTTP/1.1
Host: gemsarcadia.com
Authorization: Bearer ga_live_...
StatusMeaning
200{ valid: true, insertionId, gameId, sessionId, playerRef, coins, createdAt }
404Unknown id, a test-mode id (test_...), or another studio's insertion. Treat as invalid.
401Missing, malformed or revoked API key.

Node.js (18+)#

No dependencies: it uses the built-in fetch.

verify-insertion.mjs
const GA_BASE_URL = process.env.GA_BASE_URL ?? "https://gemsarcadia.com";

/**
 * Returns the verified insertion ({ valid: true, gameId, coins, playerRef,
 * ... }) or null when it does not exist or belongs to another studio.
 * Throws on a bad API key or a server error, so you can retry later.
 */
export async function verifyInsertion(insertionId, {
  apiKey = process.env.GA_API_KEY,
  baseUrl = GA_BASE_URL,
  fetchImpl = fetch,
} = {}) {
  if (!apiKey) throw new Error("Set GA_API_KEY (ga_live_... or ga_test_...).");
  const url = `${baseUrl}/api/v1/insertions/${encodeURIComponent(insertionId)}`;
  const res = await fetchImpl(url, {
    headers: { Authorization: `Bearer ${apiKey}` },
  });
  if (res.status === 404) return null; // unknown, test-mode, or not yours
  if (!res.ok) {
    const body = await res.json().catch(() => ({}));
    throw new Error(`Gems Arcadia API ${res.status}: ${body.error ?? "ERROR"}`);
  }
  return res.json();
}

Grant each insertion once. Check the game id, then atomically record the insertion id and apply your entitlement in one database transaction protected by a unique constraint. That makes simultaneous retries safe and prevents a crash from recording a receipt without granting its reward:

verify-insertion.mjs · grant once
/**
 * Grants a purchase at most once per insertion. `store.applyOnce` must use a
 * unique insertion-id constraint and one database transaction to record the
 * id and apply the entitlement together. It returns true only for the first
 * successful grant.
 */
export async function grantOnce(insertionId, { gameId, store, ...options }) {
  const insertion = await verifyInsertion(insertionId, options);
  if (!insertion || insertion.gameId !== gameId) {
    return { granted: false, reason: "INVALID" };
  }
  const applied = await store.applyOnce(insertion.insertionId, {
    gameId: insertion.gameId,
    playerRef: insertion.playerRef,
    coins: insertion.coins,
  });
  if (!applied) {
    return { granted: false, reason: "ALREADY_GRANTED" };
  }
  return { granted: true, playerRef: insertion.playerRef, coins: insertion.coins };
}

Python (3.8+)#

Standard library only.

verify_insertion.py
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request

GA_BASE_URL = os.environ.get("GA_BASE_URL", "https://gemsarcadia.com")


def verify_insertion(insertion_id, api_key=None, base_url=GA_BASE_URL):
    """Return the verified insertion dict, or None if it is unknown,
    test-mode or belongs to another studio. Raises on other errors."""
    api_key = api_key or os.environ.get("GA_API_KEY")
    if not api_key:
        raise RuntimeError("Set GA_API_KEY (ga_live_... or ga_test_...).")
    url = "%s/api/v1/insertions/%s" % (
        base_url,
        urllib.parse.quote(insertion_id, safe=""),
    )
    request = urllib.request.Request(
        url, headers={"Authorization": "Bearer " + api_key}
    )
    try:
        with urllib.request.urlopen(request, timeout=10) as response:
            return json.load(response)
    except urllib.error.HTTPError as error:
        if error.code == 404:
            return None
        raise
SHELL
GA_API_KEY=ga_test_... python verify_insertion.py ins_8f2c

3. Look up a whole session#

GET /api/v1/sessions/{id} returns the session's status, the player ref, the total coins and every insertion. Useful for support tickets and audits. The fields are listed in the REST API reference.

Where do insertion ids come from?#

Games run with a Content-Security-Policy that limits network access to the game's own files, so a game can't post to your server from inside the arcade. Ids reach you another way, typically as a receipt code your game shows after a coin goes in (the sample game shows RECEIPT <insertionId>) that players quote in support requests. Once you have one insertion, sessionId leads you to the rest of the session.

playerRef is pseudonymous: the same player always has the same ref in your games and a different one in other studios' games. It contains no email or account id.

Trusted high scores#

Scores sent through GemsArcadia.submitScore(score) come from the player's browser and are inherently untrusted. The arcade validates the session, paid insertion and configured maximum score, but a player can still modify the game or its runtime. Treat those scores as community leaderboard entries only.

For a trusted leaderboard, verify the run on your own backend, then submit the result with the paid insertionId and a live API key:

HTTP
POST /api/v1/insertions/ins_8f2c/score HTTP/1.1
Host: gemsarcadia.com
Authorization: Bearer ga_live_...
Content-Type: application/json

{"score": 9001, "idempotencyKey": "run-2026-0042"}

The endpoint confirms the insertion belongs to one of your games, rejects a second score for the same insertion, and is safe to retry with the same key. Never put the live key in a game build. The arcade's public board combines client and server reports, so developers who need a strictly trusted board should keep and display their verified results on their own backend.

REV 1.0 · KEEP THIS MANUAL WITH THE CABINET · QUESTIONS? ASK GLINT