{
  "openapi": "3.1.0",
  "info": {
    "title": "CEEJAY Connect API: the requests a game makes",
    "version": "1.2.0",
    "summary": "Join a player of your game to their Discord account in a community on CEEJAY, from inside the game, and show them the community's quests, shop and rank.",
    "description": "These are the requests a game itself makes for Connect and for the game loop, with the public key it ships with and the player session it is given. They are part of the CEEJAY API and live at the same address as the rest of it; the requests a studio's own server makes with a secret key are in `openapi.json`, served beside this file.\n\n## Base URL\n\nEvery request goes to the dashboard's URL of the community's instance followed by `/api/v1`.\n\n- Shared CEEJAY service: `https://ceejay.communitystudios.xyz/api/v1`\n- Dedicated instance: the URL of that instance's dashboard, followed by `/api/v1`.\n\nConnect is switched on per instance. Where it is off, every request here returns `404` with the code `not_available` and `feature: \"players\"`: hide the button.\n\n## What a game stores\n\n- **A public key** (`ceejay_pk_` and 48 hexadecimal characters). It belongs to one game of one community and is not a secret: it ships inside the game. With it alone a game can start sessions and nothing else: nothing about a member or another player can be read. Send it as `Authorization: Bearer ceejay_pk_…`.\n- **A session secret** (`ceejay_cs_…`), returned when a session starts. It reads that one session's result, in the body of a request.\n- **A refresh token** (`ceejay_rt_…`), returned when a session is approved. One per device, stored in the device's secure store, and replaced each time it is used.\n- **A player session** (`cjps1.…`), a token that lasts 15 minutes. Send it as `Authorization: Bearer cjps1.…`.\n\nEach is accepted by exactly one kind of request and recognised by its prefix: any other value returns `401` with the code `wrong_key_kind`. A secret key (`ceejay_sk_`) is never sent from a game.\n\n## Conventions\n\n- Send `Content-Type: application/json` with every request that has a body, and a JSON object as the body.\n- A successful response is `{ \"success\": true, \"data\": { … } }`.\n- A failed response has a non-2xx status and `{ \"error\": \"message\", \"code\": \"fixed_code\", \"requestId\": \"req_…\" }`. Some codes add a field (`reason`, `retryAfter`, `field`, `feature`, and for the game loop `message`, `holdUntil`, `cap`, `price` and others): the examples of each operation show them. Act on `code`: the `error` message text can change.\n- Every response includes `X-Request-Id`, and the three headers `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`. Here a `401` includes them too: a game's request is counted against its IP address before its key is read.\n- Dates are ISO 8601 strings in UTC. Discord IDs and connection IDs are strings. The IDs of quests, tasks, shop items and purchases are whole numbers; a request also takes them as a string of digits.\n- No cookie is read by any of these requests.\n- Every operation carries `x-since`, the version of the CEEJAY API it first appeared in, and `x-caller`, which says what it is called with (`public_key` or `player_session`).\n\n## From a browser game\n\nThese requests can be made from a web page. The page's origin must be on the list saved for the game, or the request returns `403` with the code `origin_not_allowed`. A request with no `Origin` header (a native game) passes. Send the two headers `Authorization` and `Content-Type` and no other: the response to the browser's preflight request allows exactly those. `Access-Control-Allow-Credentials` is never sent.\n\n## The game loop\n\nA game can show the community's quests, shop, locker, rank and daily reward for the player a player session is for. These requests are in the Game loop group. They work only where the community has switched the loop on for the game, and the member has consented to share their quests, rewards, shop and rank with it. Buying and claiming also need a device that connected with proof (when the game asks for proof) and that has passed the game's hold period, and are limited to a daily amount of coins and to the items the community's team put on the game's list. Reading, quests and the daily reward need neither proof nor a hold. A game is never sent a Discord ID, an avatar or anything about another member, and CEEJAY never sends a member a direct message for anything a game starts.\n\n## Limits\n\nOne IP address can make 600 requests a minute across all of these, and the three limit headers report that count. A player session can make 140 requests a minute of any kind: Get the player's profile and the requests of the game loop share that count. The requests of the game loop also have limits of their own for one player session: 120 reads a minute, and 20 writes a minute (Start, Verify and Answer a quest task, Buy a shop item, Claim an item and Check in). Their headers report the count of the kind of request once the player session is read. Buying and claiming together are limited to 60 an hour for one member in one game. Some requests have a limit of their own, named in their description. Over a limit the response is `429` with `retryAfter` and a `Retry-After` header.\n\nThe studio's own server never uses these requests: one server has one IP address. It uses the Connect operations of `openapi.json`."
  },
  "servers": [
    {
      "url": "https://ceejay.communitystudios.xyz/api/v1",
      "description": "The shared CEEJAY service."
    },
    {
      "url": "https://{dashboardHost}/api/v1",
      "description": "A dedicated CEEJAY instance: your dashboard's address followed by /api/v1.",
      "variables": {
        "dashboardHost": {
          "default": "dashboard.example.com",
          "description": "The domain you see in your browser when you open your dashboard, without https://."
        }
      }
    }
  ],
  "security": [
    {
      "PublicKey": []
    }
  ],
  "tags": [
    {
      "name": "Connect",
      "description": "Start a connect session, read its result, confirm or refuse it on the game's behalf, and check whether a player is connected."
    },
    {
      "name": "Player",
      "description": "What a game does for a connected player: renew the player session, read the member card, disconnect."
    },
    {
      "name": "Game loop",
      "description": "What a game shows and does for a connected player with the community's quests, shop, rank and daily reward: list and finish quests, list the shop, buy and claim, read the locker, the ranks and the balance, check in, and list and answer surveys. These need the loop switched on for the game and the member's consent; buying and claiming also need proof, a hold period that has passed and room under the daily limit."
    }
  ],
  "paths": {
    "/connect/start": {
      "post": {
        "tags": [
          "Connect"
        ],
        "summary": "Start connect",
        "description": "Starts a connect session for one player of the game, or attaches that player to a session a member started in Discord.\n\n**Called with** the game's public key, sent as `Authorization: Bearer ceejay_pk_…`.\n\nWithout `code`, the response includes a link (`url`), a code and the URL of a QR image. Open the link in a browser (never in an embedded web view: Discord refuses to sign in there), or show the code with the URL to enter it at. The player approves with their Discord account on CEEJAY's connect page. Then poll for the result every `pollIntervalSeconds`.\n\nWith `code`, no new session is created: the player is attached to the session of the member who created that code in Discord, the code is cleared, and `member` identifies whose code it was. Show that name to the player before the game approves.\n\n**Proof.** Send `proof` when the game can show who the player is: a connect pass your own server created, or the token Firebase Authentication or Unity Player Authentication issued to the game. The session is then at the verified level. An anonymous sign-in, and no proof at all, give the basic level: the game supplies an ID that CEEJAY does not verify. A game can be set up to connect verified players only.\n\n**Already connected.** When the proof is verified and was issued in the last 10 minutes, and this player is already connected at the verified level with the member's own approval, the session is approved immediately: `status` is `approved`, `alreadyConnected` is `true`, and Get the result returns the tokens with no Discord step. In every other case the member is asked, also when the player is already connected.\n\n**Earlier sessions.** A start with verified proof ends every earlier open session for the same player. A basic start ends only earlier basic sessions for that player that came from the same IP address.\n\n**Age.** A game set up for a mixed audience must send `underAge`. `true` returns `403` and stores nothing. When the proof sent with it is verified, a connection the player has is ended; the public key alone ends nothing.\n\n**Order of checks.** The fields, the age check, the proof, and only then the limits and the code.\n\n**Limits.** 600 starts from one IP address in 10 minutes, 10 verified starts for one player in 10 minutes, and 20 attempts with a code that is not taken from one IP address in 10 minutes. When more than 200 unknown codes are sent with one public key in 10 minutes, requests with a code for that key are throttled until the window ends: each response is delayed 2 seconds and the limit drops to 2 attempts per IP address instead of 20. It is never switched off.\n\nStore `sessionId`, `sessionSecret` and `expiresAt` in the same place as the refresh token until the session ends: a phone often closes a game while its player is in the browser.",
        "operationId": "startConnect",
        "x-caller": "public_key",
        "x-since": "1.1.0",
        "security": [
          {
            "PublicKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StartConnectRequest"
              },
              "examples": {
                "request": {
                  "summary": "A request",
                  "value": {
                    "playerId": "p_19",
                    "playerName": "Nova77",
                    "proof": {
                      "type": "pass",
                      "token": "ceejay_pass_4f8a2c1e9b7d3e41f8a6b0c1d25f0c2a9b7d3e41f8a6b0c1d24f8a2c1e9b7d3e"
                    },
                    "returnTo": "game"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The session.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StartConnectResponse"
                },
                "examples": {
                  "started": {
                    "summary": "A new session",
                    "value": {
                      "success": true,
                      "data": {
                        "sessionId": "kT3v9QmZ1xY7bLw2RfA8cN",
                        "status": "pending",
                        "url": "https://ceejay.communitystudios.xyz/connect/s/kT3v9QmZ1xY7bLw2RfA8cN",
                        "qrUrl": "https://ceejay.communitystudios.xyz/connect/s/kT3v9QmZ1xY7bLw2RfA8cN/qr",
                        "code": "K7M-2QX",
                        "sessionSecret": "ceejay_cs_4f8a2c1e9b7d3e41f8a6b0c1d25f0c2a9b7d3e41f8a6b0c1d24f8a2c1e9b7d3e",
                        "expiresAt": "2026-10-08T18:14:11.000Z",
                        "pollIntervalSeconds": 3,
                        "proof": "verified",
                        "alreadyConnected": false,
                        "member": null
                      }
                    }
                  },
                  "withCode": {
                    "summary": "A player attached to a member's code",
                    "value": {
                      "success": true,
                      "data": {
                        "sessionId": "kT3v9QmZ1xY7bLw2RfA8cN",
                        "status": "needs_game_confirmation",
                        "url": null,
                        "qrUrl": null,
                        "code": null,
                        "sessionSecret": "ceejay_cs_4f8a2c1e9b7d3e41f8a6b0c1d25f0c2a9b7d3e41f8a6b0c1d24f8a2c1e9b7d3e",
                        "expiresAt": "2026-10-08T18:14:11.000Z",
                        "pollIntervalSeconds": 3,
                        "proof": "verified",
                        "alreadyConnected": false,
                        "member": {
                          "displayName": "Nova",
                          "avatarUrl": "https://cdn.discordapp.com/avatars/409876543210987654/a1b2c3d4e5f60718293a4b5c6d7e8f90.png"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was refused and nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notObject": {
                    "summary": "The body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badPlayerId": {
                    "summary": "A player ID outside the alphabet",
                    "value": {
                      "error": "playerId must be 1 to 128 characters: letters, digits and . _ : @ | + = -, starting with a letter or a digit",
                      "code": "bad_player_id",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badName": {
                    "summary": "A name that is not text, or is too long",
                    "value": {
                      "error": "playerName must be text of at most 80 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "playerName"
                    }
                  },
                  "badProof": {
                    "summary": "A proof that cannot be read",
                    "value": {
                      "error": "proof must be an object with a type and a token",
                      "code": "bad_proof",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badCode": {
                    "summary": "A code that is not text",
                    "value": {
                      "error": "code must be text",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "code"
                    }
                  },
                  "unknownReturn": {
                    "summary": "A return key the game does not have",
                    "value": {
                      "error": "returnTo is not one of the return addresses saved for this game",
                      "code": "unknown_return",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badLocale": {
                    "summary": "A locale that is not a language tag",
                    "value": {
                      "error": "locale must be a language tag such as en or pt-BR",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "locale"
                    }
                  },
                  "badUnderAge": {
                    "summary": "An age value that is not true or false",
                    "value": {
                      "error": "underAge must be true or false",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "underAge"
                    }
                  },
                  "ageAnswerRequired": {
                    "summary": "No age value for a game with a mixed audience",
                    "value": {
                      "error": "This game is for a mixed audience, so underAge must be sent as true or false",
                      "code": "age_answer_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key, the secret or the token was not accepted. `code` says which.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPublicKey": {
                    "summary": "No public key, or an unknown one",
                    "value": {
                      "error": "Invalid public key",
                      "code": "invalid_public_key",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of key",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keyRetired": {
                    "summary": "A retired key",
                    "value": {
                      "error": "This public key has been retired",
                      "code": "key_retired",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keySuspended": {
                    "summary": "A suspended key",
                    "value": {
                      "error": "This public key has been suspended",
                      "code": "key_suspended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "proofRejectedExpired": {
                    "summary": "A proof that was not accepted",
                    "value": {
                      "error": "The proof of the player was not accepted",
                      "code": "proof_rejected",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "expired"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request carries an `Origin` header that is not on the game's list (`origin_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "underAge": {
                    "summary": "The player is under age",
                    "value": {
                      "error": "This player cannot be connected",
                      "code": "under_age",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "proofRequired": {
                    "summary": "The game connects verified players only",
                    "value": {
                      "error": "This game only connects players it has proven",
                      "code": "proof_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "Connect is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "codeNotFound": {
                    "summary": "No open request has this code",
                    "value": {
                      "error": "No open request has this code",
                      "code": "code_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The game is paused or suspended (`game_paused`), or the `code` belongs to an open request that was locked by this failed try, its fifth (`session_locked`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "sessionLocked": {
                    "summary": "Locked after too many attempts",
                    "value": {
                      "error": "This request was locked after too many attempts. Start again",
                      "code": "session_locked",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests. `retryAfter` and the `Retry-After` header say how many seconds to wait.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests from one IP address",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  },
                  "tooManyAttempts": {
                    "summary": "Too many starts or codes",
                    "value": {
                      "error": "Too many attempts. Wait and try again",
                      "code": "too_many_attempts",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 212
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The proof could not be checked right now (`proof_service_unavailable`). Try again.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "proofServiceUnavailable": {
                    "summary": "The sign-in service could not be asked",
                    "value": {
                      "error": "The proof could not be checked right now. Try again",
                      "code": "proof_service_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/connect/result": {
      "post": {
        "tags": [
          "Connect"
        ],
        "summary": "Get the result",
        "description": "Reads the status of a connect session, with the session secret that Start connect returned.\n\n**Called with** the game's public key, sent as `Authorization: Bearer ceejay_pk_…`.\n\nPoll every 3 seconds while a session is open, and immediately when the player returns to the game. One read per session every 2 seconds is allowed, and reading is never limited per IP address. The response has one of four shapes, distinguished by `status`.\n\nWhen `status` is `needs_game_confirmation`, show the Discord name to the player and send Confirm connect. When it is `approved`, the response includes the connection, the member summary, a player session and a refresh token.\n\n**The result of an approved session can be read for 10 minutes**, and each read returns a new refresh token and invalidates the one returned before, so a lost response can be requested again and leaves one device. After that, and when what the session approved has changed (the member disconnected, moved or signed the device out), the response is `410` with the code `result_gone`: start a new session.\n\nNo call to Discord is made, and data is written only when the session is approved.",
        "operationId": "getConnectResult",
        "x-caller": "public_key",
        "x-since": "1.1.0",
        "security": [
          {
            "PublicKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SessionSecretRequest"
              },
              "examples": {
                "request": {
                  "summary": "A request",
                  "value": {
                    "sessionSecret": "ceejay_cs_4f8a2c1e9b7d3e41f8a6b0c1d25f0c2a9b7d3e41f8a6b0c1d24f8a2c1e9b7d3e"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Where the session stands.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectResultResponse"
                },
                "examples": {
                  "waiting": {
                    "summary": "Waiting for the member",
                    "value": {
                      "success": true,
                      "data": {
                        "status": "pending",
                        "expiresAt": "2026-10-08T18:14:11.000Z"
                      }
                    }
                  },
                  "needsGame": {
                    "summary": "Waiting for the game to confirm",
                    "value": {
                      "success": true,
                      "data": {
                        "status": "needs_game_confirmation",
                        "expiresAt": "2026-10-08T18:14:11.000Z",
                        "member": {
                          "displayName": "Nova",
                          "avatarUrl": "https://cdn.discordapp.com/avatars/409876543210987654/a1b2c3d4e5f60718293a4b5c6d7e8f90.png"
                        },
                        "alreadyConnected": false
                      }
                    }
                  },
                  "ended": {
                    "summary": "Ended without a connection",
                    "value": {
                      "success": true,
                      "data": {
                        "status": "declined",
                        "reason": "member_cancelled"
                      }
                    }
                  },
                  "approved": {
                    "summary": "Connected",
                    "value": {
                      "success": true,
                      "data": {
                        "status": "approved",
                        "connection": {
                          "connectionId": "8841",
                          "game": "rugby_manager",
                          "playerId": "p_19",
                          "proof": "verified",
                          "connectedAt": "2026-10-08T18:06:40.000Z",
                          "moved": false,
                          "alreadyConnected": false,
                          "spendHoldUntil": "2026-10-09T18:06:40.000Z"
                        },
                        "member": {
                          "displayName": "Nova",
                          "nickname": "Nova | EU",
                          "avatarUrl": "https://cdn.discordapp.com/avatars/409876543210987654/a1b2c3d4e5f60718293a4b5c6d7e8f90.png",
                          "level": 12,
                          "xp": 5420,
                          "coins": 1450,
                          "roles": [
                            {
                              "id": "982371004827100394",
                              "name": "Tournament winner"
                            }
                          ],
                          "membership": "member",
                          "joinUrl": null
                        },
                        "join": {
                          "asked": false,
                          "result": "not_asked",
                          "failure": null
                        },
                        "sharing": {
                          "agreed": [
                            "progress",
                            "activity"
                          ],
                          "needed": []
                        },
                        "reward": {
                          "status": "pending",
                          "items": []
                        },
                        "manageUrl": "https://ceejay.communitystudios.xyz/connect/mine",
                        "playerSession": {
                          "token": "cjps1.eyJ2IjoxLCJhdWQiOiJnYW1lIiwicyI6IjcyMTEwNDkzODI3MTAwMzg0MiIsImciOjEyfQ.pV3mQ0b6k1y8ZtL2c9XwR4s7Nf5aHj0uEoGdBiKq3M",
                          "expiresAt": "2026-10-08T18:21:44.000Z"
                        },
                        "refreshToken": "ceejay_rt_5512.1.4f8a2c1e9b7d3e41f8a6b0c1d25f0c2a9b7d3e41f8a6b0c1d24f8a2c1e9b7d3e",
                        "refreshExpiresAt": "2027-01-06T18:06:44.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was refused and nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notObject": {
                    "summary": "The body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key, the secret or the token was not accepted. `code` says which.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPublicKey": {
                    "summary": "No public key, or an unknown one",
                    "value": {
                      "error": "Invalid public key",
                      "code": "invalid_public_key",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of key",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keyRetired": {
                    "summary": "A retired key",
                    "value": {
                      "error": "This public key has been retired",
                      "code": "key_retired",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keySuspended": {
                    "summary": "A suspended key",
                    "value": {
                      "error": "This public key has been suspended",
                      "code": "key_suspended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "invalidSessionSecret": {
                    "summary": "No session of this game has this secret",
                    "value": {
                      "error": "Invalid session secret",
                      "code": "invalid_session_secret",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request carries an `Origin` header that is not on the game's list (`origin_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "Connect is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "A rule says no, and nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "410": {
            "description": "The result of the session can no longer be read (`result_gone`). Start again.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "resultGone": {
                    "summary": "The result can no longer be read",
                    "value": {
                      "error": "The result of this request can no longer be read. Start again",
                      "code": "result_gone",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 requests in a minute from one IP address across every request a game makes, or the result of this session was requested less than 2 seconds ago. Both carry the code `rate_limited` and `retryAfter`.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests from one IP address",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  },
                  "tooSoon": {
                    "summary": "Asked again within 2 seconds",
                    "value": {
                      "error": "Ask for the result of one session at most every 2 seconds",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 2
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/connect/confirm": {
      "post": {
        "tags": [
          "Connect"
        ],
        "summary": "Confirm connect",
        "description": "Approves or declines a connect session from the game.\n\n**Called with** the game's public key, sent as `Authorization: Bearer ceejay_pk_…`.\n\nA connection is saved only after the Discord member and the game have both confirmed. `accept: true` is accepted only while the session's status is `needs_game_confirmation`: the game has been shown the Discord name by then. For a session the game started it finishes the session and saves the connection. For one a member started in Discord it moves the session to `needs_confirmation`, and the member confirms there.\n\n`accept: false` ends an open session in any state, with the reason `game_declined`. It is also the game's own Cancel.\n\nSafe to repeat: confirming twice returns the same result as the first time, and declining a session that has ended changes nothing. The response is what Get the result would return next.",
        "operationId": "confirmConnect",
        "x-caller": "public_key",
        "x-since": "1.1.0",
        "security": [
          {
            "PublicKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConfirmConnectRequest"
              },
              "examples": {
                "request": {
                  "summary": "A request",
                  "value": {
                    "sessionSecret": "ceejay_cs_4f8a2c1e9b7d3e41f8a6b0c1d25f0c2a9b7d3e41f8a6b0c1d24f8a2c1e9b7d3e",
                    "accept": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The session after it was approved or declined.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectResultResponse"
                },
                "examples": {
                  "approved": {
                    "summary": "Connected",
                    "value": {
                      "success": true,
                      "data": {
                        "status": "approved",
                        "connection": {
                          "connectionId": "8841",
                          "game": "rugby_manager",
                          "playerId": "p_19",
                          "proof": "verified",
                          "connectedAt": "2026-10-08T18:06:40.000Z",
                          "moved": false,
                          "alreadyConnected": false,
                          "spendHoldUntil": "2026-10-09T18:06:40.000Z"
                        },
                        "member": {
                          "displayName": "Nova",
                          "nickname": "Nova | EU",
                          "avatarUrl": "https://cdn.discordapp.com/avatars/409876543210987654/a1b2c3d4e5f60718293a4b5c6d7e8f90.png",
                          "level": 12,
                          "xp": 5420,
                          "coins": 1450,
                          "roles": [
                            {
                              "id": "982371004827100394",
                              "name": "Tournament winner"
                            }
                          ],
                          "membership": "member",
                          "joinUrl": null
                        },
                        "join": {
                          "asked": false,
                          "result": "not_asked",
                          "failure": null
                        },
                        "sharing": {
                          "agreed": [
                            "progress",
                            "activity"
                          ],
                          "needed": []
                        },
                        "reward": {
                          "status": "pending",
                          "items": []
                        },
                        "manageUrl": "https://ceejay.communitystudios.xyz/connect/mine",
                        "playerSession": {
                          "token": "cjps1.eyJ2IjoxLCJhdWQiOiJnYW1lIiwicyI6IjcyMTEwNDkzODI3MTAwMzg0MiIsImciOjEyfQ.pV3mQ0b6k1y8ZtL2c9XwR4s7Nf5aHj0uEoGdBiKq3M",
                          "expiresAt": "2026-10-08T18:21:44.000Z"
                        },
                        "refreshToken": "ceejay_rt_5512.1.4f8a2c1e9b7d3e41f8a6b0c1d25f0c2a9b7d3e41f8a6b0c1d24f8a2c1e9b7d3e",
                        "refreshExpiresAt": "2027-01-06T18:06:44.000Z"
                      }
                    }
                  },
                  "cancelled": {
                    "summary": "The game said no",
                    "value": {
                      "success": true,
                      "data": {
                        "status": "cancelled",
                        "reason": "game_declined"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was refused and nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notObject": {
                    "summary": "The body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badAccept": {
                    "summary": "accept is not true or false",
                    "value": {
                      "error": "accept must be true or false",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "accept"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key, the secret or the token was not accepted. `code` says which.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPublicKey": {
                    "summary": "No public key, or an unknown one",
                    "value": {
                      "error": "Invalid public key",
                      "code": "invalid_public_key",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of key",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keyRetired": {
                    "summary": "A retired key",
                    "value": {
                      "error": "This public key has been retired",
                      "code": "key_retired",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keySuspended": {
                    "summary": "A suspended key",
                    "value": {
                      "error": "This public key has been suspended",
                      "code": "key_suspended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "invalidSessionSecret": {
                    "summary": "No session of this game has this secret",
                    "value": {
                      "error": "Invalid session secret",
                      "code": "invalid_session_secret",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request carries an `Origin` header that is not on the game's list (`origin_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "Connect is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "sessionNotFound": {
                    "summary": "The session is gone",
                    "value": {
                      "error": "Connect session not found",
                      "code": "session_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "A rule says no, and nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "nothingToConfirm": {
                    "summary": "Not waiting for the game",
                    "value": {
                      "error": "There is nothing to confirm on this request right now",
                      "code": "nothing_to_confirm",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "sessionChanged": {
                    "summary": "The session changed",
                    "value": {
                      "error": "This request changed since it was shown. Read it again",
                      "code": "session_changed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "playerBlocked": {
                    "summary": "The community blocked the player",
                    "value": {
                      "error": "This player cannot be connected until the community lifts a block",
                      "code": "player_blocked",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "playerTaken": {
                    "summary": "The player is connected to another Discord account",
                    "value": {
                      "error": "This player is connected to another Discord account. Connect from the game the player is signed in to",
                      "code": "player_taken",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "410": {
            "description": "The result of the session can no longer be read (`result_gone`). Start again.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "resultGone": {
                    "summary": "The result can no longer be read",
                    "value": {
                      "error": "The result of this request can no longer be read. Start again",
                      "code": "result_gone",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests. `retryAfter` and the `Retry-After` header say how many seconds to wait.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests from one IP address",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/connect/status": {
      "post": {
        "tags": [
          "Connect"
        ],
        "summary": "Get connection status",
        "description": "Reports whether a player is connected, for a game that can verify the player.\n\n**Called with** the game's public key, sent as `Authorization: Bearer ceejay_pk_…`.\n\nThe response is `connected` and nothing else. The proof must be verified and issued in the last 10 minutes, so nobody who holds only the shipped key can test which players are connected. A connect pass is not accepted: it works once. A game with no proof cannot make this request: its state is the refresh token it holds.\n\n**Limit:** 60 requests a minute from one IP address.",
        "operationId": "getConnectionStatus",
        "x-caller": "public_key",
        "x-since": "1.1.0",
        "security": [
          {
            "PublicKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConnectionStatusRequest"
              },
              "examples": {
                "request": {
                  "summary": "A request",
                  "value": {
                    "playerId": "p_19",
                    "proof": {
                      "type": "firebase",
                      "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.eyJzdWIiOiJwXzE5In0.c2lnbmF0dXJl"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Whether the player is connected.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectionStatusResponse"
                },
                "examples": {
                  "connected": {
                    "summary": "Connected",
                    "value": {
                      "success": true,
                      "data": {
                        "connected": true
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was refused and nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notObject": {
                    "summary": "The body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badPlayerId": {
                    "summary": "A player ID outside the alphabet",
                    "value": {
                      "error": "playerId must be 1 to 128 characters: letters, digits and . _ : @ | + = -, starting with a letter or a digit",
                      "code": "bad_player_id",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badProofType": {
                    "summary": "A proof that cannot be read",
                    "value": {
                      "error": "proof must be an object with type firebase, unity or steam and a token",
                      "code": "bad_proof",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "noProject": {
                    "summary": "No project saved for that sign-in service",
                    "value": {
                      "error": "proof must be an object with a type and a token",
                      "code": "bad_proof",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key, the secret or the token was not accepted. `code` says which.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPublicKey": {
                    "summary": "No public key, or an unknown one",
                    "value": {
                      "error": "Invalid public key",
                      "code": "invalid_public_key",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of key",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keyRetired": {
                    "summary": "A retired key",
                    "value": {
                      "error": "This public key has been retired",
                      "code": "key_retired",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keySuspended": {
                    "summary": "A suspended key",
                    "value": {
                      "error": "This public key has been suspended",
                      "code": "key_suspended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "proofRejectedTooOld": {
                    "summary": "A proof that was not accepted",
                    "value": {
                      "error": "The proof of the player was not accepted",
                      "code": "proof_rejected",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "too_old"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request carries an `Origin` header that is not on the game's list (`origin_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "proofRequiredToAsk": {
                    "summary": "An anonymous sign-in",
                    "value": {
                      "error": "Only a game that has proven the player can ask whether the player is connected",
                      "code": "proof_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "Connect is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "A rule says no, and nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 requests in a minute from one IP address across every request a game makes, or more than 60 of this request in a minute from one IP address. Both carry the code `rate_limited` and `retryAfter`.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests from one IP address",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The proof could not be checked right now (`proof_service_unavailable`). Try again.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "proofServiceUnavailable": {
                    "summary": "The sign-in service could not be asked",
                    "value": {
                      "error": "The proof could not be checked right now. Try again",
                      "code": "proof_service_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/player/session": {
      "post": {
        "tags": [
          "Player"
        ],
        "summary": "Renew a player session",
        "description": "Exchanges the device's refresh token for a new player session and refresh token.\n\n**Called with** the game's public key, sent as `Authorization: Bearer ceejay_pk_…`.\n\n**The refresh token is replaced on each use.** Store the new one before you use the new player session, and renew one call at a time. When a response was lost, send the token you still have: it is still accepted until the device has used a player session issued with the newer one. A token that was replaced and then used again returns `401` with the code `refresh_reused`, and the device is signed out.\n\nWhere the member is not in the server the response is still `200`, with `playerSession` as `null`, `membership` and `joinUrl`, so the game can say so. No call to Discord is made.\n\n**Limit:** 10 renewals a minute for one device.",
        "operationId": "renewPlayerSession",
        "x-caller": "public_key",
        "x-since": "1.1.0",
        "security": [
          {
            "PublicKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefreshTokenRequest"
              },
              "examples": {
                "request": {
                  "summary": "A request",
                  "value": {
                    "refreshToken": "ceejay_rt_5512.1.4f8a2c1e9b7d3e41f8a6b0c1d25f0c2a9b7d3e41f8a6b0c1d24f8a2c1e9b7d3e"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A new player session and the next refresh token.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerSessionResponse"
                },
                "examples": {
                  "renewed": {
                    "summary": "A member of the server",
                    "value": {
                      "success": true,
                      "data": {
                        "playerSession": {
                          "token": "cjps1.eyJ2IjoxLCJhdWQiOiJnYW1lIiwicyI6IjcyMTEwNDkzODI3MTAwMzg0MiIsImciOjEyfQ.pV3mQ0b6k1y8ZtL2c9XwR4s7Nf5aHj0uEoGdBiKq3M",
                          "expiresAt": "2026-10-09T09:15:02.000Z"
                        },
                        "refreshToken": "ceejay_rt_5512.2.e3d7b9e1c2a8f42d1c0b6a8f14e3d7b9a2c0f52d1c0b6a8f14e3d7b9e1c2a8f4",
                        "refreshExpiresAt": "2027-01-07T09:00:02.000Z",
                        "membership": "member",
                        "joinUrl": null,
                        "sharing": {
                          "agreed": [
                            "progress",
                            "activity"
                          ],
                          "needed": []
                        },
                        "manageUrl": "https://ceejay.communitystudios.xyz/connect/mine"
                      }
                    }
                  },
                  "notMember": {
                    "summary": "A person who left the server",
                    "value": {
                      "success": true,
                      "data": {
                        "playerSession": null,
                        "refreshToken": "ceejay_rt_5512.2.e3d7b9e1c2a8f42d1c0b6a8f14e3d7b9a2c0f52d1c0b6a8f14e3d7b9e1c2a8f4",
                        "refreshExpiresAt": "2027-01-07T09:00:02.000Z",
                        "membership": "left",
                        "joinUrl": "https://discord.gg/harbourtown",
                        "sharing": {
                          "agreed": [
                            "progress",
                            "activity"
                          ],
                          "needed": []
                        },
                        "manageUrl": "https://ceejay.communitystudios.xyz/connect/mine"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected and nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notObject": {
                    "summary": "The body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key, the secret or the token was not accepted. `code` says which.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPublicKey": {
                    "summary": "No public key, or an unknown one",
                    "value": {
                      "error": "Invalid public key",
                      "code": "invalid_public_key",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of key",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keyRetired": {
                    "summary": "A retired key",
                    "value": {
                      "error": "This public key has been retired",
                      "code": "key_retired",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keySuspended": {
                    "summary": "A suspended key",
                    "value": {
                      "error": "This public key has been suspended",
                      "code": "key_suspended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "invalidRefreshToken": {
                    "summary": "Not a refresh token of this game",
                    "value": {
                      "error": "Invalid refresh token",
                      "code": "invalid_refresh_token",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "refreshReused": {
                    "summary": "A copy of the token was used",
                    "value": {
                      "error": "This refresh token was used before, so the device has been signed out. Connect again",
                      "code": "refresh_reused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "connectionEndedWhy": {
                    "summary": "The connection no longer serves this device",
                    "value": {
                      "error": "This connection has ended",
                      "code": "connection_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "disconnected"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request includes an `Origin` header that is not on the game's allowed list (`origin_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "Connect is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "A rule says no, and nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 requests in a minute from one IP address across every request a game makes, or more than 10 renewals in a minute for one device. Both carry the code `rate_limited` and `retryAfter`.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests from one IP address",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/player/me": {
      "get": {
        "tags": [
          "Player"
        ],
        "summary": "Get the player's profile",
        "description": "Reads the connection and the member card of the player a player session is for.\n\n**Called with** a player session, sent as `Authorization: Bearer cjps1.…`.\n\nThe card is read from data CEEJAY stores, never from Discord. The connection and the device are checked on every request, so a disconnect, a move, a ban and \"sign this device out\" take effect immediately: the response is then `401` with the code `connection_ended`.\n\n**Limit:** 140 requests a minute for one player session. Every request made with a player session, the requests of the game loop included, shares it.",
        "operationId": "getPlayerProfile",
        "x-caller": "player_session",
        "x-since": "1.1.0",
        "security": [
          {
            "PlayerSession": []
          }
        ],
        "responses": {
          "200": {
            "description": "The profile.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerProfileResponse"
                },
                "examples": {
                  "profile": {
                    "summary": "The profile",
                    "value": {
                      "success": true,
                      "data": {
                        "connection": {
                          "connectionId": "8841",
                          "game": "rugby_manager",
                          "playerId": "p_19",
                          "proof": "verified",
                          "connectedAt": "2026-10-08T18:06:40.000Z",
                          "moved": false,
                          "alreadyConnected": false,
                          "spendHoldUntil": "2026-10-09T18:06:40.000Z"
                        },
                        "member": {
                          "displayName": "Nova",
                          "nickname": "Nova | EU",
                          "avatarUrl": "https://cdn.discordapp.com/avatars/409876543210987654/a1b2c3d4e5f60718293a4b5c6d7e8f90.png",
                          "level": 12,
                          "xp": 5420,
                          "coins": 1450,
                          "roles": [
                            {
                              "id": "982371004827100394",
                              "name": "Tournament winner"
                            }
                          ],
                          "membership": "member",
                          "joinUrl": null
                        },
                        "sharing": {
                          "agreed": [
                            "progress",
                            "activity",
                            "hub"
                          ],
                          "needed": []
                        },
                        "reward": {
                          "status": "pending",
                          "items": []
                        },
                        "manageUrl": "https://ceejay.communitystudios.xyz/connect/mine",
                        "hub": {
                          "enabled": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key, the secret or the token was not accepted. `code` says which.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPlayerSession": {
                    "summary": "No player session, or one that has expired",
                    "value": {
                      "error": "Invalid or expired player session",
                      "code": "invalid_player_session",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of token",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "connectionEnded": {
                    "summary": "The connection or the device was ended",
                    "value": {
                      "error": "This connection has ended",
                      "code": "connection_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request includes an `Origin` header that is not on the game's allowed list (`origin_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "Connect is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "A rule says no, and nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 requests in a minute from one IP address across every request a game makes, or more than 140 in a minute with one player session. Both carry the code `rate_limited` and `retryAfter`.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests from one IP address",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/player/disconnect": {
      "post": {
        "tags": [
          "Player"
        ],
        "summary": "Disconnect",
        "description": "Ends the player's connection, with the refresh token stored on the device.\n\n**Called with** the game's public key, sent as `Authorization: Bearer ceejay_pk_…`.\n\nIt works in every membership state: a person who never joined the server, or who left, has a refresh token and no player session, and can still leave. Safe to repeat: a token whose connection has already ended returns `200`.\n\nEvery device of the connection is signed out.",
        "operationId": "disconnectPlayer",
        "x-caller": "public_key",
        "x-since": "1.1.0",
        "security": [
          {
            "PublicKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefreshTokenRequest"
              },
              "examples": {
                "request": {
                  "summary": "A request",
                  "value": {
                    "refreshToken": "ceejay_rt_5512.1.4f8a2c1e9b7d3e41f8a6b0c1d25f0c2a9b7d3e41f8a6b0c1d24f8a2c1e9b7d3e"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The connection is ended.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DisconnectResponse"
                },
                "examples": {
                  "disconnected": {
                    "summary": "Disconnected",
                    "value": {
                      "success": true,
                      "data": {
                        "disconnected": true
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected and nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notObject": {
                    "summary": "The body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key, the secret or the token was not accepted. `code` says which.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPublicKey": {
                    "summary": "No public key, or an unknown one",
                    "value": {
                      "error": "Invalid public key",
                      "code": "invalid_public_key",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of key",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keyRetired": {
                    "summary": "A retired key",
                    "value": {
                      "error": "This public key has been retired",
                      "code": "key_retired",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keySuspended": {
                    "summary": "A suspended key",
                    "value": {
                      "error": "This public key has been suspended",
                      "code": "key_suspended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "invalidRefreshToken": {
                    "summary": "Not a refresh token of this game",
                    "value": {
                      "error": "Invalid refresh token",
                      "code": "invalid_refresh_token",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request includes an `Origin` header that is not on the game's allowed list (`origin_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "Connect is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "A rule says no, and nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests. `retryAfter` and the `Retry-After` header indicate how many seconds to wait.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests from one IP address",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/player/quests": {
      "get": {
        "tags": [
          "Game loop"
        ],
        "summary": "List the quests",
        "description": "Lists the community's quests, each with how far the connected player has come. They are the quests the member sees in Discord, shown in the game.\n\n**Called with** a player session, sent as `Authorization: Bearer cjps1.…`.\n\n**What it needs.** The game loop must be enabled for the game (`hub_not_enabled` otherwise). The member has to be in the server (`membership_required`; a player whose member left or never joined is sent to the game's `joinUrl`), and must have consented to share their quests, rewards, shop and rank with the game (`sharing_needed`, with `manageUrl`, where the member can consent). Where the game loop is not enabled on this instance, every one of these requests returns `404` with the code `not_available`.\n\nEvery response is private to the player and includes `Cache-Control: no-store`. The responses contain only the fields listed here: no Discord ID, no avatar and nothing about another member, except the display names in the top list of Get the ranks.\n\nA quest's `status` is `available` (not started), `in_progress`, `pending_review` or `completed`. Each task says what the game can do with it in `action`: `verify` (a button that calls Verify a quest task), `answer` or `rate` (a field that calls Answer a quest task), `discord` (the task must be completed in Discord: show that and offer nothing else) or `null` (it completes automatically from what the member does in the server). A task that the community's team reviews is always `discord`: a game never sends anything to review, and no direct message is ever sent for anything a game starts.\n\nA quest that requires a role or a level is evaluated without calling Discord. The level is always known. The role is read from CEEJAY's cache of the server's members, so for a member it has not seen `locked` is `null` and `lockCheck` is `checked_on_start`: show the quest as startable and let Start a quest decide.\n\nThe list is paged: send `page.nextCursor` back as `cursor` and stop when it is `null`. A quest added or removed between two pages can make a page repeat or skip one.\n\n**Limit:** 120 reads a minute for one player session.",
        "operationId": "listPlayerQuests",
        "x-caller": "player_session",
        "x-since": "1.1.0",
        "security": [
          {
            "PlayerSession": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PlayerLoopLimit"
          },
          {
            "$ref": "#/components/parameters/PlayerLoopCursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of quests.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListPlayerQuestsResponse"
                },
                "examples": {
                  "quests": {
                    "summary": "One quest, with a task of each kind",
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": 12,
                          "title": "First steps",
                          "iconEmoji": "🏆",
                          "description": "Get started in the community.",
                          "category": "Onboarding",
                          "xpReward": 100,
                          "currencyReward": 50,
                          "badge": "Newcomer",
                          "endsAt": "2026-11-01T00:00:00.000Z",
                          "status": "in_progress",
                          "progress": {
                            "current": 1,
                            "target": 5
                          },
                          "locked": false,
                          "lockedReason": null,
                          "lockCheck": "known",
                          "taskCount": 5,
                          "tasksDone": 1,
                          "tasks": [
                            {
                              "id": 41,
                              "position": 0,
                              "type": "send_messages",
                              "title": "Say hello",
                              "description": null,
                              "verification": "automatic",
                              "status": "in_progress",
                              "progress": {
                                "current": 4,
                                "target": 10
                              },
                              "url": null,
                              "action": null,
                              "answer": null,
                              "statusLine": "4/10 messages"
                            },
                            {
                              "id": 42,
                              "position": 1,
                              "type": "text_answer",
                              "title": "Tell us your favourite",
                              "description": "One sentence is enough.",
                              "verification": "automatic",
                              "status": "available",
                              "progress": null,
                              "url": null,
                              "action": "answer",
                              "answer": {
                                "kind": "text",
                                "question": "What is your favourite game?",
                                "minLength": 10
                              },
                              "statusLine": null
                            },
                            {
                              "id": 43,
                              "position": 2,
                              "type": "number_answer",
                              "title": "Hours a week",
                              "description": null,
                              "verification": "automatic",
                              "status": "available",
                              "progress": null,
                              "url": null,
                              "action": "answer",
                              "answer": {
                                "kind": "number",
                                "question": "How many hours a week do you play?",
                                "min": 0,
                                "max": 168
                              },
                              "statusLine": null
                            },
                            {
                              "id": 44,
                              "position": 3,
                              "type": "rating",
                              "title": "Rate the last event",
                              "description": null,
                              "verification": "automatic",
                              "status": "available",
                              "progress": null,
                              "url": null,
                              "action": "rate",
                              "answer": {
                                "kind": "scale",
                                "question": "How was the last event?",
                                "scale": 5
                              },
                              "statusLine": null
                            },
                            {
                              "id": 45,
                              "position": 4,
                              "type": "social_follow",
                              "title": "Follow the studio",
                              "description": null,
                              "verification": "automatic",
                              "status": "available",
                              "progress": null,
                              "url": "https://social.example/rugby",
                              "action": "verify",
                              "answer": null,
                              "statusLine": null
                            }
                          ]
                        }
                      ],
                      "page": {
                        "nextCursor": null,
                        "limit": 50
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`limit` or `cursor` is not one this request takes. Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "badLimit": {
                    "summary": "A page size that is not a count, or above 100",
                    "value": {
                      "error": "limit must be a whole number from 1 to 100",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "limit"
                    }
                  },
                  "badCursor": {
                    "summary": "A cursor that was not issued by the list",
                    "value": {
                      "error": "cursor is not one this list gave. Send page.nextCursor as it was answered",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "cursor"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The player session was not accepted: `invalid_player_session` (missing, or past its 15 minutes: renew it with Renew a player session), `wrong_key_kind` (another kind of token) or `connection_ended` (the connection or the device was ended: connect again).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPlayerSession": {
                    "summary": "No player session, or one that has expired",
                    "value": {
                      "error": "Invalid or expired player session",
                      "code": "invalid_player_session",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of token",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "connectionEnded": {
                    "summary": "The connection or the device was ended",
                    "value": {
                      "error": "This connection has ended",
                      "code": "connection_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request includes an `Origin` header that is not on the game's allowed list (`origin_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "This instance does not offer Connect or the game loop yet (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "The game loop is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The game is paused (`game_paused`), does not show the community's quests, shop and rank (`hub_not_enabled`), the member has not consented to share them with the game (`sharing_needed`, with `manageUrl`), or the member is not in the server (`membership_required`, with `membership` and, for a member who left or never joined, `joinUrl`). Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "hubNotEnabled": {
                    "summary": "The game does not show the quests, shop and rank",
                    "value": {
                      "error": "This game does not show the community's quests, shop and rank",
                      "code": "hub_not_enabled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "sharingNeeded": {
                    "summary": "The member must consent first",
                    "value": {
                      "error": "The member has not consented to sharing their data with this game",
                      "code": "sharing_needed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "manageUrl": "https://ceejay.communitystudios.xyz/connect/mine"
                    }
                  },
                  "notInServer": {
                    "summary": "The member is not in the server",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "left",
                      "joinUrl": "https://discord.gg/harbourrugby"
                    }
                  },
                  "screening": {
                    "summary": "The member has not passed Discord's membership screening",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "pending",
                      "joinUrl": null
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 requests a minute from one IP address across every request a game makes, more than 140 a minute with one player session, more than 120 reads or 20 writes a minute with one player session, or more calls than the game as a whole is allowed. All of them carry the code `rate_limited`, `retryAfter` and the `Retry-After` header.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "CEEJAY's bot could not be reached, or was too busy, and nothing was done (`bot_unavailable`). Send the request again after `retryAfter` seconds.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botUnavailable": {
                    "summary": "The bot could not be reached",
                    "value": {
                      "error": "This could not be done right now and nothing was changed. Try again in a few seconds",
                      "code": "bot_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 5
                    }
                  }
                }
              }
            }
          },
          "504": {
            "description": "CEEJAY's bot did not respond in time (`bot_timeout`). Send the request again.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botTimeout": {
                    "summary": "The bot did not respond in time",
                    "value": {
                      "error": "The answer did not arrive in time, and the request may or may not have been done. Send the same request again: it is safe to repeat",
                      "code": "bot_timeout",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/player/quests/start": {
      "post": {
        "tags": [
          "Game loop"
        ],
        "summary": "Start a quest",
        "description": "Starts a quest for the connected player. A quest the player already has open returns `alreadyStarted: true` and changes nothing, which is also what a retry after a lost response returns. It is safe to repeat and takes no key.\n\n**Called with** a player session, sent as `Authorization: Bearer cjps1.…`.\n\n**What it needs.** The game loop must be enabled for the game (`hub_not_enabled` otherwise). The member has to be in the server (`membership_required`; a player whose member left or never joined is sent to the game's `joinUrl`), and must have consented to share their quests, rewards, shop and rank with the game (`sharing_needed`, with `manageUrl`, where the member can consent). Where the game loop is not enabled on this instance, every one of these requests returns `404` with the code `not_available`.\n\nEvery response is private to the player and includes `Cache-Control: no-store`. The responses contain only the fields listed here: no Discord ID, no avatar and nothing about another member, except the display names in the top list of Get the ranks.\n\nA quest the player cannot start is rejected with `409` and the code of the rule that stops them. For a quest that needs a role CEEJAY looks the member up in Discord once. Nothing is sent to the member in a direct message.\n\n**Limit:** 20 writes a minute for one player session.",
        "operationId": "startPlayerQuest",
        "x-caller": "player_session",
        "x-since": "1.1.0",
        "security": [
          {
            "PlayerSession": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StartPlayerQuestRequest"
              },
              "examples": {
                "request": {
                  "summary": "Start a quest",
                  "value": {
                    "questId": 12
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The quest is open for the player.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StartPlayerQuestResponse"
                },
                "examples": {
                  "started": {
                    "summary": "The quest was started",
                    "value": {
                      "success": true,
                      "data": {
                        "questId": 12,
                        "status": "in_progress",
                        "alreadyStarted": false
                      }
                    }
                  },
                  "alreadyStarted": {
                    "summary": "The player had started it already",
                    "value": {
                      "success": true,
                      "data": {
                        "questId": 12,
                        "status": "in_progress",
                        "alreadyStarted": true
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`questId` is missing or is not a whole number, or the body is not a JSON object. Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notObject": {
                    "summary": "The body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badQuestId": {
                    "summary": "A quest ID that is not a whole number",
                    "value": {
                      "error": "questId must be a whole number from 1 to 2147483647",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "questId"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The player session was not accepted: `invalid_player_session` (missing, or past its 15 minutes: renew it with Renew a player session), `wrong_key_kind` (another kind of token) or `connection_ended` (the connection or the device was ended: connect again).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPlayerSession": {
                    "summary": "No player session, or one that has expired",
                    "value": {
                      "error": "Invalid or expired player session",
                      "code": "invalid_player_session",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of token",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "connectionEnded": {
                    "summary": "The connection or the device was ended",
                    "value": {
                      "error": "This connection has ended",
                      "code": "connection_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request includes an `Origin` header that is not on the game's allowed list (`origin_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "This instance does not offer Connect or the game loop yet (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`). Or the quest does not exist in this community (`quest_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "The game loop is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "questNotFound": {
                    "summary": "The quest does not exist",
                    "value": {
                      "error": "Quest not found",
                      "code": "quest_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The game is paused (`game_paused`), does not show the community's quests, shop and rank (`hub_not_enabled`), the member has not consented to share them with the game (`sharing_needed`, with `manageUrl`), or the member is not in the server (`membership_required`, with `membership` and, for a member who left or never joined, `joinUrl`). Or a rule of the quest is not met (`quest_not_active`, `quest_not_started`, `quest_ended`, `quest_already_completed`, `quest_limit_reached`, `quest_requires_role`, `quest_requires_level`, `quest_requires_season_xp`, `quest_requires_program`, `quest_requires_quest`, `quest_full`, `quest_cooling_down` with `availableAt`, or `quest_daily_limit_reached`; `message` is a sentence the game can show). Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "hubNotEnabled": {
                    "summary": "The game does not show the quests, shop and rank",
                    "value": {
                      "error": "This game does not show the community's quests, shop and rank",
                      "code": "hub_not_enabled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "sharingNeeded": {
                    "summary": "The member must consent first",
                    "value": {
                      "error": "The member has not consented to sharing their data with this game",
                      "code": "sharing_needed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "manageUrl": "https://ceejay.communitystudios.xyz/connect/mine"
                    }
                  },
                  "notInServer": {
                    "summary": "The member is not in the server",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "left",
                      "joinUrl": "https://discord.gg/harbourrugby"
                    }
                  },
                  "screening": {
                    "summary": "The member has not passed Discord's membership screening",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "pending",
                      "joinUrl": null
                    }
                  },
                  "questNotActive": {
                    "summary": "The quest is not open",
                    "value": {
                      "error": "Quest is not active",
                      "code": "quest_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "This quest is not active right now"
                    }
                  },
                  "questNotStarted": {
                    "summary": "The quest has not started",
                    "value": {
                      "error": "Quest has not started yet",
                      "code": "quest_not_started",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "This quest has not started yet"
                    }
                  },
                  "questEnded": {
                    "summary": "The quest has ended",
                    "value": {
                      "error": "Quest has ended",
                      "code": "quest_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "This quest has ended"
                    }
                  },
                  "questAlreadyCompleted": {
                    "summary": "The player completed it already",
                    "value": {
                      "error": "Quest already completed and is not repeatable",
                      "code": "quest_already_completed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "You have completed this quest already"
                    }
                  },
                  "questLimitReached": {
                    "summary": "The quest allows no more completions",
                    "value": {
                      "error": "Max completions per user reached",
                      "code": "quest_limit_reached",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "You have completed this quest as many times as it allows"
                    }
                  },
                  "questRequiresRole": {
                    "summary": "The quest needs a role",
                    "value": {
                      "error": "The member does not have the role this quest requires",
                      "code": "quest_requires_role",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "Requires the @Gold club role"
                    }
                  },
                  "questRequiresLevel": {
                    "summary": "The quest needs a level",
                    "value": {
                      "error": "The member has not reached the level this quest requires",
                      "code": "quest_requires_level",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "Requires level 10"
                    }
                  },
                  "questRequiresSeasonXp": {
                    "summary": "The quest needs season XP",
                    "value": {
                      "error": "The member has not earned the season XP this quest requires",
                      "code": "quest_requires_season_xp",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "Requires 500 season XP"
                    }
                  },
                  "questRequiresProgram": {
                    "summary": "The quest needs a program",
                    "value": {
                      "error": "The member is not in the program this quest belongs to",
                      "code": "quest_requires_program",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "Join the program this quest belongs to first"
                    }
                  },
                  "questRequiresQuest": {
                    "summary": "The quest needs another quest first",
                    "value": {
                      "error": "The member has not completed the quest that comes before this one",
                      "code": "quest_requires_quest",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "Complete First steps first"
                    }
                  },
                  "questFull": {
                    "summary": "The quest has been fully claimed",
                    "value": {
                      "error": "Quest has reached its completion limit",
                      "code": "quest_full",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "This quest has been fully claimed"
                    }
                  },
                  "questCoolingDown": {
                    "summary": "The player must wait",
                    "value": {
                      "error": "The member has to wait before completing this quest again",
                      "code": "quest_cooling_down",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "availableAt": "2026-10-09T10:00:00.000Z",
                      "message": "Come back tomorrow to do this quest again"
                    }
                  },
                  "questDailyLimit": {
                    "summary": "The community's daily quest limit is reached",
                    "value": {
                      "error": "The member has reached this community's daily quest limit",
                      "code": "quest_daily_limit_reached",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "You have reached today's quest limit"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 16 KB.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "tooLarge": {
                    "summary": "A body over 16 KB",
                    "value": {
                      "error": "The request body is larger than 16 KB",
                      "code": "body_too_large",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 requests a minute from one IP address across every request a game makes, more than 140 a minute with one player session, more than 120 reads or 20 writes a minute with one player session, or more calls than the game as a whole is allowed. All of them carry the code `rate_limited`, `retryAfter` and the `Retry-After` header.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "CEEJAY's bot could not be reached, or was too busy, and nothing was done (`bot_unavailable`). Send the request again after `retryAfter` seconds.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botUnavailable": {
                    "summary": "The bot could not be reached",
                    "value": {
                      "error": "This could not be done right now and nothing was changed. Try again in a few seconds",
                      "code": "bot_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 5
                    }
                  }
                }
              }
            }
          },
          "504": {
            "description": "CEEJAY's bot did not respond in time, so it is not known whether the request was done (`bot_timeout`). Send the same request again: it is safe to repeat.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botTimeout": {
                    "summary": "The bot did not respond in time",
                    "value": {
                      "error": "The answer did not arrive in time, and the request may or may not have been done. Send the same request again: it is safe to repeat",
                      "code": "bot_timeout",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/player/quests/verify": {
      "post": {
        "tags": [
          "Game loop"
        ],
        "summary": "Verify a quest task",
        "description": "Checks a task whose `action` is `verify`, and completes it when the check passes, as the Verify button does in Discord. The check reads a state: a role the member has, a badge, a balance, a follow on a platform.\n\n**Called with** a player session, sent as `Authorization: Bearer cjps1.…`.\n\n**What it needs.** The game loop must be enabled for the game (`hub_not_enabled` otherwise). The member has to be in the server (`membership_required`; a player whose member left or never joined is sent to the game's `joinUrl`), and must have consented to share their quests, rewards, shop and rank with the game (`sharing_needed`, with `manageUrl`, where the member can consent). Where the game loop is not enabled on this instance, every one of these requests returns `404` with the code `not_available`.\n\nEvery response is private to the player and includes `Cache-Control: no-store`. The responses contain only the fields listed here: no Discord ID, no avatar and nothing about another member, except the display names in the top list of Get the ranks.\n\nA quest the player has not started is started first, and the rules of Start a quest apply. When the last task is completed, the quest completes and grants its rewards: `questCompleted` is `true` and `rewards` lists what was granted. Nothing is sent to the member in a direct message.\n\nA task that is not done yet returns `409` with the code `quest_task_not_done`, and `message` says why in a sentence the game can show. A task that cannot be verified this way returns `409` with the code `quest_task_unavailable` and a `reason`: `already_done`, `in_review`, `not_for_games` (its `action` is `discord`), `wrong_action` (it requires an answer, not a check) or `needs_connect` (the member must first link an account on `platform` in Discord; a game never receives a link for this). A task that is done returns `already_done` when it is asked again, so the request is safe to repeat and takes no key.\n\n**Limit:** 20 writes a minute for one player session.",
        "operationId": "verifyPlayerQuestTask",
        "x-caller": "player_session",
        "x-since": "1.1.0",
        "security": [
          {
            "PlayerSession": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerifyPlayerQuestTaskRequest"
              },
              "examples": {
                "request": {
                  "summary": "Verify a task",
                  "value": {
                    "questId": 12,
                    "taskId": 45
                  }
                },
                "legacy": {
                  "summary": "A quest made before tasks existed",
                  "value": {
                    "questId": 3
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The task is done.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerQuestTaskResponse"
                },
                "examples": {
                  "completed": {
                    "summary": "The last task: the quest is complete",
                    "value": {
                      "success": true,
                      "data": {
                        "questId": 12,
                        "taskId": 45,
                        "verified": true,
                        "questCompleted": true,
                        "tasksDone": 5,
                        "taskCount": 5,
                        "rewards": {
                          "xp": 100,
                          "coins": 50,
                          "badge": "Newcomer",
                          "roleGranted": false,
                          "leveledUp": true,
                          "newLevel": 12
                        }
                      }
                    }
                  },
                  "tasksRemain": {
                    "summary": "A task done, tasks remain",
                    "value": {
                      "success": true,
                      "data": {
                        "questId": 12,
                        "taskId": 44,
                        "verified": true,
                        "questCompleted": false,
                        "tasksDone": 4,
                        "taskCount": 5,
                        "rewards": null
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`questId` or `taskId` is not a whole number, or the body is not a JSON object. Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notObject": {
                    "summary": "The body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badQuestId": {
                    "summary": "A quest ID that is not a whole number",
                    "value": {
                      "error": "questId must be a whole number from 1 to 2147483647",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "questId"
                    }
                  },
                  "badTaskId": {
                    "summary": "A task ID that is not a whole number",
                    "value": {
                      "error": "taskId must be a whole number from 1 to 2147483647",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "taskId"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The player session was not accepted: `invalid_player_session` (missing, or past its 15 minutes: renew it with Renew a player session), `wrong_key_kind` (another kind of token) or `connection_ended` (the connection or the device was ended: connect again).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPlayerSession": {
                    "summary": "No player session, or one that has expired",
                    "value": {
                      "error": "Invalid or expired player session",
                      "code": "invalid_player_session",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of token",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "connectionEnded": {
                    "summary": "The connection or the device was ended",
                    "value": {
                      "error": "This connection has ended",
                      "code": "connection_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request includes an `Origin` header that is not on the game's allowed list (`origin_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "This instance does not offer Connect or the game loop yet (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`). Or the quest does not exist in this community, or the task does not belong to it (`quest_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "The game loop is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "questNotFound": {
                    "summary": "The quest or the task does not exist",
                    "value": {
                      "error": "Quest not found",
                      "code": "quest_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The game is paused (`game_paused`), does not show the community's quests, shop and rank (`hub_not_enabled`), the member has not consented to share them with the game (`sharing_needed`, with `manageUrl`), or the member is not in the server (`membership_required`, with `membership` and, for a member who left or never joined, `joinUrl`). Or a rule of the quest is not met (`quest_not_active`, `quest_not_started`, `quest_ended`, `quest_already_completed`, `quest_limit_reached`, `quest_requires_role`, `quest_requires_level`, `quest_requires_season_xp`, `quest_requires_program`, `quest_requires_quest`, `quest_full`, `quest_cooling_down` with `availableAt`, or `quest_daily_limit_reached`; `message` is a sentence the game can show), or the task is not done (`quest_task_not_done`), or it cannot be verified this way (`quest_task_unavailable`). Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "hubNotEnabled": {
                    "summary": "The game does not show the quests, shop and rank",
                    "value": {
                      "error": "This game does not show the community's quests, shop and rank",
                      "code": "hub_not_enabled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "sharingNeeded": {
                    "summary": "The member must consent first",
                    "value": {
                      "error": "The member has not consented to sharing their data with this game",
                      "code": "sharing_needed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "manageUrl": "https://ceejay.communitystudios.xyz/connect/mine"
                    }
                  },
                  "notInServer": {
                    "summary": "The member is not in the server",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "left",
                      "joinUrl": "https://discord.gg/harbourrugby"
                    }
                  },
                  "screening": {
                    "summary": "The member has not passed Discord's membership screening",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "pending",
                      "joinUrl": null
                    }
                  },
                  "questNotActive": {
                    "summary": "The quest is not open",
                    "value": {
                      "error": "Quest is not active",
                      "code": "quest_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "This quest is not active right now"
                    }
                  },
                  "questNotStarted": {
                    "summary": "The quest has not started",
                    "value": {
                      "error": "Quest has not started yet",
                      "code": "quest_not_started",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "This quest has not started yet"
                    }
                  },
                  "questEnded": {
                    "summary": "The quest has ended",
                    "value": {
                      "error": "Quest has ended",
                      "code": "quest_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "This quest has ended"
                    }
                  },
                  "questAlreadyCompleted": {
                    "summary": "The player completed it already",
                    "value": {
                      "error": "Quest already completed and is not repeatable",
                      "code": "quest_already_completed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "You have completed this quest already"
                    }
                  },
                  "questLimitReached": {
                    "summary": "The quest allows no more completions",
                    "value": {
                      "error": "Max completions per user reached",
                      "code": "quest_limit_reached",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "You have completed this quest as many times as it allows"
                    }
                  },
                  "questRequiresRole": {
                    "summary": "The quest needs a role",
                    "value": {
                      "error": "The member does not have the role this quest requires",
                      "code": "quest_requires_role",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "Requires the @Gold club role"
                    }
                  },
                  "questRequiresLevel": {
                    "summary": "The quest needs a level",
                    "value": {
                      "error": "The member has not reached the level this quest requires",
                      "code": "quest_requires_level",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "Requires level 10"
                    }
                  },
                  "questRequiresSeasonXp": {
                    "summary": "The quest needs season XP",
                    "value": {
                      "error": "The member has not earned the season XP this quest requires",
                      "code": "quest_requires_season_xp",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "Requires 500 season XP"
                    }
                  },
                  "questRequiresProgram": {
                    "summary": "The quest needs a program",
                    "value": {
                      "error": "The member is not in the program this quest belongs to",
                      "code": "quest_requires_program",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "Join the program this quest belongs to first"
                    }
                  },
                  "questRequiresQuest": {
                    "summary": "The quest needs another quest first",
                    "value": {
                      "error": "The member has not completed the quest that comes before this one",
                      "code": "quest_requires_quest",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "Complete First steps first"
                    }
                  },
                  "questFull": {
                    "summary": "The quest has been fully claimed",
                    "value": {
                      "error": "Quest has reached its completion limit",
                      "code": "quest_full",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "This quest has been fully claimed"
                    }
                  },
                  "questCoolingDown": {
                    "summary": "The player must wait",
                    "value": {
                      "error": "The member has to wait before completing this quest again",
                      "code": "quest_cooling_down",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "availableAt": "2026-10-09T10:00:00.000Z",
                      "message": "Come back tomorrow to do this quest again"
                    }
                  },
                  "questDailyLimit": {
                    "summary": "The community's daily quest limit is reached",
                    "value": {
                      "error": "The member has reached this community's daily quest limit",
                      "code": "quest_daily_limit_reached",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "You have reached today's quest limit"
                    }
                  },
                  "taskNotDone": {
                    "summary": "The task is not done yet",
                    "value": {
                      "error": "The task is not done yet",
                      "code": "quest_task_not_done",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "Not verified yet. Finish the task, then verify again"
                    }
                  },
                  "taskAlreadyDone": {
                    "summary": "The task is done already",
                    "value": {
                      "error": "This task cannot be done this way right now",
                      "code": "quest_task_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "already_done",
                      "message": "You have done this task already"
                    }
                  },
                  "taskInReview": {
                    "summary": "The task is with the team",
                    "value": {
                      "error": "This task cannot be done this way right now",
                      "code": "quest_task_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "in_review",
                      "message": "This task is waiting for a review"
                    }
                  },
                  "taskNotForGames": {
                    "summary": "The task has to be finished in Discord",
                    "value": {
                      "error": "This task cannot be done this way right now",
                      "code": "quest_task_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "not_for_games",
                      "message": "This task has to be finished in Discord"
                    }
                  },
                  "taskWrongAction": {
                    "summary": "The task wants another request",
                    "value": {
                      "error": "This task cannot be done this way right now",
                      "code": "quest_task_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "wrong_action",
                      "message": "This task cannot be done this way"
                    }
                  },
                  "taskNeedsConnect": {
                    "summary": "The member has to link an account first",
                    "value": {
                      "error": "This task cannot be done this way right now",
                      "code": "quest_task_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "needs_connect",
                      "platform": "twitch",
                      "message": "Link your Twitch account in Discord to do this task"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 16 KB.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "tooLarge": {
                    "summary": "A body over 16 KB",
                    "value": {
                      "error": "The request body is larger than 16 KB",
                      "code": "body_too_large",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 requests a minute from one IP address across every request a game makes, more than 140 a minute with one player session, more than 120 reads or 20 writes a minute with one player session, or more calls than the game as a whole is allowed. All of them carry the code `rate_limited`, `retryAfter` and the `Retry-After` header.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "CEEJAY's bot could not be reached, or was too busy, and nothing was done (`bot_unavailable`). Send the request again after `retryAfter` seconds.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botUnavailable": {
                    "summary": "The bot could not be reached",
                    "value": {
                      "error": "This could not be done right now and nothing was changed. Try again in a few seconds",
                      "code": "bot_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 5
                    }
                  }
                }
              }
            }
          },
          "504": {
            "description": "CEEJAY's bot did not respond in time, so it is not known whether the request was done (`bot_timeout`). Send the same request again: it is safe to repeat.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botTimeout": {
                    "summary": "The bot did not respond in time",
                    "value": {
                      "error": "The answer did not arrive in time, and the request may or may not have been done. Send the same request again: it is safe to repeat",
                      "code": "bot_timeout",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/player/quests/answer": {
      "post": {
        "tags": [
          "Game loop"
        ],
        "summary": "Answer a quest task",
        "description": "Submits the player's answer to a task whose `action` is `answer` or `rate`, and completes the task when the answer is correct. CEEJAY validates it using the rules the Discord form uses: text must contain the required content and reach its `minLength`, a number must fall between `min` and `max`, and a rating must be a whole number from 1 to `scale`.\n\n**Called with** a player session, sent as `Authorization: Bearer cjps1.…`.\n\n**What it needs.** The game loop must be enabled for the game (`hub_not_enabled` otherwise). The member has to be in the server (`membership_required`; a player whose member left or never joined is sent to the game's `joinUrl`), and must have consented to share their quests, rewards, shop and rank with the game (`sharing_needed`, with `manageUrl`, where the member can consent). Where the game loop is not enabled on this instance, every one of these requests returns `404` with the code `not_available`.\n\nEvery response is private to the player and includes `Cache-Control: no-store`. The responses contain only the fields listed here: no Discord ID, no avatar and nothing about another member, except the display names in the top list of Get the ranks.\n\nThe response is the same as Verify a quest task: the quest completes when this was its last task, and CEEJAY does not send the member a direct message. A task that the community's team reviews is rejected before anything is written (`quest_task_unavailable` with the reason `not_for_games`): a game never submits an answer for review.\n\nAn incorrect answer (text without the required content, a number the task does not accept) returns `409` with the code `quest_task_not_done`, and `message` says what is missing. An answer that is empty, too long, shorter than the task's `minLength` or outside the rating scale returns `400` with `field: \"answer\"`. A task that is done returns `already_done` when it is asked again, so the request is safe to repeat and takes no key.\n\n**Limit:** 20 writes a minute for one player session.",
        "operationId": "answerPlayerQuestTask",
        "x-caller": "player_session",
        "x-since": "1.1.0",
        "security": [
          {
            "PlayerSession": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AnswerPlayerQuestTaskRequest"
              },
              "examples": {
                "text": {
                  "summary": "Answer a text question",
                  "value": {
                    "questId": 12,
                    "taskId": 42,
                    "answer": "Rugby Manager, every weekend"
                  }
                },
                "number": {
                  "summary": "Answer a number question",
                  "value": {
                    "questId": 12,
                    "taskId": 43,
                    "answer": 12
                  }
                },
                "rating": {
                  "summary": "Rate on a scale",
                  "value": {
                    "questId": 12,
                    "taskId": 44,
                    "answer": 4
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer was correct and the task is done.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerQuestTaskResponse"
                },
                "examples": {
                  "completed": {
                    "summary": "The last task: the quest is complete",
                    "value": {
                      "success": true,
                      "data": {
                        "questId": 12,
                        "taskId": 42,
                        "verified": true,
                        "questCompleted": true,
                        "tasksDone": 5,
                        "taskCount": 5,
                        "rewards": {
                          "xp": 100,
                          "coins": 50,
                          "badge": "Newcomer",
                          "roleGranted": false,
                          "leveledUp": true,
                          "newLevel": 12
                        }
                      }
                    }
                  },
                  "tasksRemain": {
                    "summary": "A task done, tasks remain",
                    "value": {
                      "success": true,
                      "data": {
                        "questId": 12,
                        "taskId": 42,
                        "verified": true,
                        "questCompleted": false,
                        "tasksDone": 2,
                        "taskCount": 5,
                        "rewards": null
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`questId`, `taskId` or `answer` is missing, is not the right kind, is too short or too long, or is outside the task's rating scale, or the body is not a JSON object. Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notObject": {
                    "summary": "The body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badQuestId": {
                    "summary": "A quest ID that is not a whole number",
                    "value": {
                      "error": "questId must be a whole number from 1 to 2147483647",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "questId"
                    }
                  },
                  "badTaskId": {
                    "summary": "A task ID that is not a whole number",
                    "value": {
                      "error": "taskId must be a whole number from 1 to 2147483647",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "taskId"
                    }
                  },
                  "answerMissing": {
                    "summary": "No answer",
                    "value": {
                      "error": "answer is required",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "answer"
                    }
                  },
                  "answerTooLong": {
                    "summary": "An answer over 1,000 characters",
                    "value": {
                      "error": "answer cannot be longer than 1000 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "answer"
                    }
                  },
                  "answerTooShort": {
                    "summary": "A text shorter than the task asks for",
                    "value": {
                      "error": "Write at least 10 characters.",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "answer"
                    }
                  },
                  "answerOffScale": {
                    "summary": "A rating that is not on the scale",
                    "value": {
                      "error": "Pick a number from 1 to 5.",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "answer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The player session was not accepted: `invalid_player_session` (missing, or past its 15 minutes: renew it with Renew a player session), `wrong_key_kind` (another kind of token) or `connection_ended` (the connection or the device was ended: connect again).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPlayerSession": {
                    "summary": "No player session, or one that has expired",
                    "value": {
                      "error": "Invalid or expired player session",
                      "code": "invalid_player_session",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of token",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "connectionEnded": {
                    "summary": "The connection or the device was ended",
                    "value": {
                      "error": "This connection has ended",
                      "code": "connection_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request includes an `Origin` header that is not on the game's allowed list (`origin_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "This instance does not offer Connect or the game loop yet (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`). Or the quest does not exist in this community, or the task does not belong to it (`quest_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "The game loop is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "questNotFound": {
                    "summary": "The quest or the task does not exist",
                    "value": {
                      "error": "Quest not found",
                      "code": "quest_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The game is paused (`game_paused`), does not show the community's quests, shop and rank (`hub_not_enabled`), the member has not consented to share them with the game (`sharing_needed`, with `manageUrl`), or the member is not in the server (`membership_required`, with `membership` and, for a member who left or never joined, `joinUrl`). Or a rule of the quest is not met (`quest_not_active`, `quest_not_started`, `quest_ended`, `quest_already_completed`, `quest_limit_reached`, `quest_requires_role`, `quest_requires_level`, `quest_requires_season_xp`, `quest_requires_program`, `quest_requires_quest`, `quest_full`, `quest_cooling_down` with `availableAt`, or `quest_daily_limit_reached`; `message` is a sentence the game can show), or the answer is wrong (`quest_task_not_done`), or the task cannot be answered this way (`quest_task_unavailable`). Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "hubNotEnabled": {
                    "summary": "The game does not show the quests, shop and rank",
                    "value": {
                      "error": "This game does not show the community's quests, shop and rank",
                      "code": "hub_not_enabled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "sharingNeeded": {
                    "summary": "The member must consent first",
                    "value": {
                      "error": "The member has not consented to sharing their data with this game",
                      "code": "sharing_needed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "manageUrl": "https://ceejay.communitystudios.xyz/connect/mine"
                    }
                  },
                  "notInServer": {
                    "summary": "The member is not in the server",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "left",
                      "joinUrl": "https://discord.gg/harbourrugby"
                    }
                  },
                  "screening": {
                    "summary": "The member has not passed Discord's membership screening",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "pending",
                      "joinUrl": null
                    }
                  },
                  "questNotActive": {
                    "summary": "The quest is not open",
                    "value": {
                      "error": "Quest is not active",
                      "code": "quest_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "This quest is not active right now"
                    }
                  },
                  "questNotStarted": {
                    "summary": "The quest has not started",
                    "value": {
                      "error": "Quest has not started yet",
                      "code": "quest_not_started",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "This quest has not started yet"
                    }
                  },
                  "questEnded": {
                    "summary": "The quest has ended",
                    "value": {
                      "error": "Quest has ended",
                      "code": "quest_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "This quest has ended"
                    }
                  },
                  "questAlreadyCompleted": {
                    "summary": "The player completed it already",
                    "value": {
                      "error": "Quest already completed and is not repeatable",
                      "code": "quest_already_completed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "You have completed this quest already"
                    }
                  },
                  "questLimitReached": {
                    "summary": "The quest allows no more completions",
                    "value": {
                      "error": "Max completions per user reached",
                      "code": "quest_limit_reached",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "You have completed this quest as many times as it allows"
                    }
                  },
                  "questRequiresRole": {
                    "summary": "The quest needs a role",
                    "value": {
                      "error": "The member does not have the role this quest requires",
                      "code": "quest_requires_role",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "Requires the @Gold club role"
                    }
                  },
                  "questRequiresLevel": {
                    "summary": "The quest needs a level",
                    "value": {
                      "error": "The member has not reached the level this quest requires",
                      "code": "quest_requires_level",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "Requires level 10"
                    }
                  },
                  "questRequiresSeasonXp": {
                    "summary": "The quest needs season XP",
                    "value": {
                      "error": "The member has not earned the season XP this quest requires",
                      "code": "quest_requires_season_xp",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "Requires 500 season XP"
                    }
                  },
                  "questRequiresProgram": {
                    "summary": "The quest needs a program",
                    "value": {
                      "error": "The member is not in the program this quest belongs to",
                      "code": "quest_requires_program",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "Join the program this quest belongs to first"
                    }
                  },
                  "questRequiresQuest": {
                    "summary": "The quest needs another quest first",
                    "value": {
                      "error": "The member has not completed the quest that comes before this one",
                      "code": "quest_requires_quest",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "Complete First steps first"
                    }
                  },
                  "questFull": {
                    "summary": "The quest has been fully claimed",
                    "value": {
                      "error": "Quest has reached its completion limit",
                      "code": "quest_full",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "This quest has been fully claimed"
                    }
                  },
                  "questCoolingDown": {
                    "summary": "The player must wait",
                    "value": {
                      "error": "The member has to wait before completing this quest again",
                      "code": "quest_cooling_down",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "availableAt": "2026-10-09T10:00:00.000Z",
                      "message": "Come back tomorrow to do this quest again"
                    }
                  },
                  "questDailyLimit": {
                    "summary": "The community's daily quest limit is reached",
                    "value": {
                      "error": "The member has reached this community's daily quest limit",
                      "code": "quest_daily_limit_reached",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "You have reached today's quest limit"
                    }
                  },
                  "wrongAnswer": {
                    "summary": "The answer is incorrect",
                    "value": {
                      "error": "The task is not done yet",
                      "code": "quest_task_not_done",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "message": "Your answer is missing: rugby"
                    }
                  },
                  "taskAlreadyDone": {
                    "summary": "The task is done already",
                    "value": {
                      "error": "This task cannot be done this way right now",
                      "code": "quest_task_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "already_done",
                      "message": "You have done this task already"
                    }
                  },
                  "taskInReview": {
                    "summary": "The task is with the team",
                    "value": {
                      "error": "This task cannot be done this way right now",
                      "code": "quest_task_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "in_review",
                      "message": "This task is waiting for a review"
                    }
                  },
                  "taskNotForGames": {
                    "summary": "The task has to be finished in Discord",
                    "value": {
                      "error": "This task cannot be done this way right now",
                      "code": "quest_task_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "not_for_games",
                      "message": "This task has to be finished in Discord"
                    }
                  },
                  "taskWrongAction": {
                    "summary": "The task wants another request",
                    "value": {
                      "error": "This task cannot be done this way right now",
                      "code": "quest_task_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "wrong_action",
                      "message": "This task cannot be done this way"
                    }
                  },
                  "taskNeedsConnect": {
                    "summary": "The member has to link an account first",
                    "value": {
                      "error": "This task cannot be done this way right now",
                      "code": "quest_task_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "needs_connect",
                      "platform": "twitch",
                      "message": "Link your Twitch account in Discord to do this task"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 16 KB.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "tooLarge": {
                    "summary": "A body over 16 KB",
                    "value": {
                      "error": "The request body is larger than 16 KB",
                      "code": "body_too_large",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 requests a minute from one IP address across every request a game makes, more than 140 a minute with one player session, more than 120 reads or 20 writes a minute with one player session, or more calls than the game as a whole is allowed. All of them carry the code `rate_limited`, `retryAfter` and the `Retry-After` header.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "CEEJAY's bot could not be reached, or was too busy, and nothing was done (`bot_unavailable`). Send the request again after `retryAfter` seconds.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botUnavailable": {
                    "summary": "The bot could not be reached",
                    "value": {
                      "error": "This could not be done right now and nothing was changed. Try again in a few seconds",
                      "code": "bot_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 5
                    }
                  }
                }
              }
            }
          },
          "504": {
            "description": "CEEJAY's bot did not respond in time, so it is not known whether the request was done (`bot_timeout`). Send the same request again: it is safe to repeat.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botTimeout": {
                    "summary": "The bot did not respond in time",
                    "value": {
                      "error": "The answer did not arrive in time, and the request may or may not have been done. Send the same request again: it is safe to repeat",
                      "code": "bot_timeout",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/player/shop": {
      "get": {
        "tags": [
          "Game loop"
        ],
        "summary": "List the shop",
        "description": "Lists the items this game may sell, each with whether the connected player can buy it, in the shop's own order. Prices are in the community's coins.\n\n**Called with** a player session, sent as `Authorization: Bearer cjps1.…`.\n\n**What it needs.** The game loop must be enabled for the game (`hub_not_enabled` otherwise). The member has to be in the server (`membership_required`; a player whose member left or never joined is sent to the game's `joinUrl`), and must have consented to share their quests, rewards, shop and rank with the game (`sharing_needed`, with `manageUrl`, where the member can consent). Where the game loop is not enabled on this instance, every one of these requests returns `404` with the code `not_available`.\n\nEvery response is private to the player and includes `Cache-Control: no-store`. The responses contain only the fields listed here: no Discord ID, no avatar and nothing about another member, except the display names in the top list of Get the ranks.\n\n**Which items.** A game sells only the items the community's team put on its list, and only of the item types the game is set to sell. A game sells nothing until the team adds items, so an empty list is normal. An item that is not on the list cannot be bought through the game, and Buy a shop item responds as if the item does not exist.\n\n`deliveryMode` says how an item reaches the player: `instant` (at once), `claim` (the player claims it after buying, with Claim an item) or `team` (the community's team fulfils it). Nothing about where or how an item is delivered is sent. When the game's daily spending limit is `0`, every item reads `canBuy: false`. The shop is the community's: read it again before a purchase and send the price you showed as `expectedPrice`.\n\nThe community's economy must be enabled (`economy_disabled`).\n\n**Limit:** 120 reads a minute for one player session.",
        "operationId": "listPlayerShop",
        "x-caller": "player_session",
        "x-since": "1.1.0",
        "security": [
          {
            "PlayerSession": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PlayerLoopLimit"
          },
          {
            "$ref": "#/components/parameters/PlayerLoopCursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of items.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListPlayerShopResponse"
                },
                "examples": {
                  "shop": {
                    "summary": "One item",
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": 7,
                          "name": "Gold skin",
                          "description": "A golden kit for your club.",
                          "price": 500,
                          "imageUrl": "https://res.cloudinary.com/example/gold.png",
                          "itemType": "api",
                          "deliveryMode": "instant",
                          "stockLeft": 20,
                          "limitLabel": "1 per member",
                          "availabilityLabel": null,
                          "eligibilityLabels": [
                            "Level 5"
                          ],
                          "question": null,
                          "titleMaxLength": null,
                          "requiredPlatforms": [],
                          "memberState": {
                            "canBuy": true,
                            "reason": null,
                            "availableAt": null
                          }
                        }
                      ],
                      "page": {
                        "nextCursor": null,
                        "limit": 50
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`limit` or `cursor` is not one this request takes. Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "badLimit": {
                    "summary": "A page size that is not a count, or above 100",
                    "value": {
                      "error": "limit must be a whole number from 1 to 100",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "limit"
                    }
                  },
                  "badCursor": {
                    "summary": "A cursor that was not issued by the list",
                    "value": {
                      "error": "cursor is not one this list gave. Send page.nextCursor as it was answered",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "cursor"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The player session was not accepted: `invalid_player_session` (missing, or past its 15 minutes: renew it with Renew a player session), `wrong_key_kind` (another kind of token) or `connection_ended` (the connection or the device was ended: connect again).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPlayerSession": {
                    "summary": "No player session, or one that has expired",
                    "value": {
                      "error": "Invalid or expired player session",
                      "code": "invalid_player_session",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of token",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "connectionEnded": {
                    "summary": "The connection or the device was ended",
                    "value": {
                      "error": "This connection has ended",
                      "code": "connection_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request includes an `Origin` header that is not on the game's allowed list (`origin_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "This instance does not offer Connect or the game loop yet (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "The game loop is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The game is paused (`game_paused`), does not show the community's quests, shop and rank (`hub_not_enabled`), the member has not consented to share them with the game (`sharing_needed`, with `manageUrl`), or the member is not in the server (`membership_required`, with `membership` and, for a member who left or never joined, `joinUrl`). Or the community has disabled its economy (`economy_disabled`). Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "hubNotEnabled": {
                    "summary": "The game does not show the quests, shop and rank",
                    "value": {
                      "error": "This game does not show the community's quests, shop and rank",
                      "code": "hub_not_enabled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "sharingNeeded": {
                    "summary": "The member must consent first",
                    "value": {
                      "error": "The member has not consented to sharing their data with this game",
                      "code": "sharing_needed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "manageUrl": "https://ceejay.communitystudios.xyz/connect/mine"
                    }
                  },
                  "notInServer": {
                    "summary": "The member is not in the server",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "left",
                      "joinUrl": "https://discord.gg/harbourrugby"
                    }
                  },
                  "screening": {
                    "summary": "The member has not passed Discord's membership screening",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "pending",
                      "joinUrl": null
                    }
                  },
                  "economyDisabled": {
                    "summary": "The economy is off",
                    "value": {
                      "error": "The community has switched its economy off",
                      "code": "economy_disabled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 requests a minute from one IP address across every request a game makes, more than 140 a minute with one player session, more than 120 reads or 20 writes a minute with one player session, or more calls than the game as a whole is allowed. All of them carry the code `rate_limited`, `retryAfter` and the `Retry-After` header.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "CEEJAY's bot could not be reached, or was too busy, and nothing was done (`bot_unavailable`). Send the request again after `retryAfter` seconds.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botUnavailable": {
                    "summary": "The bot could not be reached",
                    "value": {
                      "error": "This could not be done right now and nothing was changed. Try again in a few seconds",
                      "code": "bot_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 5
                    }
                  }
                }
              }
            }
          },
          "504": {
            "description": "CEEJAY's bot did not respond in time (`bot_timeout`). Send the request again.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botTimeout": {
                    "summary": "The bot did not respond in time",
                    "value": {
                      "error": "The answer did not arrive in time, and the request may or may not have been done. Send the same request again: it is safe to repeat",
                      "code": "bot_timeout",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/player/purchase": {
      "post": {
        "tags": [
          "Game loop"
        ],
        "summary": "Buy a shop item",
        "description": "Buys a shop item with the member's coins. The coins are deducted, the purchase is made and the item is delivered or awaits claiming, in one step.\n\n**Called with** a player session, sent as `Authorization: Bearer cjps1.…`.\n\n**What it needs.** The game loop must be enabled for the game (`hub_not_enabled` otherwise). The member has to be in the server (`membership_required`; a player whose member left or never joined is sent to the game's `joinUrl`), and must have consented to share their quests, rewards, shop and rank with the game (`sharing_needed`, with `manageUrl`, where the member can consent). Where the game loop is not enabled on this instance, every one of these requests returns `404` with the code `not_available`.\n\nEvery response is private to the player and includes `Cache-Control: no-store`. The responses contain only the fields listed here: no Discord ID, no avatar and nothing about another member, except the display names in the top list of Get the ranks.\n\n**What it needs besides.** The community's economy must be enabled (`economy_disabled`). The device has to have connected with proof when the game asks for proof (`403` `verified_proof_required`; reading, quests and the daily reward need none). The connection and the device must have passed the game's spending hold (`409` `spend_on_hold`, with `holdUntil` and `retryAfter`): the time that counts is `spending.holdUntil` in Get the balance, the latest of the connection's hold, the device's hold and the device's age plus the game's hold hours. And the day's limit must have room for the price (`409` `daily_spend_cap_reached`, with `cap`, `spentToday`, `price`, `resetsAt` and `retryAfter`). An item priced above the limit can never be bought through the game.\n\n**The price is exact.** Send the price the player saw as `expectedPrice`. When the price has changed, nothing is bought and the response is `409` with the code `price_changed` and the current price: show the shop again. A member with an insufficient balance gets `insufficient_balance` with `price` and `balance`. An item that is not on sale, that the member cannot buy, that is sold out or whose limit the member reached is `purchase_refused`, with a `reason` and a `message` the game can show. An item that is not on the game's allow list, is disabled or does not exist returns `item_not_found`, and the three cases cannot be distinguished.\n\n**What happens next.** `status` and `waitingFor` give the purchase's status (see Read the locker). An item with `claimNeeded: true` awaits Claim an item. An item delivered by the game's own server may read `pending` with `waitingFor: studio` until that server finishes the delivery. A delivery that fails is retried and the member is refunded if it keeps failing: the balance then reflects the refund, and the locker shows `refunded`. Nothing is sent to the member in a direct message.\n\n**Retries.** `idempotencyKey` is required. Retry the same request with the same key after a lost response, a `429`, a `503` or a `504`: nothing is bought twice, and the response is the original response with `duplicate: true` and the header `Idempotent-Replayed: true` (a game in a browser cannot read that header, so read `duplicate`). A key used for another item or with other values returns `409` with the code `idempotency_key_reused`, and nothing is bought. While the first request with a key is still being processed, for up to a minute, a retry gets `idempotency_in_progress`: wait `retryAfter` seconds and retry. When the first request never finished (it timed out, or its response was lost), a retry after that minute is processed, and CEEJAY finds the first purchase by the key instead of buying again: on a `504`, retry the same request with the same key. `idempotency_outcome_unknown` is returned when two concurrent requests used the same key and CEEJAY cannot tell whether the first took effect: check the locker before buying again with a new key. A key that was rejected can be used again once the cause is fixed. On an instance that does not support idempotency keys yet, the response is `503` with the code `idempotency_unavailable`, and nothing is bought.\n\n**Limit:** 20 writes a minute for one player session, and 60 buys and claims together an hour for one member in one game.",
        "operationId": "buyShopItem",
        "x-caller": "player_session",
        "x-since": "1.1.0",
        "security": [
          {
            "PlayerSession": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BuyShopItemRequest"
              },
              "examples": {
                "request": {
                  "summary": "Buy an item",
                  "value": {
                    "itemId": 7,
                    "idempotencyKey": "buy-2026-10-08-gold-skin-1",
                    "expectedPrice": 500
                  }
                },
                "withAnswer": {
                  "summary": "Buy an item that asks a question",
                  "value": {
                    "itemId": 9,
                    "idempotencyKey": "buy-2026-10-08-kit-name-1",
                    "expectedPrice": 250,
                    "answer": "Harbour RFC"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The purchase is made, or the same request was made before and this is its response.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BuyShopItemResponse"
                },
                "examples": {
                  "bought": {
                    "summary": "An item the studio's server will finish delivering",
                    "value": {
                      "success": true,
                      "data": {
                        "purchaseId": 4242,
                        "itemId": 7,
                        "itemName": "Gold skin",
                        "pricePaid": 500,
                        "balance": 950,
                        "status": "pending",
                        "waitingFor": "studio",
                        "claimNeeded": false,
                        "delivery": {
                          "state": "pending",
                          "message": null
                        },
                        "duplicate": false,
                        "spending": {
                          "dailyCap": 5000,
                          "spentToday": 500,
                          "remainingToday": 4500,
                          "resetsAt": "2026-10-09T00:00:00.000Z"
                        }
                      }
                    }
                  },
                  "delivered": {
                    "summary": "An item bought with nothing left to do",
                    "value": {
                      "success": true,
                      "data": {
                        "purchaseId": 4242,
                        "itemId": 7,
                        "itemName": "Gold skin",
                        "pricePaid": 500,
                        "balance": 950,
                        "status": "delivered",
                        "waitingFor": null,
                        "claimNeeded": false,
                        "delivery": {
                          "state": "delivered",
                          "message": null
                        },
                        "duplicate": false,
                        "spending": {
                          "dailyCap": 5000,
                          "spentToday": 500,
                          "remainingToday": 4500,
                          "resetsAt": "2026-10-09T00:00:00.000Z"
                        }
                      }
                    }
                  },
                  "repeat": {
                    "summary": "The same request sent again with its key",
                    "value": {
                      "success": true,
                      "data": {
                        "purchaseId": 4242,
                        "itemId": 7,
                        "itemName": "Gold skin",
                        "pricePaid": 500,
                        "balance": 950,
                        "status": "pending",
                        "waitingFor": "studio",
                        "claimNeeded": false,
                        "delivery": {
                          "state": "pending",
                          "message": null
                        },
                        "duplicate": true,
                        "spending": {
                          "dailyCap": 5000,
                          "spentToday": 500,
                          "remainingToday": 4500,
                          "resetsAt": "2026-10-09T00:00:00.000Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`itemId`, `idempotencyKey`, `expectedPrice` or `answer` is missing or is not the right kind, or the body is not a JSON object. An item that asks a question needs an `answer`, and then `question` contains the question. Nothing was bought.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notObject": {
                    "summary": "The body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badItemId": {
                    "summary": "An item ID that is not a whole number",
                    "value": {
                      "error": "itemId must be a whole number from 1 to 2147483647",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "itemId"
                    }
                  },
                  "missingKey": {
                    "summary": "No idempotencyKey",
                    "value": {
                      "error": "idempotencyKey must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "badExpectedPrice": {
                    "summary": "A price that is not a count of coins",
                    "value": {
                      "error": "expectedPrice must be a whole number from 0 to 2147483647",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "expectedPrice"
                    }
                  },
                  "answerRequired": {
                    "summary": "An item that asks a question needs an answer",
                    "value": {
                      "error": "This item asks a question. Answer it first",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "answer",
                      "question": "What name should be on the kit?"
                    }
                  },
                  "answerTooLong": {
                    "summary": "An answer over 200 characters",
                    "value": {
                      "error": "answer cannot be longer than 200 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "answer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The player session was not accepted: `invalid_player_session` (missing, or past its 15 minutes: renew it with Renew a player session), `wrong_key_kind` (another kind of token) or `connection_ended` (the connection or the device was ended: connect again).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPlayerSession": {
                    "summary": "No player session, or one that has expired",
                    "value": {
                      "error": "Invalid or expired player session",
                      "code": "invalid_player_session",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of token",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "connectionEnded": {
                    "summary": "The connection or the device was ended",
                    "value": {
                      "error": "This connection has ended",
                      "code": "connection_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request includes an `Origin` header that is not on the game's allowed list (`origin_not_allowed`). Or this device connected without proof and the game only lets proven players spend (`verified_proof_required`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "verifiedProofRequired": {
                    "summary": "This device connected without proof",
                    "value": {
                      "error": "This requires a verified player. This device connected without verification",
                      "code": "verified_proof_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "This instance does not offer Connect or the game loop yet (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`). Or the item does not exist, is disabled, is not a type the game may sell or is not on the game's allow list (`item_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "The game loop is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "itemNotFound": {
                    "summary": "The item cannot be bought through this game",
                    "value": {
                      "error": "Shop item not found",
                      "code": "item_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The game is paused (`game_paused`), does not show the community's quests, shop and rank (`hub_not_enabled`), the member has not consented to share them with the game (`sharing_needed`, with `manageUrl`), or the member is not in the server (`membership_required`, with `membership` and, for a member who left or never joined, `joinUrl`). Or this device cannot spend yet (`spend_on_hold`), the daily spending limit is reached (`daily_spend_cap_reached`), the price changed (`price_changed`), the member's balance is too low (`insufficient_balance`), the item cannot be bought (`purchase_refused`), the community has disabled its economy (`economy_disabled`), or the `idempotencyKey` was used for another request (`idempotency_key_reused`), the first request with it is still running (`idempotency_in_progress`) or its outcome is not known (`idempotency_outcome_unknown`). Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "hubNotEnabled": {
                    "summary": "The game does not show the quests, shop and rank",
                    "value": {
                      "error": "This game does not show the community's quests, shop and rank",
                      "code": "hub_not_enabled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "sharingNeeded": {
                    "summary": "The member must consent first",
                    "value": {
                      "error": "The member has not consented to sharing their data with this game",
                      "code": "sharing_needed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "manageUrl": "https://ceejay.communitystudios.xyz/connect/mine"
                    }
                  },
                  "notInServer": {
                    "summary": "The member is not in the server",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "left",
                      "joinUrl": "https://discord.gg/harbourrugby"
                    }
                  },
                  "screening": {
                    "summary": "The member has not passed Discord's membership screening",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "pending",
                      "joinUrl": null
                    }
                  },
                  "spendOnHold": {
                    "summary": "A new connection or device is still on hold",
                    "value": {
                      "error": "This device cannot spend yet. New connections and devices cannot buy until the hold ends",
                      "code": "spend_on_hold",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "holdUntil": "2026-10-09T18:06:40.000Z",
                      "retryAfter": 3600
                    }
                  },
                  "dailyCapReached": {
                    "summary": "The day's limit has no room for the price",
                    "value": {
                      "error": "The daily spending limit for this member in this game has been reached",
                      "code": "daily_spend_cap_reached",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "cap": 5000,
                      "spentToday": 4800,
                      "price": 500,
                      "resetsAt": "2026-10-09T00:00:00.000Z",
                      "retryAfter": 41000
                    }
                  },
                  "priceChanged": {
                    "summary": "The item costs something else now",
                    "value": {
                      "error": "The price of this item has changed. Read the shop again",
                      "code": "price_changed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "price": 600,
                      "expectedPrice": 500
                    }
                  },
                  "insufficientBalance": {
                    "summary": "The member's balance is too low",
                    "value": {
                      "error": "Insufficient balance",
                      "code": "insufficient_balance",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "price": 500,
                      "balance": 300
                    }
                  },
                  "notOnSale": {
                    "summary": "The item is not on sale",
                    "value": {
                      "error": "This item cannot be bought right now",
                      "code": "purchase_refused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "not_on_sale",
                      "message": "This item is not on sale right now"
                    }
                  },
                  "notEligible": {
                    "summary": "The member does not meet a condition",
                    "value": {
                      "error": "This item cannot be bought right now",
                      "code": "purchase_refused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "not_eligible",
                      "missingPlatform": "steam",
                      "message": "Link your Steam account in Discord to buy this"
                    }
                  },
                  "unavailable": {
                    "summary": "The item cannot be bought now",
                    "value": {
                      "error": "This item cannot be bought right now",
                      "code": "purchase_refused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "unavailable",
                      "message": "This item is not available"
                    }
                  },
                  "outOfStock": {
                    "summary": "The item is sold out",
                    "value": {
                      "error": "This item cannot be bought right now",
                      "code": "purchase_refused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "out_of_stock",
                      "message": "This item is sold out"
                    }
                  },
                  "limitReached": {
                    "summary": "The member bought as many as are allowed",
                    "value": {
                      "error": "This item cannot be bought right now",
                      "code": "purchase_refused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "limit_reached",
                      "availableAt": "2026-10-15T00:00:00.000Z",
                      "message": "You have bought as many of this item as you can"
                    }
                  },
                  "economyDisabled": {
                    "summary": "The economy is off",
                    "value": {
                      "error": "The community has switched its economy off",
                      "code": "economy_disabled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keyReused": {
                    "summary": "The key was used for another request",
                    "value": {
                      "error": "This idempotencyKey was already used for a different request",
                      "code": "idempotency_key_reused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keyInProgress": {
                    "summary": "The first request with this key is still running",
                    "value": {
                      "error": "The first request with this idempotencyKey is still running. Try again in a few seconds.",
                      "code": "idempotency_in_progress",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 5
                    }
                  },
                  "keyOutcomeUnknown": {
                    "summary": "An earlier request with this key may or may not have been applied",
                    "value": {
                      "error": "An earlier request with this idempotencyKey may or may not have been applied. It was not sent again. Use a new key to send it again.",
                      "code": "idempotency_outcome_unknown",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 16 KB.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "tooLarge": {
                    "summary": "A body over 16 KB",
                    "value": {
                      "error": "The request body is larger than 16 KB",
                      "code": "body_too_large",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 requests a minute from one IP address across every request a game makes, more than 140 a minute with one player session, more than 120 reads or 20 writes a minute with one player session, more than 60 buys and claims an hour for one member in this game, or more calls than the game as a whole is allowed. All of them carry the code `rate_limited`, `retryAfter` and the `Retry-After` header.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "CEEJAY's bot could not be reached, or was too busy, and nothing was done (`bot_unavailable`). Send the request again after `retryAfter` seconds. Or you sent an `idempotencyKey` and this instance does not support idempotency keys yet (`idempotency_unavailable`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botUnavailable": {
                    "summary": "The bot could not be reached",
                    "value": {
                      "error": "This could not be done right now and nothing was changed. Try again in a few seconds",
                      "code": "bot_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 5
                    }
                  },
                  "keysUnavailable": {
                    "summary": "Idempotency keys are not supported yet",
                    "value": {
                      "error": "idempotencyKey cannot be used on this community yet, so nothing was changed",
                      "code": "idempotency_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "504": {
            "description": "CEEJAY's bot did not respond in time, so it is not known whether the request was done (`bot_timeout`). Send the same request again: it is safe to repeat.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botTimeout": {
                    "summary": "The bot did not respond in time",
                    "value": {
                      "error": "The answer did not arrive in time, and the request may or may not have been done. Send the same request again: it is safe to repeat",
                      "code": "bot_timeout",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/player/locker": {
      "get": {
        "tags": [
          "Game loop"
        ],
        "summary": "Read the locker",
        "description": "Lists what the member owns through this game: the purchases made through it, and purchases of items on its list made anywhere (in Discord, for example), newest first. Rewards the member earned have no shop item and are not here.\n\n**Called with** a player session, sent as `Authorization: Bearer cjps1.…`.\n\n**What it needs.** The game loop must be enabled for the game (`hub_not_enabled` otherwise). The member has to be in the server (`membership_required`; a player whose member left or never joined is sent to the game's `joinUrl`), and must have consented to share their quests, rewards, shop and rank with the game (`sharing_needed`, with `manageUrl`, where the member can consent). Where the game loop is not enabled on this instance, every one of these requests returns `404` with the code `not_available`.\n\nEvery response is private to the player and includes `Cache-Control: no-store`. The responses contain only the fields listed here: no Discord ID, no avatar and nothing about another member, except the display names in the top list of Get the ranks.\n\n`status=open` (the default) lists what can still change: `claimable`, `pending`. `status=all` adds the closed ones of the last 30 days (`delivered`, `refunded`, `rejected`, `expired`, `failed`). For each purchase, `status` and `waitingFor` give its status and `statusLabel` is a few words the game can show. A `claimable` purchase is claimed with Claim an item; `claim` says what it asks for, and `claim.options` contains the roles or giveaways the player can pick from (at most 25, read from CEEJAY's cache of the server, so a role that is not cached is left out).\n\n**A code is never sent.** `hasCode: true` says one exists; the member reads it in Discord. What the member typed when buying, and the delivery details, are not sent either.\n\nThe list is paged like List the quests, and can repeat or skip an item when it changes between two pages.\n\n**Limit:** 120 reads a minute for one player session.",
        "operationId": "listPlayerLocker",
        "x-caller": "player_session",
        "x-since": "1.1.0",
        "security": [
          {
            "PlayerSession": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "`open` (the default) lists what can still change. `all` adds the closed purchases of the last 30 days. Any other value is rejected with `400`.",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "all"
              ],
              "default": "open"
            },
            "example": "open"
          },
          {
            "$ref": "#/components/parameters/PlayerLoopLimit"
          },
          {
            "$ref": "#/components/parameters/PlayerLoopCursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of purchases.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListPlayerLockerResponse"
                },
                "examples": {
                  "locker": {
                    "summary": "One purchase awaiting claim",
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": 4242,
                          "itemId": 7,
                          "itemName": "Gold skin",
                          "itemType": "api",
                          "imageUrl": null,
                          "pricePaid": 500,
                          "status": "claimable",
                          "waitingFor": null,
                          "statusLabel": "Ready to claim",
                          "createdAt": "2026-10-08T12:00:00.000Z",
                          "claimedAt": null,
                          "expiresAt": null,
                          "claim": {
                            "needsInput": true,
                            "fields": [
                              {
                                "key": "title",
                                "kind": "text",
                                "label": "Name on the kit",
                                "max": 40
                              }
                            ],
                            "options": {
                              "roles": [
                                {
                                  "id": "1332398278477746300",
                                  "name": "Gold club",
                                  "color": "#f94801"
                                }
                              ],
                              "giveaways": [
                                {
                                  "id": 31,
                                  "title": "Weekly draw",
                                  "endsAt": "2026-10-15T18:00:00.000Z"
                                }
                              ]
                            }
                          },
                          "hasCode": false
                        }
                      ],
                      "page": {
                        "nextCursor": null,
                        "limit": 50
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`status`, `limit` or `cursor` is not one this request takes. Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "badStatus": {
                    "summary": "A status that is not open or all",
                    "value": {
                      "error": "status must be open or all",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "status"
                    }
                  },
                  "badLimit": {
                    "summary": "A page size that is not a count, or above 100",
                    "value": {
                      "error": "limit must be a whole number from 1 to 100",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "limit"
                    }
                  },
                  "badCursor": {
                    "summary": "A cursor that was not issued by the list",
                    "value": {
                      "error": "cursor is not one this list gave. Send page.nextCursor as it was answered",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "cursor"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The player session was not accepted: `invalid_player_session` (missing, or past its 15 minutes: renew it with Renew a player session), `wrong_key_kind` (another kind of token) or `connection_ended` (the connection or the device was ended: connect again).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPlayerSession": {
                    "summary": "No player session, or one that has expired",
                    "value": {
                      "error": "Invalid or expired player session",
                      "code": "invalid_player_session",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of token",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "connectionEnded": {
                    "summary": "The connection or the device was ended",
                    "value": {
                      "error": "This connection has ended",
                      "code": "connection_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request includes an `Origin` header that is not on the game's allowed list (`origin_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "This instance does not offer Connect or the game loop yet (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "The game loop is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The game is paused (`game_paused`), does not show the community's quests, shop and rank (`hub_not_enabled`), the member has not consented to share them with the game (`sharing_needed`, with `manageUrl`), or the member is not in the server (`membership_required`, with `membership` and, for a member who left or never joined, `joinUrl`). Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "hubNotEnabled": {
                    "summary": "The game does not show the quests, shop and rank",
                    "value": {
                      "error": "This game does not show the community's quests, shop and rank",
                      "code": "hub_not_enabled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "sharingNeeded": {
                    "summary": "The member must consent first",
                    "value": {
                      "error": "The member has not consented to sharing their data with this game",
                      "code": "sharing_needed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "manageUrl": "https://ceejay.communitystudios.xyz/connect/mine"
                    }
                  },
                  "notInServer": {
                    "summary": "The member is not in the server",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "left",
                      "joinUrl": "https://discord.gg/harbourrugby"
                    }
                  },
                  "screening": {
                    "summary": "The member has not passed Discord's membership screening",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "pending",
                      "joinUrl": null
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 requests a minute from one IP address across every request a game makes, more than 140 a minute with one player session, more than 120 reads or 20 writes a minute with one player session, or more calls than the game as a whole is allowed. All of them carry the code `rate_limited`, `retryAfter` and the `Retry-After` header.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "CEEJAY's bot could not be reached, or was too busy, and nothing was done (`bot_unavailable`). Send the request again after `retryAfter` seconds.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botUnavailable": {
                    "summary": "The bot could not be reached",
                    "value": {
                      "error": "This could not be done right now and nothing was changed. Try again in a few seconds",
                      "code": "bot_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 5
                    }
                  }
                }
              }
            }
          },
          "504": {
            "description": "CEEJAY's bot did not respond in time (`bot_timeout`). Send the request again.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botTimeout": {
                    "summary": "The bot did not respond in time",
                    "value": {
                      "error": "The answer did not arrive in time, and the request may or may not have been done. Send the same request again: it is safe to repeat",
                      "code": "bot_timeout",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/player/claim": {
      "post": {
        "tags": [
          "Game loop"
        ],
        "summary": "Claim an item",
        "description": "Claims a purchase awaiting the player (`status` `claimable`): the item is delivered or passed to the community's team, the role is assigned or the giveaway is entered. A purchase that was claimed before returns `alreadyClaimed: true` with its current status, which is also what a retry after a lost response returns. It is safe to repeat and takes no key.\n\n**Called with** a player session, sent as `Authorization: Bearer cjps1.…`.\n\n**What it needs.** The game loop must be enabled for the game (`hub_not_enabled` otherwise). The member has to be in the server (`membership_required`; a player whose member left or never joined is sent to the game's `joinUrl`), and must have consented to share their quests, rewards, shop and rank with the game (`sharing_needed`, with `manageUrl`, where the member can consent). Where the game loop is not enabled on this instance, every one of these requests returns `404` with the code `not_available`.\n\nEvery response is private to the player and includes `Cache-Control: no-store`. The responses contain only the fields listed here: no Discord ID, no avatar and nothing about another member, except the display names in the top list of Get the ranks.\n\nA claim redeems what a purchase bought, so it has the same requirements as buying: a device that connected with proof when the game requires proof (`403` `verified_proof_required`) and a connection and device whose spending hold has ended (`409` `spend_on_hold`). It spends no coins and is not counted against the day's limit.\n\nOnly a purchase of the member's own, made through this game or of an item on its list, can be claimed here. Any other purchase, a purchase of another member or of another community, and an earned reward all return `404` with the code `purchase_not_found`. A purchase that is not `claimable` and not claimed yet returns `409` with the code `not_claimable` and its `status`. When the item asks for something (`claim.fields`), send it as `input`; if it is missing or invalid the response is `400` with `field: \"input\"` and `fields` lists the inputs required.\n\n**Limit:** 20 writes a minute for one player session, and 60 buys and claims together an hour for one member in one game.",
        "operationId": "claimPlayerItem",
        "x-caller": "player_session",
        "x-since": "1.1.0",
        "security": [
          {
            "PlayerSession": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClaimPlayerItemRequest"
              },
              "examples": {
                "request": {
                  "summary": "Claim an item that asks for a name",
                  "value": {
                    "purchaseId": 4242,
                    "input": {
                      "title": "Harbour RFC"
                    }
                  }
                },
                "plain": {
                  "summary": "Claim an item that asks for nothing",
                  "value": {
                    "purchaseId": 4243
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The purchase is claimed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClaimPlayerItemResponse"
                },
                "examples": {
                  "claimed": {
                    "summary": "The purchase was claimed",
                    "value": {
                      "success": true,
                      "data": {
                        "purchaseId": 4242,
                        "claimed": true,
                        "alreadyClaimed": false,
                        "itemName": "Gold skin",
                        "status": "pending",
                        "waitingFor": "delivery",
                        "delivery": {
                          "state": "pending",
                          "message": null
                        }
                      }
                    }
                  },
                  "alreadyClaimed": {
                    "summary": "It was claimed before",
                    "value": {
                      "success": true,
                      "data": {
                        "purchaseId": 4242,
                        "claimed": true,
                        "alreadyClaimed": true,
                        "itemName": "Gold skin",
                        "status": "pending",
                        "waitingFor": "delivery",
                        "delivery": {
                          "state": "pending",
                          "message": null
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`purchaseId` or `input` is missing or is not the right kind, or the body is not a JSON object. Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notObject": {
                    "summary": "The body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badPurchaseId": {
                    "summary": "A purchase ID that is not a whole number",
                    "value": {
                      "error": "purchaseId must be a whole number from 1 to 2147483647",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "purchaseId"
                    }
                  },
                  "inputNeeded": {
                    "summary": "The item asks for something",
                    "value": {
                      "error": "This item needs further details before it can be claimed",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "input",
                      "fields": [
                        {
                          "key": "title",
                          "kind": "text",
                          "label": "Name on the kit",
                          "max": 40
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The player session was not accepted: `invalid_player_session` (missing, or past its 15 minutes: renew it with Renew a player session), `wrong_key_kind` (another kind of token) or `connection_ended` (the connection or the device was ended: connect again).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPlayerSession": {
                    "summary": "No player session, or one that has expired",
                    "value": {
                      "error": "Invalid or expired player session",
                      "code": "invalid_player_session",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of token",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "connectionEnded": {
                    "summary": "The connection or the device was ended",
                    "value": {
                      "error": "This connection has ended",
                      "code": "connection_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request includes an `Origin` header that is not on the game's allowed list (`origin_not_allowed`). Or this device connected without proof and the game only lets proven players spend (`verified_proof_required`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "verifiedProofRequired": {
                    "summary": "This device connected without proof",
                    "value": {
                      "error": "This requires a verified player. This device connected without verification",
                      "code": "verified_proof_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "This instance does not offer Connect or the game loop yet (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`). Or the purchase is not the member's, is not in this game's scope, is an earned reward or does not exist (`purchase_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "The game loop is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "purchaseNotFound": {
                    "summary": "No such purchase for this member and game",
                    "value": {
                      "error": "Purchase not found",
                      "code": "purchase_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The game is paused (`game_paused`), does not show the community's quests, shop and rank (`hub_not_enabled`), the member has not consented to share them with the game (`sharing_needed`, with `manageUrl`), or the member is not in the server (`membership_required`, with `membership` and, for a member who left or never joined, `joinUrl`). Or this device cannot spend yet (`spend_on_hold`), or the purchase cannot be claimed in the state it is in (`not_claimable`). Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "hubNotEnabled": {
                    "summary": "The game does not show the quests, shop and rank",
                    "value": {
                      "error": "This game does not show the community's quests, shop and rank",
                      "code": "hub_not_enabled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "sharingNeeded": {
                    "summary": "The member must consent first",
                    "value": {
                      "error": "The member has not consented to sharing their data with this game",
                      "code": "sharing_needed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "manageUrl": "https://ceejay.communitystudios.xyz/connect/mine"
                    }
                  },
                  "notInServer": {
                    "summary": "The member is not in the server",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "left",
                      "joinUrl": "https://discord.gg/harbourrugby"
                    }
                  },
                  "screening": {
                    "summary": "The member has not passed Discord's membership screening",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "pending",
                      "joinUrl": null
                    }
                  },
                  "spendOnHold": {
                    "summary": "A new connection or device is still on hold",
                    "value": {
                      "error": "This device cannot spend yet. New connections and devices cannot buy until the hold ends",
                      "code": "spend_on_hold",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "holdUntil": "2026-10-09T18:06:40.000Z",
                      "retryAfter": 3600
                    }
                  },
                  "notClaimable": {
                    "summary": "The purchase was refunded",
                    "value": {
                      "error": "This item cannot be claimed right now",
                      "code": "not_claimable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "status": "refunded"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 16 KB.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "tooLarge": {
                    "summary": "A body over 16 KB",
                    "value": {
                      "error": "The request body is larger than 16 KB",
                      "code": "body_too_large",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 requests a minute from one IP address across every request a game makes, more than 140 a minute with one player session, more than 120 reads or 20 writes a minute with one player session, more than 60 buys and claims an hour for one member in this game, or more calls than the game as a whole is allowed. All of them carry the code `rate_limited`, `retryAfter` and the `Retry-After` header.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "CEEJAY's bot could not be reached, or was too busy, and nothing was done (`bot_unavailable`). Send the request again after `retryAfter` seconds.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botUnavailable": {
                    "summary": "The bot could not be reached",
                    "value": {
                      "error": "This could not be done right now and nothing was changed. Try again in a few seconds",
                      "code": "bot_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 5
                    }
                  }
                }
              }
            }
          },
          "504": {
            "description": "CEEJAY's bot did not respond in time, so it is not known whether the request was done (`bot_timeout`). Send the same request again: it is safe to repeat.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botTimeout": {
                    "summary": "The bot did not respond in time",
                    "value": {
                      "error": "The answer did not arrive in time, and the request may or may not have been done. Send the same request again: it is safe to repeat",
                      "code": "bot_timeout",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/player/ranks": {
      "get": {
        "tags": [
          "Game loop"
        ],
        "summary": "Get the ranks",
        "description": "Reads the connected player's place in the community and the top players of this game.\n\n**Called with** a player session, sent as `Authorization: Bearer cjps1.…`.\n\n**What it needs.** The game loop must be enabled for the game (`hub_not_enabled` otherwise). The member has to be in the server (`membership_required`; a player whose member left or never joined is sent to the game's `joinUrl`), and must have consented to share their quests, rewards, shop and rank with the game (`sharing_needed`, with `manageUrl`, where the member can consent). Where the game loop is not enabled on this instance, every one of these requests returns `404` with the code `not_available`.\n\nEvery response is private to the player and includes `Cache-Control: no-store`. The responses contain only the fields listed here: no Discord ID, no avatar and nothing about another member, except the display names in the top list of Get the ranks.\n\n**Who is in the top list.** Players of this game only: members connected through Connect to this game who have consented to share their rank with it (and whose consent is current), who are in the server, are not banned, do not have a role hidden from the leaderboard and have XP. They are ordered by XP, highest first. `rank` in `top` and `me.rank` are places in this list; `communityRank` is the member's place among everyone in the community. A name is the member's display name, or \"A member\" where CEEJAY has none. **No Discord ID, no avatar, no cosmetics and no coin leaderboard** is ever sent.\n\nThe member's own `me` is there even when the member is not in the top list.\n\n**Limit:** 120 reads a minute for one player session.",
        "operationId": "getPlayerRanks",
        "x-caller": "player_session",
        "x-since": "1.1.0",
        "security": [
          {
            "PlayerSession": []
          }
        ],
        "parameters": [
          {
            "name": "top",
            "in": "query",
            "required": false,
            "description": "How many players the `top` list contains: an integer from 1 to 20. Leave it out for 10. A value above 20, a negative number, a fraction or text is rejected with `400`.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "minimum": 1,
              "maximum": 20,
              "default": 10
            },
            "example": 10
          }
        ],
        "responses": {
          "200": {
            "description": "The ranks.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerRanksResponse"
                },
                "examples": {
                  "ranks": {
                    "summary": "The player's place and the top of the game",
                    "value": {
                      "success": true,
                      "data": {
                        "me": {
                          "xp": 5420,
                          "level": 12,
                          "tier": {
                            "name": "Veteran",
                            "color": "#f94801"
                          },
                          "rank": 7,
                          "communityRank": 41,
                          "progress": {
                            "current": 120,
                            "needed": 400,
                            "percent": 30
                          }
                        },
                        "top": [
                          {
                            "rank": 1,
                            "displayName": "Nova",
                            "level": 31,
                            "xp": 98000
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`top` is not one this request takes. Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "badTop": {
                    "summary": "A count outside 1 to 20",
                    "value": {
                      "error": "top must be a whole number from 1 to 20",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "top"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The player session was not accepted: `invalid_player_session` (missing, or past its 15 minutes: renew it with Renew a player session), `wrong_key_kind` (another kind of token) or `connection_ended` (the connection or the device was ended: connect again).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPlayerSession": {
                    "summary": "No player session, or one that has expired",
                    "value": {
                      "error": "Invalid or expired player session",
                      "code": "invalid_player_session",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of token",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "connectionEnded": {
                    "summary": "The connection or the device was ended",
                    "value": {
                      "error": "This connection has ended",
                      "code": "connection_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request includes an `Origin` header that is not on the game's allowed list (`origin_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "This instance does not offer Connect or the game loop yet (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "The game loop is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The game is paused (`game_paused`), does not show the community's quests, shop and rank (`hub_not_enabled`), the member has not consented to share them with the game (`sharing_needed`, with `manageUrl`), or the member is not in the server (`membership_required`, with `membership` and, for a member who left or never joined, `joinUrl`). Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "hubNotEnabled": {
                    "summary": "The game does not show the quests, shop and rank",
                    "value": {
                      "error": "This game does not show the community's quests, shop and rank",
                      "code": "hub_not_enabled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "sharingNeeded": {
                    "summary": "The member must consent first",
                    "value": {
                      "error": "The member has not consented to sharing their data with this game",
                      "code": "sharing_needed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "manageUrl": "https://ceejay.communitystudios.xyz/connect/mine"
                    }
                  },
                  "notInServer": {
                    "summary": "The member is not in the server",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "left",
                      "joinUrl": "https://discord.gg/harbourrugby"
                    }
                  },
                  "screening": {
                    "summary": "The member has not passed Discord's membership screening",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "pending",
                      "joinUrl": null
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 requests a minute from one IP address across every request a game makes, more than 140 a minute with one player session, more than 120 reads or 20 writes a minute with one player session, or more calls than the game as a whole is allowed. All of them carry the code `rate_limited`, `retryAfter` and the `Retry-After` header.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "CEEJAY's bot could not be reached, or was too busy, and nothing was done (`bot_unavailable`). Send the request again after `retryAfter` seconds.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botUnavailable": {
                    "summary": "The bot could not be reached",
                    "value": {
                      "error": "This could not be done right now and nothing was changed. Try again in a few seconds",
                      "code": "bot_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 5
                    }
                  }
                }
              }
            }
          },
          "504": {
            "description": "CEEJAY's bot did not respond in time (`bot_timeout`). Send the request again.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botTimeout": {
                    "summary": "The bot did not respond in time",
                    "value": {
                      "error": "The answer did not arrive in time, and the request may or may not have been done. Send the same request again: it is safe to repeat",
                      "code": "bot_timeout",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/player/balance": {
      "get": {
        "tags": [
          "Game loop"
        ],
        "summary": "Get the balance",
        "description": "Reads the member's wallet: coins, the daily reward streak, perks, an XP boost, and what this device may spend today.\n\n**Called with** a player session, sent as `Authorization: Bearer cjps1.…`.\n\n**What it needs.** The game loop must be enabled for the game (`hub_not_enabled` otherwise). The member has to be in the server (`membership_required`; a player whose member left or never joined is sent to the game's `joinUrl`), and must have consented to share their quests, rewards, shop and rank with the game (`sharing_needed`, with `manageUrl`, where the member can consent). Where the game loop is not enabled on this instance, every one of these requests returns `404` with the code `not_available`.\n\nEvery response is private to the player and includes `Cache-Control: no-store`. The responses contain only the fields listed here: no Discord ID, no avatar and nothing about another member, except the display names in the top list of Get the ranks.\n\n`spending` tells a game whether to enable a Buy button before the player presses it: `maySpend` is the result Buy a shop item would give, and `reason` explains why not (`spend_on_hold`, `verified_proof_required` or `spending_off`). `holdUntil` is the effective hold: the latest of the connection's hold, the device's hold and the device's age plus the game's hold hours.\n\nThe community's economy must be enabled (`economy_disabled`).\n\n**Limit:** 120 reads a minute for one player session.",
        "operationId": "getPlayerBalance",
        "x-caller": "player_session",
        "x-since": "1.1.0",
        "security": [
          {
            "PlayerSession": []
          }
        ],
        "responses": {
          "200": {
            "description": "The wallet.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerBalanceResponse"
                },
                "examples": {
                  "balance": {
                    "summary": "A member who can spend",
                    "value": {
                      "success": true,
                      "data": {
                        "balance": 1450,
                        "currency": {
                          "name": "Coins",
                          "emoji": "🪙"
                        },
                        "streak": 4,
                        "canCheckIn": true,
                        "nextCheckInAt": null,
                        "perks": {
                          "streakSaver": 1,
                          "questSkip": 0,
                          "priorityTicket": 0
                        },
                        "xpBoost": {
                          "multiplier": 1.5,
                          "expiresAt": "2026-10-09T10:00:00.000Z"
                        },
                        "spending": {
                          "dailyCap": 5000,
                          "spentToday": 300,
                          "remainingToday": 4700,
                          "resetsAt": "2026-10-09T00:00:00.000Z",
                          "holdUntil": null,
                          "maySpend": true,
                          "reason": null
                        }
                      }
                    }
                  },
                  "onHold": {
                    "summary": "A new device that is still on hold",
                    "value": {
                      "success": true,
                      "data": {
                        "balance": 1450,
                        "currency": {
                          "name": "Coins",
                          "emoji": "🪙"
                        },
                        "streak": 4,
                        "canCheckIn": true,
                        "nextCheckInAt": null,
                        "perks": {
                          "streakSaver": 1,
                          "questSkip": 0,
                          "priorityTicket": 0
                        },
                        "xpBoost": {
                          "multiplier": 1.5,
                          "expiresAt": "2026-10-09T10:00:00.000Z"
                        },
                        "spending": {
                          "dailyCap": 5000,
                          "spentToday": 300,
                          "remainingToday": 4700,
                          "resetsAt": "2026-10-09T00:00:00.000Z",
                          "holdUntil": "2026-10-09T18:06:40.000Z",
                          "maySpend": false,
                          "reason": "spend_on_hold"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The player session was not accepted: `invalid_player_session` (missing, or past its 15 minutes: renew it with Renew a player session), `wrong_key_kind` (another kind of token) or `connection_ended` (the connection or the device was ended: connect again).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPlayerSession": {
                    "summary": "No player session, or one that has expired",
                    "value": {
                      "error": "Invalid or expired player session",
                      "code": "invalid_player_session",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of token",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "connectionEnded": {
                    "summary": "The connection or the device was ended",
                    "value": {
                      "error": "This connection has ended",
                      "code": "connection_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request includes an `Origin` header that is not on the game's allowed list (`origin_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "This instance does not offer Connect or the game loop yet (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "The game loop is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The game is paused (`game_paused`), does not show the community's quests, shop and rank (`hub_not_enabled`), the member has not consented to share them with the game (`sharing_needed`, with `manageUrl`), or the member is not in the server (`membership_required`, with `membership` and, for a member who left or never joined, `joinUrl`). Or the community has disabled its economy (`economy_disabled`). Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "hubNotEnabled": {
                    "summary": "The game does not show the quests, shop and rank",
                    "value": {
                      "error": "This game does not show the community's quests, shop and rank",
                      "code": "hub_not_enabled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "sharingNeeded": {
                    "summary": "The member must consent first",
                    "value": {
                      "error": "The member has not consented to sharing their data with this game",
                      "code": "sharing_needed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "manageUrl": "https://ceejay.communitystudios.xyz/connect/mine"
                    }
                  },
                  "notInServer": {
                    "summary": "The member is not in the server",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "left",
                      "joinUrl": "https://discord.gg/harbourrugby"
                    }
                  },
                  "screening": {
                    "summary": "The member has not passed Discord's membership screening",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "pending",
                      "joinUrl": null
                    }
                  },
                  "economyDisabled": {
                    "summary": "The economy is off",
                    "value": {
                      "error": "The community has switched its economy off",
                      "code": "economy_disabled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 requests a minute from one IP address across every request a game makes, more than 140 a minute with one player session, more than 120 reads or 20 writes a minute with one player session, or more calls than the game as a whole is allowed. All of them carry the code `rate_limited`, `retryAfter` and the `Retry-After` header.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "CEEJAY's bot could not be reached, or was too busy, and nothing was done (`bot_unavailable`). Send the request again after `retryAfter` seconds.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botUnavailable": {
                    "summary": "The bot could not be reached",
                    "value": {
                      "error": "This could not be done right now and nothing was changed. Try again in a few seconds",
                      "code": "bot_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 5
                    }
                  }
                }
              }
            }
          },
          "504": {
            "description": "CEEJAY's bot did not respond in time (`bot_timeout`). Send the request again.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botTimeout": {
                    "summary": "The bot did not respond in time",
                    "value": {
                      "error": "The answer did not arrive in time, and the request may or may not have been done. Send the same request again: it is safe to repeat",
                      "code": "bot_timeout",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/player/checkin": {
      "post": {
        "tags": [
          "Game loop"
        ],
        "summary": "Check in",
        "description": "Claims the member's daily reward, as the daily command does in Discord. This request takes no body.\n\n**Called with** a player session, sent as `Authorization: Bearer cjps1.…`.\n\n**What it needs.** The game loop must be enabled for the game (`hub_not_enabled` otherwise). The member has to be in the server (`membership_required`; a player whose member left or never joined is sent to the game's `joinUrl`), and must have consented to share their quests, rewards, shop and rank with the game (`sharing_needed`, with `manageUrl`, where the member can consent). Where the game loop is not enabled on this instance, every one of these requests returns `404` with the code `not_available`.\n\nEvery response is private to the player and includes `Cache-Control: no-store`. The responses contain only the fields listed here: no Discord ID, no avatar and nothing about another member, except the display names in the top list of Get the ranks.\n\nA reward claimed too soon returns `409` with the code `checkin_cooldown`, with `nextCheckInAt` and `retryAfter`: treat it as \"already claimed\". A retry after a lost response gets the same result, so the request is safe to repeat; a game should not retry it automatically. It earns coins and requires no proof and no spending hold: the member could claim it in Discord. Nothing is sent to the member in a direct message.\n\nThe community's economy must be enabled (`economy_disabled`).\n\n**Limit:** 20 writes a minute for one player session.",
        "operationId": "checkInPlayer",
        "x-caller": "player_session",
        "x-since": "1.1.0",
        "security": [
          {
            "PlayerSession": []
          }
        ],
        "responses": {
          "200": {
            "description": "The daily reward is claimed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckInPlayerResponse"
                },
                "examples": {
                  "checkedIn": {
                    "summary": "The daily reward",
                    "value": {
                      "success": true,
                      "data": {
                        "reward": 50,
                        "streak": 5,
                        "balance": 1500,
                        "nextCheckInAt": "2026-10-09T10:00:00.000Z",
                        "savedByStreakSaver": false
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not a JSON object. Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notObject": {
                    "summary": "The body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The player session was not accepted: `invalid_player_session` (missing, or past its 15 minutes: renew it with Renew a player session), `wrong_key_kind` (another kind of token) or `connection_ended` (the connection or the device was ended: connect again).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPlayerSession": {
                    "summary": "No player session, or one that has expired",
                    "value": {
                      "error": "Invalid or expired player session",
                      "code": "invalid_player_session",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of token",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "connectionEnded": {
                    "summary": "The connection or the device was ended",
                    "value": {
                      "error": "This connection has ended",
                      "code": "connection_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request includes an `Origin` header that is not on the game's allowed list (`origin_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "This instance does not offer Connect or the game loop yet (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "The game loop is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The game is paused (`game_paused`), does not show the community's quests, shop and rank (`hub_not_enabled`), the member has not consented to share them with the game (`sharing_needed`, with `manageUrl`), or the member is not in the server (`membership_required`, with `membership` and, for a member who left or never joined, `joinUrl`). Or the community has disabled its economy (`economy_disabled`), or the daily reward has already been claimed (`checkin_cooldown`). Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "hubNotEnabled": {
                    "summary": "The game does not show the quests, shop and rank",
                    "value": {
                      "error": "This game does not show the community's quests, shop and rank",
                      "code": "hub_not_enabled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "sharingNeeded": {
                    "summary": "The member must consent first",
                    "value": {
                      "error": "The member has not consented to sharing their data with this game",
                      "code": "sharing_needed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "manageUrl": "https://ceejay.communitystudios.xyz/connect/mine"
                    }
                  },
                  "notInServer": {
                    "summary": "The member is not in the server",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "left",
                      "joinUrl": "https://discord.gg/harbourrugby"
                    }
                  },
                  "screening": {
                    "summary": "The member has not passed Discord's membership screening",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "pending",
                      "joinUrl": null
                    }
                  },
                  "economyDisabled": {
                    "summary": "The economy is off",
                    "value": {
                      "error": "The community has switched its economy off",
                      "code": "economy_disabled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "cooldown": {
                    "summary": "The daily reward was claimed already",
                    "value": {
                      "error": "The daily reward was already claimed. Try again later",
                      "code": "checkin_cooldown",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "nextCheckInAt": "2026-10-09T10:00:00.000Z",
                      "retryAfter": 43200
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 16 KB.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "tooLarge": {
                    "summary": "A body over 16 KB",
                    "value": {
                      "error": "The request body is larger than 16 KB",
                      "code": "body_too_large",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 requests a minute from one IP address across every request a game makes, more than 140 a minute with one player session, more than 120 reads or 20 writes a minute with one player session, or more calls than the game as a whole is allowed. All of them carry the code `rate_limited`, `retryAfter` and the `Retry-After` header.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "CEEJAY's bot could not be reached, or was too busy, and nothing was done (`bot_unavailable`). Send the request again after `retryAfter` seconds.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botUnavailable": {
                    "summary": "The bot could not be reached",
                    "value": {
                      "error": "This could not be done right now and nothing was changed. Try again in a few seconds",
                      "code": "bot_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 5
                    }
                  }
                }
              }
            }
          },
          "504": {
            "description": "CEEJAY's bot did not respond in time, so it is not known whether the request was done (`bot_timeout`). Send the same request again: it is safe to repeat.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botTimeout": {
                    "summary": "The bot did not respond in time",
                    "value": {
                      "error": "The answer did not arrive in time, and the request may or may not have been done. Send the same request again: it is safe to repeat",
                      "code": "bot_timeout",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/player/surveys": {
      "get": {
        "tags": [
          "Game loop"
        ],
        "summary": "List surveys",
        "description": "Lists the surveys the community shows in this game that the player can answer now. A survey is listed when the community's team switched on **In game** for it, for this game or for every game; it is taking responses; it is open to the member (a survey limited to a role waits until CEEJAY can read the member's roles); the member has not finished it; and, for a survey that waits for a game event, the game has logged that event for the player the number of times the team set since the survey opened (with Log player events). Show a survey when it fits the game: after a match, or from a menu.\n\nEach question comes with its `key`: send the answers back by key with Answer a survey. A question's `type` says how to ask it: `text_short`, `text_long`, `number`, `email` and `url` take text; `select_string` and `radio` take one of `options`; `checkbox` takes several; `rating` and `nps` take a score from `options` (`scale` holds the words at each end); `scale` takes one of five opinions; `yes_no` takes `Yes` or `No`. A survey with a required question only Discord can ask (a member picker, a file) is not listed.\n\n**Called with** a player session, sent as `Authorization: Bearer cjps1.…`.\n\n**What it needs.** The game loop must be enabled for the game (`hub_not_enabled` otherwise). The member has to be in the server (`membership_required`; a player whose member left or never joined is sent to the game's `joinUrl`), and must have consented to share their quests, rewards, shop and rank with the game (`sharing_needed`, with `manageUrl`, where the member can consent). Where the game loop is not enabled on this instance, every one of these requests returns `404` with the code `not_available`.\n\nEvery response is private to the player and includes `Cache-Control: no-store`. The responses contain only the fields listed here: no Discord ID, no avatar and nothing about another member.\n\n**Limit:** 120 reads a minute for one player session.",
        "operationId": "listPlayerSurveys",
        "x-caller": "player_session",
        "x-since": "1.2.0",
        "security": [
          {
            "PlayerSession": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PlayerLoopLimit"
          },
          {
            "$ref": "#/components/parameters/PlayerLoopCursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of surveys.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListPlayerSurveysResponse"
                },
                "examples": {
                  "surveys": {
                    "summary": "One survey with three questions",
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "id": 41,
                          "title": "Tonight's playtest",
                          "description": "Three quick questions about the build you played.",
                          "questions": [
                            {
                              "key": "k_00000000000000a1",
                              "label": "How would you rate the build?",
                              "type": "rating",
                              "required": true,
                              "placeholder": null,
                              "description": null,
                              "options": [
                                {
                                  "label": "1",
                                  "value": "1"
                                },
                                {
                                  "label": "2",
                                  "value": "2"
                                },
                                {
                                  "label": "3",
                                  "value": "3"
                                },
                                {
                                  "label": "4",
                                  "value": "4"
                                },
                                {
                                  "label": "5",
                                  "value": "5"
                                }
                              ],
                              "minLength": null,
                              "maxLength": null,
                              "minChoices": null,
                              "maxChoices": null,
                              "scale": {
                                "low": "Poor",
                                "high": "Great",
                                "icon": "number"
                              }
                            },
                            {
                              "key": "k_00000000000000a2",
                              "label": "Which map did you play most?",
                              "type": "radio",
                              "required": false,
                              "placeholder": null,
                              "description": null,
                              "options": [
                                {
                                  "label": "Harbor",
                                  "value": "Harbor"
                                },
                                {
                                  "label": "Foundry",
                                  "value": "Foundry"
                                }
                              ],
                              "minLength": null,
                              "maxLength": null,
                              "minChoices": null,
                              "maxChoices": null,
                              "scale": null
                            },
                            {
                              "key": "k_00000000000000a3",
                              "label": "What should we change?",
                              "type": "text_long",
                              "required": false,
                              "placeholder": null,
                              "description": null,
                              "options": [],
                              "minLength": null,
                              "maxLength": 1000,
                              "minChoices": null,
                              "maxChoices": null,
                              "scale": null
                            }
                          ],
                          "closesAt": null,
                          "rewards": {
                            "xp": 50,
                            "roles": 0,
                            "names": [
                              "20 coins"
                            ]
                          }
                        }
                      ],
                      "page": {
                        "nextCursor": null,
                        "limit": 50
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`limit` or `cursor` is not one this request takes. Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "badLimit": {
                    "summary": "A page size that is not a count, or above 100",
                    "value": {
                      "error": "limit must be a whole number from 1 to 100",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "limit"
                    }
                  },
                  "badCursor": {
                    "summary": "A cursor that was not issued by the list",
                    "value": {
                      "error": "cursor is not one this list gave. Send page.nextCursor as it was answered",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "cursor"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The player session was not accepted: `invalid_player_session` (missing, or past its 15 minutes: renew it with Renew a player session), `wrong_key_kind` (another kind of token) or `connection_ended` (the connection or the device was ended: connect again).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPlayerSession": {
                    "summary": "No player session, or one that has expired",
                    "value": {
                      "error": "Invalid or expired player session",
                      "code": "invalid_player_session",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of token",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "connectionEnded": {
                    "summary": "The connection or the device was ended",
                    "value": {
                      "error": "This connection has ended",
                      "code": "connection_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request includes an `Origin` header that is not on the game's allowed list (`origin_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "This instance does not offer Connect or the game loop yet (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "The game loop is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The game is paused (`game_paused`), does not show the community's quests, shop and rank (`hub_not_enabled`), the member has not consented to share them with the game (`sharing_needed`, with `manageUrl`), or the member is not in the server (`membership_required`, with `membership` and, for a member who left or never joined, `joinUrl`). Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "hubNotEnabled": {
                    "summary": "The game does not show the quests, shop and rank",
                    "value": {
                      "error": "This game does not show the community's quests, shop and rank",
                      "code": "hub_not_enabled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "sharingNeeded": {
                    "summary": "The member must consent first",
                    "value": {
                      "error": "The member has not consented to sharing their data with this game",
                      "code": "sharing_needed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "manageUrl": "https://ceejay.communitystudios.xyz/connect/mine"
                    }
                  },
                  "notInServer": {
                    "summary": "The member is not in the server",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "left",
                      "joinUrl": "https://discord.gg/harbourrugby"
                    }
                  },
                  "screening": {
                    "summary": "The member has not passed Discord's membership screening",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "pending",
                      "joinUrl": null
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 requests a minute from one IP address across every request a game makes, more than 140 a minute with one player session, more than 120 reads or 20 writes a minute with one player session, or more calls than the game as a whole is allowed. All of them carry the code `rate_limited`, `retryAfter` and the `Retry-After` header.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "CEEJAY's bot could not be reached, or was too busy, and nothing was done (`bot_unavailable`). Send the request again after `retryAfter` seconds.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botUnavailable": {
                    "summary": "The bot could not be reached",
                    "value": {
                      "error": "This could not be done right now and nothing was changed. Try again in a few seconds",
                      "code": "bot_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 5
                    }
                  }
                }
              }
            }
          },
          "504": {
            "description": "CEEJAY's bot did not respond in time (`bot_timeout`). Send the request again.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botTimeout": {
                    "summary": "The bot did not respond in time",
                    "value": {
                      "error": "The answer did not arrive in time, and the request may or may not have been done. Send the same request again: it is safe to repeat",
                      "code": "bot_timeout",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/player/surveys/answer": {
      "post": {
        "tags": [
          "Game loop"
        ],
        "summary": "Answer a survey",
        "description": "Sends the player's answers to a survey from List surveys, by question key. CEEJAY checks every answer by the rules Discord and the Member Hub use: a required question needs an answer, a number must be a number, an email address and a link must be one, a pick must be one of the question's `options`, and a score must be on its scale. The answers are saved to the member's response, and on the first completion CEEJAY grants the survey's rewards: its XP, its roles and its reward list. CEEJAY does not send the member a direct message.\n\nA second answer to a survey that lets members edit their answers updates them and grants nothing (`updated: true`). A survey that does not returns `survey_unavailable` with the reason `already_answered`. The request is safe to repeat and takes no key.\n\n**Called with** a player session, sent as `Authorization: Bearer cjps1.…`.\n\n**What it needs.** The game loop must be enabled for the game (`hub_not_enabled` otherwise). The member has to be in the server (`membership_required`; a player whose member left or never joined is sent to the game's `joinUrl`), and must have consented to share their quests, rewards, shop and rank with the game (`sharing_needed`, with `manageUrl`, where the member can consent). Where the game loop is not enabled on this instance, every one of these requests returns `404` with the code `not_available`.\n\nEvery response is private to the player and includes `Cache-Control: no-store`. The responses contain only the fields listed here: no Discord ID, no avatar and nothing about another member.\n\n**Limit:** 20 writes a minute for one player session.",
        "operationId": "answerPlayerSurvey",
        "x-caller": "player_session",
        "x-since": "1.2.0",
        "security": [
          {
            "PlayerSession": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AnswerPlayerSurveyRequest"
              },
              "examples": {
                "answers": {
                  "summary": "Answers by question key",
                  "value": {
                    "surveyId": 41,
                    "answers": {
                      "k_00000000000000a1": "4",
                      "k_00000000000000a2": "Harbor",
                      "k_00000000000000a3": "More maps, please"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answers were saved.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerSurveyAnswerResponse"
                },
                "examples": {
                  "first": {
                    "summary": "The first completion: the rewards",
                    "value": {
                      "success": true,
                      "data": {
                        "surveyId": 41,
                        "updated": false,
                        "rewards": {
                          "xp": 50,
                          "roles": 0,
                          "names": [
                            "20 coins"
                          ]
                        }
                      }
                    }
                  },
                  "updated": {
                    "summary": "Answers updated: nothing granted",
                    "value": {
                      "success": true,
                      "data": {
                        "surveyId": 41,
                        "updated": true,
                        "rewards": null
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`surveyId` or `answers` is missing or is not the right kind, an answer is not the right kind for its question (`bad_request` with `field: \"answers\"` and the question's key in `question`; the message names the question and the rule), or the body is not a JSON object. Nothing was changed.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notObject": {
                    "summary": "The body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badSurveyId": {
                    "summary": "surveyId is not a whole number",
                    "value": {
                      "error": "surveyId must be a whole number from 1 to 2147483647",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "surveyId"
                    }
                  },
                  "answersNotObject": {
                    "summary": "answers is not an object",
                    "value": {
                      "error": "answers must be an object of answers by question key",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "answers"
                    }
                  },
                  "wrongAnswer": {
                    "summary": "An answer is not one its question takes",
                    "value": {
                      "error": "How would you rate the build?: pick one of the options.",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "answers",
                      "question": "k_00000000000000a1"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The player session was not accepted: `invalid_player_session` (missing, or past its 15 minutes: renew it with Renew a player session), `wrong_key_kind` (another kind of token) or `connection_ended` (the connection or the device was ended: connect again).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalidPlayerSession": {
                    "summary": "No player session, or one that has expired",
                    "value": {
                      "error": "Invalid or expired player session",
                      "code": "invalid_player_session",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "wrongKeyKind": {
                    "summary": "Another kind of token",
                    "value": {
                      "error": "This request takes another kind of key",
                      "code": "wrong_key_kind",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "connectionEnded": {
                    "summary": "The connection or the device was ended",
                    "value": {
                      "error": "This connection has ended",
                      "code": "connection_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The game cannot be used from here: the community removed CEEJAY (`community_not_active`), the game is made for children (`audience_not_allowed`), or the request includes an `Origin` header that is not on the game's allowed list (`origin_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "audienceNotAllowed": {
                    "summary": "A game made for children",
                    "value": {
                      "error": "A game made for children cannot connect players or send player data",
                      "code": "audience_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "originNotAllowed": {
                    "summary": "An origin that is not on the game's list",
                    "value": {
                      "error": "This origin is not on the list saved for this game",
                      "code": "origin_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "This instance does not offer Connect or the game loop yet (`not_available`, with `feature: \"players\"`), or the game was removed (`game_not_found`). Or the survey does not exist in this community (`survey_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "The game loop is not enabled on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "players"
                    }
                  },
                  "gameNotFound": {
                    "summary": "The game was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "surveyNotFound": {
                    "summary": "The survey does not exist",
                    "value": {
                      "error": "Survey not found",
                      "code": "survey_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "The operation does not accept this HTTP method.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "wrongMethod": {
                    "summary": "Wrong method",
                    "value": {
                      "error": "Method not allowed",
                      "code": "method_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The game is paused (`game_paused`), does not show the community's quests, shop and rank (`hub_not_enabled`), the member has not consented to share them with the game (`sharing_needed`, with `manageUrl`), or the member is not in the server (`membership_required`, with `membership` and, for a member who left or never joined, `joinUrl`). Or a rule of the quest is not met (`quest_not_active`, `quest_not_started`, `quest_ended`, `quest_already_completed`, `quest_limit_reached`, `quest_requires_role`, `quest_requires_level`, `quest_requires_season_xp`, `quest_requires_program`, `quest_requires_quest`, `quest_full`, `quest_cooling_down` with `availableAt`, or `quest_daily_limit_reached`; `message` is a sentence the game can show), or the answer is wrong (`quest_task_not_done`), or the task cannot be answered this way (`quest_task_unavailable`). Nothing was changed. Or the survey cannot be answered now (`survey_unavailable`, with a `reason` and a `message` to show: `not_open`, `closed`, `paused`, `role`, `already_answered`, `not_due` (its game event has not been logged enough times yet), `answer_in_discord` or `not_here` (not shown in this game)).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "hubNotEnabled": {
                    "summary": "The game does not show the quests, shop and rank",
                    "value": {
                      "error": "This game does not show the community's quests, shop and rank",
                      "code": "hub_not_enabled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "sharingNeeded": {
                    "summary": "The member must consent first",
                    "value": {
                      "error": "The member has not consented to sharing their data with this game",
                      "code": "sharing_needed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "manageUrl": "https://ceejay.communitystudios.xyz/connect/mine"
                    }
                  },
                  "notInServer": {
                    "summary": "The member is not in the server",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "left",
                      "joinUrl": "https://discord.gg/harbourrugby"
                    }
                  },
                  "screening": {
                    "summary": "The member has not passed Discord's membership screening",
                    "value": {
                      "error": "The member has to be in the server to use this",
                      "code": "membership_required",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "membership": "pending",
                      "joinUrl": null
                    }
                  },
                  "surveyClosed": {
                    "summary": "The survey has closed",
                    "value": {
                      "error": "This survey cannot be answered right now",
                      "code": "survey_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "closed",
                      "message": "This survey has closed."
                    }
                  },
                  "surveyNotDue": {
                    "summary": "The survey's game event has not been logged enough times yet",
                    "value": {
                      "error": "This survey cannot be answered right now",
                      "code": "survey_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "reason": "not_due",
                      "message": "This survey isn't available yet."
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 16 KB.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "tooLarge": {
                    "summary": "A body over 16 KB",
                    "value": {
                      "error": "The request body is larger than 16 KB",
                      "code": "body_too_large",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "More than 600 requests a minute from one IP address across every request a game makes, more than 140 a minute with one player session, more than 120 reads or 20 writes a minute with one player session, or more calls than the game as a whole is allowed. All of them carry the code `rate_limited`, `retryAfter` and the `Retry-After` header.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side. Retry with backoff.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "failed": {
                    "summary": "Server error",
                    "value": {
                      "error": "Internal server error",
                      "code": "internal_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "CEEJAY's bot could not be reached, or was too busy, and nothing was done (`bot_unavailable`). Send the request again after `retryAfter` seconds.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botUnavailable": {
                    "summary": "The bot could not be reached",
                    "value": {
                      "error": "This could not be done right now and nothing was changed. Try again in a few seconds",
                      "code": "bot_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 5
                    }
                  }
                }
              }
            }
          },
          "504": {
            "description": "CEEJAY's bot did not respond in time, so it is not known whether the request was done (`bot_timeout`). Send the same request again: it is safe to repeat.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "botTimeout": {
                    "summary": "The bot did not respond in time",
                    "value": {
                      "error": "The answer did not arrive in time, and the request may or may not have been done. Send the same request again: it is safe to repeat",
                      "code": "bot_timeout",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "PublicKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ceejay_pk_<48 hexadecimal characters>",
        "description": "The public key of one game, sent as `Authorization: Bearer ceejay_pk_…`. It is made when the game is set up for the community and ships inside the game: it is not a secret. A game can have up to 5 active keys, so a new build can carry a new key before the old one is retired. A retired or suspended key returns `401` immediately."
      },
      "PlayerSession": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "cjps1.<payload>.<signature>",
        "description": "A player session, sent as `Authorization: Bearer cjps1.…`. A game receives one in the result of a confirmed connect session and from Renew a player session. It is valid for 15 minutes and contains no Discord ID."
      }
    },
    "headers": {
      "RetryAfter": {
        "description": "Whole seconds to wait before retrying the request.",
        "schema": {
          "type": "integer",
          "minimum": 0
        },
        "example": 34
      },
      "RateLimitLimit": {
        "description": "The limit this response was counted against: `600`, the requests a minute for one IP address across every request a game makes. Once the player session is read, the limit of that session: `140` on Get the player's profile, and on the requests of the game loop `120` (a read) or `20` (a write).",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 600
        },
        "example": 600
      },
      "RateLimitRemaining": {
        "description": "Requests left in the current window of that limit. Never below 0.",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "maximum": 600
        },
        "example": 596
      },
      "RateLimitReset": {
        "description": "Whole seconds until the current window ends, from 1 to 60.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 60
        },
        "example": 42
      },
      "RequestId": {
        "description": "The ID CEEJAY gave this request: `req_` and 24 hexadecimal characters. An error body includes the same value as `requestId`. Include it when you contact support about a request.",
        "schema": {
          "type": "string",
          "pattern": "^req_[0-9a-f]{24}$"
        },
        "example": "req_5f0c2a9b7d3e41f8a6b0c1d2"
      },
      "IdempotentReplayed": {
        "description": "`true` when this response replays an earlier request with the same `idempotencyKey`: nothing was done this time. Missing otherwise. A game that runs in a browser cannot read this header: read `duplicate` in the body.",
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        },
        "example": "true"
      }
    },
    "parameters": {
      "PlayerLoopLimit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "The page size: an integer from 1 to 100. `0`, an empty value or omitting it defaults to 50. A value above 100, a negative number, a fraction or text is rejected with `400`.",
        "schema": {
          "type": "integer",
          "format": "int32",
          "minimum": 0,
          "maximum": 100,
          "default": 50
        },
        "example": 50
      },
      "PlayerLoopCursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "Where the page starts: pass the `page.nextCursor` value from the previous response unchanged. Omit it for the first page. An invalid cursor is rejected with `400`.",
        "schema": {
          "type": "string",
          "maxLength": 512
        },
        "example": "WzUwXQ"
      }
    },
    "schemas": {
      "PlayerId": {
        "type": "string",
        "pattern": "^[A-Za-z0-9][A-Za-z0-9._:@|+=-]{0,127}$",
        "description": "The game's own ID for a player: 1 to 128 characters, starting with a letter or a digit, then letters, digits and `. _ : @ | + = -`. Always a string. Encode an ID that contains any other character the same way every time (base64url, for example). Matched case-insensitively, stored as sent.",
        "examples": [
          "p_19"
        ]
      },
      "ErrorCode": {
        "type": "string",
        "description": "The fixed code of an error. Every code here is one of the closed list of the CEEJAY API (`ErrorCode` in `openapi.json`): a code is never renamed or removed. Match on the code, not on the status or on the message text in `error`.",
        "enum": [
          "method_not_allowed",
          "rate_limited",
          "internal_error",
          "bad_request",
          "not_available",
          "bad_player_id",
          "bad_proof",
          "age_answer_required",
          "unknown_return",
          "wrong_key_kind",
          "invalid_public_key",
          "key_retired",
          "key_suspended",
          "proof_rejected",
          "invalid_session_secret",
          "invalid_refresh_token",
          "refresh_reused",
          "invalid_player_session",
          "connection_ended",
          "community_not_active",
          "audience_not_allowed",
          "under_age",
          "origin_not_allowed",
          "proof_required",
          "game_not_found",
          "code_not_found",
          "session_not_found",
          "game_paused",
          "session_locked",
          "session_changed",
          "nothing_to_confirm",
          "player_blocked",
          "player_taken",
          "result_gone",
          "too_many_attempts",
          "proof_service_unavailable",
          "quest_not_found",
          "purchase_not_found",
          "quest_not_active",
          "quest_already_completed",
          "quest_limit_reached",
          "insufficient_balance",
          "quest_not_started",
          "quest_ended",
          "quest_requires_role",
          "quest_requires_level",
          "quest_requires_season_xp",
          "quest_requires_program",
          "quest_requires_quest",
          "quest_full",
          "quest_cooling_down",
          "quest_daily_limit_reached",
          "idempotency_key_reused",
          "idempotency_in_progress",
          "idempotency_outcome_unknown",
          "idempotency_unavailable",
          "bot_unavailable",
          "bot_timeout",
          "body_too_large",
          "hub_not_enabled",
          "sharing_needed",
          "membership_required",
          "spend_on_hold",
          "daily_spend_cap_reached",
          "verified_proof_required",
          "item_not_found",
          "purchase_refused",
          "price_changed",
          "not_claimable",
          "quest_task_not_done",
          "quest_task_unavailable",
          "checkin_cooldown",
          "economy_disabled",
          "survey_not_found",
          "survey_unavailable"
        ]
      },
      "ErrorResponse": {
        "type": "object",
        "description": "The body of every failed request: a sentence for a person, a fixed code for your program, and the ID of the request. Some codes add a field.",
        "required": [
          "error",
          "code",
          "requestId"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A sentence saying what was wrong. Its words can change: do not test it."
          },
          "code": {
            "$ref": "#/components/schemas/ErrorCode"
          },
          "requestId": {
            "type": "string",
            "pattern": "^req_[0-9a-f]{24}$",
            "description": "The ID of this request, the same as the `X-Request-Id` header. Include it when you contact support about a request."
          },
          "field": {
            "type": "string",
            "description": "The request field that was at fault. Only with `bad_request`, and only when one field is at fault."
          },
          "retryAfter": {
            "type": "integer",
            "minimum": 0,
            "description": "Whole seconds to wait before retrying the request. With `rate_limited`, `too_many_attempts`, `bot_unavailable`, `spend_on_hold`, `daily_spend_cap_reached`, `checkin_cooldown` and `idempotency_in_progress`. The `Retry-After` header contains the same number."
          },
          "reason": {
            "type": "string",
            "description": "Why, for the codes that say: with `proof_rejected`, why the proof was not accepted: `signature`, `expired`, `wrong_player`, `wrong_project`, `used` (a pass or Steam ticket used before) or `too_old` (issued more than 10 minutes ago, on Get connection status). With `connection_ended` on Renew a player session, why: `disconnected`, `moved`, `signed_out` or `ended`. With `purchase_refused`: `not_on_sale`, `not_eligible`, `unavailable`, `out_of_stock` or `limit_reached`. With `quest_task_unavailable`: `already_done`, `in_review`, `not_for_games`, `wrong_action` or `needs_connect`."
          },
          "feature": {
            "type": "string",
            "const": "players",
            "description": "Only with `not_available`: `players`, the name of Connect."
          },
          "message": {
            "type": "string",
            "description": "A sentence about the rejection for the player, from the community's own rules, that a game can show as it is. With `purchase_refused`, `quest_task_not_done`, `quest_task_unavailable` and the codes of a quest's rules. Its words can change: do not test it."
          },
          "manageUrl": {
            "type": "string",
            "description": "Where the member consents again. With `sharing_needed`.",
            "format": "uri"
          },
          "membership": {
            "type": "string",
            "description": "The member's membership status in the server: `pending` (in the server, with Discord's membership screening still open), `left` or `not_member`. With `membership_required`.",
            "enum": [
              "pending",
              "left",
              "not_member"
            ]
          },
          "joinUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where to send a player who is not in the server: the URL saved for the game. `null` for a member who is in the server (`pending`), and when the game has none. With `membership_required`."
          },
          "holdUntil": {
            "type": [
              "string",
              "null"
            ],
            "description": "When this device can spend. With `spend_on_hold`.",
            "format": "date-time"
          },
          "cap": {
            "type": "integer",
            "description": "The game's daily spending limit for one member, in coins. With `daily_spend_cap_reached`.",
            "minimum": 0
          },
          "spentToday": {
            "type": "integer",
            "description": "Coins this member has spent through the game today. With `daily_spend_cap_reached`.",
            "minimum": 0
          },
          "price": {
            "type": "integer",
            "description": "The item's price in coins. With `daily_spend_cap_reached`, `price_changed` and `insufficient_balance`.",
            "minimum": 0
          },
          "expectedPrice": {
            "type": "integer",
            "description": "The price the request sent. With `price_changed`.",
            "minimum": 0
          },
          "balance": {
            "type": "number",
            "description": "The member's balance in coins. With `insufficient_balance`."
          },
          "resetsAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the day ends and the daily limit starts again: the next UTC midnight. With `daily_spend_cap_reached`."
          },
          "availableAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the player can try again. With `purchase_refused` (`limit_reached`) and `quest_cooling_down`."
          },
          "nextCheckInAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the daily reward can be claimed again. With `checkin_cooldown`."
          },
          "missingPlatform": {
            "type": "string",
            "description": "The platform whose linked account the member needs. With `purchase_refused` (`not_eligible`)."
          },
          "platform": {
            "type": "string",
            "description": "The platform the member has to link an account on, in Discord. With `quest_task_unavailable` (`needs_connect`)."
          },
          "status": {
            "type": "string",
            "description": "The purchase's current status, one of the values of `PurchaseStatus`. With `not_claimable`."
          },
          "question": {
            "type": "string",
            "description": "The question an item asks. With `bad_request` on Buy a shop item, when the item needs an `answer`."
          },
          "fields": {
            "type": "array",
            "description": "What a claim asks for in `input`. With `bad_request` on Claim an item.",
            "items": {
              "$ref": "#/components/schemas/PlayerClaimField"
            }
          }
        }
      },
      "MemberName": {
        "type": "object",
        "description": "The Discord member, as the game shows them to its player before it approves.",
        "required": [
          "displayName",
          "avatarUrl"
        ],
        "properties": {
          "displayName": {
            "type": [
              "string",
              "null"
            ],
            "description": "The member's name in Discord, or `null` when CEEJAY has none."
          },
          "avatarUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "The URL of the member's avatar, or `null`. A URL served by Discord contains the account's ID."
          }
        }
      },
      "StartConnectRequest": {
        "type": "object",
        "required": [
          "playerId"
        ],
        "properties": {
          "playerId": {
            "$ref": "#/components/schemas/PlayerId",
            "description": "The player, by the game's own ID."
          },
          "playerName": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 80,
            "description": "The player's name in the game, at most 80 characters. The member is shown it beside the ID. Ignored when a connect pass carries a name."
          },
          "proof": {
            "anyOf": [
              {
                "type": "object",
                "description": "Proof of who the player is.",
                "required": [
                  "type",
                  "token"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "pass",
                      "firebase",
                      "unity",
                      "steam"
                    ],
                    "description": "`pass`: a connect pass your own server created. `firebase`: a Firebase Authentication ID token. `unity`: a Unity Player Authentication access token. `steam`: a Steam Web API ticket (`GetAuthTicketForWebApi(\"ceejay\")`), checked with your publisher key; the player ID is the SteamID64. Use each ticket once."
                  },
                  "token": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 4096,
                    "description": "The pass or the token, as the game got it. At most 4,096 characters. It is never stored."
                  }
                }
              },
              {
                "type": "null"
              }
            ],
            "description": "Proof of who the player is: a connect pass from your own server, or the token your sign-in service gave the game. Without it the session is at the basic level: the game only states the ID."
          },
          "code": {
            "type": [
              "string",
              "null"
            ],
            "description": "A code the player created in Discord and entered in the game. Send it to attach this player to that member's session. Capitals, spaces and hyphens do not matter. A code works once."
          },
          "returnTo": {
            "type": [
              "string",
              "null"
            ],
            "description": "The key of one of the return URLs saved for the game. The connect page then offers \"Return to the game\" with that URL. A URL is never taken from a request: only a key."
          },
          "locale": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 12,
            "description": "The language of the connect page, as a language tag such as `en` or `pt-BR`. Without it the game's own default is used."
          },
          "underAge": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether the player is under the minimum age allowed to connect. Required for a game set up for a mixed audience. `true` returns `403` and stores nothing; with verified proof it also ends a connection the player has."
          }
        }
      },
      "StartedSession": {
        "type": "object",
        "description": "A connect session the game started, or the session of a member's code the game attached its player to.",
        "required": [
          "sessionId",
          "status",
          "url",
          "qrUrl",
          "code",
          "sessionSecret",
          "expiresAt",
          "pollIntervalSeconds",
          "proof",
          "alreadyConnected",
          "member"
        ],
        "properties": {
          "sessionId": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{22}$",
            "description": "The session's ID. It is the last part of `url`, and contains no secret."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "needs_game_confirmation",
              "approved"
            ],
            "description": "`pending`: open the link or show the code, and poll for the result. `needs_game_confirmation`: you sent a `code`, so show `member` to the player and send the game's decision (confirm or decline). `approved`: the player was already connected, and the result can be read immediately."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "The link the player opens to approve with their Discord account. `null` when you sent a `code`, and when the session was approved at once."
          },
          "qrUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "The URL of an SVG image of a QR code for `url`, for a console or a television. `null` when `url` is."
          },
          "code": {
            "type": [
              "string",
              "null"
            ],
            "description": "Six characters, shown as `K7M-2QX`, that the player enters at your dashboard URL's `/connect` page instead of opening the link. `null` when `url` is."
          },
          "sessionSecret": {
            "type": "string",
            "pattern": "^ceejay_cs_[0-9a-f]{64}$",
            "description": "What the game sends to read this session's result and to confirm it. Returned once: store it with the open session, in the same place as the refresh token, until the session ends."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the session ends unless it is finished: 10 minutes after it was created."
          },
          "pollIntervalSeconds": {
            "type": "integer",
            "description": "How many seconds to leave between two requests for the result: `3`."
          },
          "proof": {
            "type": "string",
            "enum": [
              "basic",
              "verified"
            ],
            "description": "What the proof you sent amounts to. `basic` with no proof, and for an anonymous sign-in."
          },
          "alreadyConnected": {
            "type": "boolean",
            "description": "`true` when the session was approved immediately: the proof is verified and fresh, and this player is already connected at the verified level with the member's own approval."
          },
          "member": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/MemberName"
              },
              {
                "type": "null"
              }
            ],
            "description": "Whose code it was, when you sent a `code`: show it to the player before the game approves. Also set when the session was approved at once. `null` otherwise."
          }
        }
      },
      "StartConnectResponse": {
        "type": "object",
        "description": "The session.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/StartedSession"
          }
        }
      },
      "SessionSecretRequest": {
        "type": "object",
        "required": [
          "sessionSecret"
        ],
        "properties": {
          "sessionSecret": {
            "type": "string",
            "maxLength": 100,
            "description": "The `sessionSecret` that Start connect returned: `ceejay_cs_` and 64 hexadecimal characters."
          }
        }
      },
      "ConfirmConnectRequest": {
        "type": "object",
        "required": [
          "sessionSecret",
          "accept"
        ],
        "properties": {
          "sessionSecret": {
            "type": "string",
            "maxLength": 100,
            "description": "The `sessionSecret` that Start connect returned: `ceejay_cs_` and 64 hexadecimal characters."
          },
          "accept": {
            "type": "boolean",
            "description": "`true` is the game's confirmation: the player was shown the Discord name and approved it. `false` ends an open session in any state, and is also the game's own Cancel."
          }
        }
      },
      "Connection": {
        "type": "object",
        "description": "The connection between the player and the Discord member.",
        "required": [
          "connectionId",
          "game",
          "playerId",
          "proof",
          "connectedAt",
          "moved",
          "alreadyConnected",
          "spendHoldUntil"
        ],
        "properties": {
          "connectionId": {
            "type": "string",
            "description": "The connection's ID, as a string of digits."
          },
          "game": {
            "type": "string",
            "description": "The game's slug."
          },
          "playerId": {
            "type": "string",
            "description": "The player, as it is stored."
          },
          "proof": {
            "type": "string",
            "enum": [
              "basic",
              "verified"
            ],
            "description": "The level of the connection."
          },
          "connectedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the connection was made."
          },
          "moved": {
            "type": "boolean",
            "description": "`true` in the result of the session in which the member moved from another player of this game to this one. Always `false` in Get the player's profile."
          },
          "alreadyConnected": {
            "type": "boolean",
            "description": "`true` in the result of a session that signed another device in to a connection that existed. Always `false` in Get the player's profile."
          },
          "spendHoldUntil": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Until when this device cannot spend the member's coins: the latest of the connection's hold, the device's own hold and the device's age plus the game's hold hours. `null` when nothing holds. It is the time Buy a shop item and Claim an item enforce, and the same time as `spending.holdUntil` in Get the balance, which is returned only while that time is still in the future."
          }
        }
      },
      "MemberCard": {
        "type": "object",
        "description": "What a game is told about the Discord member a player is connected to. These fields and no others.",
        "required": [
          "displayName",
          "nickname",
          "avatarUrl",
          "level",
          "xp",
          "coins",
          "roles",
          "membership",
          "joinUrl"
        ],
        "properties": {
          "displayName": {
            "type": [
              "string",
              "null"
            ],
            "description": "The member's name in Discord."
          },
          "nickname": {
            "type": [
              "string",
              "null"
            ],
            "description": "The member's nickname in the community's server, or `null`."
          },
          "avatarUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "The URL of the member's avatar. Discord avatar URLs contain the user ID."
          },
          "level": {
            "type": "integer",
            "minimum": 0,
            "description": "The member's level in the community."
          },
          "xp": {
            "type": "number",
            "minimum": 0,
            "description": "The member's XP in the community."
          },
          "coins": {
            "type": "number",
            "description": "The member's currency balance in the community."
          },
          "roles": {
            "type": "array",
            "description": "The roles the member has among those the community chose to share with this game. Any other role is never returned.",
            "items": {
              "type": "object",
              "required": [
                "id",
                "name"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "description": "The role's ID."
                },
                "name": {
                  "type": "string",
                  "description": "The role's name."
                }
              }
            }
          },
          "membership": {
            "type": "string",
            "enum": [
              "member",
              "pending",
              "not_member",
              "left"
            ],
            "description": "`member`: in the server. `pending`: in the server, with Discord's membership screening still open. `not_member`: never joined. `left`: was in the server and is not now. For the last two no player session is issued, and `joinUrl` says where to send the player."
          },
          "joinUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where to send a player who is not in the server: the URL saved for the game. `null` for a member, and when the game has none."
          }
        }
      },
      "Join": {
        "type": "object",
        "description": "Whether the member asked to join the server while connecting, and what came of it.",
        "required": [
          "asked",
          "result",
          "failure"
        ],
        "properties": {
          "asked": {
            "type": "boolean",
            "description": "`true` when the member ticked \"Also join\" on the connect page."
          },
          "result": {
            "type": "string",
            "enum": [
              "joined",
              "already_member",
              "not_asked",
              "declined",
              "failed"
            ],
            "description": "`joined`, `already_member` or `failed` when `asked` is `true`. Otherwise `not_asked` (joining was not offered) or `declined` (it was offered and not ticked)."
          },
          "failure": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "server_full",
              "member_limit",
              "discord_refused",
              "unavailable",
              null
            ],
            "description": "Why joining failed, or `null`."
          }
        }
      },
      "Sharing": {
        "type": "object",
        "description": "What the member has consented to share with the community's team and with the game. `hub` is the community's quests, rewards, shop and rank: it is part of connecting for a game that has the game loop switched on, so the member is not offered it as a choice. It is `needed` again whenever the game switches the loop on, raises its daily spending limit or adds a shop item, until the member gives consent at `manageUrl`.",
        "required": [
          "agreed",
          "needed"
        ],
        "properties": {
          "agreed": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "progress",
                "activity",
                "purchases",
                "location",
                "hub"
              ]
            },
            "description": "The categories the member has consented to."
          },
          "needed": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "progress",
                "activity",
                "purchases",
                "location",
                "hub"
              ]
            },
            "description": "Categories the game has added since the member consented. They are not shared for this member until they give consent at `manageUrl`."
          }
        }
      },
      "Reward": {
        "type": "object",
        "description": "What the connection earned. CEEJAY's bot grants the reward for connecting shortly after the member approves, so a new connection reads `pending` at first: poll again in a few seconds. `items` lists what was granted once `status` is `paid`.",
        "required": [
          "status",
          "items"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "paid",
              "pending",
              "none",
              "not_member",
              "needs_verified_proof",
              "already_paid",
              "waiting_screening"
            ],
            "description": "The status of the reward. `pending`: the bot has not finished processing this connection yet, so poll again in a few seconds. `paid`: the reward was granted to the member, and `items` says what it was. `none`: no reward was granted and none will be: the game has no reward for connecting, or the reward ended without being granted. `already_paid`: this member, or this player, was rewarded before, and a reward is granted once. `needs_verified_proof`: the game rewards only verified connections, and this one was not. `not_member`: the member is not in the server, and no reward is granted for joining. `waiting_screening`: the member has not passed Discord's membership screening yet; CEEJAY checks again every 15 minutes for 7 days. This list may grow: handle unknown values."
          },
          "items": {
            "type": "array",
            "description": "What was granted. Empty unless `status` is `paid`.",
            "items": {
              "type": "object",
              "required": [
                "type",
                "label",
                "amount"
              ],
              "properties": {
                "type": {
                  "type": "string",
                  "description": "The kind of reward."
                },
                "label": {
                  "type": "string",
                  "description": "The reward in words."
                },
                "amount": {
                  "type": "number",
                  "description": "How much."
                }
              }
            }
          }
        }
      },
      "PlayerSessionToken": {
        "type": "object",
        "description": "A player session: the short token a game sends with the requests it makes for a connected player.",
        "required": [
          "token",
          "expiresAt"
        ],
        "properties": {
          "token": {
            "type": "string",
            "pattern": "^cjps1\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$",
            "description": "The token. Send it as `Authorization: Bearer cjps1.…`. Treat it as opaque."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the token stops working: 15 minutes after it was made. Renew it before then."
          }
        }
      },
      "WaitingResult": {
        "type": "object",
        "description": "The session is open and waiting for the member.",
        "required": [
          "status",
          "expiresAt"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "needs_confirmation"
            ],
            "description": "`pending`: waiting for the member to approve on the connect page. `needs_confirmation`: the game confirmed a session a member started in Discord, and the member confirms there. Poll again."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the session ends unless it is finished."
          }
        }
      },
      "NeedsGameConfirmationResult": {
        "type": "object",
        "description": "The session is waiting for the game to confirm.",
        "required": [
          "status",
          "expiresAt",
          "member",
          "alreadyConnected"
        ],
        "properties": {
          "status": {
            "type": "string",
            "const": "needs_game_confirmation",
            "description": "Show `member` to the player with options to confirm or cancel, and send Confirm connect."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the session ends unless it is finished."
          },
          "member": {
            "$ref": "#/components/schemas/MemberName",
            "description": "Who opted in on the Discord side, or whose code it was."
          },
          "alreadyConnected": {
            "type": "boolean",
            "description": "`true` when this Discord account is already connected to this player, so the confirmation signs another device in."
          }
        }
      },
      "EndedResult": {
        "type": "object",
        "description": "The session ended without a connection.",
        "required": [
          "status",
          "reason"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "declined",
              "cancelled",
              "expired",
              "locked",
              "refused"
            ],
            "description": "How it ended. Go back to \"not connected\"."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "member_cancelled",
              "game_declined",
              "banned",
              "player_blocked",
              "player_already_connected",
              "expired",
              "too_many_tries",
              "under_age",
              null
            ],
            "description": "Why, or `null` when the session was superseded by a newer one for the same player, or the member connected another player in the meantime. `member_cancelled`: the member pressed Cancel or kept the player they had. `game_declined`: the game declined. `banned`: the Discord account is banned from the server. `player_blocked`: the community blocked the player. `player_already_connected`: the player is connected to another Discord account and could not be moved. `expired`: the 10 minutes passed. `too_many_tries`: the code was entered incorrectly five times."
          }
        }
      },
      "ApprovedResult": {
        "type": "object",
        "description": "Both sides approved and the connection is saved.",
        "required": [
          "status",
          "connection",
          "member",
          "join",
          "sharing",
          "reward",
          "manageUrl",
          "playerSession",
          "refreshToken",
          "refreshExpiresAt"
        ],
        "properties": {
          "status": {
            "type": "string",
            "const": "approved",
            "description": "Connected."
          },
          "connection": {
            "$ref": "#/components/schemas/Connection"
          },
          "member": {
            "$ref": "#/components/schemas/MemberCard"
          },
          "join": {
            "$ref": "#/components/schemas/Join"
          },
          "sharing": {
            "$ref": "#/components/schemas/Sharing"
          },
          "reward": {
            "$ref": "#/components/schemas/Reward"
          },
          "manageUrl": {
            "type": "string",
            "format": "uri",
            "description": "Where the member manages their connections with nothing but their Discord account: sharing, devices and Disconnect."
          },
          "playerSession": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PlayerSessionToken"
              },
              {
                "type": "null"
              }
            ],
            "description": "A player session, or `null` when the member is not in the server (`membership` is `not_member` or `left`)."
          },
          "refreshToken": {
            "type": "string",
            "pattern": "^ceejay_rt_\\d{1,18}\\.\\d{1,9}\\.[0-9a-f]{64}$",
            "description": "The refresh token of this device. Keep it in the device's secure store. Each read of an approved result returns a new one and invalidates the one returned before, so keep the last."
          },
          "refreshExpiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the refresh token stops working unless it is used: 90 days after its last use."
          }
        }
      },
      "ConnectResultResponse": {
        "type": "object",
        "description": "The session, by its `status`.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/WaitingResult"
              },
              {
                "$ref": "#/components/schemas/NeedsGameConfirmationResult"
              },
              {
                "$ref": "#/components/schemas/EndedResult"
              },
              {
                "$ref": "#/components/schemas/ApprovedResult"
              }
            ],
            "description": "One of four forms, told apart by `status`."
          }
        }
      },
      "ConnectionStatusRequest": {
        "type": "object",
        "required": [
          "playerId",
          "proof"
        ],
        "properties": {
          "playerId": {
            "$ref": "#/components/schemas/PlayerId",
            "description": "The player to ask about."
          },
          "proof": {
            "type": "object",
            "description": "The token your sign-in service gave the game for this player, issued in the last 10 minutes. A connect pass is not accepted here.",
            "required": [
              "type",
              "token"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "firebase",
                  "unity",
                  "steam"
                ],
                "description": "`firebase`: a Firebase Authentication ID token. `unity`: a Unity Player Authentication access token. `steam`: a Steam Web API ticket (`GetAuthTicketForWebApi(\"ceejay\")`), checked with your publisher key; the player ID is the SteamID64. Use each ticket once."
              },
              "token": {
                "type": "string",
                "minLength": 1,
                "maxLength": 4096,
                "description": "The pass or the token, as the game got it. At most 4,096 characters. It is never stored."
              }
            }
          }
        }
      },
      "ConnectionStatusResponse": {
        "type": "object",
        "description": "Whether the player is connected.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "type": "object",
            "required": [
              "connected"
            ],
            "properties": {
              "connected": {
                "type": "boolean",
                "description": "`true` when a Discord account is connected to this player in this game. Nothing about the member is told."
              }
            }
          }
        }
      },
      "RefreshTokenRequest": {
        "type": "object",
        "required": [
          "refreshToken"
        ],
        "properties": {
          "refreshToken": {
            "type": "string",
            "description": "The refresh token the device holds: `ceejay_rt_`, then three parts separated by full stops."
          }
        }
      },
      "RenewedSession": {
        "type": "object",
        "description": "A new player session and the next refresh token.",
        "required": [
          "playerSession",
          "refreshToken",
          "refreshExpiresAt",
          "membership",
          "joinUrl",
          "sharing",
          "manageUrl"
        ],
        "properties": {
          "playerSession": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PlayerSessionToken"
              },
              {
                "type": "null"
              }
            ],
            "description": "A player session, or `null` when the member is not in the server."
          },
          "refreshToken": {
            "type": "string",
            "pattern": "^ceejay_rt_\\d{1,18}\\.\\d{1,9}\\.[0-9a-f]{64}$",
            "description": "The next refresh token. Store it before you use the new player session: it replaces the one you sent."
          },
          "refreshExpiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the refresh token stops working unless it is used: 90 days from now."
          },
          "membership": {
            "type": "string",
            "enum": [
              "member",
              "pending",
              "not_member",
              "left"
            ],
            "description": "`member`: in the server. `pending`: in the server, with Discord's membership screening still open. `not_member`: never joined. `left`: was in the server and is not now. For the last two no player session is issued, and `joinUrl` says where to send the player."
          },
          "joinUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where to send a player who is not in the server. `null` for a member, and when the game has no address saved."
          },
          "sharing": {
            "$ref": "#/components/schemas/Sharing"
          },
          "manageUrl": {
            "type": "string",
            "format": "uri",
            "description": "Where the member manages their connections."
          }
        }
      },
      "PlayerSessionResponse": {
        "type": "object",
        "description": "The renewed session.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/RenewedSession"
          }
        }
      },
      "PlayerProfile": {
        "type": "object",
        "description": "The connection and the member card of the player a player session is for.",
        "required": [
          "connection",
          "member",
          "sharing",
          "reward",
          "hub",
          "manageUrl"
        ],
        "properties": {
          "connection": {
            "$ref": "#/components/schemas/Connection"
          },
          "member": {
            "$ref": "#/components/schemas/MemberCard"
          },
          "sharing": {
            "$ref": "#/components/schemas/Sharing"
          },
          "reward": {
            "$ref": "#/components/schemas/Reward"
          },
          "hub": {
            "type": "object",
            "description": "Whether this game shows the community's quests, shop and rank.",
            "required": [
              "enabled"
            ],
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "`true` when the game has the game loop switched on and this instance can serve it. When it is `false` every request of the Game loop group returns `409` with the code `hub_not_enabled`, or `404` with the code `not_available`."
              }
            }
          },
          "manageUrl": {
            "type": "string",
            "format": "uri",
            "description": "Where the member manages their connections."
          }
        }
      },
      "PlayerProfileResponse": {
        "type": "object",
        "description": "The player's profile.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/PlayerProfile"
          }
        }
      },
      "DisconnectResponse": {
        "type": "object",
        "description": "The connection is ended.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "type": "object",
            "required": [
              "disconnected"
            ],
            "properties": {
              "disconnected": {
                "type": "boolean",
                "const": true,
                "description": "Always `true`: the connection is ended, or had ended already."
              }
            }
          }
        }
      },
      "IdempotencyKey": {
        "type": "string",
        "minLength": 1,
        "maxLength": 128,
        "description": "A key that makes a retry safe: 1 to 128 characters, chosen by the game, sent in the JSON body (an `Idempotency-Key` header is not read). A key that contains a null character (U+0000) is rejected with `400`. A key belongs to the member, the game and the connection: a player who moves to another player of the game, or connects again, starts a new set of keys, and another member's key is never this one. A key is retained for 30 days.",
        "examples": [
          "buy-2026-10-08-gold-skin-1"
        ]
      },
      "Page": {
        "type": "object",
        "description": "Pagination details for a list response. It is returned alongside `data`.",
        "required": [
          "nextCursor",
          "limit"
        ],
        "properties": {
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Send it as `cursor` to get the next page. `null` when this is the last page."
          },
          "limit": {
            "type": "integer",
            "description": "The page size that was used.",
            "minimum": 0
          }
        }
      },
      "PurchaseStatus": {
        "type": "string",
        "enum": [
          "claimable",
          "pending",
          "delivered",
          "refunded",
          "rejected",
          "expired",
          "failed"
        ],
        "description": "Where a purchase stands. `claimable`: bought, and the player has to claim it (Claim an item) before it can be delivered. `pending`: on its way; `waitingFor` says who it waits for. `delivered`: the item has been fulfilled, and may still be active or used. `refunded`: the coins went back. `rejected`: the community's team turned the purchase down and kept the coins (a rejection that gives them back reads `refunded`). `expired`: an item with a time limit that has run out. `failed`: it could not be delivered. Treat the list as open: a new value can appear."
      },
      "WaitingFor": {
        "type": "string",
        "enum": [
          "team",
          "studio",
          "delivery",
          "link"
        ],
        "description": "Who a `pending` purchase waits for. `team`: the community's team has to fulfil it or approve it. `studio`: the game's own server answered that it will finish later. `delivery`: CEEJAY is delivering it and tries again if that fails. `link`: the player has to link an account on a platform first. `null` when nothing is waited for. Treat the list as open: a new value can appear."
      },
      "PlayerDelivery": {
        "type": "object",
        "description": "How the delivery of a purchase stands.",
        "required": [
          "state",
          "message"
        ],
        "properties": {
          "state": {
            "type": "string",
            "description": "`delivered`: fulfilled. `pending`: on its way. `refunded`: it could not be delivered and the coins went back, so the balance already shows them. `rejected`: the community's team turned the purchase down and kept the coins (the balance does not change). Treat the list as open: a new value can appear.",
            "enum": [
              "delivered",
              "pending",
              "refunded",
              "rejected"
            ]
          },
          "message": {
            "type": [
              "string",
              "null"
            ],
            "description": "A fixed sentence about the delivery, or `null`. It is never the studio's own error text."
          }
        }
      },
      "PlayerProgress": {
        "type": "object",
        "description": "How far a quest or a task has come.",
        "required": [
          "current",
          "target"
        ],
        "properties": {
          "current": {
            "type": "integer",
            "description": "Where the player is.",
            "minimum": 0
          },
          "target": {
            "type": "integer",
            "description": "What the quest or the task asks for.",
            "minimum": 0
          }
        }
      },
      "PlayerSpending": {
        "type": "object",
        "description": "The player's spending through this game today. The day is the UTC day.",
        "required": [
          "dailyCap",
          "spentToday",
          "remainingToday",
          "resetsAt"
        ],
        "properties": {
          "dailyCap": {
            "type": "integer",
            "description": "The most coins this member can spend through this game in a UTC day. `0` means spending is switched off for the game.",
            "minimum": 0
          },
          "spentToday": {
            "type": "integer",
            "description": "Coins spent through this game since the day began. A refunded purchase is not counted, because its coins went back; a purchase the team rejected without giving the coins back still counts.",
            "minimum": 0
          },
          "remainingToday": {
            "type": "integer",
            "description": "What is left of `dailyCap` today.",
            "minimum": 0
          },
          "resetsAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the day ends and the count starts again: the next UTC midnight."
          }
        }
      },
      "PlayerQuestAnswer": {
        "type": "object",
        "description": "What a task asks the player to type or pick. One object for the three kinds: `text` has `question` and `minLength`, `number` has `question`, `min` and `max`, `scale` has `question` and `scale`.",
        "required": [
          "kind",
          "question"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "description": "`text`, `number` or `scale`.",
            "enum": [
              "text",
              "number",
              "scale"
            ]
          },
          "question": {
            "type": [
              "string",
              "null"
            ],
            "description": "The question the task asks."
          },
          "minLength": {
            "type": [
              "integer",
              "null"
            ],
            "description": "For `text`: the fewest characters an answer needs, or `null` for none.",
            "minimum": 1,
            "maximum": 1000
          },
          "min": {
            "type": [
              "number",
              "null"
            ],
            "description": "For `number`: the smallest answer, or `null` for none."
          },
          "max": {
            "type": [
              "number",
              "null"
            ],
            "description": "For `number`: the largest answer, or `null` for none."
          },
          "scale": {
            "type": "integer",
            "description": "For `scale`: the top of the rating, from 2 to 10. The player picks a whole number from 1 to it.",
            "minimum": 2,
            "maximum": 10
          }
        }
      },
      "PlayerQuestTask": {
        "type": "object",
        "description": "One task of a quest, with how the player stands with it and how a game finishes it.",
        "required": [
          "id",
          "position",
          "type",
          "title",
          "description",
          "verification",
          "status",
          "progress",
          "url",
          "action",
          "answer",
          "statusLine"
        ],
        "properties": {
          "id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The task's ID, a whole number. `null` for a quest made before tasks existed: it has one task, and Verify a quest task and Answer a quest task then take no `taskId`."
          },
          "position": {
            "type": "integer",
            "description": "The task's place in the quest, counted from 0.",
            "minimum": 0
          },
          "type": {
            "type": "string",
            "description": "The kind of task, for example `send_messages` or `text_answer`. A game can use it to pick an icon."
          },
          "title": {
            "type": "string",
            "description": "The task's name."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the task asks for, or `null`."
          },
          "verification": {
            "type": "string",
            "description": "How the task is checked: `automatic` (CEEJAY checks it) or `manual` (the community's team reviews it)."
          },
          "status": {
            "type": "string",
            "description": "Where the task stands for this player. `available`: not started. `in_progress`, `pending_review` (with the team), `completed` or `rejected`.",
            "enum": [
              "available",
              "in_progress",
              "pending_review",
              "completed",
              "rejected"
            ]
          },
          "progress": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PlayerProgress"
              },
              {
                "type": "null"
              }
            ],
            "description": "For a task that counts (messages sent, minutes in voice): how far it has come. `null` for any other task."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "A public address the task sends the player to (a social post, a link), or `null`. It is a plain address, never a signed link."
          },
          "action": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "verify",
              "answer",
              "rate",
              "discord",
              null
            ],
            "description": "What the game offers for this task. `verify`: a button that calls Verify a quest task. `answer`: a text or number field that calls Answer a quest task. `rate`: a rating that calls Answer a quest task. `discord`: the task has to be finished in Discord (a proof upload, a quiz, a social follow, anything the team reviews): show that, and do not offer to finish it. `null`: the task completes by itself from what the member does in the server."
          },
          "answer": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PlayerQuestAnswer"
              },
              {
                "type": "null"
              }
            ],
            "description": "For `answer` and `rate`: the question and its bounds. `null` for any other task."
          },
          "statusLine": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where the task stands in one short line (\"4/10 messages\", \"In review\"), or `null`."
          }
        }
      },
      "PlayerQuest": {
        "type": "object",
        "description": "A quest, with how the player stands with it. Names follow List quests of the secret-key API (`xpReward`, `currencyReward`).",
        "required": [
          "id",
          "title",
          "iconEmoji",
          "description",
          "category",
          "xpReward",
          "currencyReward",
          "badge",
          "endsAt",
          "status",
          "progress",
          "locked",
          "lockedReason",
          "lockCheck",
          "taskCount",
          "tasksDone",
          "tasks"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "The quest's ID, a whole number. Use it as `questId`.",
            "minimum": 1
          },
          "title": {
            "type": "string",
            "description": "The quest's name."
          },
          "iconEmoji": {
            "type": [
              "string",
              "null"
            ],
            "description": "The emoji shown with the quest, or `null`."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the quest asks for, or `null`."
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "The quest's category, or `null`."
          },
          "xpReward": {
            "type": "integer",
            "description": "XP awarded on completion. `0` for none.",
            "minimum": 0
          },
          "currencyReward": {
            "type": "integer",
            "description": "Coins given on completion. `0` for none.",
            "minimum": 0
          },
          "badge": {
            "type": [
              "string",
              "null"
            ],
            "description": "The name of the badge the quest awards, or `null`."
          },
          "endsAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the quest closes, or `null` for a quest with no end."
          },
          "status": {
            "type": "string",
            "description": "Where the quest stands for this player. `available`: not started. `in_progress`. `pending_review`: with the team. `completed`.",
            "enum": [
              "available",
              "in_progress",
              "pending_review",
              "completed"
            ]
          },
          "progress": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PlayerProgress"
              },
              {
                "type": "null"
              }
            ],
            "description": "The bar of the quest card: for a quest with one task, that task's numbers; for a quest with several, the tasks done of the tasks in all. `null` where there is nothing to count."
          },
          "locked": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "`true` when the player cannot start the quest because of a role or a level they lack, `false` when nothing stops them. `null` when CEEJAY cannot tell yet: see `lockCheck`."
          },
          "lockedReason": {
            "type": [
              "string",
              "null"
            ],
            "description": "When `locked` is `true`: why, in a sentence the game can show (\"Requires level 10\"). Otherwise `null`."
          },
          "lockCheck": {
            "type": "string",
            "description": "How `locked` was worked out. `known`: CEEJAY judged it. `checked_on_start`: the quest needs a role and CEEJAY has not seen the member in its cache of the server, so `locked` is `null`: show the quest as startable and let Start a quest decide. Nothing is shown locked for a member CEEJAY has merely not seen.",
            "enum": [
              "known",
              "checked_on_start"
            ]
          },
          "taskCount": {
            "type": "integer",
            "description": "How many tasks the quest has.",
            "minimum": 1
          },
          "tasksDone": {
            "type": "integer",
            "description": "How many of them the player has completed.",
            "minimum": 0
          },
          "tasks": {
            "type": "array",
            "description": "The quest's tasks, in order.",
            "items": {
              "$ref": "#/components/schemas/PlayerQuestTask"
            }
          }
        }
      },
      "PlayerQuestStart": {
        "type": "object",
        "description": "The quest that was started.",
        "required": [
          "questId",
          "status",
          "alreadyStarted"
        ],
        "properties": {
          "questId": {
            "type": "integer",
            "description": "The quest's ID."
          },
          "status": {
            "type": "string",
            "description": "`in_progress`.",
            "const": "in_progress"
          },
          "alreadyStarted": {
            "type": "boolean",
            "description": "`true` when the player already had this quest open: nothing changed. A retry after a lost response returns this."
          }
        }
      },
      "PlayerQuestRewards": {
        "type": "object",
        "description": "What finishing the quest paid. Sent only when the last task of the quest was finished.",
        "required": [
          "xp",
          "coins",
          "badge",
          "roleGranted",
          "leveledUp",
          "newLevel"
        ],
        "properties": {
          "xp": {
            "type": "integer",
            "description": "XP given.",
            "minimum": 0
          },
          "coins": {
            "type": "integer",
            "description": "Coins given.",
            "minimum": 0
          },
          "badge": {
            "type": [
              "string",
              "null"
            ],
            "description": "The name of the badge awarded, or `null`."
          },
          "roleGranted": {
            "type": "boolean",
            "description": "`true` when the quest gave the member a Discord role."
          },
          "leveledUp": {
            "type": "boolean",
            "description": "`true` when the XP took the member to a new level."
          },
          "newLevel": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The new level when `leveledUp` is `true`, otherwise `null`.",
            "minimum": 1
          }
        }
      },
      "PlayerQuestTaskResult": {
        "type": "object",
        "description": "The task that was finished, and where the quest stands.",
        "required": [
          "questId",
          "taskId",
          "verified",
          "questCompleted",
          "tasksDone",
          "taskCount",
          "rewards"
        ],
        "properties": {
          "questId": {
            "type": "integer",
            "description": "The quest's ID."
          },
          "taskId": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The task's ID, or `null` for a quest made before tasks existed."
          },
          "verified": {
            "type": "boolean",
            "description": "Always `true`: the task is done. A task that is not done returns `409` with the code `quest_task_not_done`.",
            "const": true
          },
          "questCompleted": {
            "type": "boolean",
            "description": "`true` when this was the last task and the quest is now complete."
          },
          "tasksDone": {
            "type": "integer",
            "description": "How many tasks of the quest the player has completed.",
            "minimum": 0
          },
          "taskCount": {
            "type": "integer",
            "description": "How many tasks the quest has.",
            "minimum": 1
          },
          "rewards": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PlayerQuestRewards"
              },
              {
                "type": "null"
              }
            ],
            "description": "What the quest paid when `questCompleted` is `true`. `null` while tasks remain."
          }
        }
      },
      "PlayerShopItemState": {
        "type": "object",
        "description": "Whether this player can buy the item now.",
        "required": [
          "canBuy",
          "reason",
          "availableAt"
        ],
        "properties": {
          "canBuy": {
            "type": "boolean",
            "description": "`true` when the player can buy the item now. This is a hint for a Buy button: Buy a shop item judges again."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "When `canBuy` is `false`: why, in a sentence the game can show. `null` otherwise. When spending is switched off for the game it reads \"Spending is switched off for this game\"."
          },
          "availableAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the player can buy it again, for an item with a limit per member. Otherwise `null`."
          }
        }
      },
      "PlayerShopItem": {
        "type": "object",
        "description": "A shop item this game may sell, with whether the player can buy it.",
        "required": [
          "id",
          "name",
          "description",
          "price",
          "imageUrl",
          "itemType",
          "deliveryMode",
          "stockLeft",
          "limitLabel",
          "availabilityLabel",
          "eligibilityLabels",
          "question",
          "titleMaxLength",
          "requiredPlatforms",
          "memberState"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "The item's ID, a whole number. Use it as `itemId`.",
            "minimum": 1
          },
          "name": {
            "type": "string",
            "description": "The item's name."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the item is, or `null`."
          },
          "price": {
            "type": "integer",
            "description": "The price in coins.",
            "minimum": 0
          },
          "imageUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "The URL of the item's image, or `null`.",
            "format": "uri"
          },
          "itemType": {
            "type": "string",
            "description": "The kind of item, for example `api`."
          },
          "deliveryMode": {
            "type": "string",
            "description": "How the item reaches the player. `instant`: at once. `claim`: the player claims it after buying (it asks for a pick or a name). `team`: the community's team fulfils it.",
            "enum": [
              "instant",
              "claim",
              "team"
            ]
          },
          "stockLeft": {
            "type": [
              "integer",
              "null"
            ],
            "description": "How many are left, or `null` for an item with no stock limit.",
            "minimum": 0
          },
          "limitLabel": {
            "type": [
              "string",
              "null"
            ],
            "description": "A line about how many one member can buy (\"1 per member\"), or `null`."
          },
          "availabilityLabel": {
            "type": [
              "string",
              "null"
            ],
            "description": "A line about when the item can be bought, or `null`."
          },
          "eligibilityLabels": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What a member needs to buy it, one line each (\"Level 5\"). Empty for none."
          },
          "question": {
            "type": [
              "string",
              "null"
            ],
            "description": "A question the item asks before it is bought, or `null`. Send the answer as `answer` in Buy a shop item."
          },
          "titleMaxLength": {
            "type": [
              "integer",
              "null"
            ],
            "description": "For an item that asks the player to name it: the longest name, or `null`.",
            "minimum": 1
          },
          "requiredPlatforms": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Platforms the member needs a linked account on to buy it. Empty for none."
          },
          "memberState": {
            "$ref": "#/components/schemas/PlayerShopItemState"
          }
        }
      },
      "PlayerPurchase": {
        "type": "object",
        "description": "The purchase that was made, and what it left of the day's spending.",
        "required": [
          "purchaseId",
          "itemId",
          "itemName",
          "pricePaid",
          "balance",
          "status",
          "waitingFor",
          "claimNeeded",
          "delivery",
          "duplicate",
          "spending"
        ],
        "properties": {
          "purchaseId": {
            "type": "integer",
            "description": "The purchase's ID, a whole number. Use it as `purchaseId` in Claim an item.",
            "minimum": 1
          },
          "itemId": {
            "type": "integer",
            "description": "The item's ID."
          },
          "itemName": {
            "type": "string",
            "description": "The item's name."
          },
          "pricePaid": {
            "type": "integer",
            "description": "The coins deducted.",
            "minimum": 0
          },
          "balance": {
            "type": "integer",
            "description": "The member's balance after the purchase. On a repeat it is the balance now.",
            "minimum": 0
          },
          "status": {
            "$ref": "#/components/schemas/PurchaseStatus"
          },
          "waitingFor": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/WaitingFor"
              },
              {
                "type": "null"
              }
            ],
            "description": "Who must act before a `pending` purchase completes, or `null`."
          },
          "claimNeeded": {
            "type": "boolean",
            "description": "`true` for an item the player has to claim (Claim an item) because it asks for a pick or a name."
          },
          "delivery": {
            "$ref": "#/components/schemas/PlayerDelivery"
          },
          "duplicate": {
            "type": "boolean",
            "description": "`true` when this response replays an earlier request with the same `idempotencyKey`: nothing was bought a second time."
          },
          "spending": {
            "$ref": "#/components/schemas/PlayerSpending"
          }
        }
      },
      "PlayerClaimField": {
        "type": "object",
        "description": "One thing a claim asks the player for.",
        "required": [
          "key",
          "kind",
          "label",
          "max"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "The key to send in `input` of Claim an item."
          },
          "kind": {
            "type": "string",
            "description": "The kind of value: `text`, `role` (the player picks one of `claim.options.roles`), `giveaway` (one of `claim.options.giveaways`), `color` (a colour such as `#f94801`) or `player` (the name the player has in the game). Treat the list as open: a new kind can appear."
          },
          "label": {
            "type": "string",
            "description": "What to call it in the game."
          },
          "max": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The longest value, in characters, or `null`.",
            "minimum": 1
          }
        }
      },
      "PlayerClaimRole": {
        "type": "object",
        "description": "A role the player can pick.",
        "required": [
          "id",
          "name",
          "color"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The role's Discord ID."
          },
          "name": {
            "type": "string",
            "description": "The role's name."
          },
          "color": {
            "type": [
              "string",
              "null"
            ],
            "description": "The role's colour as `#rrggbb`, or `null`."
          }
        }
      },
      "PlayerClaimGiveaway": {
        "type": "object",
        "description": "A giveaway the player can enter.",
        "required": [
          "id",
          "title",
          "endsAt"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "The giveaway's ID."
          },
          "title": {
            "type": "string",
            "description": "The giveaway's name."
          },
          "endsAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it ends, or `null`."
          }
        }
      },
      "PlayerClaimOptions": {
        "type": "object",
        "description": "The choices a claim offers, at most 25 of each. Read from CEEJAY's cache of the server, so a role it has not seen is left out.",
        "required": [],
        "properties": {
          "roles": {
            "type": "array",
            "description": "For an item that gives a role of the player's pick.",
            "items": {
              "$ref": "#/components/schemas/PlayerClaimRole"
            },
            "maxItems": 25
          },
          "giveaways": {
            "type": "array",
            "description": "For an item that enters a giveaway of the player's pick.",
            "items": {
              "$ref": "#/components/schemas/PlayerClaimGiveaway"
            },
            "maxItems": 25
          }
        }
      },
      "PlayerClaimInfo": {
        "type": "object",
        "description": "What the player has to do to claim the item.",
        "required": [
          "needsInput",
          "fields",
          "options"
        ],
        "properties": {
          "needsInput": {
            "type": "boolean",
            "description": "`true` when the claim needs `input`."
          },
          "fields": {
            "type": "array",
            "description": "What to ask for. Empty when `needsInput` is `false`.",
            "items": {
              "$ref": "#/components/schemas/PlayerClaimField"
            }
          },
          "options": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PlayerClaimOptions"
              },
              {
                "type": "null"
              }
            ],
            "description": "The choices a claim offers, or `null` for an item with none."
          }
        }
      },
      "PlayerLockerItem": {
        "type": "object",
        "description": "One purchase the player owns through this game.",
        "required": [
          "id",
          "itemId",
          "itemName",
          "itemType",
          "imageUrl",
          "pricePaid",
          "status",
          "waitingFor",
          "statusLabel",
          "createdAt",
          "claimedAt",
          "expiresAt",
          "claim",
          "hasCode"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "The purchase's ID, a whole number. Use it as `purchaseId` in Claim an item.",
            "minimum": 1
          },
          "itemId": {
            "type": "integer",
            "description": "The item's ID."
          },
          "itemName": {
            "type": "string",
            "description": "The item's name."
          },
          "itemType": {
            "type": "string",
            "description": "The kind of item."
          },
          "imageUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "The URL of the item's image, or `null`.",
            "format": "uri"
          },
          "pricePaid": {
            "type": "integer",
            "description": "The coins that were paid.",
            "minimum": 0
          },
          "status": {
            "$ref": "#/components/schemas/PurchaseStatus"
          },
          "waitingFor": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/WaitingFor"
              },
              {
                "type": "null"
              }
            ],
            "description": "Who must act before a `pending` purchase completes, or `null`."
          },
          "statusLabel": {
            "type": "string",
            "description": "Where the purchase stands in a few words (\"Ready to claim\"). A game can show it as it is."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "When it was bought."
          },
          "claimedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the player claimed it, or `null`."
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it can no longer be claimed, or `null` for no end."
          },
          "claim": {
            "$ref": "#/components/schemas/PlayerClaimInfo"
          },
          "hasCode": {
            "type": "boolean",
            "description": "`true` when the purchase holds a code. The code is never sent: the member reads it in Discord."
          }
        }
      },
      "PlayerClaim": {
        "type": "object",
        "description": "The purchase that was claimed.",
        "required": [
          "purchaseId",
          "claimed",
          "alreadyClaimed",
          "itemName",
          "status",
          "waitingFor",
          "delivery"
        ],
        "properties": {
          "purchaseId": {
            "type": "integer",
            "description": "The purchase's ID."
          },
          "claimed": {
            "type": "boolean",
            "description": "Always `true`: the purchase is claimed now or was before.",
            "const": true
          },
          "alreadyClaimed": {
            "type": "boolean",
            "description": "`true` when the purchase was claimed before this request: nothing changed. A retry after a lost response returns this."
          },
          "itemName": {
            "type": "string",
            "description": "The item's name."
          },
          "status": {
            "$ref": "#/components/schemas/PurchaseStatus"
          },
          "waitingFor": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/WaitingFor"
              },
              {
                "type": "null"
              }
            ],
            "description": "Who must act before a `pending` purchase completes, or `null`."
          },
          "delivery": {
            "$ref": "#/components/schemas/PlayerDelivery"
          }
        }
      },
      "PlayerTier": {
        "type": "object",
        "description": "The rank tier a level belongs to.",
        "required": [
          "name",
          "color"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "The tier's name."
          },
          "color": {
            "type": [
              "string",
              "null"
            ],
            "description": "The tier's colour as `#rrggbb`, or `null`."
          }
        }
      },
      "PlayerRankProgress": {
        "type": "object",
        "description": "How far the player is to the next level.",
        "required": [
          "current",
          "needed",
          "percent"
        ],
        "properties": {
          "current": {
            "type": "number",
            "minimum": 0,
            "description": "XP earned towards the next level."
          },
          "needed": {
            "type": "number",
            "minimum": 0,
            "description": "XP the next level takes."
          },
          "percent": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "description": "`current` as a share of `needed`, from 0 to 100."
          }
        }
      },
      "PlayerRankMe": {
        "type": "object",
        "description": "The player's own place.",
        "required": [
          "xp",
          "level",
          "tier",
          "rank",
          "communityRank",
          "progress"
        ],
        "properties": {
          "xp": {
            "type": "number",
            "minimum": 0,
            "description": "The member's XP in the community."
          },
          "level": {
            "type": "integer",
            "description": "The member's level in the community.",
            "minimum": 0
          },
          "tier": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PlayerTier"
              },
              {
                "type": "null"
              }
            ],
            "description": "The member's tier, or `null` where it is not known."
          },
          "rank": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The member's place in the list of this game's players that `top` is cut from, counted from 1. `null` when the member is not in that list (no XP yet).",
            "minimum": 1
          },
          "communityRank": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The member's place among everyone in the community, counted from 1, or `null` when it is not known.",
            "minimum": 1
          },
          "progress": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PlayerRankProgress"
              },
              {
                "type": "null"
              }
            ],
            "description": "How far to the next level, or `null` where it is not known."
          }
        }
      },
      "PlayerRankEntry": {
        "type": "object",
        "description": "One player in the top list: a display name, a level and XP. Never a Discord ID or an avatar.",
        "required": [
          "rank",
          "displayName",
          "level",
          "xp"
        ],
        "properties": {
          "rank": {
            "type": "integer",
            "description": "The place in the list, counted from 1.",
            "minimum": 1
          },
          "displayName": {
            "type": "string",
            "description": "The member's display name. \"A member\" where CEEJAY has no name."
          },
          "level": {
            "type": "integer",
            "description": "The member's level.",
            "minimum": 0
          },
          "xp": {
            "type": "number",
            "minimum": 0,
            "description": "The member's XP."
          }
        }
      },
      "PlayerRanks": {
        "type": "object",
        "description": "The player's place and the top of this game's players. The list holds only the game's own players who agreed to share it, are in the server, are not banned and are not behind a hidden leaderboard role.",
        "required": [
          "me",
          "top"
        ],
        "properties": {
          "me": {
            "$ref": "#/components/schemas/PlayerRankMe"
          },
          "top": {
            "type": "array",
            "description": "The top players of this game, highest XP first.",
            "items": {
              "$ref": "#/components/schemas/PlayerRankEntry"
            },
            "maxItems": 20
          }
        }
      },
      "PlayerCurrency": {
        "type": "object",
        "description": "The community's currency.",
        "required": [
          "name",
          "emoji"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Its name, for example `Coins`."
          },
          "emoji": {
            "type": [
              "string",
              "null"
            ],
            "description": "The emoji shown with it, or `null`."
          }
        }
      },
      "PlayerPerks": {
        "type": "object",
        "description": "The perks the member holds, as counts.",
        "required": [
          "streakSaver",
          "questSkip",
          "priorityTicket"
        ],
        "properties": {
          "streakSaver": {
            "type": "integer",
            "description": "Streak savers: one protects the daily reward streak when a day is missed.",
            "minimum": 0
          },
          "questSkip": {
            "type": "integer",
            "description": "Quest skips. A game cannot use them.",
            "minimum": 0
          },
          "priorityTicket": {
            "type": "integer",
            "description": "Priority tickets for support.",
            "minimum": 0
          }
        }
      },
      "PlayerXpBoost": {
        "type": "object",
        "description": "An XP boost the member holds.",
        "required": [
          "multiplier",
          "expiresAt"
        ],
        "properties": {
          "multiplier": {
            "type": "number",
            "minimum": 1,
            "description": "What XP is multiplied by, for example `1.5`."
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the boost ends, or `null` for none."
          }
        }
      },
      "PlayerBalanceSpending": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PlayerSpending"
          },
          {
            "type": "object",
            "description": "Whether this device may spend now.",
            "required": [
              "holdUntil",
              "maySpend",
              "reason"
            ],
            "properties": {
              "holdUntil": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "The time before which this device cannot spend: the latest of the connection's hold, the device's own hold and the device's age plus the game's hold hours. `null` when there is none."
              },
              "maySpend": {
                "type": "boolean",
                "description": "`true` when this device can buy and claim now. Use it to grey a Buy button before the player presses it."
              },
              "reason": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "spend_on_hold",
                  "verified_proof_required",
                  "spending_off",
                  null
                ],
                "description": "When `maySpend` is `false`: why. `spend_on_hold`: a new connection or device is still waiting. `verified_proof_required`: this device connected without proof and the game only lets proven players spend. `spending_off`: the game's daily limit is `0`. `null` otherwise."
              }
            }
          }
        ],
        "description": "The day's spending, and whether this device may spend now. The same judgement Buy a shop item makes."
      },
      "PlayerBalance": {
        "type": "object",
        "description": "The member's wallet.",
        "required": [
          "balance",
          "currency",
          "streak",
          "canCheckIn",
          "nextCheckInAt",
          "perks",
          "xpBoost",
          "spending"
        ],
        "properties": {
          "balance": {
            "type": "number",
            "description": "The member's balance in coins."
          },
          "currency": {
            "$ref": "#/components/schemas/PlayerCurrency"
          },
          "streak": {
            "type": "integer",
            "description": "The daily reward streak as it stands now: a streak that was missed reads 0.",
            "minimum": 0
          },
          "canCheckIn": {
            "type": "boolean",
            "description": "`true` when the daily reward can be claimed now."
          },
          "nextCheckInAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the daily reward can be claimed next, or `null` when it can be claimed now."
          },
          "perks": {
            "$ref": "#/components/schemas/PlayerPerks"
          },
          "xpBoost": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PlayerXpBoost"
              },
              {
                "type": "null"
              }
            ],
            "description": "The XP boost the member holds, or `null` for none."
          },
          "spending": {
            "$ref": "#/components/schemas/PlayerBalanceSpending"
          }
        }
      },
      "PlayerCheckIn": {
        "type": "object",
        "description": "The daily reward that was claimed.",
        "required": [
          "reward",
          "streak",
          "balance",
          "nextCheckInAt",
          "savedByStreakSaver"
        ],
        "properties": {
          "reward": {
            "type": "number",
            "minimum": 0,
            "description": "The coins given."
          },
          "streak": {
            "type": "integer",
            "description": "The streak after this claim.",
            "minimum": 0
          },
          "balance": {
            "type": "number",
            "description": "The member's balance after the reward."
          },
          "nextCheckInAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the daily reward can be claimed next."
          },
          "savedByStreakSaver": {
            "type": "boolean",
            "description": "`true` when a streak saver kept the streak alive for a day that was missed."
          }
        }
      },
      "StartPlayerQuestRequest": {
        "type": "object",
        "description": "Which quest to start.",
        "required": [
          "questId"
        ],
        "properties": {
          "questId": {
            "type": [
              "integer",
              "string"
            ],
            "minimum": 1,
            "maximum": 2147483647,
            "description": "The quest's `id`, from List the quests. A whole number from 1 to 2,147,483,647, or the same number as a string of digits written without leading zeros (`42` or `\"42\"`).",
            "examples": [
              12
            ]
          }
        }
      },
      "VerifyPlayerQuestTaskRequest": {
        "type": "object",
        "description": "Which task to verify.",
        "required": [
          "questId"
        ],
        "properties": {
          "questId": {
            "type": [
              "integer",
              "string"
            ],
            "minimum": 1,
            "maximum": 2147483647,
            "description": "The quest's `id`. A whole number from 1 to 2,147,483,647, or the same number as a string of digits written without leading zeros (`42` or `\"42\"`).",
            "examples": [
              12
            ]
          },
          "taskId": {
            "type": [
              "integer",
              "string"
            ],
            "minimum": 1,
            "maximum": 2147483647,
            "description": "The task's `id`, from List the quests. Leave it out for a quest made before tasks existed: its one task has an `id` of `null`. A whole number from 1 to 2,147,483,647, or the same number as a string of digits written without leading zeros (`42` or `\"42\"`).",
            "examples": [
              45
            ]
          }
        }
      },
      "AnswerPlayerQuestTaskRequest": {
        "type": "object",
        "description": "Which task to answer, and the answer.",
        "required": [
          "questId",
          "answer"
        ],
        "properties": {
          "questId": {
            "type": [
              "integer",
              "string"
            ],
            "minimum": 1,
            "maximum": 2147483647,
            "description": "The quest's `id`. A whole number from 1 to 2,147,483,647, or the same number as a string of digits written without leading zeros (`42` or `\"42\"`).",
            "examples": [
              12
            ]
          },
          "taskId": {
            "type": [
              "integer",
              "string"
            ],
            "minimum": 1,
            "maximum": 2147483647,
            "description": "The task's `id`, from List the quests. Leave it out for a quest made before tasks existed: its one task has an `id` of `null`. A whole number from 1 to 2,147,483,647, or the same number as a string of digits written without leading zeros (`42` or `\"42\"`).",
            "examples": [
              42
            ]
          },
          "answer": {
            "type": [
              "string",
              "number"
            ],
            "maxLength": 1000,
            "description": "The player's answer, in the form the task's `answer.kind` asks for. `text`: text of up to 1,000 characters, and at least the task's `minLength` when it has one. `number`: a number, sent as a number or as text of up to 40 characters, that the task accepts (between its `min` and `max` when it has them). `scale`: a whole number from 1 to the task's `scale`.",
            "examples": [
              "Rugby Manager, every weekend"
            ]
          }
        }
      },
      "BuyShopItemRequest": {
        "type": "object",
        "description": "Which item to buy, at which price, and the key that makes a retry safe.",
        "required": [
          "itemId",
          "idempotencyKey",
          "expectedPrice"
        ],
        "properties": {
          "itemId": {
            "type": [
              "integer",
              "string"
            ],
            "minimum": 1,
            "maximum": 2147483647,
            "description": "The item's `id`, from List the shop. A whole number from 1 to 2,147,483,647, or the same number as a string of digits written without leading zeros (`42` or `\"42\"`).",
            "examples": [
              7
            ]
          },
          "idempotencyKey": {
            "$ref": "#/components/schemas/IdempotencyKey",
            "description": "Required. Makes a retry safe: see Retries in the description of this operation."
          },
          "expectedPrice": {
            "type": "integer",
            "minimum": 0,
            "maximum": 2147483647,
            "description": "The price the player saw in List the shop, in coins: a whole number from 0 to 2,147,483,647. When the item costs something else now, nothing is bought and the answer is `409` with the code `price_changed`.",
            "examples": [
              500
            ]
          },
          "answer": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "description": "The player's answer, up to 200 characters. Only for an item that asks a question (its `question` is not `null`): then it is required.",
            "examples": [
              "Harbour RFC"
            ]
          }
        }
      },
      "ClaimPlayerItemRequest": {
        "type": "object",
        "description": "Which purchase to claim, and what it asks for.",
        "required": [
          "purchaseId"
        ],
        "properties": {
          "purchaseId": {
            "type": [
              "integer",
              "string"
            ],
            "minimum": 1,
            "maximum": 2147483647,
            "description": "The purchase's `id`, from Read the locker. A whole number from 1 to 2,147,483,647, or the same number as a string of digits written without leading zeros (`42` or `\"42\"`).",
            "examples": [
              4242
            ]
          },
          "input": {
            "type": [
              "object",
              "null"
            ],
            "maxProperties": 10,
            "additionalProperties": {
              "type": "string",
              "maxLength": 200
            },
            "description": "What the item asks for before it can be claimed: an object of up to 10 keys with text values of up to 200 characters, one key for each of the item's `claim.fields`, named by the field's `key`. For a field that is a choice (its `kind` is `role` or `giveaway`) the value is the `id` picked from `claim.options`, as text. CEEJAY checks and cleans it. Leave it out for an item that asks for nothing.",
            "examples": [
              {
                "title": "Harbour RFC"
              }
            ]
          }
        }
      },
      "ListPlayerQuestsResponse": {
        "type": "object",
        "description": "A page of the community's quests.",
        "required": [
          "success",
          "data",
          "page"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true on success.",
            "const": true
          },
          "data": {
            "type": "array",
            "description": "The quests of this page, in the community's order.",
            "items": {
              "$ref": "#/components/schemas/PlayerQuest"
            }
          },
          "page": {
            "$ref": "#/components/schemas/Page"
          }
        }
      },
      "StartPlayerQuestResponse": {
        "type": "object",
        "description": "The quest is started.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true on success.",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/PlayerQuestStart"
          }
        }
      },
      "PlayerQuestTaskResponse": {
        "type": "object",
        "description": "The task is done.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true on success.",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/PlayerQuestTaskResult"
          }
        }
      },
      "ListPlayerShopResponse": {
        "type": "object",
        "description": "A page of the items this game may sell.",
        "required": [
          "success",
          "data",
          "page"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true on success.",
            "const": true
          },
          "data": {
            "type": "array",
            "description": "The items of this page, in the shop's order.",
            "items": {
              "$ref": "#/components/schemas/PlayerShopItem"
            }
          },
          "page": {
            "$ref": "#/components/schemas/Page"
          }
        }
      },
      "BuyShopItemResponse": {
        "type": "object",
        "description": "The purchase is made.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true on success.",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/PlayerPurchase"
          }
        }
      },
      "ListPlayerLockerResponse": {
        "type": "object",
        "description": "A page of what the player owns through this game.",
        "required": [
          "success",
          "data",
          "page"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true on success.",
            "const": true
          },
          "data": {
            "type": "array",
            "description": "The purchases of this page, newest first.",
            "items": {
              "$ref": "#/components/schemas/PlayerLockerItem"
            }
          },
          "page": {
            "$ref": "#/components/schemas/Page"
          }
        }
      },
      "ClaimPlayerItemResponse": {
        "type": "object",
        "description": "The purchase is claimed.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true on success.",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/PlayerClaim"
          }
        }
      },
      "PlayerRanksResponse": {
        "type": "object",
        "description": "The player's place and the top of this game's players.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true on success.",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/PlayerRanks"
          }
        }
      },
      "PlayerBalanceResponse": {
        "type": "object",
        "description": "The member's wallet.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true on success.",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/PlayerBalance"
          }
        }
      },
      "CheckInPlayerResponse": {
        "type": "object",
        "description": "The daily reward is claimed.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true on success.",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/PlayerCheckIn"
          }
        }
      },
      "PlayerSurveyQuestion": {
        "type": "object",
        "description": "A question of a survey, in the shape the Member Hub draws it.",
        "required": [
          "key",
          "label",
          "type",
          "required",
          "placeholder",
          "description",
          "options",
          "minLength",
          "maxLength",
          "minChoices",
          "maxChoices",
          "scale"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "The question's key. Send the answer under it in `answers`.",
            "examples": [
              "k_00000000000000a1"
            ]
          },
          "label": {
            "type": "string",
            "description": "The question, up to 45 characters."
          },
          "type": {
            "type": "string",
            "description": "How to ask it: `text_short`, `text_long`, `number`, `email`, `url`, `select_string`, `radio`, `checkbox`, `rating`, `nps`, `scale` or `yes_no`. Treat the list as open: a new type can appear."
          },
          "required": {
            "type": "boolean",
            "description": "`true` when the question needs an answer."
          },
          "placeholder": {
            "type": [
              "string",
              "null"
            ],
            "description": "The hint to show in an empty text box, or `null`."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "A line to show under the question, or `null`."
          },
          "options": {
            "type": "array",
            "description": "What the player picks from, for `select_string`, `radio`, `checkbox`, `rating`, `nps`, `scale` and `yes_no`, in order. Empty for text. Send the option's `value`.",
            "items": {
              "type": "object",
              "required": [
                "label",
                "value"
              ],
              "properties": {
                "label": {
                  "type": "string",
                  "description": "What to show."
                },
                "value": {
                  "type": "string",
                  "description": "What to send."
                }
              }
            }
          },
          "minLength": {
            "type": [
              "integer",
              "null"
            ],
            "description": "For text: the fewest characters, or `null`."
          },
          "maxLength": {
            "type": [
              "integer",
              "null"
            ],
            "description": "For text: the most characters, or `null`."
          },
          "minChoices": {
            "type": [
              "integer",
              "null"
            ],
            "description": "For `checkbox`: the fewest picks, or `null`."
          },
          "maxChoices": {
            "type": [
              "integer",
              "null"
            ],
            "description": "For `checkbox`: the most picks, or `null`."
          },
          "scale": {
            "type": [
              "object",
              "null"
            ],
            "description": "For `rating`, `nps`, `scale` and `yes_no`: the words at each end and how a rating is drawn. `null` for the other types.",
            "required": [
              "low",
              "high",
              "icon"
            ],
            "properties": {
              "low": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The words at the lowest score, such as \"Poor\", or `null`."
              },
              "high": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The words at the highest score, such as \"Great\", or `null`."
              },
              "icon": {
                "type": "string",
                "description": "`number` or `star`: how the team chose to show a rating.",
                "enum": [
                  "number",
                  "star"
                ]
              }
            }
          }
        }
      },
      "PlayerSurveyRewards": {
        "type": "object",
        "description": "What finishing the survey grants, or granted.",
        "required": [
          "xp",
          "roles",
          "names"
        ],
        "properties": {
          "xp": {
            "type": "integer",
            "description": "XP.",
            "minimum": 0
          },
          "roles": {
            "type": "integer",
            "description": "How many Discord roles.",
            "minimum": 0
          },
          "names": {
            "type": "array",
            "description": "The rewards of the survey's reward list, by name.",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "PlayerSurvey": {
        "type": "object",
        "description": "A survey the player can answer now.",
        "required": [
          "id",
          "title",
          "description",
          "questions",
          "closesAt",
          "rewards"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "description": "The survey's ID. Send it as `surveyId` with Answer a survey.",
            "minimum": 1
          },
          "title": {
            "type": "string",
            "description": "The survey's title."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "A line about the survey, or `null`."
          },
          "questions": {
            "type": "array",
            "description": "The questions, in order.",
            "items": {
              "$ref": "#/components/schemas/PlayerSurveyQuestion"
            }
          },
          "closesAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the survey stops taking responses, or `null`."
          },
          "rewards": {
            "$ref": "#/components/schemas/PlayerSurveyRewards"
          }
        }
      },
      "ListPlayerSurveysResponse": {
        "type": "object",
        "description": "A page of the surveys the player can answer now.",
        "required": [
          "success",
          "data",
          "page"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true on success.",
            "const": true
          },
          "data": {
            "type": "array",
            "description": "The surveys of this page, newest first.",
            "items": {
              "$ref": "#/components/schemas/PlayerSurvey"
            }
          },
          "page": {
            "$ref": "#/components/schemas/Page"
          }
        }
      },
      "AnswerPlayerSurveyRequest": {
        "type": "object",
        "description": "Which survey to answer, and the answers by question key.",
        "required": [
          "surveyId",
          "answers"
        ],
        "properties": {
          "surveyId": {
            "type": [
              "integer",
              "string"
            ],
            "minimum": 1,
            "maximum": 2147483647,
            "description": "The survey's `id`. A whole number from 1 to 2,147,483,647, or the same number as a string of digits written without leading zeros (`41` or `\"41\"`).",
            "examples": [
              41
            ]
          },
          "answers": {
            "type": "object",
            "description": "The answers, by question `key`, up to 50. Text for a text question, the `value` of an option for one pick, a list of values for `checkbox`. Leave out a question the player skipped. The body is at most 16 KB.",
            "maxProperties": 50,
            "additionalProperties": {
              "type": [
                "string",
                "number",
                "array",
                "null"
              ],
              "items": {
                "type": "string"
              }
            }
          }
        }
      },
      "PlayerSurveyAnswer": {
        "type": "object",
        "description": "The answers were saved.",
        "required": [
          "surveyId",
          "updated",
          "rewards"
        ],
        "properties": {
          "surveyId": {
            "type": "integer",
            "description": "The survey's ID.",
            "minimum": 1
          },
          "updated": {
            "type": "boolean",
            "description": "`true` when the player had finished the survey before and the answers were updated: nothing is granted again."
          },
          "rewards": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/PlayerSurveyRewards"
              },
              {
                "type": "null"
              }
            ],
            "description": "What the first completion granted, or `null` for an update."
          }
        }
      },
      "PlayerSurveyAnswerResponse": {
        "type": "object",
        "description": "The answers were saved.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true on success.",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/PlayerSurveyAnswer"
          }
        }
      }
    }
  }
}
