{
  "openapi": "3.1.0",
  "info": {
    "title": "Gems Arcadia Coin API",
    "version": "1.0.0",
    "summary": "Play sessions, coin insertion, scores and server-to-server verification.",
    "description": "Two audiences:\n\n* **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.\n* **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.\n\n1 coin = 25 US cents. Each game charges `coinsPerCredit` (1-5) coins per credit, and the developer earns 70% of that value.\n\nErrors share one flat shape: `{ \"error\": \"<CODE>\", \"message\": \"...\" }` plus code-specific fields.\n\nClient-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.",
    "contact": {
      "name": "Gems Arcadia",
      "url": "https://gemsarcadia.com/developers"
    }
  },
  "servers": [
    {
      "url": "https://gemsarcadia.com"
    },
    {
      "url": "http://localhost:3000"
    }
  ],
  "tags": [
    {
      "name": "Player",
      "description": "Session-cookie endpoints used by the host page."
    },
    {
      "name": "Public",
      "description": "Public endpoints for published game leaderboards."
    },
    {
      "name": "Developer",
      "description": "Server-to-server endpoints authenticated with API keys."
    }
  ],
  "paths": {
    "/api/play/sessions": {
      "post": {
        "tags": [
          "Player"
        ],
        "operationId": "createSession",
        "summary": "Start a play session",
        "description": "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.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "gameSlug"
                ],
                "additionalProperties": false,
                "properties": {
                  "gameSlug": {
                    "type": "string",
                    "pattern": "^[a-z0-9-]+$",
                    "maxLength": 64
                  },
                  "preview": {
                    "type": "boolean",
                    "description": "Owner/admin only: force a test-mode session."
                  },
                  "versionId": {
                    "type": "string",
                    "description": "With `preview: true`: the stored build to preview. Defaults to the current version, else the newest non-rejected upload."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Session started.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedSession"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "GAME_UNAVAILABLE: unknown, unpublished or unplayable game.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/play/sessions/{id}/insert-coin": {
      "post": {
        "tags": [
          "Player"
        ],
        "operationId": "insertCoin",
        "summary": "Insert a coin (buy one credit)",
        "description": "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.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "idempotencyKey"
                ],
                "properties": {
                  "idempotencyKey": {
                    "type": "string",
                    "minLength": 8,
                    "maxLength": 128,
                    "pattern": "^[A-Za-z0-9_.:-]+$",
                    "description": "Client-generated, unique per intended insertion (a UUID is ideal). Reuse it only to retry."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Credit granted (or replayed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InsertCoinResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "description": "INSUFFICIENT_COINS: the wallet cannot cover coinsPerCredit. Nothing was debited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InsufficientCoinsError"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/SessionNotFound"
          },
          "409": {
            "description": "IDEMPOTENCY_KEY_CONFLICT: the key was already used for a different request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "$ref": "#/components/responses/SessionExpired"
          },
          "429": {
            "description": "RATE_LIMITED.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitedError"
                }
              }
            }
          }
        }
      }
    },
    "/api/play/sessions/{id}/heartbeat": {
      "post": {
        "tags": [
          "Player"
        ],
        "operationId": "heartbeat",
        "summary": "Keep a session alive",
        "description": "Call every few minutes while the game is open. A heartbeat never revives an ended or timed-out session.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "responses": {
          "200": {
            "description": "Session extended.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "sessionId",
                    "lastSeenAt",
                    "expiresAt"
                  ],
                  "properties": {
                    "sessionId": {
                      "type": "string"
                    },
                    "lastSeenAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/SessionNotFound"
          },
          "410": {
            "$ref": "#/components/responses/SessionExpired"
          }
        }
      }
    },
    "/api/play/sessions/{id}/end": {
      "post": {
        "tags": [
          "Player"
        ],
        "operationId": "endSession",
        "summary": "End a session",
        "description": "Idempotent: ending an ended session returns the original `endedAt`.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "responses": {
          "200": {
            "description": "Session ended.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "sessionId",
                    "endedAt"
                  ],
                  "properties": {
                    "sessionId": {
                      "type": "string"
                    },
                    "endedAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/SessionNotFound"
          }
        }
      }
    },
    "/api/v1/insertions/{insertionId}": {
      "get": {
        "tags": [
          "Developer"
        ],
        "operationId": "verifyInsertion",
        "summary": "Verify a coin insertion",
        "description": "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.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "insertionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 64
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The insertion exists and belongs to one of your games.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InsertionVerification"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "404": {
            "description": "NOT_FOUND: unknown id, or it belongs to another developer's game.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "valid": {
                          "const": false
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sessions/{id}": {
      "get": {
        "tags": [
          "Developer"
        ],
        "operationId": "getSession",
        "summary": "Get a session summary with its insertions",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "responses": {
          "200": {
            "description": "Session summary.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionSummary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "404": {
            "description": "NOT_FOUND: unknown id, or it belongs to another developer's game.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/play/sessions/{id}/score": {
      "post": {
        "tags": [
          "Player"
        ],
        "operationId": "submitPlayerScore",
        "summary": "Submit a player score",
        "description": "Client-submitted scores are untrusted. Requires an active session and an unscored paid insertion. Test-mode previews are accepted but never saved.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScoreRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Score accepted; saved is false in test mode.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoreSubmission"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/SessionNotFound"
          },
          "409": {
            "description": "COIN_REQUIRED, INSERTION_ALREADY_SCORED or IDEMPOTENCY_KEY_CONFLICT."
          },
          "410": {
            "$ref": "#/components/responses/SessionExpired"
          },
          "422": {
            "description": "SCORE_OUT_OF_RANGE."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/play/sessions/{id}/score/initials": {
      "post": {
        "tags": [
          "Player"
        ],
        "operationId": "saveScoreInitials",
        "summary": "Save high-score initials",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScoreInitialsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Initials saved to the score and player sound preferences.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "initials"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "initials": {
                      "type": "string",
                      "pattern": "^[A-Z]{3}$"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "SCORE_NOT_FOUND."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/games/{slug}/scores": {
      "get": {
        "tags": [
          "Public"
        ],
        "operationId": "getLeaderboard",
        "summary": "Get a public leaderboard",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            }
          },
          {
            "name": "period",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "week"
              ],
              "default": "all"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Top ten scores for a published game.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LeaderboardResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "404": {
            "description": "NOT_FOUND: game is not published or does not exist."
          }
        }
      }
    },
    "/api/v1/insertions/{insertionId}/score": {
      "post": {
        "tags": [
          "Developer"
        ],
        "operationId": "submitTrustedScore",
        "summary": "Submit a trusted server-verified score",
        "description": "Requires a ga_live_ key. The score is bound to this paid insertion, and each insertion can have only one score.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "insertionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 64
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScoreRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Score accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoreSubmission"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "description": "LIVE_KEY_REQUIRED."
          },
          "404": {
            "description": "NOT_FOUND: insertion is unknown or belongs to another developer."
          },
          "409": {
            "description": "INSERTION_ALREADY_SCORED or IDEMPOTENCY_KEY_CONFLICT."
          },
          "422": {
            "description": "SCORE_OUT_OF_RANGE."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/sessions/{id}/achievements": {
      "post": {
        "tags": [
          "Player"
        ],
        "operationId": "unlockAchievement",
        "summary": "Unlock one of the game's achievements",
        "description": "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.",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "key"
                ],
                "properties": {
                  "key": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 48,
                    "pattern": "^[a-z0-9][a-z0-9_-]{0,47}$",
                    "description": "The achievement key defined in the developer workshop."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Unlocked (or already unlocked).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AchievementUnlockResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "description": "SESSION_NOT_FOUND (not your session), ACHIEVEMENT_NOT_FOUND (unknown or retired key) or GAME_UNAVAILABLE.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "$ref": "#/components/responses/SessionExpired"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "sessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "auth4_session",
        "description": "Auth4 session cookie set by /auth/login (split into auth4_session.0, .1, ... when large). Same-origin only."
      },
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Developer API key from /developer/keys: `ga_live_...` or `ga_test_...`. Keep it on your server."
      }
    },
    "parameters": {
      "SessionId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "maxLength": 64
        }
      }
    },
    "responses": {
      "InvalidRequest": {
        "description": "INVALID_REQUEST: the body failed validation.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthenticated": {
        "description": "UNAUTHENTICATED: not signed in.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "EMAIL_NOT_VERIFIED (verify your email to play) or ACCOUNT_NOT_LINKED.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "SessionNotFound": {
        "description": "SESSION_NOT_FOUND: unknown session, or it belongs to another player.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "SessionExpired": {
        "description": "SESSION_EXPIRED: the session ended or was idle for more than 30 minutes. Start a new one.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InvalidApiKey": {
        "description": "UNAUTHENTICATED: missing, malformed or revoked API key.",
        "headers": {
          "WWW-Authenticate": {
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "RATE_LIMITED: wait before submitting again.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/RateLimitedError"
            }
          }
        }
      }
    },
    "schemas": {
      "ErrorCode": {
        "type": "string",
        "enum": [
          "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"
        ]
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ErrorCode"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "InsufficientCoinsError": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "required": [
              "balance"
            ],
            "properties": {
              "error": {
                "const": "INSUFFICIENT_COINS"
              },
              "balance": {
                "type": "integer",
                "minimum": 0
              },
              "coinsPerCredit": {
                "type": "integer",
                "minimum": 1,
                "maximum": 5
              }
            }
          }
        ]
      },
      "RateLimitedError": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "properties": {
              "error": {
                "const": "RATE_LIMITED"
              },
              "retryAfterMs": {
                "type": "integer",
                "minimum": 0
              }
            }
          }
        ]
      },
      "CreatedSession": {
        "type": "object",
        "required": [
          "sessionId",
          "gameId",
          "coinsPerCredit",
          "balance",
          "testMode",
          "expiresAt",
          "player"
        ],
        "properties": {
          "sessionId": {
            "type": "string"
          },
          "gameId": {
            "type": "string"
          },
          "coinsPerCredit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "balance": {
            "type": "integer",
            "minimum": 0,
            "description": "Player's wallet balance in coins."
          },
          "testMode": {
            "type": "boolean",
            "description": "Preview session: insertions are free and not recorded."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "player": {
            "type": "object",
            "required": [
              "ref",
              "displayName"
            ],
            "description": "What the game receives from the SDK's init().",
            "properties": {
              "ref": {
                "type": "string",
                "description": "Pseudonymous playerRef, stable per developer."
              },
              "displayName": {
                "type": "string"
              }
            }
          }
        }
      },
      "InsertCoinResult": {
        "type": "object",
        "required": [
          "insertionId",
          "creditsGranted",
          "coinsSpent",
          "balance",
          "testMode",
          "replayed"
        ],
        "properties": {
          "insertionId": {
            "type": "string",
            "description": "Verify it with GET /api/v1/insertions/{insertionId}. Test-mode ids start with `test_`."
          },
          "creditsGranted": {
            "const": 1
          },
          "coinsSpent": {
            "type": "integer",
            "minimum": 0
          },
          "balance": {
            "type": "integer",
            "minimum": 0
          },
          "testMode": {
            "type": "boolean"
          },
          "replayed": {
            "type": "boolean",
            "description": "True when this idempotencyKey had already been applied."
          }
        }
      },
      "InsertionVerification": {
        "type": "object",
        "required": [
          "valid",
          "insertionId",
          "gameId",
          "sessionId",
          "playerRef",
          "coins",
          "createdAt"
        ],
        "properties": {
          "valid": {
            "const": true
          },
          "insertionId": {
            "type": "string"
          },
          "gameId": {
            "type": "string"
          },
          "sessionId": {
            "type": "string"
          },
          "playerRef": {
            "$ref": "#/components/schemas/PlayerRef"
          },
          "coins": {
            "type": "integer",
            "minimum": 1
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PlayerRef": {
        "type": "string",
        "pattern": "^pr_[0-9a-f]{32}$",
        "description": "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."
      },
      "SessionSummary": {
        "type": "object",
        "required": [
          "id",
          "gameId",
          "gameVersionId",
          "playerRef",
          "testMode",
          "status",
          "startedAt",
          "lastSeenAt",
          "endedAt",
          "expiresAt",
          "totalCoins",
          "insertions"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "gameId": {
            "type": "string"
          },
          "gameVersionId": {
            "type": "string"
          },
          "playerRef": {
            "$ref": "#/components/schemas/PlayerRef"
          },
          "testMode": {
            "type": "boolean"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "idle",
              "ended"
            ]
          },
          "startedAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastSeenAt": {
            "type": "string",
            "format": "date-time"
          },
          "endedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Null unless active."
          },
          "totalCoins": {
            "type": "integer",
            "minimum": 0
          },
          "insertions": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "coins",
                "createdAt"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "coins": {
                  "type": "integer",
                  "minimum": 1
                },
                "createdAt": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          }
        }
      },
      "ScoreRequest": {
        "type": "object",
        "required": [
          "score",
          "idempotencyKey"
        ],
        "additionalProperties": false,
        "properties": {
          "score": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "idempotencyKey": {
            "type": "string",
            "minLength": 8,
            "maxLength": 128,
            "pattern": "^[A-Za-z0-9_.:-]+$"
          }
        }
      },
      "ScoreInitialsRequest": {
        "type": "object",
        "required": [
          "scoreId",
          "initials"
        ],
        "additionalProperties": false,
        "properties": {
          "scoreId": {
            "type": "string",
            "maxLength": 64
          },
          "initials": {
            "type": "string",
            "pattern": "^[A-Z]{3}$"
          }
        }
      },
      "ScoreSubmission": {
        "type": "object",
        "required": [
          "scoreId",
          "saved",
          "testMode",
          "qualifies",
          "replayed",
          "rank",
          "initials",
          "needsInitials"
        ],
        "properties": {
          "scoreId": {
            "type": [
              "string",
              "null"
            ]
          },
          "saved": {
            "type": "boolean"
          },
          "testMode": {
            "type": "boolean"
          },
          "qualifies": {
            "type": "boolean"
          },
          "replayed": {
            "type": "boolean"
          },
          "rank": {
            "type": [
              "integer",
              "null"
            ]
          },
          "initials": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[A-Z]{3}$"
          },
          "needsInitials": {
            "type": "boolean"
          }
        }
      },
      "LeaderboardEntry": {
        "type": "object",
        "required": [
          "rank",
          "initials",
          "score",
          "achievedAt"
        ],
        "properties": {
          "rank": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10
          },
          "initials": {
            "type": "string",
            "pattern": "^[A-Z]{3}$"
          },
          "score": {
            "type": "string",
            "pattern": "^\\d+$"
          },
          "achievedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "LeaderboardResponse": {
        "type": "object",
        "required": [
          "slug",
          "period",
          "scores"
        ],
        "properties": {
          "slug": {
            "type": "string"
          },
          "period": {
            "type": "string",
            "enum": [
              "all",
              "week"
            ]
          },
          "scores": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "$ref": "#/components/schemas/LeaderboardEntry"
            }
          }
        }
      },
      "AchievementUnlockResult": {
        "type": "object",
        "required": [
          "achievement",
          "unlocked",
          "newlyUnlocked",
          "unlockedAt",
          "testMode"
        ],
        "properties": {
          "achievement": {
            "type": "object",
            "required": [
              "key",
              "name",
              "description",
              "iconKey",
              "secret"
            ],
            "properties": {
              "key": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "description": {
                "type": "string"
              },
              "iconKey": {
                "type": "string"
              },
              "secret": {
                "type": "boolean",
                "description": "Hidden badges show as ??? on the game page until unlocked."
              }
            }
          },
          "unlocked": {
            "type": "boolean"
          },
          "newlyUnlocked": {
            "type": "boolean",
            "description": "False for repeats."
          },
          "unlockedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "testMode": {
            "type": "boolean",
            "description": "Preview session: nothing was stored."
          },
          "rewards": {
            "type": "array",
            "description": "Celebrations for the host page (ACHIEVEMENT UNLOCKED, maybe LEVEL UP).",
            "items": {
              "type": "object"
            }
          }
        }
      }
    }
  }
}
