{
  "openapi": "3.1.0",
  "info": {
    "title": "CEEJAY API",
    "version": "1.2.0",
    "summary": "Work with a Discord community on CEEJAY from your own game, app or platform.",
    "description": "The CEEJAY API lets your own game, app or platform work with a Discord community on CEEJAY. Give XP, move currency, complete quests, link accounts, manage roles and post messages from your server, and receive a signed webhook the moment something happens in the community.\n\n## Base URL\n\nEvery request goes to your dashboard's URL followed by `/api/v1`.\n\n- Shared CEEJAY service: `https://ceejay.communitystudios.xyz/api/v1`\n- Dedicated instance: the URL you see in your browser when you open your dashboard, followed by `/api/v1`.\n\nOne operation is the exception. The older endpoint of Complete a quest task is at the dashboard's URL followed by `/api/quests/external/complete`, with no `/v1`, and it lists its own servers. It still works; new code uses `POST /quests/tasks/complete`.\n\n## Authentication\n\nSend an API key as a bearer token: `Authorization: Bearer ceejay_sk_…`. Create keys in the dashboard under **Settings → Developers → API Keys**. A key belongs to one Discord server, so you never send a server ID: every request is for the key's server. Each operation names the scope it needs. Community details (`GET /community`) needs none, and returns what the key may do and what this instance supports: make it your first call.\n\n## Conventions\n\n- Send `Content-Type: application/json` with every request that has a body. With that header, a body that is not valid JSON is refused before the key is checked, with status `400` and the plain-text body `Invalid JSON`. A body over 1 MB gets `413`. These two responses, and the response for a path that does not exist, are returned before CEEJAY's own code runs: they are not JSON and carry no `code`. Without the header the body is not read as JSON: it reaches the operation as text and is refused with `400`. A body that is JSON but not an object (`null`, a string, a number or a list) is refused the same way. The message is `The request body must be a JSON object`, except on Complete a shop delivery and Complete a quest task, which return `400` with their own first message. A form content type, which `curl -d` sends when you set none, is read as a form: the JSON fields are not found, and the first field check fails.\n- Each field has one current name, in camelCase, in requests and responses. The member is `discordId`, a game's own ID for the member is `playerId` and its name `playerName`, a note is `reason`. Every name a request or a response had before is still accepted and still returned, for as long as version 1 exists. A request property that has an older name lists it under `x-also-accepted`; when both names are sent, the older one is used. A response field that has a newer twin is marked `deprecated`, and is still returned.\n- Discord IDs are strings of 17 to 20 digits. Always send and read them as strings. A JSON number cannot represent all the digits, so its last ones would change and it could name a different ID. A Discord ID that is not a string is refused with `400`, like one that is not 17 to 20 digits. Complete a quest task is the exception: a `discordId` of the wrong kind returns `202` there. CEEJAY's own IDs of quests, programs and purchases are numbers in every response, and a request takes them as a number or as a string of digits.\n- Text is sent as a JSON string. A text field that contains a number, a list or an object is refused with `400`, and so is text that contains a null character (U+0000) where CEEJAY stores it. Complete a quest task is the exception for `xHandle`, `wallet`, `github` and `twitch`: a whole number is read as its digits, and any other value that is not a string returns `202`.\n- A successful response is `{ \"success\": true, \"data\": … }`. A list adds `\"page\": { \"nextCursor\", \"limit\" }` beside `data`: send `nextCursor` back as `cursor` for the next page, and stop when it is `null`. Three operations keep the fields they had beside `success` before, and carry `data` as well: List quests (`total`, `limit`, `offset`), Complete a quest task (`matched`, `completed`) and Complete a shop delivery (`purchaseId`, `status`, `alreadyDone`, `refunded`).\n- A failed response has a non-2xx status and `{ \"error\": \"message\", \"code\": \"fixed_code\", \"requestId\": \"req_…\" }`. Some codes add fields, described on the operations that return them. Act on `code`: the list of codes is closed for version 1 (the `ErrorCode` schema), while the `error` message text can change. Do not act on the status alone either: four refusals keep the status `400` they have always had (`quest_not_active`, `quest_already_completed`, `quest_limit_reached`, `insufficient_balance`) where newer rule refusals return `409`.\n- Every response includes `X-Request-Id`, the same value as `requestId` in an error body. Quote it when you ask us about a request.\n- Dates are ISO 8601 strings in UTC.\n- Call the API from your own server. Never call it from a browser, a mobile app or a game client: a key in client code can be copied, and browsers on other websites are not allowed to call the API.\n- Within version 1 nothing is renamed or removed. Every operation carries `x-since`, the version it first appeared in.\n\n## Connect\n\nConnect links a player of your game to their Discord account in the community. The operations under the **Connect** tag are the ones your own server makes with a secret key that has the `players:connect` scope. The requests a game makes itself, with the public key it ships with, are in a second file, `openapi-connect.json`, served beside this one: they take another kind of key and have no scopes. Once a player is connected, the operations under the **Players** and **Field map** tags read the player, send data about them (fields and events), disconnect them and delete the data CEEJAY stores for them. Connect is switched on per instance: where it is off, these operations return `404` with the code `not_available` and `feature: \"players\"`. A game can also show the community's quests, shop, rank and daily reward, and sell from its shop, with a player session: those requests are in the second file too, and an item a player buys in a game is delivered to your server with a `player` that identifies them (see the **Shop delivery webhooks** tag).\n\n## Rate limits\n\nEach key allows 60 requests per minute unless a higher limit was set on it, and never more than 600. Requests are counted per key in one-minute windows. Every response to a request made with a known key includes the key's rate limit status: `X-RateLimit-Limit` (the key's requests per minute), `X-RateLimit-Remaining` (requests left in the window) and `X-RateLimit-Reset` (whole seconds until the window ends). Over the limit, the API returns `429` with the code `rate_limited`, a `Retry-After` header (in seconds) and `retryAfter` in the body. Every request made with a valid key is counted, whatever the response, including one refused for a missing scope (`403`). The limit is checked before the scope. A request refused for a bad key (`401`) is not counted and carries no rate limit headers.\n\nThe five Connect operations of a studio's server have a limit of their own and are not counted against the key's number: 600 requests a minute for each game. One key also has an abuse limit of 6,000 a minute across them. The player operations (the **Players** and **Field map** tags) use the key's own number: a call that carries up to 500 players or events is one request. Their three headers report the game's 600 once the request has found its game, and the key's 6,000 on a response returned before that.\n\nDiscord limits CEEJAY's bot as well. When that limit is reached, an operation that calls Discord returns `429` with the code `discord_rate_limited`, and `retryAfter` when Discord provided a retry time. That response does not mean the key's own window is used up.\n\n## Retrying safely\n\nEvery operation that creates or changes data accepts an optional `idempotencyKey` in the JSON body (an `Idempotency-Key` header is not read). When the same request is sent again with its key, nothing happens a second time: the response is the original one, with `duplicate: true` and the header `Idempotent-Replayed: true`. One key is one request, across every operation: two requests for one event need two keys, for example `match-9:coins` and `match-9:xp`. The same key on another operation, or with other values, returns `409` with the code `idempotency_key_reused`. A key belongs to the API key that sent it. A key is stored only when its request was applied, so a refused request can be retried with the same key, and keys are retained for 30 days.\n\nTwo more responses are about a request whose first attempt may still be running. `409` with the code `idempotency_in_progress` means wait the `retryAfter` seconds and retry. `409` with the code `idempotency_outcome_unknown` comes from Send a message, Emit an event and Complete a quest task when a first attempt never finished: CEEJAY cannot know whether it took effect, and does not repeat it. On an instance that does not support idempotency keys yet, a request that sends one returns `503` with the code `idempotency_unavailable` and changes nothing.\n\nGrant currency, Deduct currency and Complete a quest took a key before version 1.0.0 under the name `idempotency_key`. That name is still accepted on those three, their keys keep working on an instance that is still being updated, and a currency movement made with a key is recognised from the member's transaction history permanently. The other operations read `idempotencyKey` only.\n\n## Webhooks\n\nCEEJAY sends signed HTTP POST requests to the URLs you add under **Settings → Developers → Webhooks**. The `webhooks` section of this document lists every event and its payload.\n\n- Every delivery has the same envelope: `{ \"event\", \"data\", \"serverId\", \"timestamp\", \"id\" }`, and the headers `Content-Type: application/json`, `X-CEEJAY-Event` (the event name) and the signature headers.\n- Each subscription has its own signing secret. The dashboard shows it once, right after you press **Create**: copy it then. If you lose it, delete the subscription and add it again.\n- Every delivery carries signature version 2 in four headers: `X-CEEJAY-Delivery` (the delivery ID), `X-CEEJAY-Attempt` (`1` for the first attempt, then `2`, `3` and so on), `X-CEEJAY-Timestamp` (the Unix time in seconds at which this attempt was signed) and `X-CEEJAY-Signature-V2`. The signature is the HMAC-SHA256, as 64 lower-case hexadecimal characters, of the text `v2.<timestamp>.<delivery ID>.<attempt>.<raw request body>`, keyed with the signing secret used as text. Compute it over the exact bytes you received, before parsing them, compare in constant time, and refuse a delivery whose time is more than 300 seconds from your own clock. Inside those 300 seconds a captured delivery can be replayed, so keep each `X-CEEJAY-Delivery` with its `X-CEEJAY-Attempt` for five minutes and ignore one you have already handled.\n- The legacy signature is still sent beside it, unchanged: `X-CEEJAY-Signature` is `sha256=` followed by the HMAC-SHA256 of the raw request body, as lowercase hex, keyed with the signing secret of the subscription. It includes no time.\n- Respond with any 2xx status within 5 seconds. Any other status, a redirect (redirects are not followed) or no response is a failed delivery. The URL must be a public https URL.\n- A failed delivery is retried up to 4 more times, with the same `id` and `X-CEEJAY-Delivery`, the next `X-CEEJAY-Attempt` and a new `X-CEEJAY-Timestamp`: about 30 seconds, 2 minutes, 4½ minutes and 8 minutes after the first try. CEEJAY's retry worker runs once a minute, so each retry can be delivered up to a minute late.\n- After 10 failed deliveries in a row, a subscription is switched off. A successful delivery resets the count, and so does switching the subscription back on.\n- \"Delivered by your API\" shop items are different: they send one request to the URL set on the item, with the item's own secret. See the **Shop delivery webhooks** tag.",
    "contact": {
      "name": "CEEJAY support",
      "url": "https://communitystudios.xyz/contact"
    }
  },
  "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": [
    {
      "ApiKey": []
    }
  ],
  "tags": [
    {
      "name": "Community",
      "description": "Find out which community a key belongs to, what the key may do and what this instance supports."
    },
    {
      "name": "Members",
      "description": "Read a member and link their accounts in your game or on other platforms."
    },
    {
      "name": "Connect",
      "description": "Link a player of your game to their Discord account, from your own server: verify a player with a pass, start a connect session, read it and confirm it on the game's behalf. These need the scope `players:connect`, which a `*` key does not include. The requests a game makes itself are in `openapi-connect.json`. What happens after a player is connected (their data, events, and disconnecting or deleting them) is under the **Players** and **Field map** tags."
    },
    {
      "name": "Players",
      "description": "The players of a game who are connected to a Discord member: read them, send data about them (fields and events), disconnect one and delete the data CEEJAY stores for one. These need `players:read`, `players:write`, `players:connect` or `players:delete`. A `*` key includes the first two and not the last two. They are counted against the key's own requests per minute."
    },
    {
      "name": "Field map",
      "description": "The map between the names a game uses for its data and the fields of the community's members. A key reads the map and proposes to it with `players:read` and `players:write`. The community's team approves what is proposed."
    },
    {
      "name": "XP",
      "description": "Read, grant and deduct XP, for the whole server or inside a community program."
    },
    {
      "name": "Programs",
      "description": "List community programs."
    },
    {
      "name": "Economy",
      "description": "Read, grant and deduct a member's currency."
    },
    {
      "name": "Quests",
      "description": "List quests, complete one for a member, or report that a member finished a quest task."
    },
    {
      "name": "Roles",
      "description": "List Discord roles, and grant and revoke them."
    },
    {
      "name": "Channels",
      "description": "List the channels of your server."
    },
    {
      "name": "Messages",
      "description": "Post messages and embeds to channels in your server."
    },
    {
      "name": "Events",
      "description": "Send your own events to your webhook subscribers."
    },
    {
      "name": "Shop",
      "description": "List the deliveries of \"Delivered by your API\" shop items awaiting fulfilment by your server, and complete them."
    },
    {
      "name": "Webhook events",
      "description": "Events CEEJAY sends to the webhook subscriptions you add under **Settings → Developers → Webhooks**. A subscription lists the events it wants, or `*` for all of them.\n\nEach delivery is an HTTP POST with a JSON body of `{ \"event\", \"data\", \"serverId\", \"timestamp\", \"id\" }` and the headers `Content-Type: application/json` and `X-CEEJAY-Event`. Every delivery carries signature version 2 in four headers: `X-CEEJAY-Delivery` (the delivery ID), `X-CEEJAY-Attempt` (`1` for the first attempt, then `2`, `3` and so on), `X-CEEJAY-Timestamp` (the Unix time in seconds at which this attempt was signed) and `X-CEEJAY-Signature-V2`. The signature is the HMAC-SHA256, as 64 lower-case hexadecimal characters, of the text `v2.<timestamp>.<delivery ID>.<attempt>.<raw request body>`, keyed with the signing secret used as text. Compute it over the exact bytes you received, before parsing them, compare in constant time, and refuse a delivery whose time is more than 300 seconds from your own clock. Inside those 300 seconds a captured delivery can be replayed, so keep each `X-CEEJAY-Delivery` with its `X-CEEJAY-Attempt` for five minutes and ignore one you have already handled. The signing secret is the subscription's, which the dashboard shows once, right after you create the subscription. The delivery ID is the body's `id`, and a retry carries the same ID with the next attempt number and a new time.\n\nThe legacy signature is still sent beside it, unchanged, for as long as version 1 exists: `X-CEEJAY-Signature` is `sha256=` followed by the HMAC-SHA256 of the raw body, as lowercase hex, keyed with the same secret. It includes no time.\n\nAn event whose `data` has `userId` carries the same value as `data.discordId`, the name the rest of the API uses. Respond with any 2xx status within 5 seconds. Failed deliveries are retried up to 4 more times. After 10 failed deliveries in a row a subscription is switched off. Switching it back on starts the count again from zero."
    },
    {
      "name": "Shop delivery webhooks",
      "description": "When a member buys a \"Delivered by your API\" item, or earns a reward that is set to be delivered by your API, CEEJAY sends one signed POST to the endpoint URL set on the item. Your response decides what happens to the purchase. These requests are separate from webhook subscriptions. An item a player buys in a game (through the game's Buy a shop item) is delivered the same way, and its body also carries `player`: `{ \"game\", \"player_id\", \"proof\" }`, in snake_case like the rest of the body. It names the player the item was bought for, as it was at the purchase, so your server can deliver the item to the right player; it is left out for a purchase made in Discord.\n\nHeaders: `Content-Type: application/json`, `Accept: application/json`, `User-Agent: CEEJAY-Shop/1`, and the signature headers. Every delivery carries signature version 2 in four headers: `X-CEEJAY-Delivery` (the delivery ID), `X-CEEJAY-Attempt` (`1` for the first attempt, then `2`, `3` and so on), `X-CEEJAY-Timestamp` (the Unix time in seconds at which this attempt was signed) and `X-CEEJAY-Signature-V2`. The signature is the HMAC-SHA256, as 64 lower-case hexadecimal characters, of the text `v2.<timestamp>.<delivery ID>.<attempt>.<raw request body>`, keyed with the signing secret used as text. Compute it over the exact bytes you received, before parsing them, compare in constant time, and refuse a delivery whose time is more than 300 seconds from your own clock. Inside those 300 seconds a captured delivery can be replayed, so keep each `X-CEEJAY-Delivery` with its `X-CEEJAY-Attempt` for five minutes and ignore one you have already handled. The signing secret is the item's. The delivery ID is `shop_<purchase id>`, the same on every retry: de-duplicate on it.\n\nThe legacy signature is still sent beside it, unchanged, for as long as version 1 exists: `X-CEEJAY-Signature: t=<unix seconds>,v1=<hex>`, where `v1` is the HMAC-SHA256, as lowercase hex, of the text `<t>.<raw request body>`, keyed with the item's signing secret, and `t` is the same time as `X-CEEJAY-Timestamp`.\n\nYour response (CEEJAY allows 5 seconds and reads up to 64 KB): a 2xx with an empty body, or `{ \"status\": \"delivered\" }`, marks the purchase delivered. Status `202`, or a 2xx with `{ \"status\": \"pending\" }`, means the delivery is pending: call `POST /shop/purchases/{purchaseId}/complete` once it is complete. A 2xx with `{ \"status\": \"failed\", \"message\": \"…\" }`, a redirect or a 4xx other than `408` and `429` refuses the delivery and refunds the member. `408`, `429`, a 5xx, a timeout or a connection error is retried about every hour with the same `delivery_id`, and the member is refunded after 3 failed attempts in total. You can respond pending to a retry as well: the retries stop, the attempt is not counted as failed, and the purchase awaits your callback. A purchase still awaiting your callback after 24 hours is refunded. `GET /shop/purchases` lists the purchases awaiting your confirmation."
    }
  ],
  "paths": {
    "/community": {
      "get": {
        "tags": [
          "Community"
        ],
        "summary": "Community details",
        "description": "Says which community a key belongs to, what the key may do, and which operations this instance can serve. Make it your first call.\n\n**Required scope:** none. Any valid key can call it.\n\n`key` holds the name, the scopes, the rate limit and the expiry of the key that made the call. `features` says, per group of operations, whether this instance can serve them: a request for a feature that reads `available: false` returns `404` with the code `not_available`. `platforms` lists the names Link an account takes.\n\nIt changes nothing. It never returns the key or its hash, the community's other keys, or the name or value of any setting.",
        "operationId": "getCommunity",
        "x-required-scope": [],
        "x-since": "1.0.0",
        "responses": {
          "200": {
            "description": "The community.",
            "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/CommunityResponse"
                },
                "examples": {
                  "community": {
                    "summary": "A community",
                    "value": {
                      "success": true,
                      "data": {
                        "serverId": "721104938271003842",
                        "name": "Harbour Town",
                        "apiVersion": "1.1.0",
                        "key": {
                          "name": "Game server",
                          "scopes": [
                            "xp:grant",
                            "quests:complete"
                          ],
                          "rateLimitPerMinute": 60,
                          "expiresAt": null
                        },
                        "features": {
                          "xp": {
                            "available": true
                          },
                          "economy": {
                            "available": true,
                            "enabled": true
                          },
                          "quests": {
                            "available": true,
                            "dailyQuestLimit": 5
                          },
                          "programs": {
                            "available": true
                          },
                          "linking": {
                            "available": true
                          },
                          "roles": {
                            "available": true
                          },
                          "messages": {
                            "available": true
                          },
                          "events": {
                            "available": true
                          },
                          "shopDelivery": {
                            "available": true
                          },
                          "retryKeys": {
                            "available": true
                          },
                          "players": {
                            "available": true
                          },
                          "gameLoop": {
                            "available": true
                          }
                        },
                        "platforms": {
                          "builtIn": [
                            "github",
                            "twitch",
                            "steam",
                            "twitter",
                            "xbox",
                            "playstation",
                            "metamask",
                            "epic",
                            "youtube",
                            "riot",
                            "battlenet",
                            "roblox",
                            "reddit",
                            "kick",
                            "patreon",
                            "tiktok",
                            "bluesky",
                            "lastfm",
                            "spotify"
                          ],
                          "custom": [
                            {
                              "slug": "rugby_manager",
                              "name": "Rugby Manager"
                            }
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/CommunityRequired"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/CommunityNotCovered"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/members/{discordId}": {
      "get": {
        "tags": [
          "Members"
        ],
        "summary": "Get a member",
        "description": "Returns one member's profile, XP and level, currency and linked platform accounts in a single call.\n\n**Required scope:** `members:read` (a key with the `*` scope can also call it).\n\n`404` always means CEEJAY has no profile for that Discord ID on your server; Discord is not asked. A member who exists but has no currency returns `200` with a zeroed `economy` object. `level` is worked out from `xp` on your server's XP curve each time. Each linked account includes the game's own ID and name as `playerId` and `playerName`. A game account the member connected themselves through Connect is in `connections` only for a key that has the `players:read` scope (a `*` key does). An account your own server linked with Link an account is always there. A key limited to some games (a secret key with games listed in the dashboard) sees only the connections to those games. Each entry carries `proof`: for a connection to a game it says whether the player was verified (`verified`) or only supplied by the game (`basic`), and it is `null` for every other account. Do not grant a member anything for a player ID whose `proof` is `basic`.",
        "operationId": "getMember",
        "x-required-scope": [
          "members:read"
        ],
        "x-since": "1.0.0",
        "parameters": [
          {
            "$ref": "#/components/parameters/DiscordIdPath"
          }
        ],
        "responses": {
          "200": {
            "description": "The member.",
            "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/MemberResponse"
                },
                "examples": {
                  "member": {
                    "summary": "A member with a linked account",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "displayName": "nova",
                        "avatarUrl": "https://cdn.discordapp.com/avatars/409876543210987654/a1b2c3d4e5f6.png",
                        "xp": 8450,
                        "level": 12,
                        "totalMessages": 3411,
                        "totalVoiceMinutes": 862,
                        "joinedAt": "2025-11-04T18:22:07.000Z",
                        "lastUpdated": "2026-10-01T09:14:22.118Z",
                        "economy": {
                          "balance": 2450,
                          "totalEarned": 9100,
                          "totalSpent": 6650
                        },
                        "connections": [
                          {
                            "platform": "twitch",
                            "platformUserId": "123456789",
                            "platformUsername": "novaplays",
                            "playerId": "123456789",
                            "playerName": "novaplays",
                            "connectedAt": "2026-03-12T16:40:11.000Z",
                            "proof": null
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The Discord ID in the path is not 17 to 20 digits.",
            "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": {
                  "badId": {
                    "summary": "Invalid Discord ID",
                    "value": {
                      "error": "discordId must be a valid Discord snowflake (17-20 digits)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "discordId"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScopes"
          },
          "404": {
            "description": "CEEJAY has no profile for this member on your server.",
            "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": {
                  "unknown": {
                    "summary": "Unknown member",
                    "value": {
                      "error": "Member not found",
                      "code": "member_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/members/link": {
      "post": {
        "tags": [
          "Members"
        ],
        "summary": "Link an account",
        "description": "Links a member's account in your game or on another platform (Twitch, Steam, Roblox and others) to their Discord ID, so activity on your platform can be tied to the community member.\n\n**Required scope:** `members:link` (a key with the `*` scope can also call it).\n\n**The platform** must be one that is known. That is one of the platforms CEEJAY knows by name (`github`, `twitch`, `steam`, `twitter`, `xbox`, `playstation`, `metamask`, `epic`, `youtube`, `riot`, `battlenet`, `roblox`, `reddit`, `kick`, `patreon`, `tiktok`, `bluesky`, `lastfm`, `spotify`), a custom platform your community added under **Settings → Developers → Platforms** (it is saved there in **API Link** mode, and the value to send is its **Slug**, the name it is stored under), or a platform your community already has at least one link for. Any other name returns `400` with the code `unknown_platform`. An X account is linked as `twitter` and a wallet as `metamask`. Community details (`GET /community`) lists the platforms your key can use.\n\n**The member** must be in your server. An ID CEEJAY has no member profile for is looked up in Discord first: an ID that is not in your server returns `404` with the code `member_not_found`, and when the Discord API is unavailable the call returns `503` with the code `discord_unavailable`. Nothing is linked in either case. A member who has only just joined can take up to 15 seconds to be found. Wait 15 seconds before retrying.\n\n**The ID** is `playerId`, the game's own ID for the member. Spaces at either end are removed before it is stored.\n\nSafe to repeat: linking the same platform again for the same member updates the link (the ID, and the name when you send one) and makes it active again if it had been disconnected.\n\nTwo links are rejected with `409`, and nothing is changed. A member who linked the platform themselves, by signing in to it, keeps that account while the link is active: a call that names a different `playerId` returns the code `linked_by_sign_in`. Capitals and spaces at the ends do not make an ID different. Once the member has disconnected the account, the platform can be linked again through the API. That link is a new one made by your key, also when it names the same account: it does not bring the member's sign-in back. And an account belongs to one member of your server at a time: a `playerId` that another member has an active link for on the same platform returns the code `player_already_linked`. Concurrent calls that send one account for two members are processed one at a time: the first links it, and the second gets `player_already_linked`. A member who already has an active link for that account can always send it again, and their link is updated as before. An account moves from one member to another only after the first member's link is disconnected, and no operation of this API disconnects a link.\n\nWhen you send the account a member signed in with, in other capitals or with spaces, the link keeps the capitalisation the member signed in with, and `playerId` in the response is the stored value. A link the API points at a different account no longer carries the old account's sign-in, and neither does a link the API makes again after the member disconnected it.\n\n**A platform that is a game.** On an instance that offers Connect, a game set up for your community is a platform from the day it is created: send its slug as `platform`. A link under it is a game connection at the verified level, on your server's word, and the member is shown no page for it. For a game, `playerId` must be one a game can use (1 to 128 letters, digits and `. _ : @ | + = -`, starting with a letter or a digit), or the call returns `400` with the code `bad_player_id`. A game that is paused returns `409` with `game_paused`, a key limited to other games `403` with `game_not_allowed`, a game made for children `403` with `audience_not_allowed`, and a game that was removed `404` with `game_not_found`. A member who ended their connection to the game themselves cannot be linked to it again by a key (`409` with `member_disconnected`), and neither can a member whose connection to it ended because the player is under age (`403` with `under_age`): in both cases only the member connecting again through Connect clears it. A player the community blocked cannot be linked at all (`409` with `player_blocked`). A connection the member made themselves through Connect is protected like a sign-in: a call that names a different `playerId` returns `linked_by_sign_in`. Every other platform name is handled as before.\n\nLinking also does these things in the background. They never make the call fail. It adds a `connect_<platform>` entry to the member's activity history, which sends the `account.linked` webhook the first time the member links that platform. It fulfils rewards that were waiting for that platform. It grants the community's own reward for linking an account, if one is set up, once per member and platform. For a platform that is a game the community's own reward for linking an account is not granted. CEEJAY's bot works the connection instead, shortly after the response, as it does any connection to the game: it sends `player.connected`, and where the connection meets the proof the game asks for, it also adds the `account.linked` entry (once for each member and game), releases rewards that waited for a linked account of this game, and pays the game's own reward for connecting, if one is set up.\n\n`data` includes the link under the names the rest of the API uses, and alongside them the snake_case fields this response has always returned.\n\n**Retries.** Send `idempotencyKey` to make a retry safe. When the same request is sent again with its key, nothing happens a second time: the response is the original response, with `duplicate: true` and the header `Idempotent-Replayed: true`. For this operation the same request is the same member, platform, ID and name. Each key is tied to one request payload: two requests for one event need two keys, for example `match-9:coins` and `match-9:xp`. A key that was used for a different request returns `409` with the code `idempotency_key_reused`, and nothing is linked. A key is scoped to the API key that sent it and is retained for 30 days. On an instance that does not support idempotency keys yet, a request that sends one returns `503` with the code `idempotency_unavailable` and changes nothing.",
        "operationId": "linkMemberAccount",
        "x-required-scope": [
          "members:link"
        ],
        "x-since": "1.0.0",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LinkAccountRequest"
              },
              "examples": {
                "roblox": {
                  "summary": "Link a Roblox account",
                  "value": {
                    "discordId": "409876543210987654",
                    "platform": "roblox",
                    "playerId": "88213004",
                    "playerName": "novaplays"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The link. With `duplicate: true` it is the response to an earlier call with the same `idempotencyKey`.",
            "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/LinkAccountResponse"
                },
                "examples": {
                  "linked": {
                    "summary": "Linked",
                    "value": {
                      "success": true,
                      "data": {
                        "id": 311,
                        "server_id": "721104938271003842",
                        "user_id": "409876543210987654",
                        "platform": "roblox",
                        "platform_user_id": "88213004",
                        "platform_username": "novaplays",
                        "is_active": true,
                        "connected_at": "2026-10-01T09:14:22.118Z",
                        "connectionId": "311",
                        "discordId": "409876543210987654",
                        "playerId": "88213004",
                        "playerName": "novaplays",
                        "active": true,
                        "connectedAt": "2026-10-01T09:14:22.118Z",
                        "duplicate": false
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected and nothing was linked. The checks run in this order: the body, Discord ID, platform, the ID, the type of the name, the three lengths, a null character in any of the three, the characters of the platform, the idempotency key, then whether the platform is known. A message names the spelling you sent (`playerId` or `platformUserId`, `playerName` or `platformUsername`); a missing ID is called `playerId`. When the platform is a game, the alphabet of `playerId` is checked after the platform is known (`bad_player_id`).",
            "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": "Body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badId": {
                    "summary": "Invalid Discord ID",
                    "value": {
                      "error": "discordId must be a valid Discord snowflake (17-20 digits)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "discordId"
                    }
                  },
                  "noPlatform": {
                    "summary": "No platform, or not text",
                    "value": {
                      "error": "platform is required",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "platform"
                    }
                  },
                  "noPlayerId": {
                    "summary": "No ID, not text, or empty once trimmed",
                    "value": {
                      "error": "playerId is required",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "playerId"
                    }
                  },
                  "noPlatformUserId": {
                    "summary": "The same, when the deprecated alias was sent",
                    "value": {
                      "error": "platformUserId is required",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "platformUserId"
                    }
                  },
                  "badName": {
                    "summary": "Name not text",
                    "value": {
                      "error": "playerName must be a string",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "playerName"
                    }
                  },
                  "longPlatform": {
                    "summary": "Platform over 50 characters",
                    "value": {
                      "error": "platform cannot exceed 50 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "platform"
                    }
                  },
                  "longPlayerId": {
                    "summary": "ID over 255 characters",
                    "value": {
                      "error": "playerId cannot exceed 255 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "playerId"
                    }
                  },
                  "longName": {
                    "summary": "Name over 255 characters",
                    "value": {
                      "error": "playerName cannot exceed 255 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "playerName"
                    }
                  },
                  "nullInPlatform": {
                    "summary": "A null character in the platform",
                    "value": {
                      "error": "platform cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "platform"
                    }
                  },
                  "nullInPlayerId": {
                    "summary": "A null character in the ID",
                    "value": {
                      "error": "playerId cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "playerId"
                    }
                  },
                  "nullInName": {
                    "summary": "A null character in the name",
                    "value": {
                      "error": "playerName cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "playerName"
                    }
                  },
                  "badPlatform": {
                    "summary": "A platform with other characters",
                    "value": {
                      "error": "platform can only contain letters, digits and underscores",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "platform"
                    }
                  },
                  "badKey": {
                    "summary": "Invalid idempotency key",
                    "value": {
                      "error": "idempotencyKey must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "nullInKey": {
                    "summary": "A null character in the idempotency key",
                    "value": {
                      "error": "idempotencyKey cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "unknownPlatform": {
                    "summary": "A platform that is not known",
                    "value": {
                      "error": "Unknown platform. Add it as a custom platform under Settings, Developers, Platforms, then link again",
                      "code": "unknown_platform",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badPlayerId": {
                    "summary": "A game's 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",
                      "field": "playerId"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key does not have `members:link` (`insufficient_scope`: the body lists what was needed and what the key has, and the request still counts toward the key's rate limit). Or the platform is a game that cannot be linked under: the community removed CEEJAY (`community_not_active`), the key is limited to other games (`game_not_allowed`), the game is made for children (`audience_not_allowed`), or this member's connection to the game ended because the player is under age (`under_age`: this request carries no age answer, so a key cannot link them to the game again; the member connecting again through Connect clears it).",
            "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/ForbiddenErrorResponse"
                },
                "examples": {
                  "missingScope": {
                    "summary": "Key without the scope",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "economy:grant"
                      ],
                      "granted": [
                        "xp:grant",
                        "xp:read"
                      ]
                    }
                  },
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "gameNotAllowed": {
                    "summary": "The key is limited to other games",
                    "value": {
                      "error": "This API key is limited to other games",
                      "code": "game_not_allowed",
                      "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"
                    }
                  },
                  "underAge": {
                    "summary": "The member's connection to the game ended for age",
                    "value": {
                      "error": "This player cannot be connected",
                      "code": "under_age",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "The Discord ID is not a member of your server (`member_not_found`), or this instance does not support account linking yet (`not_available`). Nothing was linked. A member who has only just joined can take up to 15 seconds to be found. Wait 15 seconds before retrying. Or the platform is the slug of a game that 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": {
                  "notAMember": {
                    "summary": "Not a member of your server",
                    "value": {
                      "error": "Member not found",
                      "code": "member_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "notAvailable": {
                    "summary": "Not available on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "linking"
                    }
                  },
                  "gameNotFound": {
                    "summary": "A game that was removed",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "description": "The link was rejected and nothing was changed: the member linked this platform by signing in (`linked_by_sign_in`), or another member has this account (`player_already_linked`). The `idempotencyKey` was already used by this API key for a different request (`idempotency_key_reused`: another operation, or other values), or the first request with it is still being processed (`idempotency_in_progress`: wait `retryAfter` seconds and retry). Nothing was changed by this call. When the platform is a game: the game is paused (`game_paused`), the member ended their connection to it themselves (`member_disconnected`), or the community blocked the player (`player_blocked`).",
            "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": {
                  "linkedBySignIn": {
                    "summary": "The member linked this platform by signing in",
                    "value": {
                      "error": "The member linked this platform by signing in, so only they can change it",
                      "code": "linked_by_sign_in",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "playerAlreadyLinked": {
                    "summary": "Another member has this account",
                    "value": {
                      "error": "This playerId is already linked to another member in this server",
                      "code": "player_already_linked",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keyReused": {
                    "summary": "The key was already used for a different 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
                    }
                  },
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "memberDisconnected": {
                    "summary": "The member disconnected the game themselves",
                    "value": {
                      "error": "The member disconnected this game themselves, so only they can connect it again",
                      "code": "member_disconnected",
                      "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"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "A check CEEJAY needs could not be made, and nothing was changed. Either CEEJAY has no profile for this Discord ID and could not check with Discord whether it is a member of your server (`discord_unavailable`: retry the request shortly), 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"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "discordUnavailable": {
                    "summary": "Discord could not be asked",
                    "value": {
                      "error": "Could not check with Discord that this ID is a member of the server, so nothing was changed",
                      "code": "discord_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keysUnavailable": {
                    "summary": "Keys cannot be stored yet",
                    "value": {
                      "error": "idempotencyKey cannot be used on this community yet, so nothing was changed",
                      "code": "idempotency_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/xp/balance": {
      "get": {
        "tags": [
          "XP"
        ],
        "summary": "Get XP balance",
        "description": "Returns a member's total XP and level, without the rest of their profile.\n\n**Required scope:** `xp:read` (a key with the `*` scope can also call it).\n\nA member CEEJAY has not seen is not an error: the call returns `200` with `xp` and `level` at `0` and `lastUpdated` as `null`, so you can call it without checking the member first. Reading creates nothing.",
        "operationId": "getXpBalance",
        "x-required-scope": [
          "xp:read"
        ],
        "x-since": "1.0.0",
        "parameters": [
          {
            "$ref": "#/components/parameters/DiscordIdQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "The member's XP.",
            "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/XpBalanceResponse"
                },
                "examples": {
                  "member": {
                    "summary": "A member with XP",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "xp": 8450,
                        "level": 12,
                        "lastUpdated": "2026-10-01T09:14:22.118Z"
                      }
                    }
                  },
                  "unknown": {
                    "summary": "A member CEEJAY has not seen",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "xp": 0,
                        "level": 0,
                        "lastUpdated": null
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The Discord ID is missing, not 17 to 20 digits, or was sent more than once.",
            "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": {
                  "badId": {
                    "summary": "Invalid Discord ID",
                    "value": {
                      "error": "discordId must be a valid Discord snowflake (17-20 digits)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "discordId"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScopes"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/xp/grant": {
      "post": {
        "tags": [
          "XP"
        ],
        "summary": "Grant XP",
        "description": "Grants a member XP and tells you whether it took them up a level.\n\n**Required scope:** `xp:grant` (a key with the `*` scope can also call it).\n\nA member of your server CEEJAY has no profile for yet gets one, so you can reward someone who plays your game before they have said anything in Discord. CEEJAY first checks with Discord whether such an ID is a member of your server: an ID that is not returns `404` with the code `member_not_found`, and when the Discord API is unavailable the call returns `503` with the code `discord_unavailable`. Nothing is granted in either case. A member who has only just joined can take up to 15 seconds to be found. Wait 15 seconds before retrying. A successful membership lookup is cached for five minutes.\n\nThe XP, its entry in the XP history and the idempotency key are committed in a single transaction. The grant counts toward the current season when seasons are on. A grant that would take the member's XP past the largest number CEEJAY can store returns `409` with the code `xp_limit_reached`, and nothing is granted.\n\nWhen the grant takes the member up a level, CEEJAY's bot grants that level's rewards and level roles shortly afterwards. The call does not wait for it.\n\n**Retries.** Send `idempotencyKey` to make a retry safe. When the same request is sent again with its key, nothing happens a second time: the response is the original response, with `duplicate: true` and the header `Idempotent-Replayed: true`. For this operation the same request is the same member, amount and reason. Each key is tied to one request payload: two requests for one event need two keys, for example `match-9:coins` and `match-9:xp`. A key that was used for a different request returns `409` with the code `idempotency_key_reused`, and nothing is granted. A key is scoped to the API key that sent it and is retained for 30 days. On an instance that does not support idempotency keys yet, a request that sends one returns `503` with the code `idempotency_unavailable` and changes nothing. A body that sends `idempotency_key` here gets no key: this operation never read that spelling.",
        "operationId": "grantXp",
        "x-required-scope": [
          "xp:grant"
        ],
        "x-since": "1.0.0",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GrantXpRequest"
              },
              "examples": {
                "grant": {
                  "summary": "Give 250 XP",
                  "value": {
                    "discordId": "409876543210987654",
                    "amount": 250,
                    "reason": "Completed the tutorial island",
                    "idempotencyKey": "tutorial-409876543210987654"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The XP was granted. With `duplicate: true` it is the response to an earlier call with the same `idempotencyKey`.",
            "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/GrantXpResponse"
                },
                "examples": {
                  "granted": {
                    "summary": "Granted",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "previousXP": 8450,
                        "xpGranted": 250,
                        "totalXP": 8700,
                        "previousLevel": 12,
                        "currentLevel": 12,
                        "leveledUp": false,
                        "reason": "Completed the tutorial island",
                        "auditId": "10442",
                        "xp": 8700,
                        "previousXp": 8450,
                        "level": 12,
                        "duplicate": false
                      }
                    }
                  },
                  "repeated": {
                    "summary": "A repeat of an earlier call",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "previousXP": 8450,
                        "xpGranted": 250,
                        "totalXP": 8700,
                        "previousLevel": 12,
                        "currentLevel": 12,
                        "leveledUp": false,
                        "reason": "Completed the tutorial island",
                        "auditId": "10442",
                        "xp": 8700,
                        "previousXp": 8450,
                        "level": 12,
                        "duplicate": true
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected. Nothing was granted. The checks run in this order: the body, Discord ID, amount, maximum, a null character in the reason, a reason that is not a string, then the idempotency key.",
            "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": "Body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badId": {
                    "summary": "Invalid Discord ID",
                    "value": {
                      "error": "discordId must be a valid Discord snowflake (17-20 digits)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "discordId"
                    }
                  },
                  "badAmount": {
                    "summary": "Invalid amount",
                    "value": {
                      "error": "amount must be a positive integer",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "amount"
                    }
                  },
                  "tooMuch": {
                    "summary": "Amount over the maximum",
                    "value": {
                      "error": "amount cannot exceed 100000",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "amount"
                    }
                  },
                  "nullCharacter": {
                    "summary": "A null character in the reason",
                    "value": {
                      "error": "reason cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "reason"
                    }
                  },
                  "badReason": {
                    "summary": "reason is not a string",
                    "value": {
                      "error": "reason must be a string",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "reason"
                    }
                  },
                  "badKey": {
                    "summary": "Invalid idempotency key",
                    "value": {
                      "error": "idempotencyKey must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "nullInKey": {
                    "summary": "A null character in the idempotency key",
                    "value": {
                      "error": "idempotencyKey cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScopes"
          },
          "404": {
            "$ref": "#/components/responses/MemberNotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "description": "The `idempotencyKey` was already used by this API key for a different request (`idempotency_key_reused`: another operation, or other values), or the first request with it is still being processed (`idempotency_in_progress`: wait `retryAfter` seconds and retry). Nothing was changed by this call. Or the grant would take the member's XP past the largest number CEEJAY can store (`xp_limit_reached`).",
            "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": {
                  "keyReused": {
                    "summary": "The key was already used for a different 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
                    }
                  },
                  "xpLimit": {
                    "summary": "The XP would not fit",
                    "value": {
                      "error": "This grant would take the member's XP past the largest number CEEJAY can store, so nothing was changed",
                      "code": "xp_limit_reached",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "A check CEEJAY needs could not be made, and nothing was changed. Either CEEJAY has no profile for this Discord ID and could not check with Discord whether it is a member of your server (`discord_unavailable`: retry the request shortly), 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"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "discordUnavailable": {
                    "summary": "Discord could not be asked",
                    "value": {
                      "error": "Could not check with Discord that this ID is a member of the server, so nothing was changed",
                      "code": "discord_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keysUnavailable": {
                    "summary": "Keys cannot be stored yet",
                    "value": {
                      "error": "idempotencyKey cannot be used on this community yet, so nothing was changed",
                      "code": "idempotency_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/xp/deduct": {
      "post": {
        "tags": [
          "XP"
        ],
        "summary": "Deduct XP",
        "description": "Takes XP from a member. It never takes a total below 0.\n\n**Required scope:** `xp:deduct` (a key with the `*` scope can also call it).\n\nTaking more than the member has leaves them at 0 instead of failing: `xpDeducted` says what was actually taken. Unlike Grant XP, this call does not create a profile and does not ask Discord: a member CEEJAY has not seen returns `404` with the code `member_not_found`. The XP, its history entry and the idempotency key are committed in a single transaction. The deduction is added to the audit log, and to the XP history as a negative entry when something was taken. It does not change the member's season XP.\n\n**Retries.** Send `idempotencyKey` to make a retry safe. When the same request is sent again with its key, nothing happens a second time: the response is the original response, with `duplicate: true` and the header `Idempotent-Replayed: true`. For this operation the same request is the same member, amount and reason. Each key is tied to one request payload: two requests for one event need two keys, for example `match-9:coins` and `match-9:xp`. A key that was used for a different request returns `409` with the code `idempotency_key_reused`, and nothing is taken. A key is scoped to the API key that sent it and is retained for 30 days. On an instance that does not support idempotency keys yet, a request that sends one returns `503` with the code `idempotency_unavailable` and changes nothing. A body that sends `idempotency_key` here gets no key: this operation never read that spelling.",
        "operationId": "deductXp",
        "x-required-scope": [
          "xp:deduct"
        ],
        "x-since": "1.0.0",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeductXpRequest"
              },
              "examples": {
                "deduct": {
                  "summary": "Take 200 XP",
                  "value": {
                    "discordId": "409876543210987654",
                    "amount": 200,
                    "reason": "Reversed a duplicate award",
                    "idempotencyKey": "reversal-10442"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The XP was taken. With `duplicate: true` it is the response to an earlier call with the same `idempotencyKey`.",
            "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/DeductXpResponse"
                },
                "examples": {
                  "deducted": {
                    "summary": "Deducted",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "previousXP": 200,
                        "xpDeducted": 200,
                        "totalXP": 0,
                        "previousLevel": 1,
                        "currentLevel": 0,
                        "reason": "Reversed a duplicate award",
                        "auditId": "10443",
                        "xp": 0,
                        "previousXp": 200,
                        "level": 0,
                        "duplicate": false
                      }
                    }
                  },
                  "repeated": {
                    "summary": "A repeat of an earlier call",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "previousXP": 200,
                        "xpDeducted": 200,
                        "totalXP": 0,
                        "previousLevel": 1,
                        "currentLevel": 0,
                        "reason": "Reversed a duplicate award",
                        "auditId": "10443",
                        "xp": 0,
                        "previousXp": 200,
                        "level": 0,
                        "duplicate": true
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected. Nothing was taken. The checks run in this order: the body, Discord ID, amount, maximum, a null character in the reason, a reason that is not a string, then the idempotency key.",
            "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": "Body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badId": {
                    "summary": "Invalid Discord ID",
                    "value": {
                      "error": "discordId must be a valid Discord snowflake (17-20 digits)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "discordId"
                    }
                  },
                  "badAmount": {
                    "summary": "Invalid amount",
                    "value": {
                      "error": "amount must be a positive integer",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "amount"
                    }
                  },
                  "tooMuch": {
                    "summary": "Amount over the maximum",
                    "value": {
                      "error": "amount cannot exceed 2147483647",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "amount"
                    }
                  },
                  "nullCharacter": {
                    "summary": "A null character in the reason",
                    "value": {
                      "error": "reason cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "reason"
                    }
                  },
                  "badReason": {
                    "summary": "reason is not a string",
                    "value": {
                      "error": "reason must be a string",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "reason"
                    }
                  },
                  "badKey": {
                    "summary": "Invalid idempotency key",
                    "value": {
                      "error": "idempotencyKey must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "nullInKey": {
                    "summary": "A null character in the idempotency key",
                    "value": {
                      "error": "idempotencyKey cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScopes"
          },
          "404": {
            "description": "CEEJAY has no profile for this member on your server. Nothing was taken.",
            "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": {
                  "unknown": {
                    "summary": "Unknown member",
                    "value": {
                      "error": "Member not found",
                      "code": "member_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/IdempotencyUnavailable"
          }
        }
      }
    },
    "/programs": {
      "get": {
        "tags": [
          "Programs"
        ],
        "summary": "List programs",
        "description": "Lists your community's programs, so you can find the `programId` that Grant program XP and Deduct program XP need.\n\n**Required scope:** `programs:read` (a key with the `*` scope can also call it).\n\nOnly programs whose status is `active` are returned unless you pass `status`. The program XP operations work only for a program whose status is `active`. Paged by `programId`, lowest first: send `page.nextCursor` back as `cursor` and stop when it is `null`.\n\nNever returned: who made a program, its application form, its requirements, rewards and settings, its landing page, or anything about its members but how many there are.",
        "operationId": "listPrograms",
        "x-required-scope": [
          "programs:read"
        ],
        "x-since": "1.0.0",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only programs with this status, or `all` for every program. Any other value is rejected with `400`.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "draft",
                "paused",
                "archived",
                "all"
              ],
              "default": "active"
            },
            "example": "active"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "The page size: an integer from 1 to 100. A value above 100 is lowered to 100, and `0`, an empty value or omitting it defaults to 50. A negative number, a fraction or text is rejected with `400`.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "minimum": 0,
              "maximum": 100,
              "default": 50
            },
            "example": 50
          },
          {
            "$ref": "#/components/parameters/CursorQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of programs.",
            "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/ListProgramsResponse"
                },
                "examples": {
                  "programs": {
                    "summary": "One program, and no further page",
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "programId": 3,
                          "name": "Ambassadors",
                          "slug": "ambassadors",
                          "description": "Members who host events and welcome new players.",
                          "status": "active",
                          "type": "custom",
                          "memberCount": 24,
                          "maxMembers": 50,
                          "createdAt": "2026-08-12T10:00:00.000Z"
                        }
                      ],
                      "page": {
                        "nextCursor": null,
                        "limit": 50
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`status` is not one of the listed values, `limit` is not a whole number of 0 or more, or `cursor` is not one this operation gave out.",
            "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": "An unknown status",
                    "value": {
                      "error": "status must be one of active, draft, paused, archived or all",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "status"
                    }
                  },
                  "badLimit": {
                    "summary": "Invalid limit",
                    "value": {
                      "error": "limit must be a whole number of 0 or more",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "limit"
                    }
                  },
                  "badCursor": {
                    "summary": "A cursor that cannot be read",
                    "value": {
                      "error": "cursor is not one this request gave out",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "cursor"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScopes"
          },
          "404": {
            "description": "Programs are not available on this instance (`not_available`, with `feature: \"programs\"`).",
            "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/NotAvailableErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "Not available on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "programs"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/xp/program/grant": {
      "post": {
        "tags": [
          "XP"
        ],
        "summary": "Grant program XP",
        "description": "Grants a member XP inside a community program, such as an ambassador program. Program XP is tracked separately from the member's server-wide XP.\n\n**Required scope:** `xp:program:grant` (a key with the `*` scope can also call it).\n\nThe program must be `active` and the member must be enrolled with the status `accepted` or `active`. List programs (`GET /programs`) gives you the IDs. An enrolled ID CEEJAY has no member profile for is then looked up in Discord: an ID that is not in your server returns `404` with the code `member_not_found`, and when the Discord API is unavailable the call returns `503` with the code `discord_unavailable`. Nothing is changed in either case. A member who has only just joined can take up to 15 seconds to be found. Wait 15 seconds before retrying.\n\nThe XP, its line in the program's activity timeline and the idempotency key are committed in a single transaction. The grant is added to the audit log as well.\n\n**Retries.** Send `idempotencyKey` to make a retry safe. When the same request is sent again with its key, nothing happens a second time: the response is the original response, with `duplicate: true` and the header `Idempotent-Replayed: true`. For this operation the same request is the same program, member, amount and reason. Each key is tied to one request payload: two requests for one event need two keys, for example `match-9:coins` and `match-9:xp`. A key that was used for a different request returns `409` with the code `idempotency_key_reused`, and nothing is granted. A key is scoped to the API key that sent it and is retained for 30 days. On an instance that does not support idempotency keys yet, a request that sends one returns `503` with the code `idempotency_unavailable` and changes nothing.",
        "operationId": "grantProgramXp",
        "x-required-scope": [
          "xp:program:grant"
        ],
        "x-since": "1.0.0",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GrantProgramXpRequest"
              },
              "examples": {
                "grant": {
                  "summary": "Give 100 program XP",
                  "value": {
                    "programId": 3,
                    "discordId": "409876543210987654",
                    "amount": 100,
                    "reason": "Hosted a community event",
                    "idempotencyKey": "event-2026-41-host"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The program XP was granted. With `duplicate: true` it is the response to an earlier call with the same `idempotencyKey`.",
            "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/GrantProgramXpResponse"
                },
                "examples": {
                  "granted": {
                    "summary": "Granted",
                    "value": {
                      "success": true,
                      "data": {
                        "programId": 3,
                        "programName": "Ambassadors",
                        "discordId": "409876543210987654",
                        "previousXP": 400,
                        "xpGranted": 100,
                        "totalXP": 500,
                        "reason": "Hosted a community event",
                        "auditId": "10444",
                        "programXp": 500,
                        "previousProgramXp": 400,
                        "duplicate": false
                      }
                    }
                  },
                  "repeated": {
                    "summary": "A repeat of an earlier call",
                    "value": {
                      "success": true,
                      "data": {
                        "programId": 3,
                        "programName": "Ambassadors",
                        "discordId": "409876543210987654",
                        "previousXP": 400,
                        "xpGranted": 100,
                        "totalXP": 500,
                        "reason": "Hosted a community event",
                        "auditId": "10444",
                        "programXp": 500,
                        "previousProgramXp": 400,
                        "duplicate": true
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected. Nothing was changed. The checks run in this order: the body, program ID, its maximum, Discord ID, amount, its maximum, a null character in the reason, a reason that is not a string, a reason that is not well-formed Unicode, then the idempotency key.",
            "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": "Body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badProgram": {
                    "summary": "Invalid program ID",
                    "value": {
                      "error": "programId must be a positive integer",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "programId"
                    }
                  },
                  "programTooLarge": {
                    "summary": "Program ID over the maximum",
                    "value": {
                      "error": "programId cannot exceed 2147483647",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "programId"
                    }
                  },
                  "badId": {
                    "summary": "Invalid Discord ID",
                    "value": {
                      "error": "discordId must be a valid Discord snowflake (17-20 digits)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "discordId"
                    }
                  },
                  "badAmount": {
                    "summary": "Invalid amount",
                    "value": {
                      "error": "amount must be a positive integer",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "amount"
                    }
                  },
                  "tooMuch": {
                    "summary": "Amount over the maximum",
                    "value": {
                      "error": "amount cannot exceed 100000",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "amount"
                    }
                  },
                  "nullCharacter": {
                    "summary": "A null character in the reason",
                    "value": {
                      "error": "reason cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "reason"
                    }
                  },
                  "badReason": {
                    "summary": "reason is not a string",
                    "value": {
                      "error": "reason must be a string",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "reason"
                    }
                  },
                  "badUnicode": {
                    "summary": "reason contains an unpaired surrogate",
                    "value": {
                      "error": "reason must be valid Unicode text",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "reason"
                    }
                  },
                  "badKey": {
                    "summary": "Invalid idempotency key",
                    "value": {
                      "error": "idempotencyKey must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "nullInKey": {
                    "summary": "A null character in the idempotency key",
                    "value": {
                      "error": "idempotencyKey cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScopes"
          },
          "404": {
            "description": "There is no program with this ID on your server (`program_not_found`), the member is not enrolled in it (`member_not_enrolled`), the Discord ID is enrolled but is not a member of your server (`member_not_found`), or this instance does not support programs yet (`not_available`). A member who has only just joined can take up to 15 seconds to be found. Wait 15 seconds before retrying.",
            "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": {
                  "noProgram": {
                    "summary": "Program not found",
                    "value": {
                      "error": "Program not found",
                      "code": "program_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "notEnrolled": {
                    "summary": "Member not enrolled",
                    "value": {
                      "error": "Member is not enrolled in this program",
                      "code": "member_not_enrolled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "notAMember": {
                    "summary": "Not a member of your server",
                    "value": {
                      "error": "Member not found",
                      "code": "member_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "notAvailable": {
                    "summary": "Not available on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "programs"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "description": "The program or the member's status does not allow XP changes: the body names the status that blocked the call. Or the grant would take the member's program XP past the largest number CEEJAY can store (`xp_limit_reached`). The `idempotencyKey` was already used by this API key for a different request (`idempotency_key_reused`: another operation, or other values), or the first request with it is still being processed (`idempotency_in_progress`: wait `retryAfter` seconds and retry). Nothing was changed by this call.",
            "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/ProgramConflictErrorResponse"
                },
                "examples": {
                  "programNotActive": {
                    "summary": "Program is not active",
                    "value": {
                      "error": "Program is not active",
                      "code": "program_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "programStatus": "draft"
                    }
                  },
                  "memberStatus": {
                    "summary": "Member status does not allow XP changes",
                    "value": {
                      "error": "Member status does not allow XP changes",
                      "code": "member_status_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "memberStatus": "applied"
                    }
                  },
                  "xpLimit": {
                    "summary": "The XP would not fit",
                    "value": {
                      "error": "This grant would take the member's XP past the largest number CEEJAY can store, so nothing was changed",
                      "code": "xp_limit_reached",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keyReused": {
                    "summary": "The key was already used for a different 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
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "A check CEEJAY needs could not be made, and nothing was changed. Either CEEJAY has no profile for this Discord ID and could not check with Discord whether it is a member of your server (`discord_unavailable`: retry the request shortly), 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"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "discordUnavailable": {
                    "summary": "Discord could not be asked",
                    "value": {
                      "error": "Could not check with Discord that this ID is a member of the server, so nothing was changed",
                      "code": "discord_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keysUnavailable": {
                    "summary": "Keys cannot be stored yet",
                    "value": {
                      "error": "idempotencyKey cannot be used on this community yet, so nothing was changed",
                      "code": "idempotency_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/xp/program/deduct": {
      "post": {
        "tags": [
          "XP"
        ],
        "summary": "Deduct program XP",
        "description": "Takes XP from a member inside a community program. It never takes a total below 0.\n\n**Required scope:** `xp:program:deduct` (a key with the `*` scope can also call it).\n\nThe program must be `active` and the member must be enrolled with the status `accepted` or `active`. Taking more than the member has leaves them at 0: `xpDeducted` says what was actually taken. The XP, its line in the program's activity timeline and the idempotency key are committed in a single transaction. The deduction is added to the audit log as well.\n\n**Retries.** Send `idempotencyKey` to make a retry safe. When the same request is sent again with its key, nothing happens a second time: the response is the original response, with `duplicate: true` and the header `Idempotent-Replayed: true`. For this operation the same request is the same program, member, amount and reason. Each key is tied to one request payload: two requests for one event need two keys, for example `match-9:coins` and `match-9:xp`. A key that was used for a different request returns `409` with the code `idempotency_key_reused`, and nothing is taken. A key is scoped to the API key that sent it and is retained for 30 days. On an instance that does not support idempotency keys yet, a request that sends one returns `503` with the code `idempotency_unavailable` and changes nothing.",
        "operationId": "deductProgramXp",
        "x-required-scope": [
          "xp:program:deduct"
        ],
        "x-since": "1.0.0",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeductProgramXpRequest"
              },
              "examples": {
                "deduct": {
                  "summary": "Take 50 program XP",
                  "value": {
                    "programId": 3,
                    "discordId": "409876543210987654",
                    "amount": 50,
                    "reason": "Reversed a duplicate award",
                    "idempotencyKey": "reversal-10444"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The program XP was taken. With `duplicate: true` it is the response to an earlier call with the same `idempotencyKey`.",
            "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/DeductProgramXpResponse"
                },
                "examples": {
                  "deducted": {
                    "summary": "Deducted",
                    "value": {
                      "success": true,
                      "data": {
                        "programId": 3,
                        "programName": "Ambassadors",
                        "discordId": "409876543210987654",
                        "previousXP": 500,
                        "xpDeducted": 50,
                        "totalXP": 450,
                        "reason": "Reversed a duplicate award",
                        "auditId": "10445",
                        "programXp": 450,
                        "previousProgramXp": 500,
                        "duplicate": false
                      }
                    }
                  },
                  "repeated": {
                    "summary": "A repeat of an earlier call",
                    "value": {
                      "success": true,
                      "data": {
                        "programId": 3,
                        "programName": "Ambassadors",
                        "discordId": "409876543210987654",
                        "previousXP": 500,
                        "xpDeducted": 50,
                        "totalXP": 450,
                        "reason": "Reversed a duplicate award",
                        "auditId": "10445",
                        "programXp": 450,
                        "previousProgramXp": 500,
                        "duplicate": true
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected. Nothing was changed. The checks run in this order: the body, program ID, its maximum, Discord ID, amount, its maximum, a null character in the reason, a reason that is not a string, a reason that is not well-formed Unicode, then the idempotency key.",
            "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": "Body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badProgram": {
                    "summary": "Invalid program ID",
                    "value": {
                      "error": "programId must be a positive integer",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "programId"
                    }
                  },
                  "programTooLarge": {
                    "summary": "Program ID over the maximum",
                    "value": {
                      "error": "programId cannot exceed 2147483647",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "programId"
                    }
                  },
                  "badId": {
                    "summary": "Invalid Discord ID",
                    "value": {
                      "error": "discordId must be a valid Discord snowflake (17-20 digits)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "discordId"
                    }
                  },
                  "badAmount": {
                    "summary": "Invalid amount",
                    "value": {
                      "error": "amount must be a positive integer",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "amount"
                    }
                  },
                  "tooMuch": {
                    "summary": "Amount over the maximum",
                    "value": {
                      "error": "amount cannot exceed 100000",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "amount"
                    }
                  },
                  "nullCharacter": {
                    "summary": "A null character in the reason",
                    "value": {
                      "error": "reason cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "reason"
                    }
                  },
                  "badReason": {
                    "summary": "reason is not a string",
                    "value": {
                      "error": "reason must be a string",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "reason"
                    }
                  },
                  "badUnicode": {
                    "summary": "reason contains an unpaired surrogate",
                    "value": {
                      "error": "reason must be valid Unicode text",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "reason"
                    }
                  },
                  "badKey": {
                    "summary": "Invalid idempotency key",
                    "value": {
                      "error": "idempotencyKey must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "nullInKey": {
                    "summary": "A null character in the idempotency key",
                    "value": {
                      "error": "idempotencyKey cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScopes"
          },
          "404": {
            "description": "There is no program with this ID on your server (`program_not_found`), the member is not enrolled in it (`member_not_enrolled`), or this instance does not support programs yet (`not_available`).",
            "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": {
                  "noProgram": {
                    "summary": "Program not found",
                    "value": {
                      "error": "Program not found",
                      "code": "program_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "notEnrolled": {
                    "summary": "Member not enrolled",
                    "value": {
                      "error": "Member is not enrolled in this program",
                      "code": "member_not_enrolled",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "notAvailable": {
                    "summary": "Not available on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "programs"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "description": "The program or the member's status does not allow XP changes: the body names the status that blocked the call. Or the change would not fit the largest number CEEJAY can store (`xp_limit_reached`). The `idempotencyKey` was already used by this API key for a different request (`idempotency_key_reused`: another operation, or other values), or the first request with it is still being processed (`idempotency_in_progress`: wait `retryAfter` seconds and retry). Nothing was changed by this call.",
            "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/ProgramConflictErrorResponse"
                },
                "examples": {
                  "programNotActive": {
                    "summary": "Program is not active",
                    "value": {
                      "error": "Program is not active",
                      "code": "program_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "programStatus": "draft"
                    }
                  },
                  "memberStatus": {
                    "summary": "Member status does not allow XP changes",
                    "value": {
                      "error": "Member status does not allow XP changes",
                      "code": "member_status_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "memberStatus": "applied"
                    }
                  },
                  "xpLimit": {
                    "summary": "The XP would not fit",
                    "value": {
                      "error": "This change would take the member's XP past the largest number CEEJAY can store, so nothing was changed",
                      "code": "xp_limit_reached",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keyReused": {
                    "summary": "The key was already used for a different 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
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/IdempotencyUnavailable"
          }
        }
      }
    },
    "/economy/balance": {
      "get": {
        "tags": [
          "Economy"
        ],
        "summary": "Get currency balance",
        "description": "Returns a member's current balance and their lifetime totals in your community's currency.\n\n**Required scope:** `economy:read` (a key with the `*` scope can also call it).\n\nA member with no balance yet is not an error: the call returns `200` with every number at `0` and `lastUpdated` as `null`. Reading creates nothing. To tell \"no account\" from \"zero balance\", call `GET /members/{discordId}`, which returns `404` for a member CEEJAY has never seen.",
        "operationId": "getEconomyBalance",
        "x-required-scope": [
          "economy:read"
        ],
        "x-since": "1.0.0",
        "parameters": [
          {
            "$ref": "#/components/parameters/DiscordIdQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "The member's balance.",
            "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/EconomyBalanceResponse"
                },
                "examples": {
                  "member": {
                    "summary": "A member with a balance",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "balance": 2450,
                        "totalEarned": 9100,
                        "totalSpent": 6650,
                        "lastUpdated": "2026-10-01T09:14:22.118Z"
                      }
                    }
                  },
                  "noBalance": {
                    "summary": "A member with no balance yet",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "balance": 0,
                        "totalEarned": 0,
                        "totalSpent": 0,
                        "lastUpdated": null
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The Discord ID is missing, not 17 to 20 digits, or was sent more than once.",
            "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": {
                  "badId": {
                    "summary": "Invalid Discord ID",
                    "value": {
                      "error": "discordId must be a valid Discord snowflake (17-20 digits)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "discordId"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScopes"
          },
          "404": {
            "$ref": "#/components/responses/NotAvailable"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/economy/grant": {
      "post": {
        "tags": [
          "Economy"
        ],
        "summary": "Grant currency",
        "description": "Adds currency to a member's balance.\n\n**Required scope:** `economy:grant` (a key with the `*` scope can also call it).\n\nThe member's wallet is created if they have none. An ID CEEJAY has no member profile for is looked up in Discord first: a member of your server is credited, an ID that is not in your server returns `404` with the code `member_not_found`, and when Discord cannot be reached the call returns `503` with the code `discord_unavailable`. Nothing is credited in either case. A member who has only just joined can take up to 15 seconds to be found. Wait 15 seconds before retrying. A positive membership check is cached for five minutes. A repeat of a call that was already applied is handled before this lookup.\n\nThe movement is added to the member's transaction history with the `reason` you send, or `API grant`, and counts toward `totalEarned`. The grant works whether or not the community's economy is enabled.\n\nA balance cannot pass the community's maximum, which is 1,000,000 unless the community has set another. When the maximum is reached the call still succeeds: `amountGranted` is what was actually added (it can be `0`), `amountRequested` is what you asked for and `capped` is `true`. A grant that would take the member's lifetime total past the largest number CEEJAY can store returns `409` with the code `balance_limit_reached`, and nothing is credited.\n\n**Retries.** Send `idempotencyKey` to make a retry safe. When the same request is sent again with its key, nothing happens a second time: the response is the first response, with `duplicate: true` and the header `Idempotent-Replayed: true`. For this operation the same request is the same member and the same amount; the note may differ. One key is one request: two requests for one event need two keys, for example `match-9:coins` and `match-9:xp`. A key that was used for a different request returns `409` with the code `idempotency_key_reused`, and nothing moves. A key belongs to the API key that sent it and is retained for 30 days. The key still works on an instance that is still being updated. The deprecated alias `idempotency_key` is still accepted, and is the one used when both are sent. A key CEEJAY no longer has a response for (one older than 30 days, or one sent before version 1.0.0) is still recognised from the member's transaction history, permanently: the response then includes the amount and the balance of the first call, the amount and the note you sent this time, and `capped: false`.",
        "operationId": "grantEconomy",
        "x-required-scope": [
          "economy:grant"
        ],
        "x-since": "1.0.0",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GrantCurrencyRequest"
              },
              "examples": {
                "grant": {
                  "summary": "Give 500 coins",
                  "value": {
                    "discordId": "409876543210987654",
                    "amount": 500,
                    "reason": "Weekly tournament prize",
                    "idempotencyKey": "tournament-2026-40-winner"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What was credited. With `duplicate: true` it is the response to an earlier call that sent the same `idempotencyKey`.",
            "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/GrantCurrencyResponse"
                },
                "examples": {
                  "granted": {
                    "summary": "Credited in full",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "amountGranted": 500,
                        "amountRequested": 500,
                        "capped": false,
                        "duplicate": false,
                        "balance": 2950,
                        "totalEarned": 9600,
                        "description": "Weekly tournament prize",
                        "reason": "Weekly tournament prize"
                      }
                    }
                  },
                  "capped": {
                    "summary": "Cut short by the maximum balance",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "amountGranted": 200,
                        "amountRequested": 500,
                        "capped": true,
                        "duplicate": false,
                        "balance": 1000000,
                        "totalEarned": 412600,
                        "description": "Weekly tournament prize",
                        "reason": "Weekly tournament prize"
                      }
                    }
                  },
                  "repeated": {
                    "summary": "A repeat of an earlier call",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "amountGranted": 500,
                        "amountRequested": 500,
                        "capped": false,
                        "duplicate": true,
                        "balance": 2950,
                        "totalEarned": 9600,
                        "description": "Weekly tournament prize",
                        "reason": "Weekly tournament prize"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected. Nothing was credited. The checks run in this order: the body, Discord ID, amount, maximum, the idempotency key, a null character in the note, then a note that is not a string. A message names the field name you sent (`reason` or `description`, `idempotencyKey` or `idempotency_key`). A repeat of a call that was already applied is handled after these, and the member is looked up last.",
            "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": "Body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badId": {
                    "summary": "Invalid Discord ID",
                    "value": {
                      "error": "discordId must be a valid Discord snowflake (17-20 digits)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "discordId"
                    }
                  },
                  "badAmount": {
                    "summary": "Invalid amount",
                    "value": {
                      "error": "amount must be a positive integer",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "amount"
                    }
                  },
                  "tooMuch": {
                    "summary": "Amount over the maximum",
                    "value": {
                      "error": "amount cannot exceed 1000000",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "amount"
                    }
                  },
                  "badKey": {
                    "summary": "Invalid idempotency key",
                    "value": {
                      "error": "idempotencyKey must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "nullInKey": {
                    "summary": "A null character in the idempotency key",
                    "value": {
                      "error": "idempotencyKey cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "badOlderKey": {
                    "summary": "Invalid idempotency key, sent under its deprecated alias",
                    "value": {
                      "error": "idempotency_key must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotency_key"
                    }
                  },
                  "nullInReason": {
                    "summary": "A null character in the reason",
                    "value": {
                      "error": "reason cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "reason"
                    }
                  },
                  "badReason": {
                    "summary": "reason is not a string",
                    "value": {
                      "error": "reason must be a string",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "reason"
                    }
                  },
                  "nullInDescription": {
                    "summary": "A null character in the note, sent under its deprecated alias",
                    "value": {
                      "error": "description cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "description"
                    }
                  },
                  "badDescription": {
                    "summary": "The note is not a string, sent under its deprecated alias",
                    "value": {
                      "error": "description must be a string",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "description"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScopes"
          },
          "404": {
            "description": "The Discord ID is not a member of your server (`member_not_found`: CEEJAY has no profile for it, and Discord says it is not in the server), or this instance has no currency yet (`not_available`). Nothing was credited. A member who has only just joined can take up to 15 seconds to be found. Wait 15 seconds before retrying.",
            "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": {
                  "notAMember": {
                    "summary": "Not a member of your server",
                    "value": {
                      "error": "Member not found",
                      "code": "member_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "notAvailable": {
                    "summary": "Not available on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "economy"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "description": "The `idempotencyKey` was already used by this API key for a different request (`idempotency_key_reused`: another operation, or other values), or the first request with it is still being processed (`idempotency_in_progress`: wait `retryAfter` seconds, then retry). Nothing was changed by this call. Or the grant would take the member's lifetime total past the largest number CEEJAY can store (`balance_limit_reached`).",
            "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": {
                  "keyReused": {
                    "summary": "The key was already used for a different 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
                    }
                  },
                  "balanceLimit": {
                    "summary": "The total would not fit",
                    "value": {
                      "error": "This grant would take the member's total past the largest number CEEJAY can store, so nothing was changed",
                      "code": "balance_limit_reached",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "A check CEEJAY needs could not be made, and nothing was changed. Either CEEJAY has no profile for this Discord ID and could not check with Discord whether it is a member of your server (`discord_unavailable`: retry shortly), or you sent a key and this instance does not support idempotency keys (`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"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "discordUnavailable": {
                    "summary": "Discord could not be asked",
                    "value": {
                      "error": "Could not check with Discord that this ID is a member of the server, so nothing was changed",
                      "code": "discord_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keysUnavailable": {
                    "summary": "Keys cannot be stored yet",
                    "value": {
                      "error": "idempotencyKey cannot be used on this community yet, so nothing was changed",
                      "code": "idempotency_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/economy/deduct": {
      "post": {
        "tags": [
          "Economy"
        ],
        "summary": "Deduct currency",
        "description": "Deducts currency from a member.\n\n**Required scope:** `economy:deduct` (a key with the `*` scope can also call it).\n\nThe balance never goes below zero. If the member's balance is less than `amount`, nothing is deducted and the call returns `400` with the code `insufficient_balance`, `balance` and `requested`. This error keeps the status `400` it has always had: match on `code`. A member with no balance at all gets the same response, with `balance` at `0`, and no balance is created for them.\n\nA deduction is not counted as the member's spending: `totalSpent` does not change. The movement is added to the member's transaction history with the `reason` you send, or `API deduction`. The deduction works whether or not the community's economy is enabled.\n\n**Retries.** Send `idempotencyKey` to make a retry safe. When the same request is sent again with its key, nothing happens a second time: the response is the first response, with `duplicate: true` and the header `Idempotent-Replayed: true`. For this operation the same request is the same member and the same amount; the note may differ. One key is one request: two requests for one event need two keys, for example `match-9:coins` and `match-9:xp`. A key that was used for a different request returns `409` with the code `idempotency_key_reused`, and nothing moves. A key belongs to the API key that sent it and is retained for 30 days. The key still works on an instance that is still being updated. The deprecated alias `idempotency_key` is still accepted, and is the one used when both are sent. A key CEEJAY no longer has a response for (one older than 30 days, or one sent before version 1.0.0) is still recognised from the member's transaction history, permanently: the response then includes the amount and the balance of the first call, and the note you sent this time.",
        "operationId": "deductEconomy",
        "x-required-scope": [
          "economy:deduct"
        ],
        "x-since": "1.0.0",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeductCurrencyRequest"
              },
              "examples": {
                "deduct": {
                  "summary": "Take 200 coins",
                  "value": {
                    "discordId": "409876543210987654",
                    "amount": 200,
                    "reason": "Season pass",
                    "idempotencyKey": "order-88213-deduct"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What was deducted. With `duplicate: true` it is the response to an earlier call that sent the same `idempotencyKey`.",
            "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/DeductCurrencyResponse"
                },
                "examples": {
                  "deducted": {
                    "summary": "Deducted",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "amountDeducted": 200,
                        "duplicate": false,
                        "balance": 2250,
                        "description": "Season pass",
                        "reason": "Season pass"
                      }
                    }
                  },
                  "repeated": {
                    "summary": "A repeat of an earlier call",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "amountDeducted": 200,
                        "duplicate": true,
                        "balance": 2250,
                        "description": "Season pass",
                        "reason": "Season pass"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected. Nothing was deducted. The checks run in this order: the body, Discord ID, amount, maximum, the idempotency key, a null character in the note, a note that is not a string, then the balance. A message names the field name you sent.",
            "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/DeductCurrencyErrorResponse"
                },
                "examples": {
                  "notObject": {
                    "summary": "Body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badId": {
                    "summary": "Invalid Discord ID",
                    "value": {
                      "error": "discordId must be a valid Discord snowflake (17-20 digits)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "discordId"
                    }
                  },
                  "badAmount": {
                    "summary": "Invalid amount",
                    "value": {
                      "error": "amount must be a positive integer",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "amount"
                    }
                  },
                  "tooMuch": {
                    "summary": "Amount over the maximum",
                    "value": {
                      "error": "amount cannot exceed 2147483647",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "amount"
                    }
                  },
                  "badKey": {
                    "summary": "Invalid idempotency key",
                    "value": {
                      "error": "idempotencyKey must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "nullInKey": {
                    "summary": "A null character in the idempotency key",
                    "value": {
                      "error": "idempotencyKey cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "badOlderKey": {
                    "summary": "Invalid idempotency key, sent under its deprecated alias",
                    "value": {
                      "error": "idempotency_key must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotency_key"
                    }
                  },
                  "nullInReason": {
                    "summary": "A null character in the reason",
                    "value": {
                      "error": "reason cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "reason"
                    }
                  },
                  "badReason": {
                    "summary": "reason is not a string",
                    "value": {
                      "error": "reason must be a string",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "reason"
                    }
                  },
                  "nullInDescription": {
                    "summary": "A null character in the note, sent under its deprecated alias",
                    "value": {
                      "error": "description cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "description"
                    }
                  },
                  "badDescription": {
                    "summary": "The note is not a string, sent under its deprecated alias",
                    "value": {
                      "error": "description must be a string",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "description"
                    }
                  },
                  "insufficient": {
                    "summary": "The member's balance is less than the amount",
                    "value": {
                      "error": "Insufficient balance",
                      "code": "insufficient_balance",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "currentBalance": 120,
                      "requested": 500,
                      "balance": 120
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScopes"
          },
          "404": {
            "$ref": "#/components/responses/NotAvailable"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "You sent an `idempotencyKey`, and this instance does not support idempotency keys (`idempotency_unavailable`). Nothing was changed. Waiting does not change this response: contact support to finish the update, or send the request without a key.",
            "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": {
                  "keysUnavailable": {
                    "summary": "Keys cannot be stored yet",
                    "value": {
                      "error": "idempotencyKey cannot be used on this community yet, so nothing was changed",
                      "code": "idempotency_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/shop/purchases": {
      "get": {
        "tags": [
          "Shop"
        ],
        "summary": "List shop purchases",
        "description": "Lists the purchases, and the rewards members earned, that are delivered by your API and have not yet been confirmed by your server.\n\n**Required scope:** `shop:fulfil` (a key with the `*` scope can also call it).\n\nA purchase is in one of two states. `processing`: your server responded to the delivery with pending, and CEEJAY is awaiting Complete a shop delivery. `retrying`: the delivery failed and CEEJAY tries it again every hour. A purchase in `processing` is never sent again, so this list is how a server that lost a delivery learns of it. Finish each one with `POST /shop/purchases/{purchaseId}/complete`.\n\nA purchase made through a game names the player in `player`, as the delivery did, so an item that was lost on the way can still be delivered to the right player.\n\nPaged by `purchaseId`, oldest first: send `page.nextCursor` back as `cursor` and stop when it is `null`. Polling once a minute uses one of a key's requests a minute.\n\nNever returned: the item's delivery URL, its signing secret or the rest of its settings, the text of the last delivery error, team notes, member names and avatars, or purchases of items that are not delivered by your API.",
        "operationId": "listShopPurchases",
        "x-required-scope": [
          "shop:fulfil"
        ],
        "x-since": "1.0.0",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Which purchases to list. Only `waiting` exists today, and it is the default. Any other value is rejected with `400`.",
            "schema": {
              "type": "string",
              "enum": [
                "waiting"
              ],
              "default": "waiting"
            },
            "example": "waiting"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "The page size: an integer from 1 to 100. A value above 100 is lowered to 100, and `0`, an empty value or omitting it defaults to 50. A negative number, a fraction or text is rejected with `400`.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "minimum": 0,
              "maximum": 100,
              "default": 50
            },
            "example": 50
          },
          {
            "$ref": "#/components/parameters/CursorQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of waiting 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/ListShopPurchasesResponse"
                },
                "examples": {
                  "waiting": {
                    "summary": "One purchase awaiting your confirmation, and one being retried",
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "purchaseId": 4411,
                          "deliveryId": "shop_4411",
                          "event": "shop.purchase",
                          "state": "processing",
                          "item": {
                            "id": 12,
                            "name": "Gold pack",
                            "itemKey": "gold_500"
                          },
                          "discordId": "234567890123456789",
                          "pricePaid": 500,
                          "createdAt": "2026-10-07T18:00:00.000Z",
                          "pendingSince": "2026-10-07T18:00:02.000Z",
                          "completeBy": "2026-10-08T18:00:02.000Z",
                          "attempts": 1,
                          "lastAttemptAt": "2026-10-07T18:00:02.000Z",
                          "answer": null,
                          "input": null,
                          "player": {
                            "game": "rugby_manager",
                            "playerId": "p_19",
                            "proof": "verified"
                          }
                        },
                        {
                          "purchaseId": 4412,
                          "deliveryId": "shop_4412",
                          "event": "reward.earned",
                          "state": "retrying",
                          "item": {
                            "id": null,
                            "name": "Founder skin",
                            "itemKey": "skin_founder"
                          },
                          "discordId": "234567890123456789",
                          "pricePaid": 0,
                          "createdAt": "2026-10-07T18:20:00.000Z",
                          "pendingSince": null,
                          "completeBy": null,
                          "attempts": 2,
                          "lastAttemptAt": "2026-10-07T19:20:04.000Z",
                          "answer": null,
                          "input": null,
                          "player": null,
                          "reward": {
                            "id": 31,
                            "sourceKind": "level",
                            "sourceId": "10",
                            "sourceLabel": "Level 10"
                          }
                        }
                      ],
                      "page": {
                        "nextCursor": null,
                        "limit": 50
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`status` is not `waiting`, `limit` is not a whole number of 0 or more, or `cursor` is not one this operation gave out.",
            "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 other than waiting",
                    "value": {
                      "error": "status must be 'waiting'",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "status"
                    }
                  },
                  "badLimit": {
                    "summary": "Invalid limit",
                    "value": {
                      "error": "limit must be a whole number of 0 or more",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "limit"
                    }
                  },
                  "badCursor": {
                    "summary": "A cursor that cannot be read",
                    "value": {
                      "error": "cursor is not one this request gave out",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "cursor"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScopes"
          },
          "404": {
            "description": "Shop delivery is not available on this instance (`not_available`, with `feature: \"shopDelivery\"`).",
            "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/NotAvailableErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "Not available on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "shopDelivery"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/shop/purchases/{purchaseId}/complete": {
      "post": {
        "tags": [
          "Shop"
        ],
        "summary": "Complete a shop delivery",
        "description": "Tells CEEJAY how a \"Delivered by your API\" shop purchase ended, when your endpoint did not respond to the delivery request immediately.\n\n**Required scope:** `shop:fulfil` (a key with the `*` scope can also call it).\n\nUse it after CEEJAY sent you a delivery request (the `shop.purchase` webhook in this document) and your endpoint responded with `202` or `{ \"status\": \"pending\" }`, or while CEEJAY is waiting to retry the delivery. It works on purchases of your server whose item is delivered by your API, while their status is Being delivered or waiting for a retry. It also works on a reward a member earned that is set to be delivered by your API (the `reward.earned` webhook). List shop purchases (`GET /shop/purchases`) lists the ones that await your confirmation.\n\n`delivered` marks the purchase fulfilled. `failed` refunds the member and lets the team know, and `message` is shown to them. An earned reward was not paid for, so `failed` marks it failed and refunds nothing.\n\nSending the same request again is safe: the call returns `200` with `alreadyDone: true`. Sending the opposite status to a purchase that has already reached a final status returns `409` with the code `wrong_status`. A purchase that is still pending after 24 hours is refunded by CEEJAY.\n\nA body that is not a JSON object is read as an empty one, so it returns the `status` message.\n\n**Retries.** Send `idempotencyKey` to make a retry safe. When the same request is sent again with its key, nothing happens a second time: the response is the original response, with `duplicate: true` and the header `Idempotent-Replayed: true`. For this operation the same request is the same purchase, status and message. Each key is tied to one request payload: two requests for one event need two keys, for example `match-9:coins` and `match-9:xp`. A key that was used for a different request returns `409` with the code `idempotency_key_reused`, and nothing is changed. A key is scoped to the API key that sent it and is retained for 30 days. On an instance that does not support idempotency keys yet, a request that sends one returns `503` with the code `idempotency_unavailable` and changes nothing. A second completion of a purchase is the same change, so when a first request was started more than a minute ago and never finished, a retry with the same key is processed again and returns normally.",
        "operationId": "completeShopPurchase",
        "x-required-scope": [
          "shop:fulfil"
        ],
        "x-since": "1.0.0",
        "parameters": [
          {
            "$ref": "#/components/parameters/PurchaseIdPath"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompleteShopPurchaseRequest"
              },
              "examples": {
                "delivered": {
                  "summary": "Delivered",
                  "value": {
                    "status": "delivered"
                  }
                },
                "failed": {
                  "summary": "Could not deliver",
                  "value": {
                    "status": "failed",
                    "message": "The player has no account on this realm"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The purchase was updated, or was already in the state you asked for.",
            "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/CompleteShopPurchaseResponse"
                },
                "examples": {
                  "fulfilled": {
                    "summary": "Delivered",
                    "value": {
                      "success": true,
                      "purchaseId": 4242,
                      "status": "fulfilled",
                      "data": {
                        "purchaseId": 4242,
                        "status": "fulfilled",
                        "alreadyDone": false,
                        "refunded": null,
                        "duplicate": false
                      }
                    }
                  },
                  "refunded": {
                    "summary": "Failed, member refunded",
                    "value": {
                      "success": true,
                      "purchaseId": 4242,
                      "status": "refunded",
                      "refunded": 500,
                      "data": {
                        "purchaseId": 4242,
                        "status": "refunded",
                        "alreadyDone": false,
                        "refunded": 500,
                        "duplicate": false
                      }
                    }
                  },
                  "failedReward": {
                    "summary": "Failed, for a reward a member earned",
                    "value": {
                      "success": true,
                      "purchaseId": 4243,
                      "status": "failed",
                      "data": {
                        "purchaseId": 4243,
                        "status": "failed",
                        "alreadyDone": false,
                        "refunded": null,
                        "duplicate": false
                      }
                    }
                  },
                  "repeated": {
                    "summary": "You had already sent this request",
                    "value": {
                      "success": true,
                      "purchaseId": 4242,
                      "status": "fulfilled",
                      "alreadyDone": true,
                      "data": {
                        "purchaseId": 4242,
                        "status": "fulfilled",
                        "alreadyDone": true,
                        "refunded": null,
                        "duplicate": false
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The purchase ID is not a whole number or is above 2,147,483,647, the body is not valid, or CEEJAY's bot rejected the request (the last example).",
            "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": {
                  "badId": {
                    "summary": "Purchase ID not a number",
                    "value": {
                      "error": "A numeric purchase id is required",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "purchaseId"
                    }
                  },
                  "idTooLarge": {
                    "summary": "Purchase ID over the maximum",
                    "value": {
                      "error": "The purchase id cannot exceed 2147483647",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "purchaseId"
                    }
                  },
                  "badStatus": {
                    "summary": "Unknown status",
                    "value": {
                      "error": "status must be 'delivered' or 'failed'",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "status"
                    }
                  },
                  "badMessage": {
                    "summary": "Note not text",
                    "value": {
                      "error": "message must be text",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "message"
                    }
                  },
                  "longMessage": {
                    "summary": "Note too long",
                    "value": {
                      "error": "message must be 500 characters or fewer",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "message"
                    }
                  },
                  "nullInMessage": {
                    "summary": "A null character in the note",
                    "value": {
                      "error": "message cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "message"
                    }
                  },
                  "halfPairInMessage": {
                    "summary": "An unpaired surrogate in the note",
                    "value": {
                      "error": "message must be valid Unicode text",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "message"
                    }
                  },
                  "badKey": {
                    "summary": "Invalid idempotency key",
                    "value": {
                      "error": "idempotencyKey must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "nullInKey": {
                    "summary": "A null character in the idempotency key",
                    "value": {
                      "error": "idempotencyKey cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "refused": {
                    "summary": "Rejected by the bot",
                    "value": {
                      "error": "The request was not valid",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScopes"
          },
          "404": {
            "description": "There is no purchase with this ID on your server that is delivered by your API (`purchase_not_found`), or this instance does not support shop delivery yet (`not_available`).",
            "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": {
                  "noPurchase": {
                    "summary": "Purchase not found",
                    "value": {
                      "error": "Purchase not found",
                      "code": "purchase_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "gone": {
                    "summary": "The purchase disappeared while being updated",
                    "value": {
                      "error": "This purchase no longer exists on this server",
                      "code": "purchase_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "notAvailable": {
                    "summary": "Not available on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "shopDelivery"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "description": "The purchase has already changed status, so it cannot be marked this way (`wrong_status`): `status` is its current status. The `idempotencyKey` was already used by this API key for a different request (`idempotency_key_reused`: another operation, or other values), or the first request with it is still being processed (`idempotency_in_progress`: wait `retryAfter` seconds and retry). Nothing was changed by this call.",
            "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/PurchaseErrorResponse"
                },
                "examples": {
                  "wrongStatus": {
                    "summary": "Already refunded",
                    "value": {
                      "error": "This purchase is refunded and can no longer be marked delivered",
                      "code": "wrong_status",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "status": "refunded"
                    }
                  },
                  "movedOn": {
                    "summary": "Status changed while being updated",
                    "value": {
                      "error": "This reward's status has already changed",
                      "code": "wrong_status",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "status": null
                    }
                  },
                  "keyReused": {
                    "summary": "The key was already used for a different 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
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "CEEJAY's bot, which finishes the purchase, could not be reached or could not finish. 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": {
                  "unreachable": {
                    "summary": "The bot could not finish",
                    "value": {
                      "error": "The delivery could not be finished. Try again",
                      "code": "bot_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/IdempotencyUnavailable"
          },
          "504": {
            "description": "CEEJAY's bot did not respond in time. 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": {
                  "timeout": {
                    "summary": "The bot timed out",
                    "value": {
                      "error": "The bot did not answer in time. Send the same request again: it is safe to repeat",
                      "code": "bot_timeout",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/quests/list": {
      "get": {
        "tags": [
          "Quests"
        ],
        "summary": "List quests",
        "description": "Lists the quests set up on your server, with their rewards and completion rules.\n\n**Required scope:** `quests:read` (a key with the `*` scope can also call it).\n\nOnly quests whose status is `active` are returned unless you pass `status`. They are ordered by the sort order set in the dashboard, then newest first.\n\n**Paging.** There are two ways to page; send one of them. With `cursor`, send the `page.nextCursor` of the page before and stop when it is `null`: every quest comes back once, also when quests are added while you page. With `offset`, skip a number of quests, as before. When both are sent the page starts at `cursor` and `offset` is not used. `total`, `limit` and `offset` are returned alongside `data` in every response.\n\nEach quest includes the rules Complete a quest applies to it: `status`, `startsAt`, `endsAt`, `isRepeatable`, `maxCompletions`, `maxCompletionsPerMember`, `repeatCooldownHours`, `requiredLevel`, `requiredRoleId`, `prerequisiteQuestId`, `requiredSeasonXp` and `programId`. The community's daily quest limit is in Community details (`GET /community`).",
        "operationId": "listQuests",
        "x-required-scope": [
          "quests:read"
        ],
        "x-since": "1.0.0",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only quests with this status, for example `draft`, `paused` or `archived`. It is not checked against a list. Defaults to `active`. A null character (`%00`) in it is rejected with `400`.",
            "schema": {
              "type": "string",
              "default": "active"
            },
            "example": "active"
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Only quests in this category. A null character (`%00`) in it is rejected with `400`.",
            "schema": {
              "type": "string"
            },
            "example": "integrations"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "The page size: an integer from 1 to 100. A value above 100 is lowered to 100, and `0`, an empty value or omitting it defaults to 50. A negative number, a fraction or text is rejected with `400`.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "minimum": 0,
              "maximum": 100,
              "default": 50
            },
            "example": 20
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "How many quests to skip: an integer from 0 to 2,147,483,647. Not used when `cursor` is sent. A negative number, a fraction, text or a larger number is rejected with `400`.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "minimum": 0,
              "maximum": 2147483647,
              "default": 0
            },
            "example": 0
          },
          {
            "$ref": "#/components/parameters/CursorQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of quests. `page` says whether there is a next one. `total`, `limit` and `offset` are returned alongside `data`.",
            "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/ListQuestsResponse"
                },
                "examples": {
                  "page": {
                    "summary": "One quest, and no further page",
                    "value": {
                      "success": true,
                      "total": 1,
                      "limit": 20,
                      "offset": 0,
                      "data": [
                        {
                          "id": 42,
                          "title": "Join the weekend tournament",
                          "description": "Play one match in the weekend tournament.",
                          "icon_emoji": "🏆",
                          "category": "integrations",
                          "quest_type": "api_check",
                          "xp_reward": 500,
                          "currency_reward": 250,
                          "role_reward_id": "982371004827100394",
                          "badge_id": 7,
                          "is_repeatable": false,
                          "max_completions": null,
                          "max_completions_per_user": 1,
                          "starts_at": null,
                          "ends_at": null,
                          "status": "active",
                          "verification_type": "api",
                          "program_id": null,
                          "created_at": "2026-09-02T12:00:00.000Z",
                          "repeat_cooldown_hours": null,
                          "required_level": 5,
                          "required_role_id": null,
                          "prerequisite_quest_id": null,
                          "required_season_xp": null,
                          "iconEmoji": "🏆",
                          "questType": "api_check",
                          "xpReward": 500,
                          "currencyReward": 250,
                          "roleRewardId": "982371004827100394",
                          "badgeId": 7,
                          "isRepeatable": false,
                          "maxCompletions": null,
                          "maxCompletionsPerMember": 1,
                          "startsAt": null,
                          "endsAt": null,
                          "verificationType": "api",
                          "programId": null,
                          "createdAt": "2026-09-02T12:00:00.000Z",
                          "repeatCooldownHours": null,
                          "requiredLevel": 5,
                          "requiredRoleId": null,
                          "prerequisiteQuestId": null,
                          "requiredSeasonXp": null
                        }
                      ],
                      "page": {
                        "nextCursor": null,
                        "limit": 20
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`limit` or `offset` is not a whole number of 0 or more, or `offset` is above 2,147,483,647. A `limit` or `offset` sent more than once is rejected too. So is a `status` or a `category` that contains a null character (U+0000, `%00` in the URL), and a `cursor` that was not returned by List 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/ErrorResponse"
                },
                "examples": {
                  "badLimit": {
                    "summary": "Invalid limit",
                    "value": {
                      "error": "limit must be a whole number of 0 or more",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "limit"
                    }
                  },
                  "badOffset": {
                    "summary": "Invalid offset",
                    "value": {
                      "error": "offset must be a whole number from 0 to 2147483647",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "offset"
                    }
                  },
                  "nullInStatus": {
                    "summary": "A null character in the status",
                    "value": {
                      "error": "status cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "status"
                    }
                  },
                  "nullInCategory": {
                    "summary": "A null character in the category",
                    "value": {
                      "error": "category cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "category"
                    }
                  },
                  "badCursor": {
                    "summary": "A cursor that cannot be read",
                    "value": {
                      "error": "cursor cannot be read. Send page.nextCursor as it was answered",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "cursor"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScopes"
          },
          "404": {
            "$ref": "#/components/responses/NotAvailable"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/quests/complete": {
      "post": {
        "tags": [
          "Quests"
        ],
        "summary": "Complete a quest",
        "description": "Marks a quest completed for a member and grants the quest's rewards.\n\n**Required scope:** `quests:complete` (a key with the `*` scope can also call it).\n\n**What it checks.** The quest must exist on your server. The call then applies the quest's own rules, the ones CEEJAY applies when a member does the quest in Discord, in this order, and the first one that is not met is the one returned: `quest_not_active` (`400`), `quest_not_started`, `quest_ended`, `quest_requires_role`, `quest_requires_level`, `quest_requires_season_xp`, `quest_requires_program`, `quest_full`, `quest_already_completed` (`400`), `quest_limit_reached` (`400`), `quest_cooling_down`, `quest_requires_quest`, `quest_daily_limit_reached`. A rule that is not met returns `409` with a `code` that identifies it, and nothing is completed. Three of them keep the status `400` they have always had (marked in the list): match on the code, not the status. List quests returns each quest's rules, so you can look before you call. The quest's own verification and review are not run: when you call, you are vouching that the member did the quest.\n\n**The daily quest limit.** Your community's daily quest limit (5 unless the community set another; Community details reports it) counts the quests a member completes in one UTC day, across all quests. It is a rule about starting: it is applied when the member has no open run of this quest, and a run they started in Discord before they reached the limit still completes.\n\nThe role a quest requires is read from Discord. When Discord cannot be reached for it, the call returns `503` with the code `member_roles_unavailable` and completes nothing.\n\n**The member.** An ID CEEJAY has no member profile for is looked up in Discord first. A member of your server is completed for, and gets a profile from the XP and a balance from the currency. An ID that is not in your server returns `404` with the code `member_not_found`, and when the Discord API is unavailable the call returns `503` with the code `discord_unavailable`. Nothing is written in either case. A member who has only just joined can take up to 15 seconds to be found. Wait 15 seconds before retrying.\n\n**A completion is a single transaction.** The completion, the XP, the currency, the badge, the program XP and the idempotency key are committed in a single transaction: a call that fails part of the way leaves nothing behind, so it can be sent again. Concurrent requests for the same member and quest are processed one at a time, so they cannot both pass the checks. When the member has an open run of the quest (one they started in Discord, in progress or waiting for review), that run is completed. A new run is added only when there is none. A completion that takes the last place of a quest with an overall limit ends the quest.\n\n**Rewards in the response.** XP is granted the same way as `POST /xp/grant`. Currency is credited up to the community's maximum balance; while the community's economy is disabled the quest's coins are not granted, and `rewards.currencySkipped` is `true`. The quest's badge is awarded if the member does not have it yet. A quest in a program adds its XP to the member's score in that program (`rewards.programXp`).\n\n**What happens after the response.** CEEJAY's bot then assigns the quest's role (`rewards.roleId`), grants the extra rewards set on the quest in the Rewards Engine, grants level rewards and assigns level roles, and sends the `quest.completed` webhook. The call does not wait for it, and returns `200` when the bot is down: these steps run once the bot is back online. If you call Grant a role, Grant program XP or Grant currency yourself after a completion to add what this operation used to leave out, stop doing that: the member would now get it twice.\n\n**Retries.** Send `idempotencyKey` to make a retry safe. When the same request is sent again with its key, nothing happens a second time: the response is the original response, with `duplicate: true` and the header `Idempotent-Replayed: true`. For this operation the same request is the same member, quest and proof, and the key protects the whole completion: its XP, badge and program XP as well as its currency. A retry returns the original response even when the quest has ended or reached a limit since. Each key is tied to one request payload: two requests for one event need two keys, for example `match-9:coins` and `match-9:xp`. A key that was used for a different request returns `409` with the code `idempotency_key_reused`, and nothing is completed. A key is scoped to the API key that sent it and is retained for 30 days. The key keeps working on an instance that has not been updated yet. The deprecated alias `idempotency_key` is still accepted, and is the one used when both are sent. A completion that granted coins is also recognised from the member's transaction history when CEEJAY no longer stores its response (a key older than 30 days, or one sent before version 1.0.0): the response then includes the coins and the balance of the first call and the quest as it is now. Without a key, a second call is a new completion and is checked like one.\n\n**Tasks your platform checks.** To report that a member did something your platform checks, for a quest task of the type Completed by your API, use `POST /quests/tasks/complete` (Complete a quest task) instead.",
        "operationId": "completeQuest",
        "x-required-scope": [
          "quests:complete"
        ],
        "x-since": "1.0.0",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompleteQuestRequest"
              },
              "examples": {
                "complete": {
                  "summary": "Complete a quest with proof",
                  "value": {
                    "discordId": "409876543210987654",
                    "questId": 42,
                    "proof": {
                      "matchId": "m-88213",
                      "score": 14
                    },
                    "idempotencyKey": "match-m-88213"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The quest was completed and its rewards were granted. With `duplicate: true`, this is the response to an earlier call with the same `idempotencyKey`: nothing was completed or granted this time.",
            "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/CompleteQuestResponse"
                },
                "examples": {
                  "completed": {
                    "summary": "Completed",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "questId": 42,
                        "questTitle": "Join the weekend tournament",
                        "status": "completed",
                        "rewards": {
                          "xp": 500,
                          "currency": 250,
                          "roleId": "982371004827100394",
                          "badgeId": 7,
                          "currencyRequested": 250,
                          "currencyCapped": false,
                          "balance": 2700,
                          "programXp": 0
                        },
                        "progress": {
                          "id": 9012,
                          "server_id": "721104938271003842",
                          "user_id": "409876543210987654",
                          "quest_id": 42,
                          "status": "completed",
                          "progress_data": {},
                          "submission_data": {
                            "matchId": "m-88213",
                            "score": 14
                          },
                          "review_note": null,
                          "reviewed_by": null,
                          "completed_at": "2026-10-01T09:14:22.118Z",
                          "created_at": "2026-10-01T09:14:22.118Z",
                          "updated_at": "2026-10-01T09:14:22.118Z"
                        },
                        "duplicate": false,
                        "run": {
                          "id": "9012",
                          "status": "completed",
                          "startedAt": "2026-10-01T09:14:22.118Z",
                          "completedAt": "2026-10-01T09:14:22.118Z"
                        }
                      }
                    }
                  },
                  "repeated": {
                    "summary": "A repeat of an earlier call",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "questId": 42,
                        "questTitle": "Join the weekend tournament",
                        "status": "completed",
                        "rewards": {
                          "xp": 500,
                          "currency": 250,
                          "roleId": "982371004827100394",
                          "badgeId": 7,
                          "currencyRequested": 250,
                          "currencyCapped": false,
                          "balance": 2700,
                          "programXp": 0
                        },
                        "progress": {
                          "id": 9012,
                          "server_id": "721104938271003842",
                          "user_id": "409876543210987654",
                          "quest_id": 42,
                          "status": "completed",
                          "progress_data": {},
                          "submission_data": {
                            "matchId": "m-88213",
                            "score": 14
                          },
                          "review_note": null,
                          "reviewed_by": null,
                          "completed_at": "2026-10-01T09:14:22.118Z",
                          "created_at": "2026-10-01T09:14:22.118Z",
                          "updated_at": "2026-10-01T09:14:22.118Z"
                        },
                        "duplicate": true,
                        "run": {
                          "id": "9012",
                          "status": "completed",
                          "startedAt": "2026-10-01T09:14:22.118Z",
                          "completedAt": "2026-10-01T09:14:22.118Z"
                        }
                      }
                    }
                  },
                  "coinsSkipped": {
                    "summary": "Completed while the community's economy is disabled",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "questId": 42,
                        "questTitle": "Join the weekend tournament",
                        "status": "completed",
                        "rewards": {
                          "xp": 500,
                          "currency": 0,
                          "roleId": "982371004827100394",
                          "badgeId": 7,
                          "currencySkipped": true,
                          "programXp": 0
                        },
                        "progress": {
                          "id": 9012,
                          "server_id": "721104938271003842",
                          "user_id": "409876543210987654",
                          "quest_id": 42,
                          "status": "completed",
                          "progress_data": {},
                          "submission_data": {
                            "matchId": "m-88213",
                            "score": 14
                          },
                          "review_note": null,
                          "reviewed_by": null,
                          "completed_at": "2026-10-01T09:14:22.118Z",
                          "created_at": "2026-10-01T09:14:22.118Z",
                          "updated_at": "2026-10-01T09:14:22.118Z"
                        },
                        "duplicate": false,
                        "run": {
                          "id": "9012",
                          "status": "completed",
                          "startedAt": "2026-10-01T09:14:22.118Z",
                          "completedAt": "2026-10-01T09:14:22.118Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected. Nothing was completed or granted. The first checks run in this order: the body, Discord ID, quest ID, the idempotency key, a null character in the proof, a proof nested more than 32 levels deep, then a proof that is not well-formed Unicode. After those, three of the quest's rules return this status, as they always have: `quest_not_active`, `quest_already_completed` and `quest_limit_reached`.",
            "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/QuestRefusalResponse"
                },
                "examples": {
                  "notObject": {
                    "summary": "Body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badId": {
                    "summary": "Invalid Discord ID",
                    "value": {
                      "error": "discordId must be a valid Discord snowflake (17-20 digits)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "discordId"
                    }
                  },
                  "badQuest": {
                    "summary": "Quest ID missing, or not a number or a string of digits",
                    "value": {
                      "error": "questId must be a number",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "questId"
                    }
                  },
                  "questNotWhole": {
                    "summary": "Quest ID zero, negative, with a fraction, or written with a leading zero",
                    "value": {
                      "error": "questId must be a positive integer",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "questId"
                    }
                  },
                  "questTooLarge": {
                    "summary": "Quest ID over the maximum",
                    "value": {
                      "error": "questId cannot exceed 2147483647",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "questId"
                    }
                  },
                  "badKey": {
                    "summary": "Invalid idempotency key",
                    "value": {
                      "error": "idempotencyKey must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "nullInKey": {
                    "summary": "A null character in the idempotency key",
                    "value": {
                      "error": "idempotencyKey cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "badOlderKey": {
                    "summary": "Invalid idempotency key, sent under its deprecated alias",
                    "value": {
                      "error": "idempotency_key must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotency_key"
                    }
                  },
                  "nullInProof": {
                    "summary": "A null character in the proof",
                    "value": {
                      "error": "proof cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "proof"
                    }
                  },
                  "proofTooDeep": {
                    "summary": "A proof nested more than 32 levels deep",
                    "value": {
                      "error": "proof is nested too deeply",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "proof"
                    }
                  },
                  "proofBadUnicode": {
                    "summary": "A proof that contains an unpaired surrogate",
                    "value": {
                      "error": "proof must be valid Unicode text",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "proof"
                    }
                  },
                  "notActive": {
                    "summary": "Quest is not active",
                    "value": {
                      "error": "Quest is not active (status: draft)",
                      "code": "quest_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "alreadyDone": {
                    "summary": "Already completed",
                    "value": {
                      "error": "Quest already completed and is not repeatable",
                      "code": "quest_already_completed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "limitReached": {
                    "summary": "Per-member limit reached",
                    "value": {
                      "error": "Max completions per user reached",
                      "code": "quest_limit_reached",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScopes"
          },
          "404": {
            "description": "There is no quest with this ID on your server (`quest_not_found`), the Discord ID is not a member of your server (`member_not_found`), or this instance does not support quests yet (`not_available`). Nothing was completed. A member who has only just joined can take up to 15 seconds to be found. Wait 15 seconds before retrying.",
            "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": {
                  "noQuest": {
                    "summary": "Quest not found",
                    "value": {
                      "error": "Quest not found",
                      "code": "quest_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "notAMember": {
                    "summary": "Not a member of your server",
                    "value": {
                      "error": "Member not found",
                      "code": "member_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "notAvailable": {
                    "summary": "Not available on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "quests"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "description": "One of the quest's rules is not met, and nothing was completed: `code` names the rule, and some add the state that caused them. Or a reward would not fit the largest number CEEJAY can store (`xp_limit_reached`, `balance_limit_reached`). The `idempotencyKey` was already used by this API key for a different request (`idempotency_key_reused`: another operation, or other values), or the first request with it is still being processed (`idempotency_in_progress`: wait `retryAfter` seconds and retry). Nothing was changed by this call.",
            "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/QuestRefusalResponse"
                },
                "examples": {
                  "notStarted": {
                    "summary": "Before the quest's start date",
                    "value": {
                      "error": "Quest has not started yet",
                      "code": "quest_not_started",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "startsAt": "2026-11-01T00:00:00.000Z"
                    }
                  },
                  "ended": {
                    "summary": "After the quest's end date",
                    "value": {
                      "error": "Quest has ended",
                      "code": "quest_ended",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "endsAt": "2026-09-30T23:59:59.000Z"
                    }
                  },
                  "requiresRole": {
                    "summary": "Required role missing",
                    "value": {
                      "error": "The member does not have the role this quest requires",
                      "code": "quest_requires_role",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "requiredRoleId": "982371004827100394"
                    }
                  },
                  "requiresLevel": {
                    "summary": "Required level not reached",
                    "value": {
                      "error": "The member has not reached the level this quest requires",
                      "code": "quest_requires_level",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "requiredLevel": 10,
                      "level": 7
                    }
                  },
                  "requiresSeasonXp": {
                    "summary": "Season XP not earned",
                    "value": {
                      "error": "The member has not earned the season XP this quest requires",
                      "code": "quest_requires_season_xp",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "requiredSeasonXp": 500,
                      "seasonXp": 120,
                      "seasonRunning": true
                    }
                  },
                  "requiresProgram": {
                    "summary": "Not in the quest's program",
                    "value": {
                      "error": "The member is not in the program this quest belongs to",
                      "code": "quest_requires_program",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "programId": 3
                    }
                  },
                  "full": {
                    "summary": "Overall limit reached",
                    "value": {
                      "error": "Quest has reached its completion limit",
                      "code": "quest_full",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "maxCompletions": 100
                    }
                  },
                  "coolingDown": {
                    "summary": "Too soon after the last run",
                    "value": {
                      "error": "The member has to wait before completing this quest again",
                      "code": "quest_cooling_down",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "availableAt": "2026-10-08T09:14:22.118Z"
                    }
                  },
                  "requiresQuest": {
                    "summary": "Earlier quest not completed",
                    "value": {
                      "error": "The member has not completed the quest that comes before this one",
                      "code": "quest_requires_quest",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "prerequisiteQuestId": 41
                    }
                  },
                  "dailyLimit": {
                    "summary": "Daily quest limit reached",
                    "value": {
                      "error": "The member has reached this community's daily quest limit",
                      "code": "quest_daily_limit_reached",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "dailyQuestLimit": 5,
                      "completedToday": 5,
                      "resetsAt": "2026-10-02T00:00:00.000Z"
                    }
                  },
                  "xpLimit": {
                    "summary": "The XP would not fit",
                    "value": {
                      "error": "This grant would take the member's XP past the largest number CEEJAY can store, so nothing was changed",
                      "code": "xp_limit_reached",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "balanceLimit": {
                    "summary": "The total would not fit",
                    "value": {
                      "error": "This grant would take the member's total past the largest number CEEJAY can store, so nothing was changed",
                      "code": "balance_limit_reached",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keyReused": {
                    "summary": "The key was already used for a different 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
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "A check CEEJAY needs could not be made, and nothing was changed. CEEJAY has no profile for this Discord ID and could not check with Discord whether it is a member of your server (`discord_unavailable`); the quest requires a role and the member's roles could not be read from Discord (`member_roles_unavailable`); or you sent a key for a quest that pays coins and this CEEJAY cannot keep one in any way (`idempotency_unavailable`). For the first two, retry the request shortly.",
            "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": {
                  "discordUnavailable": {
                    "summary": "Discord could not be asked",
                    "value": {
                      "error": "Could not check with Discord that this ID is a member of the server, so nothing was changed",
                      "code": "discord_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "rolesUnavailable": {
                    "summary": "The member's roles could not be read",
                    "value": {
                      "error": "Could not read this member's roles from Discord, so the quest was not completed",
                      "code": "member_roles_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keysUnavailable": {
                    "summary": "Keys cannot be stored yet",
                    "value": {
                      "error": "idempotencyKey cannot be used on this community yet, so nothing was changed",
                      "code": "idempotency_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/quests/tasks/complete": {
      "post": {
        "tags": [
          "Quests"
        ],
        "summary": "Complete a quest task",
        "description": "Tells CEEJAY that a member did something your own platform checks. CEEJAY completes the quest task of the type Completed by your API that has your `taskKey`. When that was the quest's last task, it completes the quest and grants its rewards.\n\n**Required scope:** `quests:complete` (a key with the `*` scope can also call it).\n\n**Who.** Send one of `discordId`, `playerId` with `platform`, `xHandle`, `wallet`, `github` and `twitch`. With more than one, CEEJAY uses the first in that order. A `discordId` that is `null` or an empty string is skipped, and the next one is used. A `discordId` CEEJAY has a member profile for is used as it is. One it has no profile for is looked up in Discord first: an ID that is not a member of your server returns `202` with `matched: 0`, like any identifier that does not match a member, and when the Discord API is unavailable the call returns `503` with the code `discord_unavailable` and nothing happens. `playerId` is the member's ID in your game on `platform`, as you linked it with `POST /members/link`; a platform your community does not know returns `400` with the code `unknown_platform`. The others are strings, or whole numbers that are read as their digits, and must match an account the member connected in your community, which you can also set with `POST /members/link` under the platform `twitter`, `metamask`, `github` or `twitch`. When CEEJAY finds no member it returns `202` with `matched: 0`, not an error, and says nothing else about the member, so that nobody can work out who is in your community by trying names.\n\n**What it does.** CEEJAY completes every task of that type whose key is your `taskKey`, in every quest on your server that is active and inside its dates, applying the quest's own rules: the role it requires, the member's level and season XP, program, prerequisite quest, the wait between runs, the per-member and overall limits and the community's daily quest limit. A quest whose key does not match is never started. A quest that requires a role is completed only for a member who has it; a member who already has a run of that quest open is not checked again. When a rule blocks a quest, the call still returns `200` with that quest omitted. CEEJAY does not check that what you report is true, or that a key belongs to one task only: a key shared by several tasks completes all of them. When the last task of a quest is done, the quest completes and grants XP, coins, the badge, the role and any extra rewards set on it in the Rewards Engine, and `quest.completed` is sent to your webhooks.\n\nWhen a matching quest requires a role and CEEJAY's bot cannot read the member's roles from Discord, nothing is started or completed and the call returns `503` with the code `discord_unavailable`: retry shortly.\n\n**Review.** If the task's own review switch is on, or you send `review: true`, nothing completes yet. The report waits in the Review Queue, with `evidence` as the proof, until a moderator approves or rejects it, and the quest is listed in `data.sentForReview`. While it is awaiting review, the quest's other automatic tasks do not advance for that member.\n\n**The body.** A body that is not a JSON object is read as an empty one, so it returns the `taskKey` message.\n\n**Retries.** Send `idempotencyKey` to make a retry safe. A key is stored only when the call changed something: a `200` in which at least one quest was completed or sent for review. A retry then returns the original response, with `data.duplicate: true` and the header `Idempotent-Replayed: true`. A `202` and a `200` with both lists empty store no key, so a retry with the same key is processed as a new request, for example after the player has linked the account. When CEEJAY's bot does not respond in time, or its response is lost (`502` with the code `bot_timeout`), the task may or may not have been completed. `502` with the code `bot_unavailable` is the other case: the bot never received the task or rejected it, no changes were made, and the key is released. Sending it again without a key is safe for a quest a member can complete once, and can complete a repeatable quest a second time. With a key it is never sent to the bot twice: the same key returns `409` with the code `idempotency_in_progress` for a minute, and `idempotency_outcome_unknown` after that. Each key is tied to one request payload: the same key on another operation, or with other values, returns `409` with the code `idempotency_key_reused`. On an instance that does not support idempotency keys yet, a request that sends one returns `503` with the code `idempotency_unavailable`. A body that sends `idempotency_key` gets no key: this operation never read that spelling.",
        "operationId": "completeQuestTask",
        "x-required-scope": [
          "quests:complete"
        ],
        "x-since": "1.0.0",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompleteQuestTaskRequest"
              },
              "examples": {
                "byDiscordId": {
                  "summary": "Name the member by Discord ID, with a link as evidence",
                  "value": {
                    "taskKey": "tk_4k2m9x7qzb",
                    "discordId": "409876543210987654",
                    "evidence": "https://acme.example/players/nova/tutorial",
                    "idempotencyKey": "tutorial-88213004"
                  }
                },
                "byPlayerId": {
                  "summary": "Name the member by their ID in your game",
                  "value": {
                    "taskKey": "tk_4k2m9x7qzb",
                    "playerId": "88213004",
                    "platform": "roblox"
                  }
                },
                "byXHandle": {
                  "summary": "Name the member by the X account they connected",
                  "value": {
                    "taskKey": "tk_4k2m9x7qzb",
                    "xHandle": "@novaplays"
                  }
                },
                "forReview": {
                  "summary": "Ask for a moderator to look at it first",
                  "value": {
                    "taskKey": "tk_7d3p1w8hfa",
                    "github": "novaplays",
                    "evidence": "https://github.com/acme/game/pull/128",
                    "review": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The report reached CEEJAY's bot, which completes quests. `data.completed` lists the quests this call finished and `data.sentForReview` the ones whose task went to review. Both are empty when the task was completed but the quest has more tasks, when a rule stopped the completion and when no task had the key: the call does not say 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"
              },
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompleteQuestTaskResponse"
                },
                "examples": {
                  "finishedQuest": {
                    "summary": "The task was the last one, so the quest completed",
                    "value": {
                      "success": true,
                      "matched": 1,
                      "completed": [
                        {
                          "questId": 42,
                          "title": "Finish the Acme tutorial"
                        }
                      ],
                      "data": {
                        "matched": 1,
                        "completed": [
                          {
                            "questId": 42,
                            "questTitle": "Finish the Acme tutorial"
                          }
                        ],
                        "sentForReview": [],
                        "duplicate": false
                      }
                    }
                  },
                  "sentForReview": {
                    "summary": "The report went to the Review Queue",
                    "value": {
                      "success": true,
                      "matched": 1,
                      "completed": [],
                      "data": {
                        "matched": 1,
                        "completed": [],
                        "sentForReview": [
                          {
                            "questId": 57,
                            "questTitle": "Merge a pull request"
                          }
                        ],
                        "duplicate": false
                      }
                    }
                  },
                  "noQuestFinished": {
                    "summary": "No quest finished by this call",
                    "value": {
                      "success": true,
                      "matched": 1,
                      "completed": [],
                      "data": {
                        "matched": 1,
                        "completed": [],
                        "sentForReview": [],
                        "duplicate": false
                      }
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "CEEJAY found no member for the identifier. No changes were made, and no idempotency key is stored. A `discordId` that is not a string of 17 to 20 digits returns this too (a JSON number is never converted to a string), and so does an identifier that is empty or `null`, or that is neither a string nor a whole number up to 9007199254740991. A `discordId` CEEJAY has no member profile for that is not a member of your server in Discord returns it as well. One exception: a `discordId` that is `null` or empty is skipped when you also sent another identifier, and that one is used. It is not an error, so that nobody can work out who is in your community by trying names.",
            "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/CompleteQuestTaskNoMemberResponse"
                },
                "examples": {
                  "noMember": {
                    "summary": "No member found",
                    "value": {
                      "success": true,
                      "matched": 0,
                      "data": {
                        "matched": 0,
                        "completed": [],
                        "sentForReview": []
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected and no changes were made. The checks run in this order: the task key, who did it, that the evidence is a string, a null character in the evidence, half of a character pair in the evidence, a null character in the identifier the member is looked up by, `playerId` and its `platform`, the idempotency key, then whether the platform is known. A message names the spelling you sent.",
            "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": {
                  "badTaskKey": {
                    "summary": "Task key missing, not a string or the wrong shape",
                    "value": {
                      "error": "taskKey is required (4–64 letters, digits, _ or -)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "taskKey"
                    }
                  },
                  "badOlderTaskKey": {
                    "summary": "The same, when the deprecated alias was sent",
                    "value": {
                      "error": "task_key is required (4–64 letters, digits, _ or -)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "task_key"
                    }
                  },
                  "noMember": {
                    "summary": "Nobody named",
                    "value": {
                      "error": "Send one of discordId, playerId with platform, xHandle, wallet, github or twitch to say who did it",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badEvidence": {
                    "summary": "evidence is not a string",
                    "value": {
                      "error": "evidence must be a string",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "evidence"
                    }
                  },
                  "nullInEvidence": {
                    "summary": "A null character in the evidence",
                    "value": {
                      "error": "evidence cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "evidence"
                    }
                  },
                  "halfPairInEvidence": {
                    "summary": "An unpaired surrogate in the evidence",
                    "value": {
                      "error": "evidence must be valid Unicode text",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "evidence"
                    }
                  },
                  "nullInIdentifier": {
                    "summary": "A null character in the identifier the member is looked up by. The message names the field",
                    "value": {
                      "error": "xHandle cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "xHandle"
                    }
                  },
                  "badPlayerId": {
                    "summary": "playerId not a string, or empty once trimmed",
                    "value": {
                      "error": "playerId must be a string of 1 to 255 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "playerId"
                    }
                  },
                  "longPlayerId": {
                    "summary": "playerId over 255 characters",
                    "value": {
                      "error": "playerId cannot exceed 255 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "playerId"
                    }
                  },
                  "nullInPlayerId": {
                    "summary": "A null character in playerId",
                    "value": {
                      "error": "playerId cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "playerId"
                    }
                  },
                  "noPlatform": {
                    "summary": "playerId without a platform",
                    "value": {
                      "error": "platform is required with playerId",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "platform"
                    }
                  },
                  "longPlatform": {
                    "summary": "Platform over 50 characters",
                    "value": {
                      "error": "platform cannot exceed 50 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "platform"
                    }
                  },
                  "nullInPlatform": {
                    "summary": "A null character in the platform",
                    "value": {
                      "error": "platform cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "platform"
                    }
                  },
                  "badPlatform": {
                    "summary": "A platform with other characters",
                    "value": {
                      "error": "platform can only contain letters, digits and underscores",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "platform"
                    }
                  },
                  "badKey": {
                    "summary": "Invalid idempotency key",
                    "value": {
                      "error": "idempotencyKey must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "nullInKey": {
                    "summary": "A null character in the idempotency key",
                    "value": {
                      "error": "idempotencyKey cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "unknownPlatform": {
                    "summary": "A platform that is not known",
                    "value": {
                      "error": "Unknown platform. Add it as a custom platform under Settings, Developers, Platforms, then send the task again",
                      "code": "unknown_platform",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key does not have `quests:complete` (`insufficient_scope`: the body lists what was needed and what the key has, and the request still counts toward the key's rate limit). Or the member is named by a `playerId` on a `platform` that is a game's name, and the key is limited to other games of the community (`game_not_allowed`): nobody is looked up and nothing is completed.",
            "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/ForbiddenErrorResponse"
                },
                "examples": {
                  "missingScope": {
                    "summary": "Key without the scope",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "quests:complete"
                      ],
                      "granted": [
                        "xp:grant",
                        "xp:read"
                      ]
                    }
                  },
                  "gameNotAllowed": {
                    "summary": "The key is limited to other games",
                    "value": {
                      "error": "This API key is limited to other games",
                      "code": "game_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "The member is named by an account, and this instance does not support account linking yet (`not_available`, with `feature: \"linking\"`). No changes were made.",
            "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/NotAvailableErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "Not available on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "linking"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflictUnknown"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "CEEJAY's bot, which completes quests, could not be reached or rejected the request (`bot_unavailable`: no changes were made, and the idempotency key is released), or did not respond within 12 seconds, or its response was lost: it failed part way, or the connection to it dropped (`bot_timeout`: the task may or may not have been completed, and the idempotency key stays reserved). After a `bot_timeout`, sending the task again is safe for a quest a member can complete once; a repeatable quest can be completed a second time. With an `idempotencyKey` the task is never sent to the bot twice.",
            "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": {
                  "unreachable": {
                    "summary": "The bot could not be reached, or rejected: no changes were made",
                    "value": {
                      "error": "The quest engine could not be reached",
                      "code": "bot_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "timeout": {
                    "summary": "No answer in time",
                    "value": {
                      "error": "The quest engine did not answer in time, so the task may or may not have been completed",
                      "code": "bot_timeout",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "answerLost": {
                    "summary": "The bot's answer was lost: it failed part way, or the connection dropped",
                    "value": {
                      "error": "The quest engine did not give an answer, so the task may or may not have been completed",
                      "code": "bot_timeout",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Discord could not be asked, and no changes were made: retry the request shortly. Either CEEJAY has no profile for the `discordId` and could not check with Discord whether it is a member of your server, or a matching quest requires a role and the member's roles could not be read. Both carry the code `discord_unavailable`. 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"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "discordUnavailable": {
                    "summary": "Discord could not be asked",
                    "value": {
                      "error": "Could not check with Discord that this ID is a member of the server, so nothing was changed",
                      "code": "discord_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "rolesUnavailable": {
                    "summary": "The member's roles could not be read",
                    "value": {
                      "error": "Could not read this member's roles from Discord, so no quest was completed",
                      "code": "discord_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keysUnavailable": {
                    "summary": "Keys cannot be stored yet",
                    "value": {
                      "error": "idempotencyKey cannot be used on this community yet, so nothing was changed",
                      "code": "idempotency_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/quests/external/complete": {
      "servers": [
        {
          "url": "https://ceejay.communitystudios.xyz/api",
          "description": "The shared CEEJAY service. This operation is not under /api/v1: its full URL is https://ceejay.communitystudios.xyz/api/quests/external/complete."
        },
        {
          "url": "https://{dashboardHost}/api",
          "description": "A dedicated CEEJAY instance: your dashboard's URL followed by /api. This operation is not under /api/v1.",
          "variables": {
            "dashboardHost": {
              "default": "dashboard.example.com",
              "description": "The domain you see in your browser when you open your dashboard, without https://."
            }
          }
        }
      ],
      "post": {
        "tags": [
          "Quests"
        ],
        "summary": "Complete a quest task (legacy endpoint)",
        "description": "The legacy endpoint of Complete a quest task. It still works, takes the same body and returns the same response as `POST /quests/tasks/complete`. New code should use that one. This endpoint is at the dashboard's URL followed by `/api/quests/external/complete`, with no `/v1`, so it lists its own servers.\n\nTells CEEJAY that a member did something your own platform checks. CEEJAY completes the quest task of the type Completed by your API that has your `taskKey`. When that was the quest's last task, it completes the quest and grants its rewards.\n\n**Required scope:** `quests:complete` (a key with the `*` scope can also call it).\n\n**Who.** Send one of `discordId`, `playerId` with `platform`, `xHandle`, `wallet`, `github` and `twitch`. With more than one, CEEJAY uses the first in that order. A `discordId` that is `null` or an empty string is skipped, and the next one is used. A `discordId` CEEJAY has a member profile for is used as it is. One it has no profile for is looked up in Discord first: an ID that is not a member of your server returns `202` with `matched: 0`, like any identifier that does not match a member, and when the Discord API is unavailable the call returns `503` with the code `discord_unavailable` and nothing happens. `playerId` is the member's ID in your game on `platform`, as you linked it with `POST /members/link`; a platform your community does not know returns `400` with the code `unknown_platform`. The others are strings, or whole numbers that are read as their digits, and must match an account the member connected in your community, which you can also set with `POST /members/link` under the platform `twitter`, `metamask`, `github` or `twitch`. When CEEJAY finds no member it returns `202` with `matched: 0`, not an error, and says nothing else about the member, so that nobody can work out who is in your community by trying names.\n\n**What it does.** CEEJAY completes every task of that type whose key is your `taskKey`, in every quest on your server that is active and inside its dates, applying the quest's own rules: the role it requires, the member's level and season XP, program, prerequisite quest, the wait between runs, the per-member and overall limits and the community's daily quest limit. A quest whose key does not match is never started. A quest that requires a role is completed only for a member who has it; a member who already has a run of that quest open is not checked again. When a rule blocks a quest, the call still returns `200` with that quest omitted. CEEJAY does not check that what you report is true, or that a key belongs to one task only: a key shared by several tasks completes all of them. When the last task of a quest is done, the quest completes and grants XP, coins, the badge, the role and any extra rewards set on it in the Rewards Engine, and `quest.completed` is sent to your webhooks.\n\nWhen a matching quest requires a role and CEEJAY's bot cannot read the member's roles from Discord, nothing is started or completed and the call returns `503` with the code `discord_unavailable`: retry shortly.\n\n**Review.** If the task's own review switch is on, or you send `review: true`, nothing completes yet. The report waits in the Review Queue, with `evidence` as the proof, until a moderator approves or rejects it, and the quest is listed in `data.sentForReview`. While it is awaiting review, the quest's other automatic tasks do not advance for that member.\n\n**The body.** A body that is not a JSON object is read as an empty one, so it returns the `taskKey` message.\n\n**Retries.** Send `idempotencyKey` to make a retry safe. A key is stored only when the call changed something: a `200` in which at least one quest was completed or sent for review. A retry then returns the original response, with `data.duplicate: true` and the header `Idempotent-Replayed: true`. A `202` and a `200` with both lists empty store no key, so a retry with the same key is processed as a new request, for example after the player has linked the account. When CEEJAY's bot does not respond in time (`502` with the code `bot_timeout`), the task may or may not have been completed. Sending it again without a key is safe for a quest a member can complete once, and can complete a repeatable quest a second time. With a key it is never sent to the bot twice: the same key returns `409` with the code `idempotency_in_progress` for a minute, and `idempotency_outcome_unknown` after that. Each key is tied to one request payload: the same key on another operation, or with other values, returns `409` with the code `idempotency_key_reused`. On an instance that does not support idempotency keys yet, a request that sends one returns `503` with the code `idempotency_unavailable`. A body that sends `idempotency_key` gets no key: this operation never read that spelling.",
        "operationId": "completeQuestTaskLegacy",
        "x-required-scope": [
          "quests:complete"
        ],
        "x-since": "1.0.0",
        "deprecated": true,
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompleteQuestTaskRequest"
              },
              "examples": {
                "byDiscordId": {
                  "summary": "Name the member by Discord ID, with a link as evidence",
                  "value": {
                    "taskKey": "tk_4k2m9x7qzb",
                    "discordId": "409876543210987654",
                    "evidence": "https://acme.example/players/nova/tutorial",
                    "idempotencyKey": "tutorial-88213004"
                  }
                },
                "byPlayerId": {
                  "summary": "Name the member by their ID in your game",
                  "value": {
                    "taskKey": "tk_4k2m9x7qzb",
                    "playerId": "88213004",
                    "platform": "roblox"
                  }
                },
                "byXHandle": {
                  "summary": "Name the member by the X account they connected",
                  "value": {
                    "taskKey": "tk_4k2m9x7qzb",
                    "xHandle": "@novaplays"
                  }
                },
                "forReview": {
                  "summary": "Ask for a moderator to look at it first",
                  "value": {
                    "taskKey": "tk_7d3p1w8hfa",
                    "github": "novaplays",
                    "evidence": "https://github.com/acme/game/pull/128",
                    "review": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The report reached CEEJAY's bot, which completes quests. `data.completed` lists the quests this call finished and `data.sentForReview` the ones whose task went to review. Both are empty when the task was completed but the quest has more tasks, when a rule stopped the completion and when no task had the key: the call does not say 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"
              },
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompleteQuestTaskResponse"
                },
                "examples": {
                  "finishedQuest": {
                    "summary": "The task was the last one, so the quest completed",
                    "value": {
                      "success": true,
                      "matched": 1,
                      "completed": [
                        {
                          "questId": 42,
                          "title": "Finish the Acme tutorial"
                        }
                      ],
                      "data": {
                        "matched": 1,
                        "completed": [
                          {
                            "questId": 42,
                            "questTitle": "Finish the Acme tutorial"
                          }
                        ],
                        "sentForReview": [],
                        "duplicate": false
                      }
                    }
                  },
                  "sentForReview": {
                    "summary": "The report went to the Review Queue",
                    "value": {
                      "success": true,
                      "matched": 1,
                      "completed": [],
                      "data": {
                        "matched": 1,
                        "completed": [],
                        "sentForReview": [
                          {
                            "questId": 57,
                            "questTitle": "Merge a pull request"
                          }
                        ],
                        "duplicate": false
                      }
                    }
                  },
                  "noQuestFinished": {
                    "summary": "No quest finished by this call",
                    "value": {
                      "success": true,
                      "matched": 1,
                      "completed": [],
                      "data": {
                        "matched": 1,
                        "completed": [],
                        "sentForReview": [],
                        "duplicate": false
                      }
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "CEEJAY found no member for the identifier. No changes were made, and no idempotency key is stored. A `discordId` that is not a string of 17 to 20 digits returns this too (a JSON number is never converted to a string), and so does an identifier that is empty or `null`, or that is neither a string nor a whole number up to 9007199254740991. A `discordId` CEEJAY has no member profile for that is not a member of your server in Discord returns it as well. One exception: a `discordId` that is `null` or empty is skipped when you also sent another identifier, and that one is used. It is not an error, so that nobody can work out who is in your community by trying names.",
            "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/CompleteQuestTaskNoMemberResponse"
                },
                "examples": {
                  "noMember": {
                    "summary": "No member found",
                    "value": {
                      "success": true,
                      "matched": 0,
                      "data": {
                        "matched": 0,
                        "completed": [],
                        "sentForReview": []
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected and no changes were made. The checks run in this order: the task key, who did it, that the evidence is a string, a null character in the evidence, half of a character pair in the evidence, a null character in the identifier the member is looked up by, `playerId` and its `platform`, the idempotency key, then whether the platform is known. A message names the spelling you sent.",
            "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": {
                  "badTaskKey": {
                    "summary": "Task key missing, not a string or the wrong shape",
                    "value": {
                      "error": "taskKey is required (4–64 letters, digits, _ or -)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "taskKey"
                    }
                  },
                  "badOlderTaskKey": {
                    "summary": "The same, when the deprecated alias was sent",
                    "value": {
                      "error": "task_key is required (4–64 letters, digits, _ or -)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "task_key"
                    }
                  },
                  "noMember": {
                    "summary": "Nobody named",
                    "value": {
                      "error": "Send one of discordId, playerId with platform, xHandle, wallet, github or twitch to say who did it",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badEvidence": {
                    "summary": "evidence is not a string",
                    "value": {
                      "error": "evidence must be a string",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "evidence"
                    }
                  },
                  "nullInEvidence": {
                    "summary": "A null character in the evidence",
                    "value": {
                      "error": "evidence cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "evidence"
                    }
                  },
                  "halfPairInEvidence": {
                    "summary": "An unpaired surrogate in the evidence",
                    "value": {
                      "error": "evidence must be valid Unicode text",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "evidence"
                    }
                  },
                  "nullInIdentifier": {
                    "summary": "A null character in the identifier the member is looked up by. The message names the field",
                    "value": {
                      "error": "xHandle cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "xHandle"
                    }
                  },
                  "badPlayerId": {
                    "summary": "playerId not a string, or empty once trimmed",
                    "value": {
                      "error": "playerId must be a string of 1 to 255 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "playerId"
                    }
                  },
                  "longPlayerId": {
                    "summary": "playerId over 255 characters",
                    "value": {
                      "error": "playerId cannot exceed 255 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "playerId"
                    }
                  },
                  "nullInPlayerId": {
                    "summary": "A null character in playerId",
                    "value": {
                      "error": "playerId cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "playerId"
                    }
                  },
                  "noPlatform": {
                    "summary": "playerId without a platform",
                    "value": {
                      "error": "platform is required with playerId",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "platform"
                    }
                  },
                  "longPlatform": {
                    "summary": "Platform over 50 characters",
                    "value": {
                      "error": "platform cannot exceed 50 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "platform"
                    }
                  },
                  "nullInPlatform": {
                    "summary": "A null character in the platform",
                    "value": {
                      "error": "platform cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "platform"
                    }
                  },
                  "badPlatform": {
                    "summary": "A platform with other characters",
                    "value": {
                      "error": "platform can only contain letters, digits and underscores",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "platform"
                    }
                  },
                  "badKey": {
                    "summary": "Invalid idempotency key",
                    "value": {
                      "error": "idempotencyKey must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "nullInKey": {
                    "summary": "A null character in the idempotency key",
                    "value": {
                      "error": "idempotencyKey cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "unknownPlatform": {
                    "summary": "A platform that is not known",
                    "value": {
                      "error": "Unknown platform. Add it as a custom platform under Settings, Developers, Platforms, then send the task again",
                      "code": "unknown_platform",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScopes"
          },
          "404": {
            "description": "The member is named by an account, and this instance does not support account linking yet (`not_available`, with `feature: \"linking\"`). No changes were made.",
            "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/NotAvailableErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "Not available on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "linking"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflictUnknown"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "description": "CEEJAY's bot, which completes quests, could not be reached or rejected the request (`bot_unavailable`: no changes were made, and the idempotency key is released), or did not respond within 12 seconds, or its response was lost: it failed part way, or the connection to it dropped (`bot_timeout`: the task may or may not have been completed, and the idempotency key stays reserved). After a `bot_timeout`, sending the task again is safe for a quest a member can complete once; a repeatable quest can be completed a second time. With an `idempotencyKey` the task is never sent to the bot twice.",
            "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": {
                  "unreachable": {
                    "summary": "The bot could not be reached, or rejected: no changes were made",
                    "value": {
                      "error": "The quest engine could not be reached",
                      "code": "bot_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "timeout": {
                    "summary": "No answer in time",
                    "value": {
                      "error": "The quest engine did not answer in time, so the task may or may not have been completed",
                      "code": "bot_timeout",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "answerLost": {
                    "summary": "The bot's answer was lost: it failed part way, or the connection dropped",
                    "value": {
                      "error": "The quest engine did not give an answer, so the task may or may not have been completed",
                      "code": "bot_timeout",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Discord could not be asked, and no changes were made: retry the request shortly. Either CEEJAY has no profile for the `discordId` and could not check with Discord whether it is a member of your server, or a matching quest requires a role and the member's roles could not be read. Both carry the code `discord_unavailable`. 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"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "discordUnavailable": {
                    "summary": "Discord could not be asked",
                    "value": {
                      "error": "Could not check with Discord that this ID is a member of the server, so nothing was changed",
                      "code": "discord_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "rolesUnavailable": {
                    "summary": "The member's roles could not be read",
                    "value": {
                      "error": "Could not read this member's roles from Discord, so no quest was completed",
                      "code": "discord_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keysUnavailable": {
                    "summary": "Keys cannot be stored yet",
                    "value": {
                      "error": "idempotencyKey cannot be used on this community yet, so nothing was changed",
                      "code": "idempotency_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/roles": {
      "get": {
        "tags": [
          "Roles"
        ],
        "summary": "List roles",
        "description": "Lists your server's roles, highest first, without `@everyone`, and says for each whether a key may grant and revoke it through Grant a role and Revoke a role.\n\n**Required scope:** `roles:manage` (a key with the `*` scope can also call it).\n\nUse it to find a `roleId`, and to see beforehand which roles the API rejects. `grantable` is decided by the same checks the two role operations make. `can_mention_everyone` is reported only where your server's `@everyone` role cannot mention everyone itself. `above_bot` is reported when CEEJAY could read the bot's own highest role and the role is at or above it; when that could not be read it is never reported, and Discord may still reject such a role.\n\nThe whole list is returned in one response (Discord allows 250 roles), so `page.nextCursor` is always `null`. CEEJAY reads the list from Discord and caches it for up to a minute. A role is never reported as grantable when your community's bot master roles could not be read: the call then returns `503`.\n\nNever returned: a role's permissions, which members have it, or the tags of a managed role.",
        "operationId": "listRoles",
        "x-required-scope": [
          "roles:manage"
        ],
        "x-since": "1.0.0",
        "responses": {
          "200": {
            "description": "The roles.",
            "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/ListRolesResponse"
                },
                "examples": {
                  "roles": {
                    "summary": "Three roles",
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "roleId": "982371004827100301",
                          "name": "Moderator",
                          "color": 15158332,
                          "position": 12,
                          "managed": false,
                          "grantable": false,
                          "notGrantableReason": "can_moderate"
                        },
                        {
                          "roleId": "982371004827100394",
                          "name": "Tournament winner",
                          "color": 15844367,
                          "position": 5,
                          "managed": false,
                          "grantable": true,
                          "notGrantableReason": null
                        },
                        {
                          "roleId": "982371004827100377",
                          "name": "Server Booster",
                          "color": 16023551,
                          "position": 4,
                          "managed": true,
                          "grantable": false,
                          "notGrantableReason": "managed"
                        }
                      ],
                      "page": {
                        "nextCursor": null,
                        "limit": 3
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScopes"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "description": "The key has used up its requests for this minute (`rate_limited`), or Discord is rate limiting the bot for the role list (`discord_rate_limited`, with `retryAfter` and `Retry-After` when Discord gave a 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",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  },
                  "discordLimited": {
                    "summary": "Discord rate limit, with the wait Discord requested",
                    "value": {
                      "error": "Discord API rate limited, try again later",
                      "code": "discord_rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 3
                    }
                  },
                  "discordLimitedNoWait": {
                    "summary": "Discord rate limit, with no wait given",
                    "value": {
                      "error": "Discord API rate limited, try again later",
                      "code": "discord_rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side (`internal_error`), or this instance has no connection to Discord (`discord_not_configured`).",
            "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"
                    }
                  },
                  "noBot": {
                    "summary": "This instance has no connection to Discord",
                    "value": {
                      "error": "This instance cannot reach Discord yet",
                      "code": "discord_not_configured",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The list could not be put together, so nothing is listed. Either the server's roles could not be read from Discord (`roles_unavailable`), or CEEJAY could not read which roles are your community's bot master roles (`dashboard_roles_unavailable`). 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": {
                  "noRoleList": {
                    "summary": "The role list could not be read",
                    "value": {
                      "error": "Could not read this server's roles from Discord",
                      "code": "roles_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "noDashboardRoles": {
                    "summary": "The bot master roles could not be read",
                    "value": {
                      "error": "Could not read which roles give access to the CEEJAY dashboard, so no role is listed",
                      "code": "dashboard_roles_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/roles/grant": {
      "post": {
        "tags": [
          "Roles"
        ],
        "summary": "Grant a role",
        "description": "Grants a member a Discord role. A member who already has it is not an error.\n\n**Required scope:** `roles:manage` (a key with the `*` scope can also call it).\n\nOnly an ordinary role of your own server can be granted. Before anything is changed CEEJAY looks the role up in your server's role list, and rejects it with `403` and the code `role_not_allowed` when it is the `@everyone` role, a role that Discord or another app manages (a bot's own role, the server booster role), a role that can moderate or manage the server (one with Administrator, Kick Members, Ban Members, Timeout Members, Manage Server, Manage Channels, Manage Roles, View Audit Log, Manage Messages, Manage Nicknames, Manage Webhooks, Manage Expressions, Manage Events, Manage Threads, Mute Members, Deafen Members or Move Members), or a role that can mention `@everyone`, `@here` and all roles. That last check is made only where your server's own `@everyone` role cannot: where it can, every member already has the permission, a role that also has it adds nothing, and it is not rejected. A role that moderates only through the permission overrides of a channel cannot be seen from the role list. A role that is not in your server returns `404` with the code `role_not_found`. List roles (`GET /roles`) says for every role whether a key may change it, and why not.\n\nA bot master role is rejected as well, with `403` and the code `role_not_allowed`. That is a role your community added under **Settings → General → Team Management**, as one of its **Additional Bot Master Roles**: anyone with this role has full access to the CEEJAY dashboard, whatever its Discord permissions are. CEEJAY reads that list on every call. When it cannot be read the call returns `503` with the code `dashboard_roles_unavailable` and nothing is changed.\n\nCEEJAY reads the role list from Discord and caches it for up to a minute, so a change to a role's permissions can take up to a minute to apply. When the list cannot be read the call returns `503` with the code `roles_unavailable` and nothing is changed.\n\nThe CEEJAY bot needs **Manage Roles**, and its own role must be higher than the role you are granting in **Server Settings → Roles**. Otherwise Discord rejects and the call returns `403` with the code `bot_missing_permission`.\n\nThe change shows in your server's audit log in Discord, made by the CEEJAY bot, with the reason `API key: ` followed by the name of the key that made the call.\n\nDiscord's own rate limit can also return `429`, with the code `discord_rate_limited`. `retryAfter` and the `Retry-After` header contain the wait Discord requested, in whole seconds, if it gave one. Do not retry in a tight loop.\n\n**Retries.** Send `idempotencyKey` to make a retry safe. When the same request is sent again with its key, nothing happens a second time: the response is the original response, with `duplicate: true` and the header `Idempotent-Replayed: true`. For this operation the same request is the same member and role. Each key is tied to one request payload: two requests for one event need two keys, for example `match-9:coins` and `match-9:xp`. A key that was used for a different request returns `409` with the code `idempotency_key_reused`, and nothing is changed. A key is scoped to the API key that sent it and is retained for 30 days. On an instance that does not support idempotency keys yet, a request that sends one returns `503` with the code `idempotency_unavailable` and changes nothing. A second change of the same role has the same effect on Discord, so when a first request was started more than a minute ago and never finished, a retry with the same key makes the change again and returns normally.",
        "operationId": "grantRole",
        "x-required-scope": [
          "roles:manage"
        ],
        "x-since": "1.0.0",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RoleChangeRequest"
              },
              "examples": {
                "change": {
                  "summary": "Grant a role",
                  "value": {
                    "discordId": "409876543210987654",
                    "roleId": "982371004827100394"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The role was granted. With `duplicate: true` it is the response to an earlier call with the same `idempotencyKey`.",
            "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/GrantRoleResponse"
                },
                "examples": {
                  "done": {
                    "summary": "Grant a role",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "roleId": "982371004827100394",
                        "action": "granted",
                        "duplicate": false
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected, either by CEEJAY (the body, Discord ID, role ID, then the idempotency key) or by Discord (the last example, with the code `discord_error`).",
            "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/DiscordErrorResponse"
                },
                "examples": {
                  "notObject": {
                    "summary": "Body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badId": {
                    "summary": "Invalid Discord ID",
                    "value": {
                      "error": "discordId must be a valid Discord snowflake (17-20 digits)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "discordId"
                    }
                  },
                  "badRole": {
                    "summary": "Invalid role ID",
                    "value": {
                      "error": "roleId must be a valid Discord snowflake (17-20 digits)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "roleId"
                    }
                  },
                  "badKey": {
                    "summary": "Invalid idempotency key",
                    "value": {
                      "error": "idempotencyKey must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "nullInKey": {
                    "summary": "A null character in the idempotency key",
                    "value": {
                      "error": "idempotencyKey cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "discordRefused": {
                    "summary": "Discord rejected the change",
                    "value": {
                      "error": "Discord API error: 400",
                      "code": "discord_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "details": "{\"message\": \"Invalid Form Body\", \"code\": 50035}"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key lacks the scope (`insufficient_scope`), the bot cannot manage this role (`bot_missing_permission`), or the role is one the API never changes (`role_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": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ScopeErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    }
                  ]
                },
                "examples": {
                  "missingScope": {
                    "summary": "Key without the scope",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "roles:manage"
                      ],
                      "granted": [
                        "xp:read"
                      ]
                    }
                  },
                  "botCannotManage": {
                    "summary": "The bot lacks permission",
                    "value": {
                      "error": "Bot lacks permission to manage this role",
                      "code": "bot_missing_permission",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "canModerate": {
                    "summary": "A role that can moderate or manage the server",
                    "value": {
                      "error": "Roles that can moderate or manage the server cannot be given or removed through the API",
                      "code": "role_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "canMentionEveryone": {
                    "summary": "A role that can mention everyone, in a server whose @everyone role cannot",
                    "value": {
                      "error": "Roles that can mention everyone cannot be given or removed through the API",
                      "code": "role_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "managedRole": {
                    "summary": "A role Discord or another app manages",
                    "value": {
                      "error": "Roles managed by Discord or by another app cannot be given or removed through the API",
                      "code": "role_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "everyoneRole": {
                    "summary": "The @everyone role",
                    "value": {
                      "error": "The @everyone role cannot be given or removed",
                      "code": "role_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "dashboardRole": {
                    "summary": "A bot master role, which gives access to the CEEJAY dashboard",
                    "value": {
                      "error": "Roles that give access to the CEEJAY dashboard cannot be given or removed through the API",
                      "code": "role_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "The role is not one of your server's (`role_not_found`), or Discord found no such member in your server (`member_not_found`: the role was found a moment before, so it is the member who is not there). A member who has only just joined can take up to 15 seconds to be found. Wait 15 seconds before retrying.",
            "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": {
                  "roleNotFound": {
                    "summary": "Role not in your server",
                    "value": {
                      "error": "Role not found in this server",
                      "code": "role_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "missing": {
                    "summary": "Member not in your server",
                    "value": {
                      "error": "Member or role not found in this server",
                      "code": "member_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "description": "The key has used up its requests for this minute (`rate_limited`), or Discord is rate limiting the bot, for the role list or for the change itself (`discord_rate_limited`). For Discord's limit, `retryAfter` and `Retry-After` contain the wait Discord requested, if it gave one, and `X-RateLimit-Remaining` is not `0` unless the key's own window is used up too.",
            "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
                    }
                  },
                  "discordLimited": {
                    "summary": "Discord rate limit, with the wait Discord requested",
                    "value": {
                      "error": "Discord API rate limited, try again later",
                      "code": "discord_rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 3
                    }
                  },
                  "discordLimitedNoWait": {
                    "summary": "Discord rate limit, with no wait given",
                    "value": {
                      "error": "Discord API rate limited, try again later",
                      "code": "discord_rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side (`internal_error`), or this instance has no connection to Discord (`discord_not_configured`).",
            "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"
                    }
                  },
                  "noBot": {
                    "summary": "This instance has no connection to Discord",
                    "value": {
                      "error": "This instance cannot reach Discord yet",
                      "code": "discord_not_configured",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "A check CEEJAY makes before it changes a role could not be made, and nothing was changed. Either it could not read your server's role list from Discord (`roles_unavailable`), or it could not read whether the role is one of your community's bot master roles (`dashboard_roles_unavailable`). Retry with backoff. Or Discord did not answer the change itself within 10 seconds (`discord_unavailable`): the change may or may not have been made, the idempotency key stays reserved and the request is safe to repeat. 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"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "noRoleList": {
                    "summary": "The role list could not be read",
                    "value": {
                      "error": "Could not read this server's roles from Discord, so nothing was changed",
                      "code": "roles_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "noDashboardCheck": {
                    "summary": "The bot master roles could not be read",
                    "value": {
                      "error": "Could not check whether this role gives access to the CEEJAY dashboard, so nothing was changed",
                      "code": "dashboard_roles_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "changeTimedOut": {
                    "summary": "The change timed out",
                    "value": {
                      "error": "Discord did not answer in time, so the role may or may not have been given",
                      "code": "discord_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keysUnavailable": {
                    "summary": "Keys cannot be stored yet",
                    "value": {
                      "error": "idempotencyKey cannot be used on this community yet, so nothing was changed",
                      "code": "idempotency_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "description": "Any other status Discord returns is passed on, with the code `discord_error`. `error` is `Discord API error: <status>` and `details` is Discord's reply. After a `5xx` from Discord the change may or may not have been made.",
            "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/DiscordErrorResponse"
                },
                "examples": {
                  "discord": {
                    "summary": "Discord is unavailable",
                    "value": {
                      "error": "Discord API error: 503",
                      "code": "discord_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "details": "{\"message\": \"Service Unavailable\"}"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/roles/revoke": {
      "post": {
        "tags": [
          "Roles"
        ],
        "summary": "Revoke a role",
        "description": "Takes a Discord role away from a member. A member who does not have it is not an error.\n\n**Required scope:** `roles:manage` (a key with the `*` scope can also call it).\n\nOnly an ordinary role of your own server can be revoked. Before anything is changed CEEJAY looks the role up in your server's role list, and rejects it with `403` and the code `role_not_allowed` when it is the `@everyone` role, a role that Discord or another app manages (a bot's own role, the server booster role), a role that can moderate or manage the server (one with Administrator, Kick Members, Ban Members, Timeout Members, Manage Server, Manage Channels, Manage Roles, View Audit Log, Manage Messages, Manage Nicknames, Manage Webhooks, Manage Expressions, Manage Events, Manage Threads, Mute Members, Deafen Members or Move Members), or a role that can mention `@everyone`, `@here` and all roles. That last check is made only where your server's own `@everyone` role cannot: where it can, every member already has the permission, a role that also has it adds nothing, and it is not rejected. A role that moderates only through the permission overrides of a channel cannot be seen from the role list. A role that is not in your server returns `404` with the code `role_not_found`. List roles (`GET /roles`) says for every role whether a key may change it, and why not.\n\nA bot master role is rejected as well, with `403` and the code `role_not_allowed`. That is a role your community added under **Settings → General → Team Management**, as one of its **Additional Bot Master Roles**: anyone with this role has full access to the CEEJAY dashboard, whatever its Discord permissions are. CEEJAY reads that list on every call. When it cannot be read the call returns `503` with the code `dashboard_roles_unavailable` and nothing is changed.\n\nCEEJAY reads the role list from Discord and caches it for up to a minute, so a change to a role's permissions can take up to a minute to apply. When the list cannot be read the call returns `503` with the code `roles_unavailable` and nothing is changed.\n\nThe CEEJAY bot needs **Manage Roles**, and its own role must be higher than the role you are revoking in **Server Settings → Roles**. Otherwise Discord rejects and the call returns `403` with the code `bot_missing_permission`.\n\nThe change shows in your server's audit log in Discord, made by the CEEJAY bot, with the reason `API key: ` followed by the name of the key that made the call.\n\nDiscord's own rate limit can also return `429`, with the code `discord_rate_limited`. `retryAfter` and the `Retry-After` header contain the wait Discord requested, in whole seconds, if it gave one. Do not retry in a tight loop.\n\n**Retries.** Send `idempotencyKey` to make a retry safe. When the same request is sent again with its key, nothing happens a second time: the response is the original response, with `duplicate: true` and the header `Idempotent-Replayed: true`. For this operation the same request is the same member and role. Each key is tied to one request payload: two requests for one event need two keys, for example `match-9:coins` and `match-9:xp`. A key that was used for a different request returns `409` with the code `idempotency_key_reused`, and nothing is changed. A key is scoped to the API key that sent it and is retained for 30 days. On an instance that does not support idempotency keys yet, a request that sends one returns `503` with the code `idempotency_unavailable` and changes nothing. A second change of the same role has the same effect on Discord, so when a first request was started more than a minute ago and never finished, a retry with the same key makes the change again and returns normally.",
        "operationId": "revokeRole",
        "x-required-scope": [
          "roles:manage"
        ],
        "x-since": "1.0.0",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RoleChangeRequest"
              },
              "examples": {
                "change": {
                  "summary": "Revoke a role",
                  "value": {
                    "discordId": "409876543210987654",
                    "roleId": "982371004827100394"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The role was revoked. With `duplicate: true` it is the response to an earlier call with the same `idempotencyKey`.",
            "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/RevokeRoleResponse"
                },
                "examples": {
                  "done": {
                    "summary": "Revoke a role",
                    "value": {
                      "success": true,
                      "data": {
                        "discordId": "409876543210987654",
                        "roleId": "982371004827100394",
                        "action": "revoked",
                        "duplicate": false
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected, either by CEEJAY (the body, Discord ID, role ID, then the idempotency key) or by Discord (the last example, with the code `discord_error`).",
            "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/DiscordErrorResponse"
                },
                "examples": {
                  "notObject": {
                    "summary": "Body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badId": {
                    "summary": "Invalid Discord ID",
                    "value": {
                      "error": "discordId must be a valid Discord snowflake (17-20 digits)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "discordId"
                    }
                  },
                  "badRole": {
                    "summary": "Invalid role ID",
                    "value": {
                      "error": "roleId must be a valid Discord snowflake (17-20 digits)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "roleId"
                    }
                  },
                  "badKey": {
                    "summary": "Invalid idempotency key",
                    "value": {
                      "error": "idempotencyKey must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "nullInKey": {
                    "summary": "A null character in the idempotency key",
                    "value": {
                      "error": "idempotencyKey cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "discordRefused": {
                    "summary": "Discord rejected the change",
                    "value": {
                      "error": "Discord API error: 400",
                      "code": "discord_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "details": "{\"message\": \"Invalid Form Body\", \"code\": 50035}"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key lacks the scope (`insufficient_scope`), the bot cannot manage this role (`bot_missing_permission`), or the role is one the API never changes (`role_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": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ScopeErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    }
                  ]
                },
                "examples": {
                  "missingScope": {
                    "summary": "Key without the scope",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "roles:manage"
                      ],
                      "granted": [
                        "xp:read"
                      ]
                    }
                  },
                  "botCannotManage": {
                    "summary": "The bot lacks permission",
                    "value": {
                      "error": "Bot lacks permission to manage this role",
                      "code": "bot_missing_permission",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "canModerate": {
                    "summary": "A role that can moderate or manage the server",
                    "value": {
                      "error": "Roles that can moderate or manage the server cannot be given or removed through the API",
                      "code": "role_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "canMentionEveryone": {
                    "summary": "A role that can mention everyone, in a server whose @everyone role cannot",
                    "value": {
                      "error": "Roles that can mention everyone cannot be given or removed through the API",
                      "code": "role_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "managedRole": {
                    "summary": "A role Discord or another app manages",
                    "value": {
                      "error": "Roles managed by Discord or by another app cannot be given or removed through the API",
                      "code": "role_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "everyoneRole": {
                    "summary": "The @everyone role",
                    "value": {
                      "error": "The @everyone role cannot be given or removed",
                      "code": "role_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "dashboardRole": {
                    "summary": "A bot master role, which gives access to the CEEJAY dashboard",
                    "value": {
                      "error": "Roles that give access to the CEEJAY dashboard cannot be given or removed through the API",
                      "code": "role_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "The role is not one of your server's (`role_not_found`), or Discord found no such member in your server (`member_not_found`: the role was found a moment before, so it is the member who is not there). A member who has only just joined can take up to 15 seconds to be found. Wait 15 seconds before retrying.",
            "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": {
                  "roleNotFound": {
                    "summary": "Role not in your server",
                    "value": {
                      "error": "Role not found in this server",
                      "code": "role_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "missing": {
                    "summary": "Member not in your server",
                    "value": {
                      "error": "Member or role not found in this server",
                      "code": "member_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "description": "The key has used up its requests for this minute (`rate_limited`), or Discord is rate limiting the bot, for the role list or for the change itself (`discord_rate_limited`). For Discord's limit, `retryAfter` and `Retry-After` contain the wait Discord requested, if it gave one, and `X-RateLimit-Remaining` is not `0` unless the key's own window is used up too.",
            "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
                    }
                  },
                  "discordLimited": {
                    "summary": "Discord rate limit, with the wait Discord requested",
                    "value": {
                      "error": "Discord API rate limited, try again later",
                      "code": "discord_rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 3
                    }
                  },
                  "discordLimitedNoWait": {
                    "summary": "Discord rate limit, with no wait given",
                    "value": {
                      "error": "Discord API rate limited, try again later",
                      "code": "discord_rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side (`internal_error`), or this instance has no connection to Discord (`discord_not_configured`).",
            "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"
                    }
                  },
                  "noBot": {
                    "summary": "This instance has no connection to Discord",
                    "value": {
                      "error": "This instance cannot reach Discord yet",
                      "code": "discord_not_configured",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "A check CEEJAY makes before it changes a role could not be made, and nothing was changed. Either it could not read your server's role list from Discord (`roles_unavailable`), or it could not read whether the role is one of your community's bot master roles (`dashboard_roles_unavailable`). Retry with backoff. Or Discord did not answer the change itself within 10 seconds (`discord_unavailable`): the change may or may not have been made, the idempotency key stays reserved and the request is safe to repeat. 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"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "noRoleList": {
                    "summary": "The role list could not be read",
                    "value": {
                      "error": "Could not read this server's roles from Discord, so nothing was changed",
                      "code": "roles_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "noDashboardCheck": {
                    "summary": "The bot master roles could not be read",
                    "value": {
                      "error": "Could not check whether this role gives access to the CEEJAY dashboard, so nothing was changed",
                      "code": "dashboard_roles_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "changeTimedOut": {
                    "summary": "The change timed out",
                    "value": {
                      "error": "Discord did not answer in time, so the role may or may not have been taken away",
                      "code": "discord_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keysUnavailable": {
                    "summary": "Keys cannot be stored yet",
                    "value": {
                      "error": "idempotencyKey cannot be used on this community yet, so nothing was changed",
                      "code": "idempotency_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "description": "Any other status Discord returns is passed on, with the code `discord_error`. `error` is `Discord API error: <status>` and `details` is Discord's reply. After a `5xx` from Discord the change may or may not have been made.",
            "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/DiscordErrorResponse"
                },
                "examples": {
                  "discord": {
                    "summary": "Discord is unavailable",
                    "value": {
                      "error": "Discord API error: 503",
                      "code": "discord_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "details": "{\"message\": \"Service Unavailable\"}"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/channels": {
      "get": {
        "tags": [
          "Channels"
        ],
        "summary": "List channels",
        "description": "Lists your server's text, announcement and forum channels and its categories, in Discord's order, so you can find the `channelId` Send a message needs.\n\n**Required scope:** `messages:send` (a key with the `*` scope can also call it).\n\n`sendable` is `true` for the kinds of channel Send a message can post to. Whether the bot has permission in a given channel cannot be told from the list. `private` marks a channel that is hidden from `@everyone`.\n\nThe whole list is returned in one response (Discord allows 500 channels), so `page.nextCursor` is always `null`. CEEJAY reads the list from Discord and caches it for up to a minute. A failed read is an error, never an empty list.\n\nNever returned: topics, permission settings, last message IDs, the age-restricted flag, slow-mode settings or members.",
        "operationId": "listChannels",
        "x-required-scope": [
          "messages:send"
        ],
        "x-since": "1.0.0",
        "responses": {
          "200": {
            "description": "The channels.",
            "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/ListChannelsResponse"
                },
                "examples": {
                  "channels": {
                    "summary": "A category and two channels",
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "channelId": "718230049827100300",
                          "name": "Community",
                          "type": "category",
                          "parentId": null,
                          "position": 0,
                          "private": false,
                          "sendable": false
                        },
                        {
                          "channelId": "718230049827100384",
                          "name": "announcements",
                          "type": "announcement",
                          "parentId": "718230049827100300",
                          "position": 0,
                          "private": false,
                          "sendable": true
                        },
                        {
                          "channelId": "718230049827100391",
                          "name": "game-log",
                          "type": "text",
                          "parentId": "718230049827100300",
                          "position": 1,
                          "private": true,
                          "sendable": true
                        }
                      ],
                      "page": {
                        "nextCursor": null,
                        "limit": 3
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScopes"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "description": "The key has used up its requests for this minute (`rate_limited`), or Discord is rate limiting the bot for the channel list (`discord_rate_limited`, with `retryAfter` and `Retry-After` when Discord gave a 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",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  },
                  "discordLimited": {
                    "summary": "Discord rate limit, with the wait Discord gave",
                    "value": {
                      "error": "Discord API rate limited, try again later",
                      "code": "discord_rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 3
                    }
                  },
                  "discordLimitedNoWait": {
                    "summary": "Discord rate limit, with no wait given",
                    "value": {
                      "error": "Discord API rate limited, try again later",
                      "code": "discord_rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side (`internal_error`), or this instance has no connection to Discord (`discord_not_configured`).",
            "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"
                    }
                  },
                  "noBot": {
                    "summary": "This instance has no connection to Discord",
                    "value": {
                      "error": "This instance cannot reach Discord yet",
                      "code": "discord_not_configured",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The server's channels could not be read from Discord, so nothing is listed. 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": {
                  "noChannelList": {
                    "summary": "The channel list could not be read",
                    "value": {
                      "error": "Could not read this server's channels from Discord",
                      "code": "channels_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/messages/send": {
      "post": {
        "tags": [
          "Messages"
        ],
        "summary": "Send a message",
        "description": "Posts a message, embeds or both to a channel in your Discord server, as the CEEJAY bot.\n\n**Required scope:** `messages:send` (a key with the `*` scope can also call it).\n\nThe channel must be in your server. A channel in another server gets the same `404` as one that does not exist. The bot needs **View Channel** and **Send Messages** in the channel, and **Embed Links** for embeds to show. List channels (`GET /channels`) gives you the IDs.\n\n**Mentions.** Every message is sent so that only member mentions (`<@id>`) notify people. A role mention (`<@&id>`) notifies the role only when you list that role in `allowedRoleIds`. `@everyone` and `@here` never notify anyone through the API, and your server's own ID, which is the ID of its `@everyone` role, is rejected in `allowedRoleIds`.\n\n`content` is cut at 2,000 characters and only the first 10 `embeds` are sent. Neither is rejected. `embeds` must be a list: with no `content`, a value that is not a list returns `400`; with `content`, it is left out and the text is posted. Embeds go to Discord unchanged, so Discord decides whether they are valid. When Discord rejects a message, the status is Discord's and the body includes the code `discord_error` and `details`.\n\nDiscord's own rate limit can also return `429`, with the code `discord_rate_limited`. `retryAfter` and the `Retry-After` header contain the wait Discord requested, in whole seconds, if it gave one. A limit Discord sets for one channel stops calls for that channel only. Do not retry in a tight loop.\n\n**Retries.** Send `idempotencyKey` to make a retry safe. When the same request is sent again with its key, nothing happens a second time: the response is the original response, with `duplicate: true` and the header `Idempotent-Replayed: true`. For this operation the same request is the same channel, text, embeds and `allowedRoleIds`, as they would be posted. Each key is tied to one request payload: two requests for one event need two keys, for example `match-9:coins` and `match-9:xp`. A key that was used for a different request returns `409` with the code `idempotency_key_reused`, and nothing is posted. A key is scoped to the API key that sent it and is retained for 30 days. On an instance that does not support idempotency keys yet, a request that sends one returns `503` with the code `idempotency_unavailable` and changes nothing. CEEJAY cannot undo a posted message, so it never posts twice with one key: when a first request was started more than a minute ago and never finished (a timeout, or a `5xx` from Discord), the same key returns `409` with the code `idempotency_outcome_unknown`. Look in the channel, then use a new key if the message has to be sent again. A rejection from Discord (any `4xx`) does not use up the key, so the same key can be reused.",
        "operationId": "sendMessage",
        "x-required-scope": [
          "messages:send"
        ],
        "x-since": "1.0.0",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendMessageRequest"
              },
              "examples": {
                "text": {
                  "summary": "A plain message",
                  "value": {
                    "channelId": "718230049827100384",
                    "content": "Tournament results are in. Congratulations to everyone who played."
                  }
                },
                "embed": {
                  "summary": "A message with an embed",
                  "value": {
                    "channelId": "718230049827100384",
                    "content": "Tournament results are in.",
                    "embeds": [
                      {
                        "title": "Weekend tournament",
                        "description": "Congratulations to everyone who played.",
                        "color": 5793266
                      }
                    ]
                  }
                },
                "roleMention": {
                  "summary": "A message that pings one role",
                  "value": {
                    "channelId": "718230049827100384",
                    "content": "Sign-ups are open, <@&718230049827100399>.",
                    "allowedRoleIds": [
                      "718230049827100399"
                    ]
                  }
                },
                "withKey": {
                  "summary": "A message that is safe to retry",
                  "value": {
                    "channelId": "718230049827100384",
                    "content": "Season 4 starts now.",
                    "idempotencyKey": "season-4-start"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The message was posted. With `duplicate: true` it is the response to an earlier call with the same `idempotencyKey`: nothing was posted this time.",
            "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/SendMessageResponse"
                },
                "examples": {
                  "sent": {
                    "summary": "Posted",
                    "value": {
                      "success": true,
                      "data": {
                        "messageId": "1178230049827100384",
                        "channelId": "718230049827100384",
                        "duplicate": false
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected, either by CEEJAY or by Discord (the last example, with the code `discord_error`). CEEJAY checks in this order: the body, channel ID, that `content` is a string, that `content` and the embeds are valid Unicode text, content or embeds, that `embeds` is a list when there is no `content`, `allowedRoleIds`, the idempotency key, then, with a key, how deeply the embeds are nested.",
            "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/DiscordErrorResponse"
                },
                "examples": {
                  "notObject": {
                    "summary": "Body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "badChannel": {
                    "summary": "Invalid channel ID",
                    "value": {
                      "error": "channelId must be a valid Discord snowflake (17-20 digits)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "channelId"
                    }
                  },
                  "badContent": {
                    "summary": "content is not a string",
                    "value": {
                      "error": "content must be a string",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "content"
                    }
                  },
                  "badContentUnicode": {
                    "summary": "content contains an unpaired surrogate",
                    "value": {
                      "error": "content must be valid Unicode text",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "content"
                    }
                  },
                  "badEmbedsUnicode": {
                    "summary": "The embeds contain an unpaired surrogate",
                    "value": {
                      "error": "embeds must be valid Unicode text",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "embeds"
                    }
                  },
                  "nothingToSend": {
                    "summary": "Neither content nor embeds",
                    "value": {
                      "error": "Either content or embeds is required",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "embedsNotList": {
                    "summary": "No content, and embeds that are not a list",
                    "value": {
                      "error": "embeds must be a list",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "embeds"
                    }
                  },
                  "badRoles": {
                    "summary": "allowedRoleIds is not a list of up to 20 role IDs, or contains your server's own ID",
                    "value": {
                      "error": "allowedRoleIds must be a list of up to 20 valid Discord snowflakes (17-20 digits)",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "allowedRoleIds"
                    }
                  },
                  "badKey": {
                    "summary": "Invalid idempotency key",
                    "value": {
                      "error": "idempotencyKey must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "nullInKey": {
                    "summary": "A null character in the idempotency key",
                    "value": {
                      "error": "idempotencyKey cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "embedsTooDeep": {
                    "summary": "With a key, embeds nested more than 32 levels deep",
                    "value": {
                      "error": "embeds is nested too deeply",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "embeds"
                    }
                  },
                  "discordRefused": {
                    "summary": "Discord rejected the message",
                    "value": {
                      "error": "Discord API error: 400",
                      "code": "discord_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "details": "{\"message\": \"Cannot send an empty message\", \"code\": 50006}"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key lacks the scope (`insufficient_scope`), or the bot cannot send messages in this channel (`bot_missing_permission`).",
            "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": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ScopeErrorResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ErrorResponse"
                    }
                  ]
                },
                "examples": {
                  "missingScope": {
                    "summary": "Key without the scope",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "messages:send"
                      ],
                      "granted": [
                        "xp:read"
                      ]
                    }
                  },
                  "botCannotSend": {
                    "summary": "The bot lacks permission",
                    "value": {
                      "error": "Bot lacks permission to send messages in this channel",
                      "code": "bot_missing_permission",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "The channel does not exist, is in another server, or the bot cannot see it.",
            "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": {
                  "noChannel": {
                    "summary": "Channel not found",
                    "value": {
                      "error": "Channel not found or bot has no access",
                      "code": "channel_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflictUnknown"
          },
          "429": {
            "description": "The key has used up its requests for this minute (`rate_limited`), or Discord is rate limiting the bot for this channel (`discord_rate_limited`, with `retryAfter` and `Retry-After` when Discord gave a 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",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  },
                  "discordLimited": {
                    "summary": "Discord rate limit, with the wait Discord requested",
                    "value": {
                      "error": "Discord API rate limited, try again later",
                      "code": "discord_rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 3
                    }
                  },
                  "discordLimitedNoWait": {
                    "summary": "Discord rate limit, with no wait given",
                    "value": {
                      "error": "Discord API rate limited, try again later",
                      "code": "discord_rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something failed on CEEJAY's side (`internal_error`), or this instance has no connection to Discord (`discord_not_configured`).",
            "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"
                    }
                  },
                  "noBot": {
                    "summary": "This instance has no connection to Discord",
                    "value": {
                      "error": "This instance cannot reach Discord yet",
                      "code": "discord_not_configured",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Discord did not respond within 10 seconds (`discord_unavailable`). Either the channel check timed out and nothing was posted, or the post timed out and the message may or may not have been posted: the idempotency key stays reserved, so see Retrying safely before you send it again. Or you sent an `idempotencyKey` and this instance does not support idempotency keys yet (`idempotency_unavailable`): nothing was changed, and waiting does not change that 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"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "lookupTimedOut": {
                    "summary": "The channel could not be checked in time",
                    "value": {
                      "error": "Discord did not answer in time, so the channel could not be checked and nothing was posted",
                      "code": "discord_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "postTimedOut": {
                    "summary": "The post timed out",
                    "value": {
                      "error": "Discord did not answer in time, so the message may or may not have been posted",
                      "code": "discord_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "keysUnavailable": {
                    "summary": "Keys cannot be stored yet",
                    "value": {
                      "error": "idempotencyKey cannot be used on this community yet, so nothing was changed",
                      "code": "idempotency_unavailable",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "description": "Any other status Discord returns is passed on, with the code `discord_error`. `error` is `Discord API error: <status>` and `details` is Discord's reply. After a `5xx` from Discord the message may or may not have been posted.",
            "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/DiscordErrorResponse"
                },
                "examples": {
                  "discord": {
                    "summary": "Discord is unavailable",
                    "value": {
                      "error": "Discord API error: 503",
                      "code": "discord_error",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "details": "{\"message\": \"Service Unavailable\"}"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/events/emit": {
      "post": {
        "tags": [
          "Events"
        ],
        "summary": "Emit an event",
        "description": "Sends a custom event from your own system to the webhook subscriptions on your server that listen for it.\n\n**Required scope:** `events:emit` (a key with the `*` scope can also call it).\n\nThe event is delivered as `custom.<event>`. A name that already starts with `custom.` is not given the prefix twice. A name contains 1 to 93 letters, digits, `.`, `_`, `:` or `-` after that prefix. Any other name is refused with `400`, and nothing is sent. A subscription receives it when it lists that exact name, `custom.event` (every custom event) or `*` (every event).\n\nEach delivery is the same signed request as any other webhook (see the `custom.event` webhook in this document), sent as attempt 1. One call creates one delivery ID: every subscription receives the same body and the same `X-CEEJAY-Delivery`. CEEJAY calls the subscriptions one after another, with up to 5 seconds for each, and responds when all have been attempted, so the call takes as long as the subscribers do. `results` says how each one went. A failed delivery is retried by CEEJAY, up to 4 more times, and a subscription that fails 10 times in a row is switched off. An event nobody listens for still succeeds, with `subscribersNotified` at `0`. Your `data` is passed on as you sent it: CEEJAY adds nothing to it.\n\n**Retries.** Send `idempotencyKey` to make a retry safe. When the same request is sent again with its key, nothing happens a second time: the response is the first response, with `duplicate: true` and the header `Idempotent-Replayed: true`. For this operation the same request is the same event name and data. One key is one request: two requests for one event need two keys, for example `match-9:coins` and `match-9:xp`. A key that was used for a different request returns `409` with the code `idempotency_key_reused`, and nothing is sent. A key belongs to the API key that sent it and is retained for 30 days. On an instance that does not support idempotency keys yet, a request that sends one returns `503` with the code `idempotency_unavailable` and changes nothing. A call is stored against its key once it returned `200`, also when some or all of its deliveries failed: CEEJAY retries those itself, so a repeat with the same key sends nothing. To send the event again, use a new key. A webhook cannot be recalled once sent, so when a first request was started more than a minute ago and never finished, the same key returns `409` with the code `idempotency_outcome_unknown`.",
        "operationId": "emitEvent",
        "x-required-scope": [
          "events:emit"
        ],
        "x-since": "1.0.0",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmitEventRequest"
              },
              "examples": {
                "purchase": {
                  "summary": "A purchase on your own store",
                  "value": {
                    "event": "purchase.completed",
                    "data": {
                      "discordId": "409876543210987654",
                      "sku": "season-pass",
                      "value": 24.99
                    },
                    "idempotencyKey": "order-88213-event"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The event was sent to every matching subscription. With `duplicate: true` it is the response to an earlier call that sent the same `idempotencyKey`: nothing was sent this time.",
            "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/EmitEventResponse"
                },
                "examples": {
                  "delivered": {
                    "summary": "Two subscribers",
                    "value": {
                      "success": true,
                      "data": {
                        "event": "custom.purchase.completed",
                        "subscribersNotified": 2,
                        "results": [
                          {
                            "subscriptionId": 4,
                            "name": "Ops relay",
                            "status": "delivered",
                            "httpStatus": 200
                          },
                          {
                            "subscriptionId": 7,
                            "name": "Data warehouse",
                            "status": "failed",
                            "httpStatus": 503,
                            "error": "HTTP 503"
                          }
                        ],
                        "duplicate": false
                      }
                    }
                  },
                  "nobody": {
                    "summary": "Nobody listening",
                    "value": {
                      "success": true,
                      "data": {
                        "event": "custom.purchase.completed",
                        "subscribersNotified": 0,
                        "results": [],
                        "duplicate": false
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was refused and nothing was sent. The checks run in this order: the body, a missing name or one that is not text, a name over 100 characters, the characters of the name, a null character anywhere in `data`, `data` nested more than 32 levels deep, `data` that is not well-formed Unicode, then the idempotency key.",
            "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": "Body is not a JSON object",
                    "value": {
                      "error": "The request body must be a JSON object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "missing": {
                    "summary": "No event name",
                    "value": {
                      "error": "event is required and must be a string",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "event"
                    }
                  },
                  "tooLong": {
                    "summary": "Name over 100 characters",
                    "value": {
                      "error": "event name cannot exceed 100 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "event"
                    }
                  },
                  "badName": {
                    "summary": "Name with a character that is not allowed, or over 93 characters after the prefix",
                    "value": {
                      "error": "event name must be 1-93 letters, digits, \".\", \"_\", \":\" or \"-\" (not counting a leading \"custom.\")",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "event"
                    }
                  },
                  "nullInData": {
                    "summary": "A null character in data",
                    "value": {
                      "error": "data cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "data"
                    }
                  },
                  "dataTooDeep": {
                    "summary": "data nested more than 32 levels deep",
                    "value": {
                      "error": "data is nested too deeply",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "data"
                    }
                  },
                  "dataBadUnicode": {
                    "summary": "data that contains half of a surrogate pair",
                    "value": {
                      "error": "data must be valid Unicode text",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "data"
                    }
                  },
                  "badKey": {
                    "summary": "Invalid idempotency key",
                    "value": {
                      "error": "idempotencyKey must be a string of 1-128 characters",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  },
                  "nullInKey": {
                    "summary": "A null character in the idempotency key",
                    "value": {
                      "error": "idempotencyKey cannot contain a null character",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "idempotencyKey"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/InsufficientScopes"
          },
          "404": {
            "description": "This instance has no webhook subscriptions yet (`not_available`, with `feature: \"events\"`). Nothing was sent.",
            "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/NotAvailableErrorResponse"
                },
                "examples": {
                  "notAvailable": {
                    "summary": "Not available on this instance yet",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "events"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflictUnknown"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/IdempotencyUnavailable"
          }
        }
      }
    },
    "/games/{game}/connect/passes": {
      "post": {
        "tags": [
          "Connect"
        ],
        "summary": "Create a connect pass",
        "description": "Creates a pass for one player your server has authenticated. The game sends it as `proof` when it starts a connect session, which is then at the verified level: CEEJAY trusts your server's verification of the player.\n\n**Required scope:** `players:connect`. A key with the `*` scope does not include it: tick **Act for a player** when you create the key.\n\nA pass lasts 10 minutes, works once, and only for this game and this `playerId`. Only its hash is stored, so it is returned once. When the pass carries a `playerName`, that name is what the member is shown.\n\nIt takes no `idempotencyKey`: creating a pass has no side effects, and a second call creates a second pass.\n\nA key that can create a pass can sign in any player who is already connected at the verified level, so keep `players:connect` on your own server.\n\n**Limit.** 600 requests a minute for the game, across the Connect operations, and not counted against the key's own number.",
        "operationId": "createConnectPass",
        "x-required-scope": [
          "players:connect"
        ],
        "x-since": "1.1.0",
        "parameters": [
          {
            "$ref": "#/components/parameters/GamePath"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateConnectPassRequest"
              },
              "examples": {
                "request": {
                  "summary": "A request",
                  "value": {
                    "playerId": "p_19",
                    "playerName": "Nova77"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The pass.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectPassResponse"
                },
                "examples": {
                  "pass": {
                    "summary": "A pass",
                    "value": {
                      "success": true,
                      "data": {
                        "pass": "ceejay_pass_4f8a2c1e9b7d3e41f8a6b0c1d25f0c2a9b7d3e41f8a6b0c1d24f8a2c1e9b7d3e",
                        "expiresAt": "2026-10-08T18:14:11.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected and no pass was created. The checks run in this order: the body, `playerId`, `playerName`.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "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"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key does not hold `players:connect` (`insufficient_scope`: the body adds `required` and `granted`, and a `*` key does not hold this scope), or the game cannot be used: the community removed CEEJAY (`community_not_active`), the key is limited to other games (`game_not_allowed`), or the game is made for children (`audience_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForbiddenErrorResponse"
                },
                "examples": {
                  "missingScope": {
                    "summary": "Key without players:connect",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "players:connect"
                      ],
                      "granted": [
                        "*"
                      ]
                    }
                  },
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "gameNotAllowed": {
                    "summary": "The key is limited to other games",
                    "value": {
                      "error": "This API key is limited to other games",
                      "code": "game_not_allowed",
                      "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"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Something the request names is not there, or Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "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": "No such game",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/ConnectMethodNotAllowed"
          },
          "409": {
            "description": "The game is paused or suspended (`game_paused`). No pass was created.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "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": {
            "$ref": "#/components/responses/ConnectRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ConnectInternalError"
          }
        }
      }
    },
    "/games/{game}/connect/sessions": {
      "post": {
        "tags": [
          "Connect"
        ],
        "summary": "Create a connect session",
        "description": "Starts a connect session for a player your server has authenticated, or attaches that player to a session a member started in Discord.\n\n**Required scope:** `players:connect`. A key with the `*` scope does not include it: tick **Act for a player** when you create the key.\n\nWithout `code`, the response includes a link (`url`), a code and the URL of a QR image: pass one of them to the player, who approves with their Discord account on CEEJAY's connect page. The session lasts 10 minutes and ends every earlier open session for the same player.\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**Two confirmations.** A connection is saved only after the Discord member and the game have both confirmed. The game's confirmation comes from the game (with `sessionSecret`), from your server (Confirm a connect session), or in advance with `confirmInGame: false`.\n\n**Already connected.** When 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 `url`, `qrUrl` and `code` are `null`.\n\n**Age.** A game set up for a mixed audience must send `underAge`. `true` returns `403`, stores nothing and ends a connection the player has.\n\nIt takes no `idempotencyKey`: a second call creates a second session and ends the first.\n\n**Limit.** 600 requests a minute for the game, across the Connect operations, and not counted against the key's own number.",
        "operationId": "createConnectSession",
        "x-required-scope": [
          "players:connect"
        ],
        "x-since": "1.1.0",
        "parameters": [
          {
            "$ref": "#/components/parameters/GamePath"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateConnectSessionRequest"
              },
              "examples": {
                "request": {
                  "summary": "A request",
                  "value": {
                    "playerId": "p_19",
                    "playerName": "Nova77",
                    "returnTo": "game"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The session. With `code` it is the session of the member who created the code.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StartedConnectSessionResponse"
                },
                "examples": {
                  "started": {
                    "summary": "A new session",
                    "value": {
                      "success": true,
                      "data": {
                        "sessionId": "kT3v9QmZ1xY7bLw2RfA8cN",
                        "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",
                        "status": "pending",
                        "alreadyConnected": false,
                        "member": null
                      }
                    }
                  },
                  "withCode": {
                    "summary": "A player attached to a member's code",
                    "value": {
                      "success": true,
                      "data": {
                        "sessionId": "kT3v9QmZ1xY7bLw2RfA8cN",
                        "url": null,
                        "qrUrl": null,
                        "code": null,
                        "sessionSecret": "ceejay_cs_4f8a2c1e9b7d3e41f8a6b0c1d25f0c2a9b7d3e41f8a6b0c1d24f8a2c1e9b7d3e",
                        "expiresAt": "2026-10-08T18:14:11.000Z",
                        "status": "needs_game_confirmation",
                        "alreadyConnected": false,
                        "member": {
                          "displayName": "Nova",
                          "avatarUrl": "https://cdn.discordapp.com/avatars/409876543210987654/a1b2c3d4e5f60718293a4b5c6d7e8f90.png"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was refused and nothing was started. The game in the path is looked up after the body is read and before the fields are checked. The fields are then checked in this order: `playerId`, `playerName`, `code`, `returnTo`, `locale`, `underAge`, `confirmInGame`, then the age check of a game for a mixed audience.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "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"
                    }
                  },
                  "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"
                    }
                  },
                  "badConfirmInGame": {
                    "summary": "confirmInGame is not true or false",
                    "value": {
                      "error": "confirmInGame must be true or false",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "confirmInGame"
                    }
                  },
                  "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": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key does not hold `players:connect` (`insufficient_scope`: the body adds `required` and `granted`, and a `*` key does not hold this scope), or the game cannot be used: the community removed CEEJAY (`community_not_active`), the key is limited to other games (`game_not_allowed`), or the game is made for children (`audience_not_allowed`). Or the player is under age (`under_age`): nothing is stored, and a connection the player had is ended.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForbiddenErrorResponse"
                },
                "examples": {
                  "missingScope": {
                    "summary": "Key without players:connect",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "players:connect"
                      ],
                      "granted": [
                        "*"
                      ]
                    }
                  },
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "gameNotAllowed": {
                    "summary": "The key is limited to other games",
                    "value": {
                      "error": "This API key is limited to other games",
                      "code": "game_not_allowed",
                      "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"
                    }
                  },
                  "underAge": {
                    "summary": "The player is under age",
                    "value": {
                      "error": "This player cannot be connected",
                      "code": "under_age",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Something the request names is not there, or Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`). Or no open request has the `code` you sent (`code_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "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": "No such game",
                    "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": {
            "$ref": "#/components/responses/ConnectMethodNotAllowed"
          },
          "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/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "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": {
            "$ref": "#/components/responses/ConnectRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ConnectInternalError"
          }
        }
      },
      "get": {
        "tags": [
          "Connect"
        ],
        "summary": "List connect sessions",
        "description": "Reads up to 100 connect sessions of one game by their IDs, for a server that follows several at once.\n\n**Required scope:** `players:connect`. A key with the `*` scope does not include it: tick **Act for a player** when you create the key.\n\nSend the IDs as `ids`, separated by commas. An ID of another game or community is left out of the response, and so is one that does not exist. The whole list is returned in one response, so `page.nextCursor` is always `null` and `page.limit` is the number of sessions in it.\n\nA paused game can still be read.\n\n**Limit.** 600 requests a minute for the game, across the Connect operations, and not counted against the key's own number.",
        "operationId": "listConnectSessions",
        "x-required-scope": [
          "players:connect"
        ],
        "x-since": "1.1.0",
        "parameters": [
          {
            "$ref": "#/components/parameters/GamePath"
          },
          {
            "name": "ids",
            "in": "query",
            "required": true,
            "description": "The IDs of 1 to 100 connect sessions, separated by commas. Spaces around an ID are ignored.",
            "schema": {
              "type": "string"
            },
            "example": "kT3v9QmZ1xY7bLw2RfA8cN,B8nLq2WxZ5cD7vJ1mK4pRs"
          }
        ],
        "responses": {
          "200": {
            "description": "The sessions that were found.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListConnectSessionsResponse"
                },
                "examples": {
                  "sessions": {
                    "summary": "Two sessions",
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "sessionId": "kT3v9QmZ1xY7bLw2RfA8cN",
                          "status": "approved",
                          "reason": null,
                          "expiresAt": "2026-10-08T18:14:11.000Z",
                          "member": {
                            "displayName": "Nova",
                            "avatarUrl": "https://cdn.discordapp.com/avatars/409876543210987654/a1b2c3d4e5f60718293a4b5c6d7e8f90.png"
                          },
                          "connection": {
                            "discordId": "409876543210987654",
                            "playerId": "p_19",
                            "proof": "verified",
                            "connectedAt": "2026-10-08T18:06:40.000Z",
                            "moved": false,
                            "previousPlayerId": null
                          }
                        },
                        {
                          "sessionId": "B8nLq2WxZ5cD7vJ1mK4pRs",
                          "status": "expired",
                          "reason": "expired",
                          "expiresAt": "2026-10-08T17:40:02.000Z",
                          "member": null,
                          "connection": null
                        }
                      ],
                      "page": {
                        "nextCursor": null,
                        "limit": 2
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`ids` is missing or cannot be read. Nothing was looked up.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "badIds": {
                    "summary": "No IDs, too many, or one of another form",
                    "value": {
                      "error": "ids must be 1 to 100 session IDs, separated by commas",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "ids"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key does not hold `players:connect` (`insufficient_scope`: the body adds `required` and `granted`, and a `*` key does not hold this scope), or the game cannot be used: the community removed CEEJAY (`community_not_active`), the key is limited to other games (`game_not_allowed`), or the game is made for children (`audience_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForbiddenErrorResponse"
                },
                "examples": {
                  "missingScope": {
                    "summary": "Key without players:connect",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "players:connect"
                      ],
                      "granted": [
                        "*"
                      ]
                    }
                  },
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "gameNotAllowed": {
                    "summary": "The key is limited to other games",
                    "value": {
                      "error": "This API key is limited to other games",
                      "code": "game_not_allowed",
                      "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"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Something the request names is not there, or Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "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": "No such game",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/ConnectMethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/ConnectRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ConnectInternalError"
          }
        }
      }
    },
    "/games/{game}/connect/sessions/{sessionId}": {
      "get": {
        "tags": [
          "Connect"
        ],
        "summary": "Get a connect session",
        "description": "Reads one connect session: its status, who approved, and the connection once it is approved.\n\n**Required scope:** `players:connect`. A key with the `*` scope does not include it: tick **Act for a player** when you create the key.\n\nA session a member's code was attached to is read by the same ID. A session past its 10 minutes reads `expired` whether or not it has been cleaned up. A paused game can still be read.\n\n**Limit.** 600 requests a minute for the game, across the Connect operations, and not counted against the key's own number.",
        "operationId": "getConnectSession",
        "x-required-scope": [
          "players:connect"
        ],
        "x-since": "1.1.0",
        "parameters": [
          {
            "$ref": "#/components/parameters/GamePath"
          },
          {
            "$ref": "#/components/parameters/ConnectSessionIdPath"
          }
        ],
        "responses": {
          "200": {
            "description": "The session.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectSessionResponse"
                },
                "examples": {
                  "approved": {
                    "summary": "An approved session",
                    "value": {
                      "success": true,
                      "data": {
                        "sessionId": "kT3v9QmZ1xY7bLw2RfA8cN",
                        "status": "approved",
                        "reason": null,
                        "expiresAt": "2026-10-08T18:14:11.000Z",
                        "member": {
                          "displayName": "Nova",
                          "avatarUrl": "https://cdn.discordapp.com/avatars/409876543210987654/a1b2c3d4e5f60718293a4b5c6d7e8f90.png"
                        },
                        "connection": {
                          "discordId": "409876543210987654",
                          "playerId": "p_19",
                          "proof": "verified",
                          "connectedAt": "2026-10-08T18:06:40.000Z",
                          "moved": false,
                          "previousPlayerId": null
                        }
                      }
                    }
                  },
                  "waitingForGame": {
                    "summary": "Waiting for the game to confirm",
                    "value": {
                      "success": true,
                      "data": {
                        "sessionId": "kT3v9QmZ1xY7bLw2RfA8cN",
                        "status": "needs_game_confirmation",
                        "reason": null,
                        "expiresAt": "2026-10-08T18:14:11.000Z",
                        "member": {
                          "displayName": "Nova",
                          "avatarUrl": "https://cdn.discordapp.com/avatars/409876543210987654/a1b2c3d4e5f60718293a4b5c6d7e8f90.png"
                        },
                        "connection": null
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key does not hold `players:connect` (`insufficient_scope`: the body adds `required` and `granted`, and a `*` key does not hold this scope), or the game cannot be used: the community removed CEEJAY (`community_not_active`), the key is limited to other games (`game_not_allowed`), or the game is made for children (`audience_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForbiddenErrorResponse"
                },
                "examples": {
                  "missingScope": {
                    "summary": "Key without players:connect",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "players:connect"
                      ],
                      "granted": [
                        "*"
                      ]
                    }
                  },
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "gameNotAllowed": {
                    "summary": "The key is limited to other games",
                    "value": {
                      "error": "This API key is limited to other games",
                      "code": "game_not_allowed",
                      "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"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Something the request names is not there, or Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`). Or the game has no session with this ID (`session_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "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": "No such game",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "sessionNotFound": {
                    "summary": "No such session",
                    "value": {
                      "error": "Connect session not found",
                      "code": "session_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/ConnectMethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/ConnectRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ConnectInternalError"
          }
        }
      }
    },
    "/games/{game}/connect/sessions/{sessionId}/confirm": {
      "post": {
        "tags": [
          "Connect"
        ],
        "summary": "Confirm a connect session",
        "description": "Approves or declines a connect session from your server, for a game client that holds no session secret.\n\n**Required scope:** `players:connect`. A key with the `*` scope does not include it: tick **Act for a player** when you create the key.\n\nRead the session first, show your player the Discord name it returns, and send the player's decision. `accept: true` is accepted only while `status` is `needs_game_confirmation`. For a session your server or 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. `accept: false` ends an open session in any state, with the reason `game_declined`.\n\nSafe to repeat, and it takes no `idempotencyKey`: confirming twice returns the session as it is, and declining a session that has ended changes nothing.\n\nThe response is the session, in the same shape that Get a connect session returns. A paused game can still decline.\n\n**Limit.** 600 requests a minute for the game, across the Connect operations, and not counted against the key's own number.",
        "operationId": "confirmConnectSession",
        "x-required-scope": [
          "players:connect"
        ],
        "x-since": "1.1.0",
        "parameters": [
          {
            "$ref": "#/components/parameters/GamePath"
          },
          {
            "$ref": "#/components/parameters/ConnectSessionIdPath"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConfirmConnectSessionRequest"
              },
              "examples": {
                "request": {
                  "summary": "A request",
                  "value": {
                    "accept": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The session after it was approved or declined.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConnectSessionResponse"
                },
                "examples": {
                  "approved": {
                    "summary": "An approved session",
                    "value": {
                      "success": true,
                      "data": {
                        "sessionId": "kT3v9QmZ1xY7bLw2RfA8cN",
                        "status": "approved",
                        "reason": null,
                        "expiresAt": "2026-10-08T18:14:11.000Z",
                        "member": {
                          "displayName": "Nova",
                          "avatarUrl": "https://cdn.discordapp.com/avatars/409876543210987654/a1b2c3d4e5f60718293a4b5c6d7e8f90.png"
                        },
                        "connection": {
                          "discordId": "409876543210987654",
                          "playerId": "p_19",
                          "proof": "verified",
                          "connectedAt": "2026-10-08T18:06:40.000Z",
                          "moved": false,
                          "previousPlayerId": null
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was refused and nothing was changed. The checks run in this order: the body, then `accept`.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "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": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key does not hold `players:connect` (`insufficient_scope`: the body adds `required` and `granted`, and a `*` key does not hold this scope), or the game cannot be used: the community removed CEEJAY (`community_not_active`), the key is limited to other games (`game_not_allowed`), or the game is made for children (`audience_not_allowed`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ForbiddenErrorResponse"
                },
                "examples": {
                  "missingScope": {
                    "summary": "Key without players:connect",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "players:connect"
                      ],
                      "granted": [
                        "*"
                      ]
                    }
                  },
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "gameNotAllowed": {
                    "summary": "The key is limited to other games",
                    "value": {
                      "error": "This API key is limited to other games",
                      "code": "game_not_allowed",
                      "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"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Something the request names is not there, or Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`). Or the game has no session with this ID (`session_not_found`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "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": "No such game",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "sessionNotFound": {
                    "summary": "No such session",
                    "value": {
                      "error": "Connect session not found",
                      "code": "session_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/ConnectMethodNotAllowed"
          },
          "409": {
            "description": "The confirmation was not accepted. The session is not waiting for the game (`nothing_to_confirm`), it changed since the member approved (`session_changed`), the player is blocked (`player_blocked`) or connected to another Discord account (`player_taken`), or the game is paused (`game_paused`).",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/ConnectRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/ConnectRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "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"
                    }
                  },
                  "gamePaused": {
                    "summary": "The game is paused",
                    "value": {
                      "error": "This game is paused",
                      "code": "game_paused",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/ConnectRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ConnectInternalError"
          }
        }
      }
    },
    "/games/{game}/players/{playerId}": {
      "get": {
        "tags": [
          "Players"
        ],
        "summary": "Get a player",
        "description": "Reads one player of a game: who they are connected to, the member summary, and the game's approved fields that are not sensitive.\n\n**Required scope:** `players:read` (a key with the `*` scope can also call it).\n\nThe request is for one game of the key's community, named in the URL path. The key may be limited to some games. A player is found by the game's own ID, matched case-insensitively. `fields` holds the fields that are approved and not sensitive and have a current value, by their fixed keys, with numbers as numbers and yes or no fields as `true` or `false`. A sensitive field is never returned: no scope adds it. `member` is `null` when CEEJAY has no profile for the member. When `player.membership` is neither `member` nor `pending`, `member` is `null` and `fields` is empty as well: consent covers server members only.\n\nA player with no active connection is `404` with the code `player_not_found`. When the game holds data for players who have not connected and has some for this ID, the body adds `held: true`: the data is not returned.\n\nA paused game can still be read. It is counted against the key's own requests per minute (60 unless a higher limit was set on it).",
        "operationId": "getPlayer",
        "x-required-scope": [
          "players:read"
        ],
        "x-since": "1.1.0",
        "parameters": [
          {
            "$ref": "#/components/parameters/GamePath"
          },
          {
            "$ref": "#/components/parameters/PlayerIdPath"
          }
        ],
        "responses": {
          "200": {
            "description": "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/PlayerDetailResponse"
                },
                "examples": {
                  "player": {
                    "summary": "A verified player",
                    "value": {
                      "success": true,
                      "data": {
                        "player": {
                          "playerId": "p_19",
                          "discordId": "409876543210987654",
                          "playerName": "Nova77",
                          "proof": "verified",
                          "origin": "connect",
                          "connectedAt": "2026-10-08T18:06:40.000Z",
                          "membership": "member",
                          "sharing": {
                            "agreed": [
                              "progress",
                              "activity"
                            ],
                            "needed": []
                          }
                        },
                        "member": {
                          "displayName": "Nova",
                          "avatarUrl": "https://cdn.discordapp.com/avatars/409876543210987654/a1b2c3d4e5f60718293a4b5c6d7e8f90.png",
                          "level": 12,
                          "xp": 5420,
                          "coins": 1450
                        },
                        "fields": {
                          "std.level": 14,
                          "std.last_played": "2026-10-07",
                          "league_position": 3,
                          "club": "Harbour Town"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The player ID in the URL path contains a character that is 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": {
                  "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"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key does not hold `players:read` (`insufficient_scope`: the body adds `required` and `granted`), or the game cannot be used: the community removed CEEJAY (`community_not_active`), the key is limited to other games (`game_not_allowed`), or the game is made for children (`audience_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/ForbiddenErrorResponse"
                },
                "examples": {
                  "missingScope": {
                    "summary": "Key without players:read",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "players:read"
                      ],
                      "granted": [
                        "xp:grant"
                      ]
                    }
                  },
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "gameNotAllowed": {
                    "summary": "The key is limited to other games",
                    "value": {
                      "error": "This API key is limited to other games",
                      "code": "game_not_allowed",
                      "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"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Something the request names is not there, or Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`). Or no connected player has this ID (`player_not_found`). When the game holds data for the ID, the body adds `held: true`.",
            "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/PlayerNotFoundErrorResponse"
                },
                "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": "No such game",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "playerNotFound": {
                    "summary": "No connected player has this ID",
                    "value": {
                      "error": "No connected player has this playerId",
                      "code": "player_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "playerHeld": {
                    "summary": "The player is only held for a connection that has not happened",
                    "value": {
                      "error": "No connected player has this playerId",
                      "code": "player_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "held": true
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/games/{game}/players": {
      "get": {
        "tags": [
          "Players"
        ],
        "summary": "List players",
        "description": "Lists the players of a game who are connected to a Discord member, newest connection first, a page at a time.\n\n**Required scope:** `players:read` (a key with the `*` scope can also call it).\n\nThe request is for one game of the key's community, named in the URL path. The key may be limited to some games. The list contains each player's `playerId`, `discordId`, `playerName`, `proof`, `origin`, `connectedAt` and `membership`. Read one player with Get a player for the member summary and the fields. A player who has not connected is not in the list. The response is the list in `data` with `page` alongside it: send `page.nextCursor` back as `cursor` for the next page, and stop when it is `null`.\n\nA paused game can still be read. It is counted against the key's own requests per minute (60 unless a higher limit was set on it).",
        "operationId": "listPlayers",
        "x-required-scope": [
          "players:read"
        ],
        "x-since": "1.1.0",
        "parameters": [
          {
            "$ref": "#/components/parameters/GamePath"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "The page size, from 1 to 200, written in digits. A value above 200 is lowered to 200, and `0`, an empty value or none gives 50. A negative number, a fraction or text is refused with `400`.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "minimum": 0,
              "maximum": 200,
              "default": 50
            },
            "example": 50
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Where the page starts: the `page.nextCursor` of the page before, sent back exactly as returned. Leave it out for the first page. A cursor that cannot be read, or an empty one, is refused with `400`.",
            "schema": {
              "type": "string",
              "maxLength": 512
            },
            "example": "WzQyXQ"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of players.",
            "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/ListPlayersResponse"
                },
                "examples": {
                  "players": {
                    "summary": "Two players",
                    "value": {
                      "success": true,
                      "data": [
                        {
                          "playerId": "p_19",
                          "discordId": "409876543210987654",
                          "playerName": "Nova77",
                          "proof": "verified",
                          "origin": "connect",
                          "connectedAt": "2026-10-08T18:06:40.000Z",
                          "membership": "member"
                        },
                        {
                          "playerId": "p_77",
                          "discordId": "521776109822345216",
                          "playerName": null,
                          "proof": "basic",
                          "origin": "connect",
                          "connectedAt": "2026-10-08T17:41:12.000Z",
                          "membership": "pending"
                        }
                      ],
                      "page": {
                        "nextCursor": "WzQyXQ",
                        "limit": 2
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`limit` is not a whole number of 0 or more written in digits, or sent more than once, or `cursor` cannot be read. Nothing was looked up.",
            "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": "Invalid limit",
                    "value": {
                      "error": "limit must be a whole number of 0 or more",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "limit"
                    }
                  },
                  "badCursor": {
                    "summary": "A cursor that cannot be read",
                    "value": {
                      "error": "cursor is not one this list gave",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "cursor"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key does not hold `players:read` (`insufficient_scope`: the body adds `required` and `granted`), or the game cannot be used: the community removed CEEJAY (`community_not_active`), the key is limited to other games (`game_not_allowed`), or the game is made for children (`audience_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/ForbiddenErrorResponse"
                },
                "examples": {
                  "missingScope": {
                    "summary": "Key without players:read",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "players:read"
                      ],
                      "granted": [
                        "xp:grant"
                      ]
                    }
                  },
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "gameNotAllowed": {
                    "summary": "The key is limited to other games",
                    "value": {
                      "error": "This API key is limited to other games",
                      "code": "game_not_allowed",
                      "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"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Something the request names is not there, or Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`).",
            "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": "No such game",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/games/{game}/players/{playerId}/disconnect": {
      "post": {
        "tags": [
          "Players"
        ],
        "summary": "Disconnect a player",
        "description": "Ends a player's connection from your server. The member's connected role is removed, the player's devices are signed out, and the event `player.disconnected` is sent shortly afterwards.\n\n**Required scope:** `players:connect`. A key with the `*` scope does not hold it: tick it by name when you make the key.\n\nThe request is for one game of the key's community, named in the URL path. The key may be limited to some games. The request takes no body. A player with no active connection is `404` with the code `player_not_found`. A reward already granted is not revoked. The values CEEJAY stores for the player are cleared from the member, and the member can connect again. It takes no `idempotencyKey`: ending a connection that has ended finds nothing to end.\n\nPlayers of a paused game, and of a game made for children, can still be disconnected. It is counted against the key's own requests per minute (60 unless a higher limit was set on it).",
        "operationId": "disconnectPlayerByKey",
        "x-required-scope": [
          "players:connect"
        ],
        "x-since": "1.1.0",
        "parameters": [
          {
            "$ref": "#/components/parameters/GamePath"
          },
          {
            "$ref": "#/components/parameters/PlayerIdPath"
          }
        ],
        "responses": {
          "200": {
            "description": "The connection was 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/DisconnectPlayerResponse"
                },
                "examples": {
                  "disconnected": {
                    "summary": "A connection ended",
                    "value": {
                      "success": true,
                      "data": {
                        "disconnected": true
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The player ID in the URL path contains a character that is not allowed, or a body was sent that is not a JSON object.",
            "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"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key does not hold `players:connect` (`insufficient_scope`: the body adds `required` and `granted`, and a `*` key does not hold this scope), or the game cannot be used: the community removed CEEJAY (`community_not_active`), the key is limited to other games (`game_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/ForbiddenErrorResponse"
                },
                "examples": {
                  "missingScope": {
                    "summary": "Key without players:connect",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "players:connect"
                      ],
                      "granted": [
                        "xp:grant"
                      ]
                    }
                  },
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "gameNotAllowed": {
                    "summary": "The key is limited to other games",
                    "value": {
                      "error": "This API key is limited to other games",
                      "code": "game_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Something the request names is not there, or Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`). Or no active connection exists for this player (`player_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": "No such game",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "playerNotFound": {
                    "summary": "No connected player has this ID",
                    "value": {
                      "error": "No connected player has this playerId",
                      "code": "player_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/games/{game}/players/{playerId}/delete": {
      "post": {
        "tags": [
          "Players"
        ],
        "summary": "Delete a player",
        "description": "Deletes what CEEJAY stores for one player ID in one game. Use it when a player asks for their data to be erased.\n\n**Required scope:** `players:delete`. A key with the `*` scope does not hold it: tick it by name when you make the key.\n\nThe request is for one game of the key's community, named in the URL path. The key may be limited to some games. First the player's connection is ended and the bot is given up to 5 seconds to remove the connected role and send `player.disconnected`. Then these are removed: every connection of this player in this game, and its refresh tokens and consent details; the member's values for this game's fields, current and older; the player's game events in the member history, and the entries that say the player connected or disconnected; the connection log for this game and member; the player ID from the previous player of a moved connection; the player's connect sessions and passes; work still queued for the player; and everything held for the player if they had not connected. For the purchases the player made through the game, the player's ID, proof, connection and device are removed from the purchase and from what the game spent (the coins spent stay: they belong to the member), and the idempotency keys stored for the player's connections are removed.\n\nWhat stays: rewards already granted (they store a hash and never the ID, so \"once per player\" still holds), a restriction set by the community, and an `erased` marker, so data the game sends later for this ID is not held again. The request log of this request contains the route, not the ID.\n\nIt is a `POST` of its own, not a `DELETE`, because the scope it needs is for the whole route. Safe to repeat: when nothing is found it returns `200` with zeros. It takes no `idempotencyKey`.\n\nDeleting a player is available for paused games and games made for children too. It is counted against the key's own requests per minute (60 unless a higher limit was set on it).",
        "operationId": "deletePlayer",
        "x-required-scope": [
          "players:delete"
        ],
        "x-since": "1.1.0",
        "parameters": [
          {
            "$ref": "#/components/parameters/GamePath"
          },
          {
            "$ref": "#/components/parameters/PlayerIdPath"
          }
        ],
        "responses": {
          "200": {
            "description": "What was deleted.",
            "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/DeletePlayerResponse"
                },
                "examples": {
                  "deleted": {
                    "summary": "A player deleted",
                    "value": {
                      "success": true,
                      "data": {
                        "deleted": true,
                        "removed": {
                          "connection": 1,
                          "values": 6,
                          "events": 12,
                          "pending": 0,
                          "purchases": 2
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The player ID in the URL path contains a character that is not allowed, or a body was sent that is not a JSON object.",
            "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"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key does not hold `players:delete` (`insufficient_scope`: the body adds `required` and `granted`, and a `*` key does not hold this scope), or the game cannot be used: the community removed CEEJAY (`community_not_active`), the key is limited to other games (`game_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/ForbiddenErrorResponse"
                },
                "examples": {
                  "missingScope": {
                    "summary": "Key without players:delete",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "players:delete"
                      ],
                      "granted": [
                        "xp:grant"
                      ]
                    }
                  },
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "gameNotAllowed": {
                    "summary": "The key is limited to other games",
                    "value": {
                      "error": "This API key is limited to other games",
                      "code": "game_not_allowed",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Something the request names is not there, or Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`).",
            "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": "No such game",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/games/{game}/player-fields": {
      "post": {
        "tags": [
          "Players"
        ],
        "summary": "Set player fields",
        "description": "Sends values about your players, to be stored on the members they are connected to: a league position, a club, the day a player last played. Up to 500 players in one call, each with up to 40 fields.\n\n**Required scope:** `players:write` (a key with the `*` scope can also call it).\n\nThe request is for one game of the key's community, named in the URL path. The key may be limited to some games. Fields are **set**, not added: a value that equals the current one is not a new history entry, `null` clears a field, and a field that is not in the object is left as it is. A name is matched to the name your game uses for the field first, then to the field's key, case-insensitively. A name nothing matches is added to the field map as a proposal and returns `not_approved`; its value is written once the community's team approves it. Seven standard fields (the ones that need no choices) are approved by the first value.\n\nA value is text (up to 500 characters), a finite number (beyond 1e15 either way it is refused, and it is multiplied by the field's scale and rounded to 6 places), `true` or `false`, or `null`. A date is `YYYY-MM-DD`, or a full time with an offset, which becomes its UTC day. A dropdown accepts one of its choices, after your game's value is mapped through the field's value map. An object or a list is refused with `bad_value`.\n\nValues are stored for **members** only: a connected player whose member is not in the server gets the status `not_member`. A player who has not connected is `not_connected`, or `held` when the game holds data for players who have not. A connection a member made through Connect stores the categories the member consented to, and a field of another category is `not_consented`. A connection your own server created stores none, and your server is responsible for the consent. A connection made at the basic level, where the game supplied an unverified player ID, does not accept fields of the `purchases` or `location` category: each such field is a problem with the code `needs_verified_proof`, and the player's other fields are still written. Fields of `progress` and players your own server linked are not affected.\n\n`underAge: true` on a player stores nothing and ends their connection. It needs the key to hold `players:connect` as well: without it the whole call is `403` with the code `insufficient_scope` and `required: [\"players:connect\"]`, and nothing is written. A call can carry at most 50 players with `underAge: true`, or it is `400` with the code `bad_request`.\n\nA batch with problems still returns `200`, with a result for each player. Only a body that is not shaped right is `400`. The body is read before it is parsed, up to 512 KB; a larger one is `413`. With `dryRun: true` every check runs and nothing is written.\n\nIt takes no `idempotencyKey`: a field is set, not added, so sending it again changes nothing. It is counted against the key's own requests per minute (60 unless a higher limit was set on it). A batch of 500 players is one request.",
        "operationId": "setPlayerFields",
        "x-required-scope": [
          "players:write"
        ],
        "x-since": "1.1.0",
        "parameters": [
          {
            "$ref": "#/components/parameters/GamePath"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetPlayerFieldsRequest"
              },
              "examples": {
                "request": {
                  "summary": "Two players",
                  "value": {
                    "players": [
                      {
                        "playerId": "p_19",
                        "fields": {
                          "league_position": 3,
                          "club": "Harbour Town",
                          "last_played": "2026-10-07T21:10:00+02:00",
                          "coach_notes": "Ready for the cup"
                        }
                      },
                      {
                        "playerId": "p_77",
                        "fields": {
                          "league_position": 9
                        }
                      }
                    ],
                    "dryRun": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What happened to each 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/SetPlayerFieldsResponse"
                },
                "examples": {
                  "results": {
                    "summary": "One written, one not connected",
                    "value": {
                      "success": true,
                      "data": {
                        "results": [
                          {
                            "index": 0,
                            "playerId": "p_19",
                            "status": "written",
                            "written": 2,
                            "unchanged": 1,
                            "cleared": 0,
                            "problems": [
                              {
                                "field": "coach_notes",
                                "code": "not_approved"
                              }
                            ]
                          },
                          {
                            "index": 1,
                            "playerId": "p_77",
                            "status": "not_connected",
                            "written": 0,
                            "unchanged": 0,
                            "cleared": 0,
                            "problems": []
                          }
                        ],
                        "summary": {
                          "players": 2,
                          "written": 2,
                          "unchanged": 1,
                          "cleared": 0,
                          "held": 0,
                          "refused": 1
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not shaped right. Nothing was looked up or written. A player that cannot be read is not an error of the call: it gets the status `bad_item` in the results.",
            "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"
                    }
                  },
                  "notJson": {
                    "summary": "The body is not JSON",
                    "value": {
                      "error": "The request body must be JSON",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "noPlayers": {
                    "summary": "No players",
                    "value": {
                      "error": "players must be a list of 1 to 500 players",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "players"
                    }
                  },
                  "tooManyPlayers": {
                    "summary": "More than 500 players",
                    "value": {
                      "error": "A call can carry up to 500 players",
                      "code": "too_many_players",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "tooManyUnderAge": {
                    "summary": "More than 50 players with underAge",
                    "value": {
                      "error": "A call can carry up to 50 players with underAge: true",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "players"
                    }
                  },
                  "badDryRun": {
                    "summary": "dryRun that is not true or false",
                    "value": {
                      "error": "dryRun must be true or false",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "dryRun"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key does not hold `players:write` (`insufficient_scope`: the body adds `required` and `granted`), or the game cannot be used: the community removed CEEJAY (`community_not_active`), the key is limited to other games (`game_not_allowed`), or the game is made for children (`audience_not_allowed`). It also returns `insufficient_scope` with `required: [\"players:connect\"]` when an entry says `underAge: true` and the key does not hold `players:connect`; a `*` key does not hold it.",
            "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/ForbiddenErrorResponse"
                },
                "examples": {
                  "missingScope": {
                    "summary": "Key without players:write",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "players:write"
                      ],
                      "granted": [
                        "xp:grant"
                      ]
                    }
                  },
                  "missingConnectScope": {
                    "summary": "underAge without players:connect",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "players:connect"
                      ],
                      "granted": [
                        "players:write"
                      ]
                    }
                  },
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "gameNotAllowed": {
                    "summary": "The key is limited to other games",
                    "value": {
                      "error": "This API key is limited to other games",
                      "code": "game_not_allowed",
                      "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"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Something the request names is not there, or Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`).",
            "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": "No such game",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "description": "The game is paused or suspended (`game_paused`). Nothing was written.",
            "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"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 512 KB. It is read before it is parsed, and nothing was looked at or written. Send fewer players or events in one call.",
            "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 512 KB",
                    "value": {
                      "error": "The request body is larger than 512 KB. Send fewer players or events in one call",
                      "code": "body_too_large",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/games/{game}/player-events": {
      "post": {
        "tags": [
          "Players"
        ],
        "summary": "Log player events",
        "description": "Logs things your players did in the game, to be shown on the profile of the member they are connected to, counted in segments, and rewarded. Up to 500 events in one call.\n\n**Required scope:** `players:write` (a key with the `*` scope can also call it).\n\nThe request is for one game of the key's community, named in the URL path. The key may be limited to some games. Each event names a player (`playerId` or `discordId`), an `eventId` of your own and an `event`. The `eventId` is what makes a retry safe: events are idempotent on `eventId`, so a repeated ID is ignored and its status is `duplicate`. An event name that is not approved in the field map is not stored: it is proposed, gets the status `not_approved`, and can be sent again after approval, within 7 days. `occurredAt` can be at most 5 minutes ahead and at most 7 days back.\n\nEvents are stored for **members** only (`not_member` otherwise), and a player who has not connected is `not_connected`, or `held` when the game holds data for players who have not. The quantity of an event is stored only where its entry keeps one. Events take no free text: send money and other private values as fields, not as events. A member who has not consented to sharing the `activity` category, because they connected before the game shared it and have not consented since on their connections page, gets the status `not_consented`. A connection your own server created carries no consent and is not checked.\n\n`underAge: true` on an event stores nothing and ends the player's connection. It needs the key to hold `players:connect` as well: without it the whole call is `403` with the code `insufficient_scope` and `required: [\"players:connect\"]`. A call can carry at most 50 events with `underAge: true`, or it is `400` with the code `bad_request`.\n\nThe game has to share the `activity` category, or the whole call is `409` with the code `activity_not_shared`. The member history has to be enabled on this instance, or the call is `404` with the code `not_available` and `feature: \"member_history\"`. Over the game's events a minute (3,000 unless another number was set for the game) the whole call is `429` with the code `event_rate_exceeded`, `retryAfter` and a `Retry-After` header.\n\nAn event that earns a reward returns `reward: \"queued\"`: the call does not wait for the bot, which grants the reward asynchronously and only for a connection that meets the verification level the game requires. A reward is queued only for an event less than 24 hours old.\n\nThe body is read before it is parsed, up to 512 KB; a larger one is `413`. With `dryRun: true` every check runs and nothing is written or counted.\n\nIt takes no `idempotencyKey`: the `eventId` of each event is the key. It is counted against the key's own requests per minute (60 unless a higher limit was set on it). A batch of 500 events is one request.",
        "operationId": "logPlayerEvents",
        "x-required-scope": [
          "players:write"
        ],
        "x-since": "1.1.0",
        "parameters": [
          {
            "$ref": "#/components/parameters/GamePath"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LogPlayerEventsRequest"
              },
              "examples": {
                "request": {
                  "summary": "Two events",
                  "value": {
                    "events": [
                      {
                        "eventId": "m-88213",
                        "playerId": "p_19",
                        "event": "match_won",
                        "occurredAt": "2026-10-07T19:02:00Z",
                        "quantity": 1
                      },
                      {
                        "eventId": "m-88214",
                        "playerId": "p_77",
                        "event": "match_won",
                        "occurredAt": "2026-10-07T19:05:00Z"
                      }
                    ],
                    "dryRun": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What happened to each event.",
            "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/LogPlayerEventsResponse"
                },
                "examples": {
                  "results": {
                    "summary": "One logged with a reward, one not connected",
                    "value": {
                      "success": true,
                      "data": {
                        "results": [
                          {
                            "index": 0,
                            "eventId": "m-88213",
                            "status": "logged",
                            "reward": "queued"
                          },
                          {
                            "index": 1,
                            "eventId": "m-88214",
                            "status": "not_connected",
                            "reward": "none"
                          }
                        ],
                        "summary": {
                          "logged": 1,
                          "duplicate": 0,
                          "held": 0,
                          "refused": 1,
                          "rewardsQueued": 1
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not shaped right. Nothing was looked up or written. An event that cannot be read is not an error of the call: it gets the status `bad_item` in the results.",
            "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"
                    }
                  },
                  "notJson": {
                    "summary": "The body is not JSON",
                    "value": {
                      "error": "The request body must be JSON",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "noEvents": {
                    "summary": "No events",
                    "value": {
                      "error": "events must be a list of 1 to 500 events",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "events"
                    }
                  },
                  "tooManyEvents": {
                    "summary": "More than 500 events",
                    "value": {
                      "error": "A call can carry up to 500 events",
                      "code": "too_many_events",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "tooManyUnderAge": {
                    "summary": "More than 50 events with underAge",
                    "value": {
                      "error": "A call can carry up to 50 events with underAge: true",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "events"
                    }
                  },
                  "badDryRun": {
                    "summary": "dryRun that is not true or false",
                    "value": {
                      "error": "dryRun must be true or false",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "dryRun"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key does not hold `players:write` (`insufficient_scope`: the body adds `required` and `granted`), or the game cannot be used: the community removed CEEJAY (`community_not_active`), the key is limited to other games (`game_not_allowed`), or the game is made for children (`audience_not_allowed`). It also returns `insufficient_scope` with `required: [\"players:connect\"]` when an entry says `underAge: true` and the key does not hold `players:connect`; a `*` key does not hold it.",
            "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/ForbiddenErrorResponse"
                },
                "examples": {
                  "missingScope": {
                    "summary": "Key without players:write",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "players:write"
                      ],
                      "granted": [
                        "xp:grant"
                      ]
                    }
                  },
                  "missingConnectScope": {
                    "summary": "underAge without players:connect",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "players:connect"
                      ],
                      "granted": [
                        "players:write"
                      ]
                    }
                  },
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "gameNotAllowed": {
                    "summary": "The key is limited to other games",
                    "value": {
                      "error": "This API key is limited to other games",
                      "code": "game_not_allowed",
                      "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"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Something the request names is not there, or Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`). Or the member history is disabled on this instance (`not_available`, with `feature: \"member_history\"`): events are stored in it.",
            "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"
                    }
                  },
                  "memberHistoryOff": {
                    "summary": "The member history is switched off here",
                    "value": {
                      "error": "This is not available on this instance yet.",
                      "code": "not_available",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "feature": "member_history"
                    }
                  },
                  "gameNotFound": {
                    "summary": "No such game",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "description": "The game is paused or suspended (`game_paused`), or it does not share the `activity` category (`activity_not_shared`). Nothing was written.",
            "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"
                    }
                  },
                  "activityNotShared": {
                    "summary": "The game does not share activity",
                    "value": {
                      "error": "This game does not share the activity category, so events cannot be logged. The team adds it to what the game shares first",
                      "code": "activity_not_shared",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The body is larger than 512 KB. It is read before it is parsed, and nothing was looked at or written. Send fewer players or events in one call.",
            "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 512 KB",
                    "value": {
                      "error": "The request body is larger than 512 KB. Send fewer players or events in one call",
                      "code": "body_too_large",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "The key has used up its requests for this minute (`rate_limited`), or the game sent more events this minute than it is allowed (`event_rate_exceeded`). `Retry-After`, `X-RateLimit-Reset` for the first and `retryAfter` say how many seconds are left. Nothing was written.",
            "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/RateLimitErrorResponse"
                },
                "examples": {
                  "limited": {
                    "summary": "Too many requests",
                    "value": {
                      "error": "Rate limit exceeded",
                      "code": "rate_limited",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 34
                    }
                  },
                  "eventRate": {
                    "summary": "Too many events this minute",
                    "value": {
                      "error": "This game has sent more events this minute than it is allowed. Wait and send them again",
                      "code": "event_rate_exceeded",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "retryAfter": 41
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/games/{game}/field-map": {
      "get": {
        "tags": [
          "Field map"
        ],
        "summary": "Get the field map",
        "description": "Reads a game's field map: every field and event the game sends, with the state each is in.\n\n**Required scope:** `players:read` (a key with the `*` scope can also call it).\n\nThe request is for one game of the key's community, named in the URL path. The key may be limited to some games. The map starts with the 14 standard fields. A name your game sends that nothing matches becomes a **proposed** entry; the community's team **approves** it (and chooses its type, choices and category) or leaves it **unused**. A key can read the map and propose to it, and cannot approve its own map. Seven standard fields that need no choices are approved by the first value your game sends for them. A game can have 25 approved fields of its own and 25 approved events, besides the standard fields, and up to 50 proposed entries of each kind at a time.\n\nNo value of a player, no sample and no value waiting for a choice is in the map: those are players' values that nobody has marked sensitive or not.\n\nA paused game can still be read. It is counted against the key's own requests per minute (60 unless a higher limit was set on it).",
        "operationId": "getFieldMap",
        "x-required-scope": [
          "players:read"
        ],
        "x-since": "1.1.0",
        "parameters": [
          {
            "$ref": "#/components/parameters/GamePath"
          }
        ],
        "responses": {
          "200": {
            "description": "The field map.",
            "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/FieldMapResponse"
                },
                "examples": {
                  "map": {
                    "summary": "A map with approved and proposed entries",
                    "value": {
                      "success": true,
                      "data": {
                        "fields": [
                          {
                            "sourceKey": "level",
                            "options": [],
                            "unit": null,
                            "scale": null,
                            "category": "progress",
                            "standard": true,
                            "sensitive": false,
                            "state": "approved",
                            "key": "std.level",
                            "name": "Game level",
                            "type": "number"
                          },
                          {
                            "sourceKey": "last_played",
                            "options": [],
                            "unit": null,
                            "scale": null,
                            "category": "progress",
                            "standard": true,
                            "sensitive": false,
                            "state": "approved",
                            "key": "std.last_played",
                            "name": "Last played",
                            "type": "date"
                          },
                          {
                            "sourceKey": "league_position",
                            "options": [],
                            "unit": null,
                            "scale": null,
                            "category": "progress",
                            "standard": false,
                            "sensitive": false,
                            "state": "approved",
                            "key": "league_position",
                            "name": "League position",
                            "type": "number"
                          },
                          {
                            "sourceKey": "club",
                            "options": [],
                            "unit": null,
                            "scale": null,
                            "category": "progress",
                            "standard": false,
                            "sensitive": false,
                            "state": "approved",
                            "key": "club",
                            "name": "Club",
                            "type": "text"
                          },
                          {
                            "sourceKey": "lifetime_spend",
                            "options": [],
                            "unit": "USD",
                            "scale": null,
                            "category": "purchases",
                            "standard": true,
                            "sensitive": true,
                            "state": "approved",
                            "key": "std.total_spend",
                            "name": "Total spend",
                            "type": "number"
                          },
                          {
                            "sourceKey": "coins_spent_cents",
                            "options": [],
                            "unit": null,
                            "scale": 0.01,
                            "category": "progress",
                            "standard": false,
                            "sensitive": false,
                            "state": "proposed",
                            "key": "coins_spent_cents",
                            "name": "Coins spent cents",
                            "type": "number"
                          }
                        ],
                        "events": [
                          {
                            "key": "match_won",
                            "sourceKey": "match_won",
                            "label": "Won a match",
                            "keepsQuantity": false,
                            "state": "approved",
                            "rewardMode": "each"
                          },
                          {
                            "key": "cup_entered",
                            "sourceKey": "cup_entered",
                            "label": "Entered the cup",
                            "keepsQuantity": false,
                            "state": "proposed",
                            "rewardMode": "once"
                          }
                        ],
                        "limits": {
                          "ownFields": 25,
                          "events": 25
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key does not hold `players:read` (`insufficient_scope`: the body adds `required` and `granted`), or the game cannot be used: the community removed CEEJAY (`community_not_active`), the key is limited to other games (`game_not_allowed`), or the game is made for children (`audience_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/ForbiddenErrorResponse"
                },
                "examples": {
                  "missingScope": {
                    "summary": "Key without players:read",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "players:read"
                      ],
                      "granted": [
                        "xp:grant"
                      ]
                    }
                  },
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "gameNotAllowed": {
                    "summary": "The key is limited to other games",
                    "value": {
                      "error": "This API key is limited to other games",
                      "code": "game_not_allowed",
                      "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"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Something the request names is not there, or Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`).",
            "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": "No such game",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/games/{game}/field-map/propose": {
      "post": {
        "tags": [
          "Field map"
        ],
        "summary": "Propose to the field map",
        "description": "Tells CEEJAY which fields and events your game sends, before it sends them, so the community's team can approve them first.\n\n**Required scope:** `players:write` (a key with the `*` scope can also call it).\n\nThe request is for one game of the key's community, named in the URL path. The key may be limited to some games. Send names with an example value in `sample`, or say more in `fields` and `events`. It adds or updates **proposed** entries and returns the entries your request named as the map shows them now. It never modifies an approved entry or one the team marked unused: those are returned with the reason in `skipped`. The team is notified when a new entry is added to the map, at most once a day for each game.\n\nThe proposal is made by fixed rules, with no AI model, so no player value leaves CEEJAY. A name that is a known alias of a standard field maps to that standard field. `true` and `false` are yes or no, a number is a number, text that reads as a date is a date, and everything else is text. A name that ends in `_cents` proposes a scale of `0.01`. A key is generated from the name. An example value is stored as a sample for the team, truncated to 80 characters.\n\nAn API key cannot approve proposals. A team member does that in the dashboard. It takes no `idempotencyKey`: a name that is already proposed is matched, not created twice. It is counted against the key's own requests per minute (60 unless a higher limit was set on it).",
        "operationId": "proposeFieldMap",
        "x-required-scope": [
          "players:write"
        ],
        "x-since": "1.1.0",
        "parameters": [
          {
            "$ref": "#/components/parameters/GamePath"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProposeFieldMapRequest"
              },
              "examples": {
                "request": {
                  "summary": "Names with example values",
                  "value": {
                    "sample": {
                      "fields": {
                        "league_position": 3,
                        "club": "Harbour Town",
                        "coins_spent_cents": 12900
                      },
                      "events": [
                        "match_won",
                        "cup_entered"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The proposal.",
            "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/ProposeFieldMapResponse"
                },
                "examples": {
                  "proposal": {
                    "summary": "Three fields and two events proposed",
                    "value": {
                      "success": true,
                      "data": {
                        "fields": [
                          {
                            "sourceKey": "league_position",
                            "options": [],
                            "unit": null,
                            "scale": null,
                            "category": "progress",
                            "standard": false,
                            "sensitive": false,
                            "state": "proposed",
                            "key": "league_position",
                            "name": "League position",
                            "type": "number"
                          },
                          {
                            "sourceKey": "club",
                            "options": [],
                            "unit": null,
                            "scale": null,
                            "category": "progress",
                            "standard": false,
                            "sensitive": false,
                            "state": "proposed",
                            "key": "club",
                            "name": "Club",
                            "type": "text"
                          },
                          {
                            "sourceKey": "coins_spent_cents",
                            "options": [],
                            "unit": null,
                            "scale": 0.01,
                            "category": "progress",
                            "standard": false,
                            "sensitive": false,
                            "state": "proposed",
                            "key": "coins_spent_cents",
                            "name": "Coins spent cents",
                            "type": "number"
                          }
                        ],
                        "events": [
                          {
                            "key": "match_won",
                            "sourceKey": "match_won",
                            "label": "Won a match",
                            "keepsQuantity": false,
                            "state": "proposed",
                            "rewardMode": "once"
                          },
                          {
                            "key": "cup_entered",
                            "sourceKey": "cup_entered",
                            "label": "Entered the cup",
                            "keepsQuantity": false,
                            "state": "proposed",
                            "rewardMode": "once"
                          }
                        ],
                        "skipped": []
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is not shaped right. Nothing was proposed.",
            "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"
                    }
                  },
                  "nothingToPropose": {
                    "summary": "Nothing to propose",
                    "value": {
                      "error": "Send at least one field or event to propose, in sample, fields or events",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "fields"
                    }
                  },
                  "sampleNotObject": {
                    "summary": "A sample that is not an object",
                    "value": {
                      "error": "sample must be an object",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "sample"
                    }
                  },
                  "badFieldType": {
                    "summary": "A type that is not known",
                    "value": {
                      "error": "type must be one of: text, number, date, choice, yes_no, ordered",
                      "code": "bad_request",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "field": "fields.type"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key does not hold `players:write` (`insufficient_scope`: the body adds `required` and `granted`), or the game cannot be used: the community removed CEEJAY (`community_not_active`), the key is limited to other games (`game_not_allowed`), or the game is made for children (`audience_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/ForbiddenErrorResponse"
                },
                "examples": {
                  "missingScope": {
                    "summary": "Key without players:write",
                    "value": {
                      "error": "Insufficient scopes",
                      "code": "insufficient_scope",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                      "required": [
                        "players:write"
                      ],
                      "granted": [
                        "xp:grant"
                      ]
                    }
                  },
                  "communityNotActive": {
                    "summary": "The community removed CEEJAY",
                    "value": {
                      "error": "This community is no longer active on CEEJAY",
                      "code": "community_not_active",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  },
                  "gameNotAllowed": {
                    "summary": "The key is limited to other games",
                    "value": {
                      "error": "This API key is limited to other games",
                      "code": "game_not_allowed",
                      "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"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Something the request names is not there, or Connect is not enabled for this instance (`not_available`, with `feature: \"players\"`).",
            "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": "No such game",
                    "value": {
                      "error": "Game not found",
                      "code": "game_not_found",
                      "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "description": "The game is paused or suspended (`game_paused`). Nothing was proposed.",
            "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": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "webhooks": {
    "member.joined": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member joined",
        "description": "Sent when someone joins your Discord server. It is sent after CEEJAY has worked out how they joined, which can take a moment. Not sent for bot accounts: adding a bot to the server sends nothing.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveMemberJoined",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "member.joined"
                      },
                      "data": {
                        "$ref": "#/components/schemas/MemberJoinedData"
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member joined",
                  "value": {
                    "event": "member.joined",
                    "data": {
                      "userId": "409876543210987654",
                      "username": "nova",
                      "joinedAt": "2026-10-01T09:14:22.118Z",
                      "inviteType": "normal",
                      "memberCount": 12842,
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a60"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "member.left": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member left",
        "description": "Sent when someone leaves your Discord server or is removed from it. Not sent for bot accounts: removing a bot sends nothing.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveMemberLeft",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "member.left"
                      },
                      "data": {
                        "$ref": "#/components/schemas/MemberLeftData"
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member left",
                  "value": {
                    "event": "member.left",
                    "data": {
                      "userId": "409876543210987654",
                      "username": "nova",
                      "leftAt": "2026-10-01T09:14:22.118Z",
                      "memberCount": 12841,
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a61"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "quest.completed": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A quest was completed",
        "description": "Sent when CEEJAY's bot completes a quest for a member. A completion made through `POST /quests/complete` does not send it. One made by Complete a quest task does, when it finishes the quest. It is sent for a quest completed in Discord and for one completed through the API (Complete a quest, or the last task of a quest through Complete a quest task). For a completion made through Complete a quest it is sent once, shortly after the answer; `rewards.roleGranted` is `false` when the quest's role had not been given yet at that moment, and no second event follows when it is.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveQuestCompleted",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "quest.completed"
                      },
                      "data": {
                        "$ref": "#/components/schemas/QuestCompletedData"
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A quest was completed",
                  "value": {
                    "event": "quest.completed",
                    "data": {
                      "userId": "409876543210987654",
                      "questId": 42,
                      "questTitle": "Join the weekend tournament",
                      "questType": "api_check",
                      "rewards": {
                        "xp": 500,
                        "currency": 250,
                        "badge": "Tournament player",
                        "roleGranted": false
                      },
                      "completedAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a62"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "ticket.created": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A ticket was opened",
        "description": "Sent when a member opens a support ticket.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveTicketCreated",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "ticket.created"
                      },
                      "data": {
                        "$ref": "#/components/schemas/TicketCreatedData"
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A ticket was opened",
                  "value": {
                    "event": "ticket.created",
                    "data": {
                      "ticketId": 318,
                      "ticketNumber": 42,
                      "panelName": "Support",
                      "userId": "409876543210987654",
                      "username": "nova",
                      "channelId": "718230049827100384",
                      "createdAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a63"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "poll.voted": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member voted in a poll",
        "description": "Sent when a member votes in a poll, once per member and poll: doing it again sends nothing more.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receivePollVoted",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "poll.voted"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "polls"
                              },
                              "action": {
                                "const": "voted"
                              },
                              "targetType": {
                                "const": "poll"
                              },
                              "targetId": {
                                "description": "The poll's ID."
                              },
                              "quantity": {
                                "type": "null"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member voted in a poll",
                  "value": {
                    "event": "poll.voted",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "polls",
                      "action": "voted",
                      "targetType": "poll",
                      "targetId": "41",
                      "targetName": null,
                      "itemId": "41",
                      "quantity": null,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a64"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "form.submitted": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member submitted a survey or form",
        "description": "Sent when a member submits a survey or form, once per member and form: doing it again sends nothing more.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveFormSubmitted",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "form.submitted"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "forms"
                              },
                              "action": {
                                "const": "submitted"
                              },
                              "targetType": {
                                "const": "form"
                              },
                              "targetId": {
                                "description": "The form's ID."
                              },
                              "quantity": {
                                "type": "null"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member submitted a survey or form",
                  "value": {
                    "event": "form.submitted",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "forms",
                      "action": "submitted",
                      "targetType": "form",
                      "targetId": "12",
                      "targetName": null,
                      "itemId": "12",
                      "quantity": null,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a65"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "suggestion.posted": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member posted a suggestion",
        "description": "Sent when a member posts a suggestion, once per member and suggestion: doing it again sends nothing more.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveSuggestionPosted",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "suggestion.posted"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "upvote"
                              },
                              "action": {
                                "const": "suggested"
                              },
                              "targetType": {
                                "const": "suggestion"
                              },
                              "targetId": {
                                "description": "The suggestion's ID."
                              },
                              "quantity": {
                                "type": "null"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member posted a suggestion",
                  "value": {
                    "event": "suggestion.posted",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "upvote",
                      "action": "suggested",
                      "targetType": "suggestion",
                      "targetId": "208",
                      "targetName": null,
                      "itemId": null,
                      "quantity": null,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a66"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "suggestion.upvoted": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member upvoted a suggestion",
        "description": "Sent when a member upvotes a suggestion, once per member and suggestion: doing it again sends nothing more.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveSuggestionUpvoted",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "suggestion.upvoted"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "upvote"
                              },
                              "action": {
                                "const": "upvoted"
                              },
                              "targetType": {
                                "const": "suggestion"
                              },
                              "targetId": {
                                "description": "The suggestion's ID."
                              },
                              "quantity": {
                                "type": "null"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member upvoted a suggestion",
                  "value": {
                    "event": "suggestion.upvoted",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "upvote",
                      "action": "upvoted",
                      "targetType": "suggestion",
                      "targetId": "208",
                      "targetName": null,
                      "itemId": null,
                      "quantity": null,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a67"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "event.rsvp": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member said they are going to an event",
        "description": "Sent when a member says they are going to an event, once per member and event: doing it again sends nothing more.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveEventRsvp",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "event.rsvp"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "events"
                              },
                              "action": {
                                "const": "rsvp"
                              },
                              "targetType": {
                                "const": "event"
                              },
                              "targetId": {
                                "description": "The event's ID."
                              },
                              "quantity": {
                                "type": "null"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member said they are going to an event",
                  "value": {
                    "event": "event.rsvp",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "events",
                      "action": "rsvp",
                      "targetType": "event",
                      "targetId": "19",
                      "targetName": null,
                      "itemId": "19",
                      "quantity": null,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a68"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "event.attended": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member attended an event",
        "description": "Sent when a member attends an event, once per member and event: doing it again sends nothing more.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveEventAttended",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "event.attended"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "events"
                              },
                              "action": {
                                "const": "attended"
                              },
                              "targetType": {
                                "const": "event"
                              },
                              "targetId": {
                                "description": "The event's ID."
                              },
                              "quantity": {
                                "type": "null"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member attended an event",
                  "value": {
                    "event": "event.attended",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "events",
                      "action": "attended",
                      "targetType": "event",
                      "targetId": "19",
                      "targetName": null,
                      "itemId": "19",
                      "quantity": null,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a69"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "member.level_up": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member reached a new level",
        "description": "Sent when a member reaches a new level, every time it happens.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveMemberLevelUp",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "member.level_up"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "xp"
                              },
                              "action": {
                                "const": "leveled_up"
                              },
                              "targetType": {
                                "type": "null"
                              },
                              "targetId": {
                                "type": "null"
                              },
                              "quantity": {
                                "type": "number",
                                "description": "The level reached."
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member reached a new level",
                  "value": {
                    "event": "member.level_up",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "xp",
                      "action": "leveled_up",
                      "targetType": null,
                      "targetId": null,
                      "targetName": null,
                      "itemId": null,
                      "quantity": 13,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a60"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "badge.earned": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member earned a badge",
        "description": "Sent when a member earns a badge, once per member and badge: doing it again sends nothing more.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveBadgeEarned",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "badge.earned"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "achievements"
                              },
                              "action": {
                                "const": "earned_badge"
                              },
                              "targetType": {
                                "const": "badge"
                              },
                              "targetId": {
                                "description": "The badge's ID."
                              },
                              "quantity": {
                                "type": "null"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member earned a badge",
                  "value": {
                    "event": "badge.earned",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "achievements",
                      "action": "earned_badge",
                      "targetType": "badge",
                      "targetId": "7",
                      "targetName": null,
                      "itemId": "7",
                      "quantity": null,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a61"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "shop.purchased": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member bought something from the shop",
        "description": "Sent when a member buys something from the shop, every time it happens.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveShopPurchased",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "shop.purchased"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "shop-economy"
                              },
                              "action": {
                                "const": "purchased"
                              },
                              "targetType": {
                                "const": "item"
                              },
                              "targetId": {
                                "description": "The shop item's ID."
                              },
                              "quantity": {
                                "type": "number",
                                "description": "The price paid."
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member bought something from the shop",
                  "value": {
                    "event": "shop.purchased",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "shop-economy",
                      "action": "purchased",
                      "targetType": "item",
                      "targetId": "15",
                      "targetName": null,
                      "itemId": "15",
                      "quantity": 500,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a62"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "member.checked_in": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member did their daily check-in",
        "description": "Sent when a member does their daily check-in, at most once per member per day.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveMemberCheckedIn",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "member.checked_in"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "shop-economy"
                              },
                              "action": {
                                "const": "checked_in"
                              },
                              "targetType": {
                                "type": "null"
                              },
                              "targetId": {
                                "type": "null"
                              },
                              "quantity": {
                                "type": "number",
                                "description": "The length of the check-in streak."
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member did their daily check-in",
                  "value": {
                    "event": "member.checked_in",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "shop-economy",
                      "action": "checked_in",
                      "targetType": null,
                      "targetId": null,
                      "targetName": null,
                      "itemId": null,
                      "quantity": 5,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a63"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "giveaway.entered": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member entered a giveaway",
        "description": "Sent when a member enters a giveaway, once per member and giveaway: doing it again sends nothing more.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveGiveawayEntered",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "giveaway.entered"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "giveaways"
                              },
                              "action": {
                                "const": "entered"
                              },
                              "targetType": {
                                "const": "giveaway"
                              },
                              "targetId": {
                                "description": "The giveaway's ID."
                              },
                              "quantity": {
                                "type": "null"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member entered a giveaway",
                  "value": {
                    "event": "giveaway.entered",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "giveaways",
                      "action": "entered",
                      "targetType": "giveaway",
                      "targetId": "6",
                      "targetName": null,
                      "itemId": "6",
                      "quantity": null,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a64"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "giveaway.won": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member won a giveaway",
        "description": "Sent when a member wins a giveaway, once per member and giveaway: doing it again sends nothing more.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveGiveawayWon",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "giveaway.won"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "giveaways"
                              },
                              "action": {
                                "const": "won"
                              },
                              "targetType": {
                                "const": "giveaway"
                              },
                              "targetId": {
                                "description": "The giveaway's ID."
                              },
                              "quantity": {
                                "type": "null"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member won a giveaway",
                  "value": {
                    "event": "giveaway.won",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "giveaways",
                      "action": "won",
                      "targetType": "giveaway",
                      "targetId": "6",
                      "targetName": null,
                      "itemId": "6",
                      "quantity": null,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a65"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "reward.received": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member received a reward",
        "description": "Sent when a member receives a reward, once per member and reward: doing it again sends nothing more.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveRewardReceived",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "reward.received"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "rewards-engine"
                              },
                              "action": {
                                "const": "received_reward"
                              },
                              "targetType": {
                                "const": "reward"
                              },
                              "targetId": {
                                "description": "The ID of the reward's grant."
                              },
                              "quantity": {
                                "type": "number",
                                "description": "The amount of the reward."
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member received a reward",
                  "value": {
                    "event": "reward.received",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "rewards-engine",
                      "action": "received_reward",
                      "targetType": "reward",
                      "targetId": "5120",
                      "targetName": null,
                      "itemId": null,
                      "quantity": 250,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "details": {
                        "sourceKind": "level",
                        "sourceId": "12",
                        "rewardId": "9",
                        "rewardType": "coins"
                      },
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a66"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "content.posted": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member posted in a content channel",
        "description": "Sent when a member posts in a content channel, every time it happens.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveContentPosted",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "content.posted"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "ugc-content"
                              },
                              "action": {
                                "const": "posted"
                              },
                              "targetType": {
                                "const": "channel"
                              },
                              "targetId": {
                                "description": "The channel's ID."
                              },
                              "quantity": {
                                "type": "null"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member posted in a content channel",
                  "value": {
                    "event": "content.posted",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "ugc-content",
                      "action": "posted",
                      "targetType": "channel",
                      "targetId": "718230049827100384",
                      "targetName": null,
                      "itemId": null,
                      "quantity": null,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a67"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "ticket.closed": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member's ticket was closed",
        "description": "Sent when a member's ticket is closed, once per member and ticket: doing it again sends nothing more.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveTicketClosed",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "ticket.closed"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "ticket-support"
                              },
                              "action": {
                                "const": "closed"
                              },
                              "targetType": {
                                "const": "ticket"
                              },
                              "targetId": {
                                "description": "The ticket's ID."
                              },
                              "quantity": {
                                "type": "null"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member's ticket was closed",
                  "value": {
                    "event": "ticket.closed",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "ticket-support",
                      "action": "closed",
                      "targetType": "ticket",
                      "targetId": "318",
                      "targetName": null,
                      "itemId": "2",
                      "quantity": null,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a68"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "program.joined": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member joined a program",
        "description": "Sent when a member joins a program, once per member and program: doing it again sends nothing more.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveProgramJoined",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "program.joined"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "programs"
                              },
                              "action": {
                                "const": "joined"
                              },
                              "targetType": {
                                "const": "program"
                              },
                              "targetId": {
                                "description": "The program's ID."
                              },
                              "quantity": {
                                "type": "null"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member joined a program",
                  "value": {
                    "event": "program.joined",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "programs",
                      "action": "joined",
                      "targetType": "program",
                      "targetId": "3",
                      "targetName": null,
                      "itemId": "3",
                      "quantity": null,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a69"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "account.linked": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member linked an account on another platform",
        "description": "Sent when a member links an account on another platform, once per member and platform: doing it again sends nothing more.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveAccountLinked",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "account.linked"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "connect"
                              },
                              "action": {
                                "const": "linked"
                              },
                              "targetType": {
                                "const": "platform"
                              },
                              "targetId": {
                                "description": "The platform, for example `steam`."
                              },
                              "quantity": {
                                "type": "null"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member linked an account on another platform",
                  "value": {
                    "event": "account.linked",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "connect",
                      "action": "linked",
                      "targetType": "platform",
                      "targetId": "steam",
                      "targetName": null,
                      "itemId": null,
                      "quantity": null,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a60"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "email.opened": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member opened an email for the first time",
        "description": "Sent when a member opens an email for the first time, once per member and email: doing it again sends nothing more.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveEmailOpened",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "email.opened"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "communication"
                              },
                              "action": {
                                "const": "opened_email"
                              },
                              "targetType": {
                                "const": "email"
                              },
                              "targetId": {
                                "description": "The email campaign's ID."
                              },
                              "quantity": {
                                "type": "null"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member opened an email for the first time",
                  "value": {
                    "event": "email.opened",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "communication",
                      "action": "opened_email",
                      "targetType": "email",
                      "targetId": "27",
                      "targetName": null,
                      "itemId": "27",
                      "quantity": null,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a61"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "role.picked": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member picked a role",
        "description": "Sent when a member picks a role, every time it happens.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveRolePicked",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "role.picked"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "react-roles"
                              },
                              "action": {
                                "const": "picked_role"
                              },
                              "targetType": {
                                "const": "role"
                              },
                              "targetId": {
                                "description": "The ID of the role picked (the first, when several were picked at once)."
                              },
                              "quantity": {
                                "type": "null"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member picked a role",
                  "value": {
                    "event": "role.picked",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "react-roles",
                      "action": "picked_role",
                      "targetType": "role",
                      "targetId": "982371004827100394",
                      "targetName": null,
                      "itemId": null,
                      "quantity": null,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a62"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "member.verified": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member passed verification",
        "description": "Sent when a member passes verification, once per member.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveMemberVerified",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "member.verified"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "onboarding"
                              },
                              "action": {
                                "const": "verified"
                              },
                              "targetType": {
                                "type": "null"
                              },
                              "targetId": {
                                "type": "null"
                              },
                              "quantity": {
                                "type": "null"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member passed verification",
                  "value": {
                    "event": "member.verified",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "onboarding",
                      "action": "verified",
                      "targetType": null,
                      "targetId": null,
                      "targetName": null,
                      "itemId": null,
                      "quantity": null,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a63"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "member.invited": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A member invited someone who joined",
        "description": "Sent when a member invites someone who joins, once per member and invited member: doing it again sends nothing more.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, and only where that history is switched on (the dashboard only offers these events as choices there). An action that is added more than 10 minutes after it happened, when CEEJAY catches up on past activity, sends nothing. Every member-action event carries the same fields in `data`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveMemberInvited",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "member.invited"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "app": {
                                "const": "invite-tracking"
                              },
                              "action": {
                                "const": "invited"
                              },
                              "targetType": {
                                "const": "member"
                              },
                              "targetId": {
                                "description": "The Discord ID of the member who joined through the invite."
                              },
                              "quantity": {
                                "type": "null"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member invited someone who joined",
                  "value": {
                    "event": "member.invited",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "invite-tracking",
                      "action": "invited",
                      "targetType": "member",
                      "targetId": "512345678901234567",
                      "targetName": null,
                      "itemId": null,
                      "quantity": null,
                      "occurredAt": "2026-10-01T09:14:22.118Z",
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a64"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "custom.event": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A custom event you emitted",
        "description": "Sent when you emit an event with `POST /events/emit`. Subscribe to `custom.event` to receive every custom event, to the exact name (for example `custom.purchase.completed`) to receive one, or to `*` to receive everything.\n\nThe `event` in the body and in the `X-CEEJAY-Event` header is the name you sent with `custom.` in front, not `custom.event`. `data` is exactly what you sent: CEEJAY adds nothing to it. Every subscription of one call receives the same `id` and `X-CEEJAY-Delivery`. The `id` looks like `evt_<time>_<random>`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveCustomEvent",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "type": "string",
                        "pattern": "^custom\\.",
                        "description": "The name you sent, with `custom.` in front."
                      },
                      "data": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A custom event you emitted",
                  "value": {
                    "event": "custom.purchase.completed",
                    "data": {
                      "discordId": "409876543210987654",
                      "sku": "season-pass",
                      "value": 24.99
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_1790000062118_k3n8xq2p"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "test.ping": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A test message",
        "description": "Sent when someone on your team presses the test button on a webhook subscription in the dashboard. It is not an event that happened in your community: answer it with any 2xx status and do nothing else. It is signed like every other delivery.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receiveTestPing",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "test.ping"
                      },
                      "data": {
                        "$ref": "#/components/schemas/TestPingData"
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A test message",
                  "value": {
                    "event": "test.ping",
                    "data": {
                      "message": "This is a test webhook from CEEJAY",
                      "subscriptionId": 4,
                      "subscriptionName": "Ops relay"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-01T09:14:22.431Z",
                    "id": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a66"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "shop.purchase": {
      "post": {
        "tags": [
          "Shop delivery webhooks"
        ],
        "summary": "A member bought a \"Delivered by your API\" item",
        "description": "Sent to the URL set on the item when a member buys it. Unlock `item.item_key` for `member` and answer. An item bought in a game also carries `player`: unlock it for that player. Finish a delivery you answered `202` for with `POST /shop/purchases/{purchaseId}/complete`. This is not the `shop.purchased` subscription event, which goes to webhook subscriptions.\n\nThe request, its signature and how CEEJAY reads your answer are described on the **Shop delivery webhooks** tag.",
        "operationId": "receiveShopDeliveryPurchase",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShopDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/ShopDeliverySignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ShopDeliveryPayload"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "shop.purchase"
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member bought a \"Delivered by your API\" item",
                  "value": {
                    "event": "shop.purchase",
                    "delivery_id": "shop_4242",
                    "purchase_id": 4242,
                    "item": {
                      "id": 7,
                      "name": "Gold skin",
                      "item_key": "skin_gold"
                    },
                    "member": {
                      "discord_id": "409876543210987654",
                      "username": "nova",
                      "display_name": "Nova",
                      "connections": {
                        "steam": "76561198000000000",
                        "xbox": null,
                        "playstation": null,
                        "epic": null,
                        "riot": null,
                        "roblox": null,
                        "battlenet": null,
                        "github": null,
                        "twitch": null
                      }
                    },
                    "price_paid": 500,
                    "currency": "Coins",
                    "server_id": "721104938271003842",
                    "created_at": "2026-10-01T09:14:22.000Z"
                  }
                },
                "fromGame": {
                  "summary": "An item bought in a game: the delivery names the player",
                  "value": {
                    "event": "shop.purchase",
                    "delivery_id": "shop_4243",
                    "purchase_id": 4243,
                    "item": {
                      "id": 7,
                      "name": "Gold skin",
                      "item_key": "skin_gold"
                    },
                    "member": {
                      "discord_id": "409876543210987654",
                      "username": "nova",
                      "display_name": "Nova",
                      "connections": {
                        "steam": null,
                        "xbox": null,
                        "playstation": null,
                        "epic": null,
                        "riot": null,
                        "roblox": null,
                        "battlenet": null,
                        "github": null,
                        "twitch": null
                      }
                    },
                    "price_paid": 500,
                    "currency": "Coins",
                    "server_id": "721104938271003842",
                    "created_at": "2026-10-08T18:30:00.000Z",
                    "player": {
                      "game": "rugby_manager",
                      "player_id": "p_19",
                      "proof": "verified"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "408": {
            "description": "Retried later."
          },
          "429": {
            "description": "Retried later."
          },
          "2XX": {
            "description": "Delivered (an empty body, or `{ \"status\": \"delivered\" }`), pending (status `202` or `{ \"status\": \"pending\" }`) or failed (`{ \"status\": \"failed\", \"message\": \"…\" }`). Any other body counts as delivered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShopDeliveryAnswer"
                },
                "examples": {
                  "delivered": {
                    "summary": "Delivered",
                    "value": {
                      "status": "delivered"
                    }
                  },
                  "pending": {
                    "summary": "You will finish later",
                    "value": {
                      "status": "pending"
                    }
                  },
                  "failed": {
                    "summary": "You cannot deliver",
                    "value": {
                      "status": "failed",
                      "message": "The player has no account on this realm"
                    }
                  }
                }
              }
            }
          },
          "3XX": {
            "description": "Refused. Redirects are not followed. The member is refunded."
          },
          "4XX": {
            "description": "Refused, and the member is refunded. Status `408` and `429` are retried instead."
          },
          "5XX": {
            "description": "Retried later: about every hour, with the same `delivery_id`, until 3 attempts have failed in total. Then the member is refunded."
          }
        }
      }
    },
    "reward.earned": {
      "post": {
        "tags": [
          "Shop delivery webhooks"
        ],
        "summary": "A member earned a reward delivered by your API",
        "description": "Sent when a member earns a reward that is set to be delivered by your API (for example for reaching a level). Nothing was bought: `price_paid` is `0` and `reward` says what earned it. Match on `item.item_key` as you would for a purchase, and answer in the same way. Nothing was paid, so a refused delivery marks the reward failed and refunds nothing.\n\nThe request, its signature and how CEEJAY reads your answer are described on the **Shop delivery webhooks** tag.",
        "operationId": "receiveShopDeliveryReward",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShopDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/ShopDeliverySignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ShopDeliveryPayload"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "reward.earned"
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A member earned a reward delivered by your API",
                  "value": {
                    "event": "reward.earned",
                    "delivery_id": "shop_4243",
                    "purchase_id": 4243,
                    "item": {
                      "id": 7,
                      "name": "Gold skin",
                      "item_key": "skin_gold"
                    },
                    "member": {
                      "discord_id": "409876543210987654",
                      "username": "nova",
                      "display_name": "Nova",
                      "connections": {
                        "steam": "76561198000000000",
                        "xbox": null,
                        "playstation": null,
                        "epic": null,
                        "riot": null,
                        "roblox": null,
                        "battlenet": null,
                        "github": null,
                        "twitch": null
                      }
                    },
                    "price_paid": 0,
                    "currency": "Coins",
                    "server_id": "721104938271003842",
                    "created_at": "2026-10-01T09:14:22.000Z",
                    "reward": {
                      "id": 9,
                      "source_kind": "level",
                      "source_id": "12",
                      "source_label": "Level 12"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "408": {
            "description": "Retried later."
          },
          "429": {
            "description": "Retried later."
          },
          "2XX": {
            "description": "Delivered (an empty body, or `{ \"status\": \"delivered\" }`), pending (status `202` or `{ \"status\": \"pending\" }`) or failed (`{ \"status\": \"failed\", \"message\": \"…\" }`). Any other body counts as delivered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShopDeliveryAnswer"
                },
                "examples": {
                  "delivered": {
                    "summary": "Delivered",
                    "value": {
                      "status": "delivered"
                    }
                  },
                  "pending": {
                    "summary": "You will finish later",
                    "value": {
                      "status": "pending"
                    }
                  },
                  "failed": {
                    "summary": "You cannot deliver",
                    "value": {
                      "status": "failed",
                      "message": "The player has no account on this realm"
                    }
                  }
                }
              }
            }
          },
          "3XX": {
            "description": "Refused. Redirects are not followed. The member is refunded."
          },
          "4XX": {
            "description": "Refused, and the member is refunded. Status `408` and `429` are retried instead."
          },
          "5XX": {
            "description": "Retried later: about every hour, with the same `delivery_id`, until 3 attempts have failed in total. Then the member is refunded."
          }
        }
      }
    },
    "shop.test": {
      "post": {
        "tags": [
          "Shop delivery webhooks"
        ],
        "summary": "A test delivery",
        "description": "Sent when someone on your team presses Send test delivery on the item in the dashboard. `delivery_id` and `purchase_id` start with `test_` and `member` is the team member who pressed the button. Answer with any 2xx status and unlock nothing.\n\nThe request, its signature and how CEEJAY reads your answer are described on the **Shop delivery webhooks** tag.",
        "operationId": "receiveShopDeliveryTest",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShopDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/ShopDeliverySignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ShopDeliveryPayload"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "shop.test"
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A test delivery",
                  "value": {
                    "event": "shop.test",
                    "delivery_id": "test_3f9a1c7e5b2d4a60",
                    "purchase_id": "test_3f9a1c7e5b2d4a60",
                    "item": {
                      "id": 7,
                      "name": "Gold skin",
                      "item_key": "skin_gold"
                    },
                    "member": {
                      "discord_id": "409876543210987654",
                      "username": "nova",
                      "display_name": "Nova",
                      "connections": {
                        "steam": "76561198000000000",
                        "xbox": null,
                        "playstation": null,
                        "epic": null,
                        "riot": null,
                        "roblox": null,
                        "battlenet": null,
                        "github": null,
                        "twitch": null
                      }
                    },
                    "price_paid": 0,
                    "currency": "Coins",
                    "server_id": "721104938271003842",
                    "created_at": "2026-10-01T09:14:22.000Z"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "408": {
            "description": "Retried later."
          },
          "429": {
            "description": "Retried later."
          },
          "2XX": {
            "description": "Delivered (an empty body, or `{ \"status\": \"delivered\" }`), pending (status `202` or `{ \"status\": \"pending\" }`) or failed (`{ \"status\": \"failed\", \"message\": \"…\" }`). Any other body counts as delivered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShopDeliveryAnswer"
                },
                "examples": {
                  "delivered": {
                    "summary": "Delivered",
                    "value": {
                      "status": "delivered"
                    }
                  },
                  "pending": {
                    "summary": "You will finish later",
                    "value": {
                      "status": "pending"
                    }
                  },
                  "failed": {
                    "summary": "You cannot deliver",
                    "value": {
                      "status": "failed",
                      "message": "The player has no account on this realm"
                    }
                  }
                }
              }
            }
          },
          "3XX": {
            "description": "Refused. Redirects are not followed. The member is refunded."
          },
          "4XX": {
            "description": "Refused, and the member is refunded. Status `408` and `429` are retried instead."
          },
          "5XX": {
            "description": "Retried later: about every hour, with the same `delivery_id`, until 3 attempts have failed in total. Then the member is refunded."
          }
        }
      }
    },
    "player.connected": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A player was connected to a member",
        "description": "Sent when a player of a game is connected to a Discord member in your community: a new connection, a connection made again, or one moved to another player. It is the main way for your server to learn the result of a connect session; Get a connect session and List connect sessions are the fallback.\n\nA player the member only stated, at the `basic` level, is announced too: read `details.proof` before you trust the ID.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, which has to be switched on in this CEEJAY. An action that is added more than 10 minutes after it happened sends nothing. CEEJAY writes the line when its bot works through the connection, so `occurredAt` is the time of that work and the time of the connection is `details.connectedAt`. `data.discordId` is the member, beside `userId`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receivePlayerConnected",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "player.connected"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "required": [
                              "details"
                            ],
                            "properties": {
                              "app": {
                                "const": "connect"
                              },
                              "action": {
                                "const": "game_connected"
                              },
                              "targetType": {
                                "const": "game"
                              },
                              "targetId": {
                                "description": "The game's slug, for example `rugby_manager`."
                              },
                              "quantity": {
                                "type": "null"
                              },
                              "details": {
                                "type": "object",
                                "description": "What the connection was.",
                                "required": [
                                  "playerId",
                                  "proof",
                                  "origin",
                                  "connectedAt"
                                ],
                                "properties": {
                                  "playerId": {
                                    "type": "string",
                                    "description": "The game's own ID for the player, as it is stored."
                                  },
                                  "proof": {
                                    "type": "string",
                                    "enum": [
                                      "basic",
                                      "verified"
                                    ],
                                    "description": "The level of the connection. `verified`: the player was proven. `basic`: the game only stated the ID."
                                  },
                                  "origin": {
                                    "type": "string",
                                    "enum": [
                                      "connect",
                                      "api"
                                    ],
                                    "description": "`connect`: the member made the connection through Connect. `api`: your own server linked the player."
                                  },
                                  "connectedAt": {
                                    "type": "string",
                                    "format": "date-time",
                                    "description": "When the connection was made."
                                  },
                                  "previousPlayerId": {
                                    "type": "string",
                                    "description": "The player the member was connected to before, when the connection was moved. Only when both the old and the new connection were verified."
                                  }
                                }
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A player was connected to a member",
                  "value": {
                    "event": "player.connected",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "connect",
                      "action": "game_connected",
                      "targetType": "game",
                      "targetId": "rugby_manager",
                      "targetName": null,
                      "itemId": null,
                      "quantity": null,
                      "occurredAt": "2026-10-08T18:06:41.002Z",
                      "details": {
                        "playerId": "p_19",
                        "proof": "verified",
                        "origin": "connect",
                        "connectedAt": "2026-10-08T18:06:40.000Z"
                      },
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-08T18:06:41.318Z",
                    "id": "evt_8a2f0c7e9a3d4e2f8c6a1b0d9e8f7b11"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    },
    "player.disconnected": {
      "post": {
        "tags": [
          "Webhook events"
        ],
        "summary": "A player was disconnected from a member",
        "description": "Sent when the connection of a player ends, whoever ended it: the member, the community's team, your server (Disconnect a player or Delete a player), a ban, the member leaving the server for 30 days, an erasure of the member's data, or the game being removed.\n\nThis is a member-action event. It is sent when the action is added to the member's activity history, which has to be switched on in this CEEJAY. An action that is added more than 10 minutes after it happened sends nothing. CEEJAY writes the line when its bot works through the connection, so `occurredAt` is the time of that work and the time of the connection is `details.connectedAt`. `data.discordId` is the member, beside `userId`.\n\nDelivery, signing and retries are described in the introduction of this document and on the **Webhook events** tag.",
        "operationId": "receivePlayerDisconnected",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/WebhookEventHeader"
          },
          {
            "$ref": "#/components/parameters/WebhookDeliveryIdHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryAttemptHeader"
          },
          {
            "$ref": "#/components/parameters/DeliveryTimestampHeader"
          },
          {
            "$ref": "#/components/parameters/DeliverySignatureV2Header"
          },
          {
            "$ref": "#/components/parameters/WebhookSignatureHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/WebhookEnvelope"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "event": {
                        "const": "player.disconnected"
                      },
                      "data": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/MemberActionData"
                          },
                          {
                            "type": "object",
                            "required": [
                              "details"
                            ],
                            "properties": {
                              "app": {
                                "const": "connect"
                              },
                              "action": {
                                "const": "game_disconnected"
                              },
                              "targetType": {
                                "const": "game"
                              },
                              "targetId": {
                                "description": "The game's slug, for example `rugby_manager`."
                              },
                              "quantity": {
                                "type": "null"
                              },
                              "details": {
                                "type": "object",
                                "description": "What the connection was.",
                                "required": [
                                  "playerId",
                                  "proof",
                                  "origin",
                                  "connectedAt"
                                ],
                                "properties": {
                                  "playerId": {
                                    "type": "string",
                                    "description": "The game's own ID for the player, as it is stored."
                                  },
                                  "proof": {
                                    "type": "string",
                                    "enum": [
                                      "basic",
                                      "verified"
                                    ],
                                    "description": "The level of the connection. `verified`: the player was proven. `basic`: the game only stated the ID."
                                  },
                                  "origin": {
                                    "type": "string",
                                    "enum": [
                                      "connect",
                                      "api"
                                    ],
                                    "description": "`connect`: the member made the connection through Connect. `api`: your own server linked the player."
                                  },
                                  "connectedAt": {
                                    "type": "string",
                                    "format": "date-time",
                                    "description": "When the connection was made."
                                  },
                                  "reason": {
                                    "type": "string",
                                    "description": "Why it ended: `member`, `team`, `studio`, `banned`, `moved`, `superseded`, `left`, `under_age`, `erased` or `game_removed`. Left out when none was kept. Treat the list as open."
                                  }
                                }
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                ]
              },
              "examples": {
                "delivery": {
                  "summary": "A player was disconnected from a member",
                  "value": {
                    "event": "player.disconnected",
                    "data": {
                      "userId": "409876543210987654",
                      "app": "connect",
                      "action": "game_disconnected",
                      "targetType": "game",
                      "targetId": "rugby_manager",
                      "targetName": null,
                      "itemId": null,
                      "quantity": null,
                      "occurredAt": "2026-10-08T18:06:41.002Z",
                      "details": {
                        "playerId": "p_19",
                        "proof": "verified",
                        "origin": "connect",
                        "connectedAt": "2026-10-08T18:06:40.000Z",
                        "reason": "member"
                      },
                      "discordId": "409876543210987654"
                    },
                    "serverId": "721104938271003842",
                    "timestamp": "2026-10-08T18:06:41.318Z",
                    "id": "evt_8a2f0c7e9a3d4e2f8c6a1b0d9e8f7b12"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge the delivery with any 2xx status within 5 seconds. The body is ignored."
          },
          "default": {
            "description": "Any other status, a redirect (redirects are not followed) or no answer within 5 seconds counts as a failed delivery. It is tried again later."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ceejay_sk_<64 hexadecimal characters>",
        "x-scopes": {
          "*": "Every operation except those that need `players:connect` or `players:delete` (Full Access in the dashboard). Avoid it in production.",
          "members:read": "Read member profiles, XP, balances and linked accounts.",
          "members:link": "Link an account on another platform to a Discord user.",
          "xp:read": "Read a member's XP and level.",
          "xp:grant": "Grant XP.",
          "xp:deduct": "Deduct XP.",
          "xp:program:grant": "Grant XP inside a community program.",
          "xp:program:deduct": "Deduct XP inside a community program.",
          "programs:read": "List community programs.",
          "economy:read": "Read currency balances.",
          "economy:grant": "Grant currency.",
          "economy:deduct": "Deduct currency.",
          "quests:read": "List quests.",
          "quests:complete": "Complete a quest and grant its rewards, or report that a member finished a quest task.",
          "roles:manage": "List the roles of your server, and grant and revoke them.",
          "messages:send": "List the channels of your server, and post messages and embeds to them.",
          "events:emit": "Send custom events to your webhook subscribers.",
          "shop:fulfil": "List the shop deliveries awaiting fulfilment by your server, and confirm or fail them.",
          "players:read": "Read the players of a game and its field map: Get a player, List players and Get the field map. It also decides whether Get a member lists the game accounts a member connected through Connect.",
          "players:write": "Send player data: Set player fields, Log player events and Propose to the field map.",
          "players:connect": "Act for a player: make a connect pass, start, read and confirm connect sessions, disconnect a player, and flag a player as under age in Set player fields and Log player events. A `*` key does not include it.",
          "players:delete": "Delete the data CEEJAY stores for a player (Delete a player). A `*` key does not include it."
        },
        "description": "A CEEJAY API key, sent as `Authorization: Bearer ceejay_sk_…`. Write `Bearer` with a capital B, then one space, then the key.\n\nCreate a key in the dashboard under **Settings → Developers → API Keys** with **Generate Key**. Name it after where it will run and choose only the scopes it needs. The whole key is shown once: copy it then, because CEEJAY stores only a hash and cannot show it again. A key belongs to one Discord server.\n\nEach operation names the scope it needs. A key with the `*` scope (**Full Access**) can call every operation except the ones that need `players:connect` or `players:delete` (the Connect requests of your server, Disconnect a player and Delete a player): those two are never part of `*`, and a key has them only when they are ticked by name. Community details (`GET /community`) needs no scope: any valid key can call it. A key that is missing, malformed, unknown, revoked or expired gets `401`, with a `code` that says which (`missing_api_key`, `invalid_api_key`, `api_key_revoked`, `api_key_expired`), and a key without the scope gets `403` with the code `insufficient_scope`. An organisation's key, created on the **API keys** tab of the organisation's settings, covers several communities. Set the `CEEJAY-Community` header to the Discord server ID of the community each request is for. Without it the request returns `400` with the code `community_required`, and a community the key does not cover returns `403` with the code `community_not_covered`. From there on the request runs as the community's own key would: the community's rate limit for the key, its request log and its retry keys."
      }
    },
    "parameters": {
      "DiscordIdQuery": {
        "name": "discordId",
        "in": "query",
        "required": true,
        "description": "The member's Discord ID: a string of 17 to 20 digits.",
        "schema": {
          "$ref": "#/components/schemas/Snowflake"
        },
        "example": "409876543210987654"
      },
      "DiscordIdPath": {
        "name": "discordId",
        "in": "path",
        "required": true,
        "description": "The member's Discord ID: a string of 17 to 20 digits.",
        "schema": {
          "$ref": "#/components/schemas/Snowflake"
        },
        "example": "409876543210987654"
      },
      "PurchaseIdPath": {
        "name": "purchaseId",
        "in": "path",
        "required": true,
        "description": "The purchase's ID, as a whole number from 1 to 2,147,483,647, written without leading zeros. It is the `purchase_id` CEEJAY sent in the delivery request.",
        "schema": {
          "type": "integer",
          "format": "int32",
          "minimum": 1,
          "maximum": 2147483647
        },
        "example": 4242
      },
      "WebhookEventHeader": {
        "name": "X-CEEJAY-Event",
        "in": "header",
        "required": true,
        "description": "The event name, the same as `event` in the body.",
        "schema": {
          "type": "string"
        },
        "example": "member.joined"
      },
      "WebhookSignatureHeader": {
        "name": "X-CEEJAY-Signature",
        "in": "header",
        "required": true,
        "description": "`sha256=` followed by the HMAC-SHA256 of the raw request body, as lowercase hex, keyed with the subscription's signing secret.",
        "schema": {
          "type": "string",
          "pattern": "^sha256=[0-9a-f]{64}$"
        },
        "example": "sha256=9f2b1c4e7a8d0b3c5e6f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d"
      },
      "ShopDeliveryIdHeader": {
        "name": "X-CEEJAY-Delivery",
        "in": "header",
        "required": true,
        "description": "The delivery's ID, the same as `delivery_id` in the body: `shop_<purchase id>`, or `test_<hex>` for a test. The same on every retry.",
        "schema": {
          "type": "string"
        },
        "example": "shop_4242"
      },
      "ShopDeliverySignatureHeader": {
        "name": "X-CEEJAY-Signature",
        "in": "header",
        "required": true,
        "description": "`t=<unix seconds>,v1=<hex>`. `v1` is the HMAC-SHA256, as lowercase hex, of the text `<t>.<raw request body>`, keyed with the item's signing secret.",
        "schema": {
          "type": "string",
          "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
        },
        "example": "t=1790000000,v1=9f2b1c4e7a8d0b3c5e6f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d"
      },
      "WebhookDeliveryIdHeader": {
        "name": "X-CEEJAY-Delivery",
        "in": "header",
        "required": true,
        "description": "The delivery's ID, the same as `id` in the body. A retry of the same delivery carries the same ID.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9_-]{1,80}$"
        },
        "example": "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a60"
      },
      "DeliveryAttemptHeader": {
        "name": "X-CEEJAY-Attempt",
        "in": "header",
        "required": true,
        "description": "Which attempt of the delivery this is: `1` for the first, then `2`, `3` and so on. Digits, with no zeros in front.",
        "schema": {
          "type": "string",
          "pattern": "^[1-9][0-9]{0,8}$"
        },
        "example": "1"
      },
      "DeliveryTimestampHeader": {
        "name": "X-CEEJAY-Timestamp",
        "in": "header",
        "required": true,
        "description": "The Unix time, in seconds, at which this attempt was signed. Digits, with no zeros in front. Refuse a delivery whose time is more than 300 seconds from your own clock.",
        "schema": {
          "type": "string",
          "pattern": "^[1-9][0-9]{0,11}$"
        },
        "example": "1791364462"
      },
      "DeliverySignatureV2Header": {
        "name": "X-CEEJAY-Signature-V2",
        "in": "header",
        "required": true,
        "description": "Signature version 2: the HMAC-SHA256, as 64 lower-case hexadecimal characters, of the text `v2.<X-CEEJAY-Timestamp>.<X-CEEJAY-Delivery>.<X-CEEJAY-Attempt>.<raw request body>`, keyed with the signing secret used as text.",
        "schema": {
          "type": "string",
          "pattern": "^[0-9a-f]{64}$"
        },
        "example": "1a3c64a6265c1984c3e1efbb3aca6fe338803b2d8f38f29f187103f030233529"
      },
      "CursorQuery": {
        "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 empty value counts as omitted. An invalid cursor is rejected with `400`.",
        "schema": {
          "type": "string",
          "maxLength": 512
        },
        "example": "WzQyXQ"
      },
      "GamePath": {
        "name": "game",
        "in": "path",
        "required": true,
        "description": "The game's slug: 2 to 32 lower-case letters, digits and underscores, starting with a letter. It is the name the game was set up under in the community, and the `platform` its players' links are stored under.",
        "schema": {
          "type": "string",
          "pattern": "^[a-z][a-z0-9_]{1,31}$"
        },
        "example": "rugby_manager"
      },
      "ConnectSessionIdPath": {
        "name": "sessionId",
        "in": "path",
        "required": true,
        "description": "The session's ID: the `sessionId` that Create a connect session returned, 22 characters. An ID of another form is not found.",
        "schema": {
          "type": "string"
        },
        "example": "kT3v9QmZ1xY7bLw2RfA8cN"
      },
      "PlayerIdPath": {
        "name": "playerId",
        "in": "path",
        "required": true,
        "description": "The game's own ID for the player: 1 to 128 characters, starting with a letter or a digit, then letters, digits and `. _ : @ | + = -`. It is sent in the URL path, so an ID that contains another character should be encoded the same way every time, for example as base64url. Matched case-insensitively.",
        "schema": {
          "$ref": "#/components/schemas/PlayerId"
        },
        "example": "p_19"
      }
    },
    "headers": {
      "RetryAfter": {
        "description": "Whole seconds to wait before retrying the request.",
        "schema": {
          "type": "integer",
          "minimum": 0
        },
        "example": 34
      },
      "RateLimitLimit": {
        "description": "The key's requests per minute. A key's limit is 60 unless a higher one was set on it, and at most 600.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 600
        },
        "example": 60
      },
      "RateLimitRemaining": {
        "description": "Requests the key has left in the current one-minute window. Never below 0.",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "maximum": 600
        },
        "example": 57
      },
      "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.",
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        },
        "example": "true"
      },
      "ConnectRateLimitLimit": {
        "description": "On a Connect operation: `600`, the requests a minute for the game, once the request has resolved its game. On a response returned before that (a missing scope, an instance without Connect, a game that is not found or not allowed, a wrong method) it is `6000`, the limit for one key across all its games.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 6000
        },
        "example": 600
      },
      "ConnectRateLimitRemaining": {
        "description": "Requests left in the current one-minute window of the limit `X-RateLimit-Limit` names. Never below 0.",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "maximum": 6000
        },
        "example": 594
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "The key is missing, not in the right format, unknown, revoked or expired. `code` says which. No key is known, so this response carries no rate limit headers and is not counted against any key.",
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/RequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "missingHeader": {
                "summary": "No bearer token",
                "value": {
                  "error": "Missing or invalid Authorization header. Expected: Bearer ceejay_sk_xxx",
                  "code": "missing_api_key",
                  "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                }
              },
              "badFormat": {
                "summary": "Not a CEEJAY key",
                "value": {
                  "error": "Invalid API key format. Expected: ceejay_sk_xxx",
                  "code": "invalid_api_key",
                  "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                }
              },
              "unknownKey": {
                "summary": "Unknown key",
                "value": {
                  "error": "Invalid API key",
                  "code": "invalid_api_key",
                  "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                }
              },
              "revoked": {
                "summary": "Revoked key",
                "value": {
                  "error": "API key has been revoked",
                  "code": "api_key_revoked",
                  "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                }
              },
              "expired": {
                "summary": "Expired key",
                "value": {
                  "error": "API key has expired",
                  "code": "api_key_expired",
                  "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                }
              }
            }
          }
        }
      },
      "InsufficientScopes": {
        "description": "The key does not have the scope this operation needs, and is not a `*` key. The body lists what was needed and what the key has. The request still counts toward the key's rate limit.",
        "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/ScopeErrorResponse"
            },
            "examples": {
              "missingScope": {
                "summary": "Key without the scope",
                "value": {
                  "error": "Insufficient scopes",
                  "code": "insufficient_scope",
                  "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                  "required": [
                    "economy:grant"
                  ],
                  "granted": [
                    "xp:grant",
                    "xp:read"
                  ]
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "The key has used up its requests for this minute. `X-RateLimit-Remaining` is `0`, and `Retry-After`, `X-RateLimit-Reset` and `retryAfter` all say how many seconds are left. The limit is checked before the key's scopes, so a key that is over its limit gets this response from every operation.",
        "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/RateLimitErrorResponse"
            },
            "examples": {
              "limited": {
                "summary": "Too many requests",
                "value": {
                  "error": "Rate limit exceeded",
                  "code": "rate_limited",
                  "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                  "retryAfter": 34
                }
              }
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "The operation does not accept this HTTP method. The method is checked after the key, so a wrong method with a bad key returns `401`.",
        "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"
                }
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Something failed on CEEJAY's side. Retry with backoff. In the rare case where the key itself could not be looked up, the rate limit headers are missing.",
        "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"
                }
              }
            }
          }
        }
      },
      "MemberNotFound": {
        "description": "The Discord ID is not a member of your server. CEEJAY has no profile for it, and Discord says it is not in the server. Nothing was changed. A member who has only just joined can take up to 15 seconds to be found. Wait 15 seconds before retrying.",
        "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": {
              "notAMember": {
                "summary": "Not a member of your server",
                "value": {
                  "error": "Member not found",
                  "code": "member_not_found",
                  "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                }
              }
            }
          }
        }
      },
      "NotAvailable": {
        "description": "This instance does not support what the operation needs yet. `feature` names what is missing, under the name Community details reports it (`GET /community`). Nothing was changed, and waiting does not change the 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"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/NotAvailableErrorResponse"
            },
            "examples": {
              "notAvailable": {
                "summary": "Not available on this instance yet",
                "value": {
                  "error": "This is not available on this instance yet.",
                  "code": "not_available",
                  "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
                  "feature": "economy"
                }
              }
            }
          }
        }
      },
      "IdempotencyConflict": {
        "description": "The `idempotencyKey` was already used by this API key for a different request (`idempotency_key_reused`: another operation, or other values), or the first request with it is still being processed (`idempotency_in_progress`: wait `retryAfter` seconds and retry). Nothing was changed by this call.",
        "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": {
              "keyReused": {
                "summary": "The key was already used for a different 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
                }
              }
            }
          }
        }
      },
      "IdempotencyConflictUnknown": {
        "description": "The `idempotencyKey` was already used by this API key for a different request (`idempotency_key_reused`: another operation, or other values), or the first request with it is still being processed (`idempotency_in_progress`: wait `retryAfter` seconds and retry). Nothing was changed by this call. When the first request was started more than a minute ago and never finished, it is not known whether it took effect: the response is `idempotency_outcome_unknown`, and CEEJAY does not repeat it. Check what happened, then use a new key if it has to be sent 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"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "keyReused": {
                "summary": "The key was already used for a different 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
                }
              },
              "keyUnknown": {
                "summary": "Nobody can say whether the first request was 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"
                }
              }
            }
          }
        }
      },
      "DiscordUnavailable": {
        "description": "CEEJAY has no profile for this Discord ID and could not confirm with Discord whether it is a member of your server: Discord did not respond, is rate limiting the bot, or returned something that does not say whether the ID is a member. Nothing was changed. Retry shortly. If the error persists, CEEJAY's bot may no longer be in your server, and retrying will not fix that.",
        "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": {
              "discordUnavailable": {
                "summary": "Discord could not be asked",
                "value": {
                  "error": "Could not check with Discord that this ID is a member of the server, so nothing was changed",
                  "code": "discord_unavailable",
                  "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                }
              }
            }
          }
        }
      },
      "IdempotencyUnavailable": {
        "description": "You sent an `idempotencyKey`, and this instance does not support idempotency keys yet (`idempotency_unavailable`). Nothing was changed. Waiting does not change this response: contact support to finish the update, or send the request without a key.",
        "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": {
              "keysUnavailable": {
                "summary": "Keys cannot be stored yet",
                "value": {
                  "error": "idempotencyKey cannot be used on this community yet, so nothing was changed",
                  "code": "idempotency_unavailable",
                  "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                }
              }
            }
          }
        }
      },
      "ConnectMethodNotAllowed": {
        "description": "The operation does not accept this HTTP method. The method is checked after the key and before the game, so the limit headers report the key's stop of 6,000.",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/ConnectRateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/ConnectRateLimitRemaining"
          },
          "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"
                }
              }
            }
          }
        }
      },
      "ConnectRateLimited": {
        "description": "More than 600 Connect requests in a minute for this game, or more than 6,000 in a minute with this key. `Retry-After`, `X-RateLimit-Reset` and `retryAfter` say how many seconds are left. The key's own requests per minute are not used up by these operations.",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/ConnectRateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/ConnectRateLimitRemaining"
          },
          "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
                }
              }
            }
          }
        }
      },
      "ConnectInternalError": {
        "description": "Something failed on CEEJAY's side. Retry with backoff.",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/ConnectRateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/ConnectRateLimitRemaining"
          },
          "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"
                }
              }
            }
          }
        }
      },
      "CommunityRequired": {
        "description": "An organisation's key was sent without the community the request is for. Name it in the `CEEJAY-Community` header, as the community's Discord server ID. The call counts toward the key's rate limit.",
        "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": {
              "communityRequired": {
                "summary": "Organisation's key without a community",
                "value": {
                  "error": "This is an organisation’s key: name the community in the CEEJAY-Community header",
                  "code": "community_required",
                  "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                }
              }
            }
          }
        }
      },
      "CommunityNotCovered": {
        "description": "An organisation's key named a community it does not cover: the community is not in the organisation, is not covered by the key, or has turned off the organisation's keys. The message is the same in all three cases. The call counts toward the key's rate limit.",
        "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": {
              "communityNotCovered": {
                "summary": "A community the key does not cover",
                "value": {
                  "error": "This key doesn’t cover that community",
                  "code": "community_not_covered",
                  "requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2"
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Snowflake": {
        "type": "string",
        "pattern": "^\\d{17,20}$",
        "description": "A Discord ID (a snowflake): a string of 17 to 20 digits. Always send and read it as a string. It cannot be represented exactly as a JavaScript number, so a value that is not a string, such as a JSON number or a list, is rejected.",
        "examples": [
          "409876543210987654"
        ]
      },
      "IdempotencyKey": {
        "type": "string",
        "minLength": 1,
        "maxLength": 128,
        "description": "An optional key that makes a retry safe: 1 to 128 characters, chosen by you, 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`. So is a key that is not well-formed Unicode: an unpaired surrogate, such as the JSON `\"\\ud800\"`. A key belongs to the API key that sent it: another API key of the same community can send the same text, and it is a different key. When the same request is sent again with its key, nothing happens a second time and the response is the original one, with `duplicate: true` and the header `Idempotent-Replayed: true`. Each key is tied to one request payload across every operation: the same key on another operation, or with other values, returns `409` with the code `idempotency_key_reused`, and nothing is changed. A key is stored only when its request was applied, so a rejected request can be retried with the same key once the cause is fixed. Keys are retained for 30 days.",
        "examples": [
          "tournament-2026-40-winner"
        ]
      },
      "SuccessEnvelope": {
        "type": "object",
        "description": "The wrapper around every successful response. `data` holds the result: an object, or a list on the list operations, which add `page` beside it. Three operations keep the fields they had beside `success` before this wrapper was fixed, and carry `data` as well: List quests (`total`, `limit`, `offset`), Complete a quest task (`matched`, `completed`) and Complete a shop delivery (`purchaseId`, `status`, `alreadyDone`, `refunded`).",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "description": "The result of the call."
          },
          "page": {
            "$ref": "#/components/schemas/Page",
            "description": "Only on a list."
          }
        }
      },
      "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 fields, described on the operations that return them.",
        "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 invalid request field, under the name you sent. With `bad_request` when one field is at fault, and with `bad_player_id` on Link an account."
          },
          "retryAfter": {
            "type": "integer",
            "minimum": 0,
            "description": "Whole seconds to wait before retrying the request. Only with `rate_limited`, `idempotency_in_progress` and, when Discord gave a wait, `discord_rate_limited`. The `Retry-After` header contains the same number."
          }
        }
      },
      "ErrorWithCodeResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ErrorResponse"
          }
        ],
        "description": "The same as ErrorResponse. Every error has carried a `code` since version 1.0.0, so this older name says nothing more."
      },
      "ScopeErrorResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ErrorResponse"
          },
          {
            "type": "object",
            "required": [
              "required",
              "granted"
            ],
            "properties": {
              "required": {
                "type": "array",
                "description": "The scopes the operation needs.",
                "items": {
                  "type": "string"
                }
              },
              "granted": {
                "type": "array",
                "description": "The scopes the key has.",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        ],
        "description": "The body of a `403` with the code `insufficient_scope`: the key lacks the scope an operation needs."
      },
      "RateLimitErrorResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ErrorResponse"
          },
          {
            "type": "object",
            "required": [
              "retryAfter"
            ]
          }
        ],
        "description": "The body of a `429` with the code `rate_limited`: the key sent too many requests. `retryAfter` is the seconds until the key may send requests again."
      },
      "DiscordErrorResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ErrorResponse"
          },
          {
            "type": "object",
            "properties": {
              "details": {
                "type": "string",
                "description": "Discord's reply, as text. Usually a JSON object with a `message` and a `code`."
              }
            }
          }
        ],
        "description": "The body of an error that came from Discord and is passed on with Discord's own status and the code `discord_error`. `error` is `Discord API error: <status>`."
      },
      "EconomyBalanceResult": {
        "type": "object",
        "description": "A member's balance.",
        "required": [
          "discordId",
          "balance",
          "totalEarned",
          "totalSpent",
          "lastUpdated"
        ],
        "properties": {
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member."
          },
          "balance": {
            "type": "integer",
            "description": "The current balance."
          },
          "totalEarned": {
            "type": "integer",
            "description": "The total ever credited to the member."
          },
          "totalSpent": {
            "type": "integer",
            "description": "The total the member has ever spent in the shop."
          },
          "lastUpdated": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the balance last changed. `null` when the member has no balance yet."
          }
        }
      },
      "EconomyBalanceResponse": {
        "type": "object",
        "description": "A member's balance.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/EconomyBalanceResult"
          }
        }
      },
      "DeductCurrencyRequest": {
        "type": "object",
        "description": "What to take from whom.",
        "required": [
          "discordId",
          "amount"
        ],
        "properties": {
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member to take currency from."
          },
          "amount": {
            "type": "integer",
            "description": "How much to deduct: a whole number from 1 to 2,147,483,647. The balance cannot go below zero.",
            "format": "int32",
            "minimum": 1,
            "maximum": 2147483647
          },
          "reason": {
            "type": "string",
            "x-also-accepted": [
              "description"
            ],
            "description": "A note shown in the member's transaction history, as a string. Defaults to `API deduction`. Any other value, such as a number or a list, is refused with `400`, and so is a null character (U+0000) in it. `null` counts as left out. Deprecated alias, still accepted: `description`. If both are sent, the deprecated one is used."
          },
          "idempotencyKey": {
            "$ref": "#/components/schemas/IdempotencyKey",
            "x-also-accepted": [
              "idempotency_key"
            ],
            "description": "Makes a retry safe: see Retries in the description of this operation. Deprecated alias, still accepted: `idempotency_key`. If both are sent, the deprecated one is used."
          }
        }
      },
      "DeductCurrencyResult": {
        "type": "object",
        "description": "What was deducted.",
        "required": [
          "discordId",
          "amountDeducted",
          "duplicate",
          "balance",
          "description",
          "reason"
        ],
        "properties": {
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member."
          },
          "amountDeducted": {
            "type": "integer",
            "description": "How much was taken."
          },
          "duplicate": {
            "type": "boolean",
            "description": "`true` when this response repeats an earlier request that sent the same `idempotencyKey`: nothing was changed this time. `false` otherwise, and always `false` without a key."
          },
          "balance": {
            "type": "integer",
            "description": "The balance right after the deduction. On a repeat it is the balance right after the first deduction, not the balance now."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "The note the deduction was made with, or `null` when there was none."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "The note the deduction was made with, or `null`. The same value as `reason`, under its deprecated name. Still sent.",
            "deprecated": true
          }
        }
      },
      "DeductCurrencyResponse": {
        "type": "object",
        "description": "What was deducted.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/DeductCurrencyResult"
          }
        }
      },
      "DeductCurrencyErrorResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ErrorResponse"
          },
          {
            "type": "object",
            "properties": {
              "balance": {
                "type": "integer",
                "description": "The member's balance. Only with `insufficient_balance`."
              },
              "currentBalance": {
                "type": "integer",
                "description": "The member's balance. Only with `insufficient_balance`. The same value as `balance`, under its deprecated name. Still sent.",
                "deprecated": true
              },
              "requested": {
                "type": "integer",
                "description": "The amount you asked for. Only with `insufficient_balance`."
              }
            }
          }
        ],
        "description": "The body of a `400` from Deduct currency. `balance`, `currentBalance` and `requested` are only present with the code `insufficient_balance`."
      },
      "GrantCurrencyRequest": {
        "type": "object",
        "description": "What to give to whom.",
        "required": [
          "discordId",
          "amount"
        ],
        "properties": {
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member to credit. An ID CEEJAY has no profile for must be a member of your server in Discord."
          },
          "amount": {
            "type": "integer",
            "description": "How much to grant: a whole number from 1 to 1,000,000.",
            "format": "int32",
            "minimum": 1,
            "maximum": 1000000
          },
          "reason": {
            "type": "string",
            "x-also-accepted": [
              "description"
            ],
            "description": "A note shown in the member's transaction history, as a string. Defaults to `API grant`. Any other value, such as a number or a list, is refused with `400`, and so is a null character (U+0000) in it. `null` counts as left out. Deprecated alias, still accepted: `description`. If both are sent, the deprecated one is used."
          },
          "idempotencyKey": {
            "$ref": "#/components/schemas/IdempotencyKey",
            "x-also-accepted": [
              "idempotency_key"
            ],
            "description": "Makes a retry safe: see Retries in the description of this operation. Deprecated alias, still accepted: `idempotency_key`. If both are sent, the deprecated one is used."
          }
        }
      },
      "GrantCurrencyResult": {
        "type": "object",
        "description": "What was credited.",
        "required": [
          "discordId",
          "amountGranted",
          "amountRequested",
          "capped",
          "duplicate",
          "balance",
          "totalEarned",
          "description",
          "reason"
        ],
        "properties": {
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member."
          },
          "amountGranted": {
            "type": "integer",
            "description": "What was actually added. It is less than `amountRequested` when the balance hit the community's maximum, and can be `0`."
          },
          "amountRequested": {
            "type": "integer",
            "description": "The amount you sent."
          },
          "capped": {
            "type": "boolean",
            "description": "`true` when the community's maximum balance cut the credit short."
          },
          "duplicate": {
            "type": "boolean",
            "description": "`true` when this response repeats an earlier request that sent the same `idempotencyKey`: nothing was changed this time. `false` otherwise, and always `false` without a key."
          },
          "balance": {
            "type": "integer",
            "description": "The balance right after the credit. On a repeat it is the balance right after the first credit, not the balance now."
          },
          "totalEarned": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Everything the member has been credited, over time, including this credit."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "The note the grant was made with, or `null` when there was none."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "The note the grant was made with, or `null`. The same value as `reason`, under its deprecated name. Still sent.",
            "deprecated": true
          }
        }
      },
      "GrantCurrencyResponse": {
        "type": "object",
        "description": "What was credited.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/GrantCurrencyResult"
          }
        }
      },
      "EmitEventRequest": {
        "type": "object",
        "description": "The event to send.",
        "required": [
          "event"
        ],
        "properties": {
          "event": {
            "type": "string",
            "description": "The event name: 1 to 93 letters, digits, `.`, `_`, `:` or `-`, with or without `custom.` in front. Subscribers receive it as `custom.<event>` (a name that already starts with `custom.` is used as it is), and that delivered name is at most 100 characters. `custom.` with nothing after it is not a name. Any other character is refused, because the name also travels in the `X-CEEJAY-Event` header.",
            "pattern": "^(custom\\.)?[A-Za-z0-9._:-]{1,93}$",
            "minLength": 1,
            "maxLength": 100,
            "examples": [
              "purchase.completed"
            ]
          },
          "data": {
            "description": "Anything you want subscribers to receive, as any JSON value (usually an object). Delivered as the `data` of the webhook, as you sent it. A missing value, or a falsy one (`null`, `false`, `0`, `\"\"`), is delivered as `{}`. Three things are refused with `400`, and nothing is sent: a null character (U+0000) anywhere in it, in a value or in a field name; a value that nests lists and objects more than 32 levels deep; and text that is not well-formed Unicode (half of a surrogate pair)."
          },
          "idempotencyKey": {
            "$ref": "#/components/schemas/IdempotencyKey",
            "description": "Makes a retry safe: see Retries in the description of this operation. This operation reads the key under this name only."
          }
        }
      },
      "EventDelivery": {
        "type": "object",
        "description": "How the delivery to one subscription went.",
        "required": [
          "subscriptionId",
          "name",
          "status"
        ],
        "properties": {
          "subscriptionId": {
            "type": "integer",
            "format": "int32",
            "description": "The subscription."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The subscription's name."
          },
          "status": {
            "type": "string",
            "description": "Whether the subscriber responded with a 2xx status.",
            "enum": [
              "delivered",
              "failed"
            ]
          },
          "httpStatus": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The status the subscriber responded with. `null` when nothing was sent because the URL is not allowed. Missing when the request failed before a response."
          },
          "error": {
            "type": "string",
            "description": "Why the delivery failed. Only present when `status` is `failed`."
          }
        }
      },
      "EmitEventResult": {
        "type": "object",
        "description": "Who the event went to.",
        "required": [
          "event",
          "subscribersNotified",
          "results",
          "duplicate"
        ],
        "properties": {
          "event": {
            "type": "string",
            "description": "The event name as delivered, with `custom.` in front."
          },
          "subscribersNotified": {
            "type": "integer",
            "format": "int32",
            "description": "How many subscriptions matched the event and were tried, whether or not the delivery worked."
          },
          "results": {
            "type": "array",
            "description": "One entry per subscription that was tried.",
            "items": {
              "$ref": "#/components/schemas/EventDelivery"
            }
          },
          "duplicate": {
            "type": "boolean",
            "description": "`true` when this response repeats an earlier request that sent the same `idempotencyKey`: nothing was changed this time. `false` otherwise, and always `false` without a key."
          }
        }
      },
      "EmitEventResponse": {
        "type": "object",
        "description": "Who the event went to.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/EmitEventResult"
          }
        }
      },
      "MemberConnection": {
        "type": "object",
        "description": "An account the member has linked on another platform.",
        "required": [
          "platform",
          "platformUserId",
          "platformUsername",
          "playerId",
          "playerName",
          "connectedAt",
          "proof"
        ],
        "properties": {
          "platform": {
            "type": "string",
            "description": "The platform, lower case, for example `twitch`."
          },
          "playerId": {
            "type": "string",
            "description": "The member's ID on that platform."
          },
          "playerName": {
            "type": [
              "string",
              "null"
            ],
            "description": "The member's name on that platform."
          },
          "connectedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the account was linked."
          },
          "proof": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "basic",
              "verified",
              null
            ],
            "description": "How far the player ID of a game connection can be trusted. `verified`: the player was verified, by the game (a pass from your server, or a sign-in token) or by your own server linking it with a secret key. `basic`: the member connected through Connect with a player ID the game supplied and CEEJAY did not verify, so nothing confirms that the player is theirs: anyone with the public key a game ships with can supply any ID. `null` for an account that is not a connection to a game set up for Connect, and on an instance that does not offer Connect."
          },
          "platformUserId": {
            "type": "string",
            "description": "The member's ID on that platform. The same value as `playerId`, under its deprecated name. Still sent.",
            "deprecated": true
          },
          "platformUsername": {
            "type": [
              "string",
              "null"
            ],
            "description": "The member's name on that platform. The same value as `playerName`, under its deprecated name. Still sent.",
            "deprecated": true
          }
        }
      },
      "MemberEconomy": {
        "type": "object",
        "description": "The member's currency.",
        "required": [
          "balance",
          "totalEarned",
          "totalSpent"
        ],
        "properties": {
          "balance": {
            "type": "integer",
            "description": "The current balance."
          },
          "totalEarned": {
            "type": "integer",
            "description": "The total ever credited to the member."
          },
          "totalSpent": {
            "type": "integer",
            "description": "The total the member has ever spent in the shop."
          }
        }
      },
      "Member": {
        "type": "object",
        "description": "A member of your community.",
        "required": [
          "discordId",
          "displayName",
          "avatarUrl",
          "xp",
          "level",
          "totalMessages",
          "totalVoiceMinutes",
          "joinedAt",
          "lastUpdated",
          "economy",
          "connections"
        ],
        "properties": {
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member."
          },
          "displayName": {
            "type": [
              "string",
              "null"
            ],
            "description": "The name shown for the member."
          },
          "avatarUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "The URL of the member's avatar."
          },
          "xp": {
            "type": "integer",
            "format": "int32",
            "description": "Total XP in your server."
          },
          "level": {
            "type": "integer",
            "format": "int32",
            "description": "The level that XP reaches on your server's XP curve. Not stored: worked out on each call."
          },
          "totalMessages": {
            "type": "integer",
            "description": "Total messages sent in your server, all-time."
          },
          "totalVoiceMinutes": {
            "type": "integer",
            "description": "Total minutes spent in voice channels, all-time."
          },
          "joinedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the member joined your server."
          },
          "lastUpdated": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When CEEJAY last updated the member's profile."
          },
          "economy": {
            "$ref": "#/components/schemas/MemberEconomy"
          },
          "connections": {
            "type": "array",
            "description": "The accounts the member has linked and not disconnected. A game account the member connected themselves through Connect is listed only for a key that holds `players:read`.",
            "items": {
              "$ref": "#/components/schemas/MemberConnection"
            }
          }
        }
      },
      "MemberResponse": {
        "type": "object",
        "description": "A member of your community.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/Member"
          }
        }
      },
      "LinkAccountRequest": {
        "type": "object",
        "description": "Which account to link to whom.",
        "required": [
          "discordId",
          "platform",
          "playerId"
        ],
        "properties": {
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member. An ID CEEJAY has no profile for must be a member of your server in Discord."
          },
          "platform": {
            "type": "string",
            "minLength": 1,
            "maxLength": 50,
            "pattern": "^[a-z0-9_]{1,50}$",
            "examples": [
              "roblox"
            ],
            "description": "The name of the platform: 1 to 50 lower-case letters, digits and underscores, for example `twitch`, `steam` or `roblox`. It must be a known platform (see the operation). For a custom platform, send the Slug shown on its card under **Settings → Developers → Platforms**. Capital letters, and spaces around the name, are accepted too: the name is trimmed and put in lower case before the pattern is applied, and that is the form it is stored in. Any other character is rejected, and so is a name over 50 characters as sent."
          },
          "playerId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "examples": [
              "88213004"
            ],
            "x-also-accepted": [
              "platformUserId"
            ],
            "description": "The member's ID in your game or on that platform. Send it as a string even when the IDs are numbers: a number is rejected. Spaces at either end are removed before anything is checked, and an ID that is empty after that is rejected. At most 255 characters. A null character (U+0000) in it is rejected. Capitals and spaces at the ends are ignored when CEEJAY looks for the member who holds an account, so `0xAbC` and `0xabc` are one account. Deprecated alias, still accepted: `platformUserId`. If both are sent, the alias is used. When `platform` is a game, the ID must also be one a game can use: at most 128 characters, starting with a letter or a digit, then letters, digits and `. _ : @ | + = -`."
          },
          "playerName": {
            "type": "string",
            "maxLength": 255,
            "examples": [
              "novaplays"
            ],
            "x-also-accepted": [
              "platformUsername"
            ],
            "description": "The member's name in your game or on that platform, as a string. Kept as it is when you link the same platform again without one. At most 255 characters. A null character (U+0000) in it is rejected. Deprecated alias, still accepted: `platformUsername`. If both are sent, the alias is used."
          },
          "idempotencyKey": {
            "$ref": "#/components/schemas/IdempotencyKey",
            "description": "Makes a retry safe: see Retries in the description of this operation. This operation reads the key under this name only."
          }
        }
      },
      "LinkedAccount": {
        "type": "object",
        "description": "The link.",
        "required": [
          "id",
          "server_id",
          "user_id",
          "platform",
          "platform_user_id",
          "platform_username",
          "is_active",
          "connected_at",
          "connectionId",
          "discordId",
          "playerId",
          "playerName",
          "active",
          "connectedAt",
          "duplicate"
        ],
        "properties": {
          "connectionId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The link's ID, as a string of digits."
          },
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member."
          },
          "platform": {
            "type": "string",
            "description": "The platform, in lower case."
          },
          "playerId": {
            "type": "string",
            "description": "The member's ID on that platform, as it is stored: trimmed. When you send the account a member signed in with in other capitals, it stays written as the member signed in with it."
          },
          "playerName": {
            "type": [
              "string",
              "null"
            ],
            "description": "The member's name on that platform."
          },
          "active": {
            "type": "boolean",
            "description": "Always `true` here: linking (or linking again) makes the link active."
          },
          "connectedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the account was first linked."
          },
          "duplicate": {
            "type": "boolean",
            "description": "`true` when this response replays an earlier request with the same `idempotencyKey`: nothing was changed this time. `false` otherwise, and always `false` without a key."
          },
          "server_id": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "Your server."
          },
          "id": {
            "type": "integer",
            "format": "int32",
            "description": "The link's ID, as a number. The same value as `connectionId`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "user_id": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member. The same value as `discordId`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "platform_user_id": {
            "type": "string",
            "description": "The member's ID on that platform. The same value as `playerId`, under its deprecated name. Still sent.",
            "deprecated": true
          },
          "platform_username": {
            "type": [
              "string",
              "null"
            ],
            "description": "The member's name on that platform. The same value as `playerName`, under its deprecated name. Still sent.",
            "deprecated": true
          },
          "is_active": {
            "type": "boolean",
            "description": "Always `true` here. The same value as `active`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "connected_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the account was first linked. The same value as `connectedAt`, under the name it had first. Still sent.",
            "deprecated": true
          }
        }
      },
      "LinkAccountResponse": {
        "type": "object",
        "description": "The link.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/LinkedAccount"
          }
        }
      },
      "DiscordEmbed": {
        "type": "object",
        "description": "An embed as Discord defines it. CEEJAY passes it to Discord unchanged, so Discord's own rules and limits apply to it. The common fields are listed here. See Discord's documentation for the whole object.",
        "properties": {
          "title": {
            "type": "string",
            "description": "The title."
          },
          "description": {
            "type": "string",
            "description": "The main text."
          },
          "url": {
            "type": "string",
            "description": "A link the title points to.",
            "format": "uri"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "A time shown in the footer."
          },
          "color": {
            "type": "integer",
            "format": "int32",
            "description": "The colour of the left border, as a number."
          },
          "footer": {
            "type": "object",
            "description": "The footer.",
            "required": [
              "text"
            ],
            "properties": {
              "text": {
                "type": "string",
                "description": "The footer text."
              },
              "icon_url": {
                "type": "string",
                "description": "A small image next to the footer text.",
                "format": "uri"
              }
            }
          },
          "image": {
            "type": "object",
            "description": "A large image.",
            "required": [
              "url"
            ],
            "properties": {
              "url": {
                "type": "string",
                "description": "The URL of the image.",
                "format": "uri"
              }
            }
          },
          "thumbnail": {
            "type": "object",
            "description": "A small image in the corner.",
            "required": [
              "url"
            ],
            "properties": {
              "url": {
                "type": "string",
                "description": "The URL of the image.",
                "format": "uri"
              }
            }
          },
          "author": {
            "type": "object",
            "description": "The author line.",
            "required": [
              "name"
            ],
            "properties": {
              "name": {
                "type": "string",
                "description": "The author's name."
              },
              "url": {
                "type": "string",
                "description": "A link the name points to.",
                "format": "uri"
              },
              "icon_url": {
                "type": "string",
                "description": "A small image next to the name.",
                "format": "uri"
              }
            }
          },
          "fields": {
            "type": "array",
            "description": "Name and value pairs, shown under the main text.",
            "items": {
              "type": "object",
              "description": "One field.",
              "required": [
                "name",
                "value"
              ],
              "properties": {
                "name": {
                  "type": "string",
                  "description": "The field's title."
                },
                "value": {
                  "type": "string",
                  "description": "The field's text."
                },
                "inline": {
                  "type": "boolean",
                  "description": "Show it beside other inline fields."
                }
              }
            }
          }
        },
        "additionalProperties": true
      },
      "SendMessageRequest": {
        "type": "object",
        "description": "What to post, and where.",
        "required": [
          "channelId"
        ],
        "properties": {
          "channelId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "A channel in your server. A channel in any other server is answered like one that does not exist."
          },
          "content": {
            "type": "string",
            "description": "The message text, as a string: any other value, such as a number or an object, is rejected. Cut at 2,000 characters, at a whole character (the rest is dropped, not rejected). Text that contains an unpaired surrogate, such as an emoji cut in two, is rejected with `400`. Required unless you send `embeds`. Member mentions in it ping. Role mentions ping only for the roles in `allowedRoleIds`, and `@everyone` and `@here` never ping."
          },
          "embeds": {
            "type": "array",
            "description": "Discord embeds, as a list. Only the first 10 are sent (the rest are dropped, not rejected). Required unless you send `content`. With no `content`, a value that is not a list is rejected with `400`; with `content`, it is left out and the text is posted. With an `idempotencyKey`, embeds nested more than 32 levels deep are rejected with `400`. Text anywhere in them that contains an unpaired surrogate is rejected with `400`.",
            "items": {
              "$ref": "#/components/schemas/DiscordEmbed"
            }
          },
          "allowedRoleIds": {
            "type": "array",
            "description": "The roles this message may ping: up to 20 role IDs, each a string of 17 to 20 digits. A role mention in `content` for a role that is not listed notifies nobody. Left out or empty, the message pings no role. A longer list, or a value in it that is not a role ID, is rejected. So is your server's own ID: it is the ID of the `@everyone` role, which the API never pings.",
            "maxItems": 20,
            "items": {
              "$ref": "#/components/schemas/Snowflake"
            },
            "examples": [
              [
                "718230049827100399"
              ]
            ]
          },
          "idempotencyKey": {
            "$ref": "#/components/schemas/IdempotencyKey",
            "description": "Makes a retry safe: see Retries in the description of this operation. This operation reads the key under this name only."
          }
        },
        "anyOf": [
          {
            "required": [
              "content"
            ]
          },
          {
            "required": [
              "embeds"
            ]
          }
        ]
      },
      "SendMessageResult": {
        "type": "object",
        "description": "The message that was posted.",
        "required": [
          "messageId",
          "channelId",
          "duplicate"
        ],
        "properties": {
          "messageId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The ID Discord gave the message."
          },
          "channelId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The channel it was posted in."
          },
          "duplicate": {
            "type": "boolean",
            "description": "`true` when this response replays an earlier request with the same `idempotencyKey`: nothing was changed this time. `false` otherwise, and always `false` without a key."
          }
        }
      },
      "SendMessageResponse": {
        "type": "object",
        "description": "The message that was posted.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/SendMessageResult"
          }
        }
      },
      "Quest": {
        "type": "object",
        "description": "A quest. Every field whose name has two words is sent twice: under the one spelling of the API (`xpReward`), and under the snake_case name it had first (`xp_reward`), which stays for as long as version 1 exists.",
        "required": [
          "id",
          "title",
          "description",
          "category",
          "status",
          "iconEmoji",
          "questType",
          "xpReward",
          "currencyReward",
          "roleRewardId",
          "badgeId",
          "isRepeatable",
          "maxCompletions",
          "maxCompletionsPerMember",
          "startsAt",
          "endsAt",
          "verificationType",
          "programId",
          "createdAt",
          "repeatCooldownHours",
          "requiredLevel",
          "requiredRoleId",
          "prerequisiteQuestId",
          "requiredSeasonXp",
          "icon_emoji",
          "quest_type",
          "xp_reward",
          "currency_reward",
          "role_reward_id",
          "badge_id",
          "is_repeatable",
          "max_completions",
          "max_completions_per_user",
          "starts_at",
          "ends_at",
          "verification_type",
          "program_id",
          "created_at",
          "repeat_cooldown_hours",
          "required_level",
          "required_role_id",
          "prerequisite_quest_id",
          "required_season_xp"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int32",
            "description": "The quest's ID. Use it as `questId` when completing the quest."
          },
          "title": {
            "type": "string",
            "description": "The quest's title."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the quest asks for."
          },
          "category": {
            "type": "string",
            "description": "The quest's category. Quests made in the dashboard use `discord`, `community`, `tools`, `social`, `connect`, `onchain`, `answers`, `integrations` or `custom`."
          },
          "status": {
            "type": "string",
            "description": "The quest's status, for example `active`, `draft`, `paused` or `archived`."
          },
          "iconEmoji": {
            "type": [
              "string",
              "null"
            ],
            "description": "The emoji shown with the quest."
          },
          "questType": {
            "type": "string",
            "description": "The kind of task, for example `have_role` or `api_check`. For a quest with several tasks it is the first task's."
          },
          "xpReward": {
            "type": "integer",
            "format": "int32",
            "description": "XP awarded on completion. `0` for none."
          },
          "currencyReward": {
            "type": "integer",
            "format": "int32",
            "description": "Currency awarded on completion. `0` for none."
          },
          "roleRewardId": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Snowflake"
              },
              {
                "type": "null"
              }
            ],
            "description": "The Discord role the quest rewards, if any. CEEJAY's bot assigns it after a completion, including one made through the API."
          },
          "badgeId": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The badge the quest awards, if any."
          },
          "isRepeatable": {
            "type": "boolean",
            "description": "Whether a member can complete the quest more than once."
          },
          "maxCompletions": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The most completions the quest allows in total, if limited."
          },
          "maxCompletionsPerMember": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The most times one member can complete it, if limited."
          },
          "startsAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the quest opens, if it has a start."
          },
          "endsAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the quest closes, if it has an end."
          },
          "verificationType": {
            "type": "string",
            "description": "How completion is checked in the dashboard: `automatic`, `manual` or `api` for quests made today."
          },
          "programId": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The program the quest belongs to, if any. A quest in a program can only be completed for a member of that program."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the quest was created."
          },
          "repeatCooldownHours": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "How many hours a member has to wait between two runs of a repeatable quest. `null` or `0` for no wait."
          },
          "requiredLevel": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The level a member must have reached. `null` or `0` for none."
          },
          "requiredRoleId": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Snowflake"
              },
              {
                "type": "null"
              }
            ],
            "description": "The Discord role a member must hold, if any. Complete a quest reads the member's roles from Discord when this is set."
          },
          "prerequisiteQuestId": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The ID of the quest a member must have completed first, if any."
          },
          "requiredSeasonXp": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The XP a member must have earned in the season running now. `null` or `0` for none, and always `null` on a CEEJAY instance without XP seasons."
          },
          "icon_emoji": {
            "type": [
              "string",
              "null"
            ],
            "description": "The emoji shown with the quest. The same value as `iconEmoji`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "quest_type": {
            "type": "string",
            "description": "The kind of task, for example `have_role` or `api_check`. For a quest with several tasks it is the first task's. The same value as `questType`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "xp_reward": {
            "type": "integer",
            "format": "int32",
            "description": "XP awarded on completion. `0` for none. The same value as `xpReward`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "currency_reward": {
            "type": "integer",
            "format": "int32",
            "description": "Currency awarded on completion. `0` for none. The same value as `currencyReward`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "role_reward_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Snowflake"
              },
              {
                "type": "null"
              }
            ],
            "description": "The Discord role the quest rewards, if any. CEEJAY's bot assigns it after a completion, including one made through the API. The same value as `roleRewardId`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "badge_id": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The badge the quest awards, if any. The same value as `badgeId`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "is_repeatable": {
            "type": "boolean",
            "description": "Whether a member can complete the quest more than once. The same value as `isRepeatable`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "max_completions": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The most completions the quest allows in total, if limited. The same value as `maxCompletions`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "max_completions_per_user": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The most times one member can complete it, if limited. The same value as `maxCompletionsPerMember`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "starts_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the quest opens, if it has a start. The same value as `startsAt`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "ends_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the quest closes, if it has an end. The same value as `endsAt`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "verification_type": {
            "type": "string",
            "description": "How completion is checked in the dashboard: `automatic`, `manual` or `api` for quests made today. The same value as `verificationType`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "program_id": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The program the quest belongs to, if any. A quest in a program can only be completed for a member of that program. The same value as `programId`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the quest was created. The same value as `createdAt`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "repeat_cooldown_hours": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "How many hours a member has to wait between two runs of a repeatable quest. `null` or `0` for no wait. The same value as `repeatCooldownHours`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "required_level": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The level a member must have reached. `null` or `0` for none. The same value as `requiredLevel`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "required_role_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Snowflake"
              },
              {
                "type": "null"
              }
            ],
            "description": "The Discord role a member must hold, if any. Complete a quest reads the member's roles from Discord when this is set. The same value as `requiredRoleId`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "prerequisite_quest_id": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The ID of the quest a member must have completed first, if any. The same value as `prerequisiteQuestId`, under the name it had first. Still sent.",
            "deprecated": true
          },
          "required_season_xp": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The XP a member must have earned in the season running now. `null` or `0` for none, and always `null` on a CEEJAY instance without XP seasons. The same value as `requiredSeasonXp`, under the name it had first. Still sent.",
            "deprecated": true
          }
        }
      },
      "ListQuestsResponse": {
        "type": "object",
        "description": "A page of quests.",
        "required": [
          "success",
          "data",
          "total",
          "limit",
          "offset",
          "page"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "type": "array",
            "description": "The quests on this page.",
            "items": {
              "$ref": "#/components/schemas/Quest"
            }
          },
          "page": {
            "$ref": "#/components/schemas/Page"
          },
          "total": {
            "type": "integer",
            "description": "How many quests match your filters, across all pages."
          },
          "limit": {
            "type": "integer",
            "description": "The page size that was used. The same number as `page.limit`.",
            "format": "int32"
          },
          "offset": {
            "type": "integer",
            "description": "How many quests were skipped. `0` when the page was asked for with `cursor`."
          }
        }
      },
      "CompleteQuestRequest": {
        "type": "object",
        "description": "Which quest to complete, and for whom.",
        "required": [
          "discordId",
          "questId"
        ],
        "properties": {
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member. An ID CEEJAY has no profile for must be a member of your server in Discord."
          },
          "questId": {
            "type": [
              "integer",
              "string"
            ],
            "minimum": 1,
            "maximum": 2147483647,
            "examples": [
              42
            ],
            "description": "The quest's ID, from List 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\"`)."
          },
          "proof": {
            "description": "Anything you want kept with the completion (a score, a match ID, a link), as any JSON value. Stored as it is. A falsy value (`null`, `false`, `0`, `\"\"`) or none stores `{}`, or leaves what the member submitted when the call completes a run they had started. A null character (U+0000) anywhere in it, in a value or in a field name, is rejected with `400`. So is a value that nests lists and objects more than 32 levels deep, and text that is not well-formed Unicode (an unpaired surrogate)."
          },
          "idempotencyKey": {
            "$ref": "#/components/schemas/IdempotencyKey",
            "x-also-accepted": [
              "idempotency_key"
            ],
            "description": "Protects the whole completion. A repeat of the same call answers with the first result and `duplicate: true`, and pays nothing again: see Retries in the description of this operation. Deprecated alias, still accepted: `idempotency_key`. If both are sent, the alias is used."
          }
        }
      },
      "QuestRewards": {
        "type": "object",
        "description": "What the completion granted. On a repeat with the same `idempotencyKey`, what the first call paid.",
        "required": [
          "xp",
          "currency",
          "roleId",
          "badgeId",
          "programXp"
        ],
        "properties": {
          "xp": {
            "type": "integer",
            "description": "XP granted: the quest's XP reward, or `0` when it has none.",
            "format": "int32"
          },
          "currency": {
            "type": "integer",
            "description": "Currency credited. It is less than the quest's reward when the member's balance hit the community's maximum, and `0` when the quest has no currency reward or the coins were skipped."
          },
          "currencyRequested": {
            "type": "integer",
            "description": "The quest's currency reward. Only present when the coins were granted."
          },
          "currencyCapped": {
            "type": "boolean",
            "description": "`true` when the community's maximum balance cut the credit short. Only present when the coins were granted."
          },
          "balance": {
            "type": "integer",
            "description": "The member's balance after the credit. Only present when the coins were granted."
          },
          "currencySkipped": {
            "type": "boolean",
            "const": true,
            "description": "Only present, and then `true`, when the quest has a currency reward and it was not paid because the community's economy is disabled. `currency` is then `0` and there is no `balance`."
          },
          "programXp": {
            "type": "integer",
            "description": "Program XP added to the member's score in the quest's program: the quest's XP reward for a quest in a program, `0` otherwise.",
            "format": "int32"
          },
          "roleId": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Snowflake"
              },
              {
                "type": "null"
              }
            ],
            "description": "The Discord role the quest rewards, or `null`. CEEJAY's bot gives it shortly after the answer; the role is not yet given when you read this."
          },
          "badgeId": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The badge the quest awards, whether or not the member already had it."
          }
        }
      },
      "QuestProgress": {
        "type": "object",
        "description": "The completion as stored: one row of the quest progress table, with its stored field names (snake_case). It is the run the member had open when there was one, and a new run otherwise. Other fields can appear on some servers.",
        "required": [
          "id",
          "server_id",
          "user_id",
          "quest_id",
          "status",
          "completed_at",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int32",
            "description": "The completion's ID."
          },
          "server_id": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "Your server."
          },
          "user_id": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member."
          },
          "quest_id": {
            "type": "integer",
            "format": "int32",
            "description": "The quest."
          },
          "status": {
            "type": "string",
            "description": "Always `completed` here.",
            "enum": [
              "completed"
            ]
          },
          "progress_data": {
            "type": "object",
            "additionalProperties": true,
            "description": "Progress details. Empty for a run this call added. For a run the member had started, it holds what the run had gathered on the way."
          },
          "submission_data": {
            "description": "The `proof` you sent. When you sent none: `{}`, or what the member submitted on a run they had started."
          },
          "review_note": {
            "type": [
              "string",
              "null"
            ],
            "description": "A reviewer's note. `null` unless the run the member had started carried one."
          },
          "reviewed_by": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who reviewed it. `null` unless the run the member had started carried one."
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "description": "When it was completed."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the run was created: now for a run this call added, earlier for one the member had started."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the row last changed."
          }
        },
        "additionalProperties": true
      },
      "CompleteQuestResult": {
        "type": "object",
        "description": "The completion and what it granted. On a repeat (`duplicate: true`) it is the first call's answer.",
        "required": [
          "discordId",
          "questId",
          "questTitle",
          "status",
          "rewards",
          "progress",
          "duplicate",
          "run"
        ],
        "properties": {
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member."
          },
          "questId": {
            "type": "integer",
            "description": "The quest.",
            "format": "int32"
          },
          "questTitle": {
            "type": [
              "string",
              "null"
            ],
            "description": "The quest's title. `null` only on a repeat of a quest that has been deleted since."
          },
          "status": {
            "type": "string",
            "description": "Always `completed`.",
            "enum": [
              "completed"
            ]
          },
          "rewards": {
            "$ref": "#/components/schemas/QuestRewards"
          },
          "run": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/QuestRun"
              },
              {
                "type": "null"
              }
            ],
            "description": "The run this call completed. `null` only on a repeat whose run can no longer be found."
          },
          "duplicate": {
            "type": "boolean",
            "description": "`true` when this response replays an earlier request with the same `idempotencyKey`: nothing was changed this time. `false` otherwise, and always `false` without a key."
          },
          "progress": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/QuestProgress"
              },
              {
                "type": "null"
              }
            ],
            "deprecated": true,
            "description": "The run as stored, with its snake_case field names. Use `run`. Still sent. On a repeat it is the run as it is now, and `null` when the run no longer exists."
          }
        }
      },
      "CompleteQuestResponse": {
        "type": "object",
        "description": "The completion and what it granted.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/CompleteQuestResult"
          }
        }
      },
      "QuestRefusalResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ErrorResponse"
          },
          {
            "type": "object",
            "properties": {
              "startsAt": {
                "type": "string",
                "format": "date-time",
                "description": "When the quest starts. Only with `quest_not_started`."
              },
              "endsAt": {
                "type": "string",
                "format": "date-time",
                "description": "When the quest ended. Only with `quest_ended`."
              },
              "requiredRoleId": {
                "$ref": "#/components/schemas/Snowflake",
                "description": "The role the quest requires. Only with `quest_requires_role`."
              },
              "requiredLevel": {
                "type": "integer",
                "description": "The level the quest requires. Only with `quest_requires_level`.",
                "format": "int32"
              },
              "level": {
                "type": "integer",
                "description": "The member's level now. Only with `quest_requires_level`.",
                "format": "int32"
              },
              "requiredSeasonXp": {
                "type": "integer",
                "description": "The season XP the quest requires. Only with `quest_requires_season_xp`.",
                "format": "int32"
              },
              "seasonXp": {
                "type": "integer",
                "description": "What the member has earned in the season running now. Only with `quest_requires_season_xp`.",
                "format": "int32"
              },
              "seasonRunning": {
                "type": "boolean",
                "description": "`false` when no season is running, so nobody can meet the rule. Only with `quest_requires_season_xp`."
              },
              "seasonUnreadable": {
                "type": "boolean",
                "description": "Only present, and then `true`, when the season XP could not be read. Only with `quest_requires_season_xp`."
              },
              "programId": {
                "type": "integer",
                "description": "The program the quest belongs to. Only with `quest_requires_program`.",
                "format": "int32"
              },
              "maxCompletions": {
                "type": "integer",
                "description": "The quest's overall completion limit. Only with `quest_full`.",
                "format": "int32"
              },
              "availableAt": {
                "type": "string",
                "format": "date-time",
                "description": "When the member can complete the quest again. Only with `quest_cooling_down`."
              },
              "prerequisiteQuestId": {
                "type": "integer",
                "description": "The quest that has to be completed first. Only with `quest_requires_quest`.",
                "format": "int32"
              },
              "dailyQuestLimit": {
                "type": "integer",
                "description": "The community's daily quest limit. Only with `quest_daily_limit_reached`.",
                "format": "int32"
              },
              "completedToday": {
                "type": "integer",
                "description": "How many quests the member has completed today, counted in UTC. Only with `quest_daily_limit_reached`.",
                "format": "int32"
              },
              "resetsAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "When the day ends and the count starts again: the next midnight in UTC. Only with `quest_daily_limit_reached`."
              }
            }
          }
        ],
        "description": "The body of a rejection from Complete a quest when one of the quest's rules is not met. `code` names the rule. Some rejections add the state that caused them."
      },
      "CompleteQuestTaskRequest": {
        "type": "object",
        "description": "Which quest task, and who finished it. Name the member in one way. If you send more than one of `discordId`, `playerId`, `xHandle`, `wallet`, `github` and `twitch`, CEEJAY uses the first of them in that order and ignores the rest.",
        "required": [
          "taskKey"
        ],
        "properties": {
          "taskKey": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{4,64}$",
            "examples": [
              "tk_4k2m9x7qzb"
            ],
            "x-also-accepted": [
              "task_key"
            ],
            "description": "The Task key of a quest task of the type Completed by your API, from the quest builder: 4 to 64 letters, digits, `_` or `-`, sent as a string. A number or any other value is rejected like a missing key. Spaces around it are ignored. It must match the task's key exactly, capitals included. Every live task that has this key is completed, in every quest on your server. Deprecated alias, still accepted: `task_key`. If both are sent, the alias is used."
          },
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "x-also-accepted": [
              "discord_id"
            ],
            "description": "The member's Discord ID, as a string. An ID CEEJAY has no member profile for must be a member of your server in Discord: one that is not does not match a member and returns 202. `null` and an empty string are skipped, and the next identifier you sent is used. Any other value that is not a string of 17 to 20 digits, a JSON number included, does not match a member and returns 202. Deprecated alias, still accepted: `discord_id`. If both are sent, the alias is used."
          },
          "playerId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "examples": [
              "88213004"
            ],
            "description": "The member's ID in your game, as you linked it with Link an account. Send `platform` with it. A string of 1 to 255 characters once the spaces at its ends are removed; any other value, or a null character (U+0000) in it, is rejected with 400. Capitals are ignored when CEEJAY looks for the member."
          },
          "platform": {
            "type": "string",
            "minLength": 1,
            "maxLength": 50,
            "pattern": "^[a-z0-9_]{1,50}$",
            "examples": [
              "roblox"
            ],
            "description": "The platform `playerId` belongs to, by the rule of Link an account: 1 to 50 lower-case letters, digits and underscores, and a platform your community knows. The name is trimmed and put in lower case before the pattern is applied. It is read only when `playerId` is the identifier in use: with a `discordId`, or with no `playerId`, whatever the body holds under this name is ignored. Required with `playerId`."
          },
          "xHandle": {
            "type": "string",
            "examples": [
              "@novaplays"
            ],
            "x-also-accepted": [
              "x_handle"
            ],
            "description": "The member's X handle, with or without the leading `@`, as a string. A whole number up to 9007199254740991 is read too, as its digits. A larger number, a number with a fraction, a list, an object or a boolean does not match a member and returns 202. A null character (U+0000) in it is rejected with 400 when this is the identifier the member is looked up by. An X user ID is larger than that number, so send it in quotes. Matched, ignoring capitals, against the ID or username of the X account the member connected in your community (platform `twitter`). Deprecated alias, still accepted: `x_handle`. If both are sent, the alias is used."
          },
          "wallet": {
            "type": "string",
            "examples": [
              "0x4b8d2f0a6c1e93d7a5b2c8e1f4a7d03b9c6e5a21"
            ],
            "description": "The member's web3 wallet address, as a string. A whole number up to 9007199254740991 is read too, as its digits. A larger number, a number with a fraction, a list, an object or a boolean does not match a member and returns 202. A null character (U+0000) in it is rejected with 400 when this is the identifier the member is looked up by. Matched, ignoring capitals, against the wallet the member connected in your community (platform `metamask`)."
          },
          "github": {
            "type": "string",
            "examples": [
              "novaplays"
            ],
            "description": "The member's GitHub login or ID. Send a login as a string, and an ID as a string or as a JSON number. A whole number up to 9007199254740991 is read too, as its digits. A larger number, a number with a fraction, a list, an object or a boolean does not match a member and returns 202. A null character (U+0000) in it is rejected with 400 when this is the identifier the member is looked up by. Matched, ignoring capitals, against the GitHub account the member connected in your community (platform `github`)."
          },
          "twitch": {
            "type": "string",
            "examples": [
              "novaplays"
            ],
            "description": "The member's Twitch login or ID. Send a login as a string, and an ID as a string or as a JSON number. A whole number up to 9007199254740991 is read too, as its digits. A larger number, a number with a fraction, a list, an object or a boolean does not match a member and returns 202. A null character (U+0000) in it is rejected with 400 when this is the identifier the member is looked up by. Matched, ignoring capitals, against the Twitch account the member connected in your community (platform `twitch`)."
          },
          "evidence": {
            "type": "string",
            "description": "Text or a link that shows what the member did. Up to 2,000 characters are kept and the rest is dropped, not rejected; the cut is made at a whole character, so an emoji is never cut in two. Send a string: any other value, such as a number or an object, is rejected with 400. `null` counts as not sent. A null character (U+0000) in it is rejected with 400, and so is an unpaired surrogate, such as the JSON `\"\\ud800\"`. With review, the moderator sees it as the proof, and with none it reads `Reported done by your platform`. Without review it is kept with the member's progress on the task.",
            "examples": [
              "https://acme.example/players/nova/tutorial"
            ]
          },
          "review": {
            "type": "boolean",
            "description": "`true` sends this completion to the Review Queue, even when the task's own review switch is off. Only the JSON value `true` counts. Leaving it out, or sending `false`, never turns off a task's own switch.",
            "default": false
          },
          "idempotencyKey": {
            "$ref": "#/components/schemas/IdempotencyKey",
            "description": "Makes a retry safe: see Retries in the description of this operation. This operation reads the key under this name only."
          }
        },
        "anyOf": [
          {
            "required": [
              "discordId"
            ]
          },
          {
            "required": [
              "playerId"
            ]
          },
          {
            "required": [
              "xHandle"
            ]
          },
          {
            "required": [
              "wallet"
            ]
          },
          {
            "required": [
              "github"
            ]
          },
          {
            "required": [
              "twitch"
            ]
          }
        ]
      },
      "CompletedQuestSummary": {
        "type": "object",
        "description": "A quest this call finished, as the older list beside `data` names it.",
        "required": [
          "questId",
          "title"
        ],
        "properties": {
          "questId": {
            "type": "integer",
            "description": "The quest's ID.",
            "format": "int32"
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "The quest's title."
          }
        }
      },
      "CompleteQuestTaskResponse": {
        "type": "object",
        "description": "The report reached CEEJAY's bot. `data` says what it did. `matched` and `completed` also sit beside `success`, where this answer has always had them.",
        "required": [
          "success",
          "matched",
          "completed",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/QuestTaskResult"
          },
          "matched": {
            "type": "integer",
            "const": 1,
            "deprecated": true,
            "description": "The same value as `data.matched`. Still sent."
          },
          "completed": {
            "type": "array",
            "deprecated": true,
            "description": "The same quests as `data.completed`, with the title under `title`. Still sent.",
            "items": {
              "$ref": "#/components/schemas/CompletedQuestSummary"
            }
          }
        }
      },
      "CompleteQuestTaskNoMemberResponse": {
        "type": "object",
        "description": "CEEJAY found no member for the identifier, so no changes were made.",
        "required": [
          "success",
          "matched",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true."
          },
          "data": {
            "type": "object",
            "description": "What the report did: nothing.",
            "required": [
              "matched",
              "completed",
              "sentForReview"
            ],
            "properties": {
              "matched": {
                "type": "integer",
                "description": "Always 0 here: no member was found.",
                "const": 0
              },
              "completed": {
                "type": "array",
                "maxItems": 0,
                "description": "Always empty here.",
                "items": {
                  "$ref": "#/components/schemas/QuestTaskQuest"
                }
              },
              "sentForReview": {
                "type": "array",
                "maxItems": 0,
                "description": "Always empty here.",
                "items": {
                  "$ref": "#/components/schemas/QuestTaskQuest"
                }
              }
            }
          },
          "matched": {
            "type": "integer",
            "const": 0,
            "deprecated": true,
            "description": "The same value as `data.matched`. Still sent."
          }
        }
      },
      "RoleChangeRequest": {
        "type": "object",
        "description": "Which role, and for whom.",
        "required": [
          "discordId",
          "roleId"
        ],
        "properties": {
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member."
          },
          "roleId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The Discord role. It must be a role of your server that is not `@everyone`, is not managed by Discord or another app, cannot moderate or manage the server, cannot mention everyone (unless your server's `@everyone` role can), and is not one of your community's bot master roles. List roles says which roles those are."
          },
          "idempotencyKey": {
            "$ref": "#/components/schemas/IdempotencyKey",
            "description": "Makes a retry safe: see Retries in the description of this operation. This operation reads the key under this name only."
          }
        }
      },
      "GrantRoleResult": {
        "type": "object",
        "description": "The role that was granted.",
        "required": [
          "discordId",
          "roleId",
          "action",
          "duplicate"
        ],
        "properties": {
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member."
          },
          "roleId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The role."
          },
          "action": {
            "type": "string",
            "description": "Always `granted`.",
            "enum": [
              "granted"
            ]
          },
          "duplicate": {
            "type": "boolean",
            "description": "`true` when this response replays an earlier request with the same `idempotencyKey`: nothing was changed this time. `false` otherwise, and always `false` without a key."
          }
        }
      },
      "GrantRoleResponse": {
        "type": "object",
        "description": "The role that was granted.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/GrantRoleResult"
          }
        }
      },
      "RevokeRoleResult": {
        "type": "object",
        "description": "The role that was revoked.",
        "required": [
          "discordId",
          "roleId",
          "action",
          "duplicate"
        ],
        "properties": {
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member."
          },
          "roleId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The role."
          },
          "action": {
            "type": "string",
            "description": "Always `revoked`.",
            "enum": [
              "revoked"
            ]
          },
          "duplicate": {
            "type": "boolean",
            "description": "`true` when this response replays an earlier request with the same `idempotencyKey`: nothing was changed this time. `false` otherwise, and always `false` without a key."
          }
        }
      },
      "RevokeRoleResponse": {
        "type": "object",
        "description": "The role that was revoked.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/RevokeRoleResult"
          }
        }
      },
      "CompleteShopPurchaseRequest": {
        "type": "object",
        "description": "The answer for a purchase that was waiting for you.",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "description": "`delivered` when you have delivered the item. `failed` when you cannot.",
            "enum": [
              "delivered",
              "failed"
            ]
          },
          "message": {
            "type": "string",
            "description": "An optional note, 500 characters or fewer. Leading and trailing spaces are removed, and a blank note is the same as none. The team sees it when you send `failed`. A null character (U+0000) in it is rejected with `400`, and so is an unpaired surrogate, such as the JSON `\"\\ud800\"`.",
            "maxLength": 500
          },
          "idempotencyKey": {
            "$ref": "#/components/schemas/IdempotencyKey",
            "description": "Makes a retry safe: see Retries in the description of this operation. This operation reads the key under this name only."
          }
        }
      },
      "CompleteShopPurchaseResponse": {
        "type": "object",
        "description": "The purchase after your update. `data` says it in the one shape of the API. `purchaseId`, `status`, `alreadyDone` and `refunded` also sit beside `success`, where this answer has always had them.",
        "required": [
          "success",
          "purchaseId",
          "status",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/ShopPurchaseResult"
          },
          "purchaseId": {
            "type": "integer",
            "format": "int32",
            "deprecated": true,
            "description": "The same value as `data.purchaseId`. Still sent."
          },
          "status": {
            "type": "string",
            "enum": [
              "fulfilled",
              "refunded",
              "failed"
            ],
            "description": "The same value as `data.status`. Still sent.",
            "deprecated": true
          },
          "alreadyDone": {
            "type": "boolean",
            "deprecated": true,
            "description": "`true` when you had already sent this same answer. Missing otherwise. Use `data.alreadyDone`, which is always there."
          },
          "refunded": {
            "type": "integer",
            "minimum": 0,
            "deprecated": true,
            "description": "How much currency went back to the member. Only present when this call refunded the purchase. Use `data.refunded`, which is always there."
          }
        }
      },
      "PurchaseErrorResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ErrorResponse"
          },
          {
            "type": "object",
            "properties": {
              "status": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The purchase's status now, when known. Only with `wrong_status`."
              }
            }
          }
        ],
        "description": "The body of a `409` from Complete a shop delivery."
      },
      "XpBalanceResult": {
        "type": "object",
        "description": "A member's XP.",
        "required": [
          "discordId",
          "xp",
          "level",
          "lastUpdated"
        ],
        "properties": {
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member."
          },
          "xp": {
            "type": "integer",
            "format": "int32",
            "description": "Total XP in your server."
          },
          "level": {
            "type": "integer",
            "format": "int32",
            "description": "The level that XP reaches on your server's XP curve."
          },
          "lastUpdated": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When CEEJAY last updated the member's profile. `null` for a member CEEJAY has not seen."
          }
        }
      },
      "XpBalanceResponse": {
        "type": "object",
        "description": "A member's XP.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/XpBalanceResult"
          }
        }
      },
      "GrantXpRequest": {
        "type": "object",
        "description": "How much XP to give to whom.",
        "required": [
          "discordId",
          "amount"
        ],
        "properties": {
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member. CEEJAY creates their profile if it has none, once Discord says the ID is a member of your server."
          },
          "amount": {
            "type": "integer",
            "description": "XP to grant: a whole number from 1 to 100,000.",
            "format": "int32",
            "minimum": 1,
            "maximum": 100000
          },
          "reason": {
            "type": "string",
            "description": "Why, as a string. Optional. Shown in the XP history and the audit log. Any other value, such as a number or a list, is rejected with `400`, and so is a null character (U+0000) in it. `null` counts as left out."
          },
          "idempotencyKey": {
            "$ref": "#/components/schemas/IdempotencyKey",
            "description": "Makes a retry safe: see Retries in the description of this operation. This operation reads the key under this name only."
          }
        }
      },
      "GrantXpResult": {
        "type": "object",
        "description": "The grant.",
        "required": [
          "discordId",
          "previousXP",
          "xpGranted",
          "totalXP",
          "previousLevel",
          "currentLevel",
          "leveledUp",
          "reason",
          "auditId",
          "xp",
          "previousXp",
          "level",
          "duplicate"
        ],
        "properties": {
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member."
          },
          "xp": {
            "type": "integer",
            "description": "Total XP after the grant.",
            "format": "int32"
          },
          "previousXp": {
            "type": "integer",
            "description": "Total XP before the grant.",
            "format": "int32"
          },
          "xpGranted": {
            "type": "integer",
            "description": "The amount you sent.",
            "format": "int32"
          },
          "level": {
            "type": "integer",
            "description": "The level after the grant.",
            "format": "int32"
          },
          "previousLevel": {
            "type": "integer",
            "description": "The member's level before the grant.",
            "format": "int32"
          },
          "leveledUp": {
            "type": "boolean",
            "description": "`true` when the grant took the member up at least one level."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "The reason you sent, or `null`."
          },
          "auditId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The ID of the audit log entry, as a string of digits, because it is a 64-bit number. `null` when the audit log is not available on your server.",
            "pattern": "^\\d+$",
            "examples": [
              "10442"
            ]
          },
          "duplicate": {
            "type": "boolean",
            "description": "`true` when this response replays an earlier request with the same `idempotencyKey`: nothing was changed this time. `false` otherwise, and always `false` without a key."
          },
          "totalXP": {
            "type": "integer",
            "description": "Total XP after the grant. The same value as `xp`, under the name it had first. Still sent.",
            "format": "int32",
            "deprecated": true
          },
          "previousXP": {
            "type": "integer",
            "description": "Total XP before the grant. The same value as `previousXp`, under the name it had first. Still sent.",
            "format": "int32",
            "deprecated": true
          },
          "currentLevel": {
            "type": "integer",
            "description": "The level after the grant. The same value as `level`, under the name it had first. Still sent.",
            "format": "int32",
            "deprecated": true
          }
        }
      },
      "GrantXpResponse": {
        "type": "object",
        "description": "The grant.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/GrantXpResult"
          }
        }
      },
      "DeductXpRequest": {
        "type": "object",
        "description": "How much XP to take from whom.",
        "required": [
          "discordId",
          "amount"
        ],
        "properties": {
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member. They must already have a profile."
          },
          "amount": {
            "type": "integer",
            "description": "XP to take: a whole number from 1 to 2,147,483,647. Taking more than the member has leaves them at 0.",
            "format": "int32",
            "minimum": 1,
            "maximum": 2147483647
          },
          "reason": {
            "type": "string",
            "description": "Why, as a string. Optional. Shown in the XP history and the audit log. Any other value, such as a number or a list, is rejected with `400`, and so is a null character (U+0000) in it. `null` counts as left out."
          },
          "idempotencyKey": {
            "$ref": "#/components/schemas/IdempotencyKey",
            "description": "Makes a retry safe: see Retries in the description of this operation. This operation reads the key under this name only."
          }
        }
      },
      "DeductXpResult": {
        "type": "object",
        "description": "The deduction.",
        "required": [
          "discordId",
          "previousXP",
          "xpDeducted",
          "totalXP",
          "previousLevel",
          "currentLevel",
          "reason",
          "auditId",
          "xp",
          "previousXp",
          "level",
          "duplicate"
        ],
        "properties": {
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member."
          },
          "xp": {
            "type": "integer",
            "description": "Total XP after the deduction. Never below 0.",
            "format": "int32"
          },
          "previousXp": {
            "type": "integer",
            "description": "Total XP before the deduction.",
            "format": "int32"
          },
          "xpDeducted": {
            "type": "integer",
            "description": "The XP actually deducted. Less than `amount` when the member had less than that.",
            "format": "int32"
          },
          "level": {
            "type": "integer",
            "description": "The level after the deduction.",
            "format": "int32"
          },
          "previousLevel": {
            "type": "integer",
            "description": "The member's level before the deduction.",
            "format": "int32"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "The reason you sent, or `null`."
          },
          "auditId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The ID of the audit log entry, as a string of digits, because it is a 64-bit number. `null` when the audit log is not available on your server.",
            "pattern": "^\\d+$",
            "examples": [
              "10442"
            ]
          },
          "duplicate": {
            "type": "boolean",
            "description": "`true` when this response replays an earlier request with the same `idempotencyKey`: nothing was changed this time. `false` otherwise, and always `false` without a key."
          },
          "totalXP": {
            "type": "integer",
            "description": "Total XP after the deduction. The same value as `xp`, under the name it had first. Still sent.",
            "format": "int32",
            "deprecated": true
          },
          "previousXP": {
            "type": "integer",
            "description": "Total XP before the deduction. The same value as `previousXp`, under the name it had first. Still sent.",
            "format": "int32",
            "deprecated": true
          },
          "currentLevel": {
            "type": "integer",
            "description": "The level after the deduction. The same value as `level`, under the name it had first. Still sent.",
            "format": "int32",
            "deprecated": true
          }
        }
      },
      "DeductXpResponse": {
        "type": "object",
        "description": "The deduction.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/DeductXpResult"
          }
        }
      },
      "GrantProgramXpRequest": {
        "type": "object",
        "description": "How much program XP to give to whom.",
        "required": [
          "programId",
          "discordId",
          "amount"
        ],
        "properties": {
          "programId": {
            "type": [
              "integer",
              "string"
            ],
            "minimum": 1,
            "maximum": 2147483647,
            "description": "The community program's ID, from List programs: a whole number from 1 to 2,147,483,647, or the same number as a string of digits written without leading zeros (`3` or `\"3\"`).",
            "examples": [
              3
            ]
          },
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member. They must be enrolled in the program, and an ID CEEJAY has no member profile for must be a member of your server in Discord."
          },
          "amount": {
            "type": "integer",
            "description": "XP to grant: a whole number from 1 to 100,000.",
            "format": "int32",
            "minimum": 1,
            "maximum": 100000
          },
          "reason": {
            "type": "string",
            "description": "Why, as a string. Optional. Shown in the XP history and the audit log. Any other value, such as a number or a list, is rejected with `400`, and so is a null character (U+0000) in it. So is text that is not well-formed Unicode: an unpaired surrogate, such as the JSON `\"\\ud800\"`. `null` counts as left out."
          },
          "idempotencyKey": {
            "$ref": "#/components/schemas/IdempotencyKey",
            "description": "Makes a retry safe: see Retries in the description of this operation. This operation reads the key under this name only."
          }
        }
      },
      "GrantProgramXpResult": {
        "type": "object",
        "description": "The grant.",
        "required": [
          "programId",
          "programName",
          "discordId",
          "previousXP",
          "xpGranted",
          "totalXP",
          "reason",
          "auditId",
          "programXp",
          "previousProgramXp",
          "duplicate"
        ],
        "properties": {
          "programId": {
            "type": "integer",
            "description": "The program.",
            "format": "int32"
          },
          "programName": {
            "type": "string",
            "description": "The program's name."
          },
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member."
          },
          "programXp": {
            "type": "integer",
            "description": "The member's program XP after the grant.",
            "format": "int32"
          },
          "previousProgramXp": {
            "type": "integer",
            "description": "The member's program XP before the grant.",
            "format": "int32"
          },
          "xpGranted": {
            "type": "integer",
            "description": "The amount you sent.",
            "format": "int32"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "The reason you sent, or `null`."
          },
          "auditId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The ID of the audit log entry, as a string of digits, because it is a 64-bit number. `null` when the audit log is not available on your server.",
            "pattern": "^\\d+$",
            "examples": [
              "10442"
            ]
          },
          "duplicate": {
            "type": "boolean",
            "description": "`true` when this response replays an earlier request with the same `idempotencyKey`: nothing was changed this time. `false` otherwise, and always `false` without a key."
          },
          "totalXP": {
            "type": "integer",
            "description": "The member's program XP after the grant. The same value as `programXp`, under the name it had first. Still sent.",
            "format": "int32",
            "deprecated": true
          },
          "previousXP": {
            "type": "integer",
            "description": "The member's program XP before the grant. The same value as `previousProgramXp`, under the name it had first. Still sent.",
            "format": "int32",
            "deprecated": true
          }
        }
      },
      "GrantProgramXpResponse": {
        "type": "object",
        "description": "The grant.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/GrantProgramXpResult"
          }
        }
      },
      "DeductProgramXpRequest": {
        "type": "object",
        "description": "How much program XP to take from whom.",
        "required": [
          "programId",
          "discordId",
          "amount"
        ],
        "properties": {
          "programId": {
            "type": [
              "integer",
              "string"
            ],
            "minimum": 1,
            "maximum": 2147483647,
            "description": "The community program's ID, from List programs: a whole number from 1 to 2,147,483,647, or the same number as a string of digits written without leading zeros (`3` or `\"3\"`).",
            "examples": [
              3
            ]
          },
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member. They must be enrolled in the program."
          },
          "amount": {
            "type": "integer",
            "description": "XP to take: a whole number from 1 to 100,000. Taking more than the member has leaves them at 0.",
            "format": "int32",
            "minimum": 1,
            "maximum": 100000
          },
          "reason": {
            "type": "string",
            "description": "Why, as a string. Optional. Shown in the XP history and the audit log. Any other value, such as a number or a list, is rejected with `400`, and so is a null character (U+0000) in it. So is text that is not well-formed Unicode: an unpaired surrogate, such as the JSON `\"\\ud800\"`. `null` counts as left out."
          },
          "idempotencyKey": {
            "$ref": "#/components/schemas/IdempotencyKey",
            "description": "Makes a retry safe: see Retries in the description of this operation. This operation reads the key under this name only."
          }
        }
      },
      "DeductProgramXpResult": {
        "type": "object",
        "description": "The deduction.",
        "required": [
          "programId",
          "programName",
          "discordId",
          "previousXP",
          "xpDeducted",
          "totalXP",
          "reason",
          "auditId",
          "programXp",
          "previousProgramXp",
          "duplicate"
        ],
        "properties": {
          "programId": {
            "type": "integer",
            "description": "The program.",
            "format": "int32"
          },
          "programName": {
            "type": "string",
            "description": "The program's name."
          },
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member."
          },
          "programXp": {
            "type": "integer",
            "description": "The member's program XP after the deduction.",
            "format": "int32"
          },
          "previousProgramXp": {
            "type": "integer",
            "description": "The member's program XP before the deduction.",
            "format": "int32"
          },
          "xpDeducted": {
            "type": "integer",
            "description": "The XP actually deducted. Less than `amount` when the member had less than that.",
            "format": "int32"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "The reason you sent, or `null`."
          },
          "auditId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The ID of the audit log entry, as a string of digits, because it is a 64-bit number. `null` when the audit log is not available on your server.",
            "pattern": "^\\d+$",
            "examples": [
              "10442"
            ]
          },
          "duplicate": {
            "type": "boolean",
            "description": "`true` when this response replays an earlier request with the same `idempotencyKey`: nothing was changed this time. `false` otherwise, and always `false` without a key."
          },
          "totalXP": {
            "type": "integer",
            "description": "The member's program XP after the deduction. The same value as `programXp`, under the name it had first. Still sent.",
            "format": "int32",
            "deprecated": true
          },
          "previousXP": {
            "type": "integer",
            "description": "The member's program XP before the deduction. The same value as `previousProgramXp`, under the name it had first. Still sent.",
            "format": "int32",
            "deprecated": true
          }
        }
      },
      "DeductProgramXpResponse": {
        "type": "object",
        "description": "The deduction.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/DeductProgramXpResult"
          }
        }
      },
      "ProgramConflictErrorResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ErrorResponse"
          },
          {
            "type": "object",
            "properties": {
              "programStatus": {
                "type": "string",
                "description": "The program's status, for example `draft`. Only with `program_not_active`."
              },
              "memberStatus": {
                "type": "string",
                "description": "The member's status in the program, for example `applied`. Only with `member_status_not_allowed`. Only `accepted` and `active` allow XP changes."
              }
            }
          }
        ],
        "description": "The body of a `409` from the program XP operations."
      },
      "WebhookEnvelope": {
        "type": "object",
        "description": "The body of every webhook delivery from a subscription.",
        "required": [
          "event",
          "data",
          "serverId",
          "timestamp",
          "id"
        ],
        "properties": {
          "event": {
            "type": "string",
            "description": "The event name. It is also in the `X-CEEJAY-Event` header."
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "The details of the event. Their fields depend on the event."
          },
          "serverId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "Your server."
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "When CEEJAY sent this delivery."
          },
          "id": {
            "type": "string",
            "description": "A unique ID for this delivery, starting with `evt_`. It is also the `X-CEEJAY-Delivery` header. A retry of the same delivery has the same `id` and `timestamp`: use it to ignore repeats.",
            "pattern": "^evt_[0-9a-z_]+$",
            "examples": [
              "evt_5b1f0c7e9a3d4e2f8c6a1b0d9e8f7a6c"
            ]
          }
        }
      },
      "MemberJoinedData": {
        "type": "object",
        "description": "Who joined.",
        "required": [
          "userId",
          "username",
          "joinedAt",
          "inviteType",
          "memberCount",
          "discordId"
        ],
        "properties": {
          "userId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member who joined."
          },
          "username": {
            "type": "string",
            "description": "Their Discord username."
          },
          "joinedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When they joined."
          },
          "inviteType": {
            "type": [
              "string",
              "null"
            ],
            "description": "How they got in, as the invite tracker saw it: `normal` (an invite link), `vanity` (the server's vanity link), `permissions` (added by a permission, such as an integration) or `unknown`. `null` when that is not known."
          },
          "memberCount": {
            "type": "integer",
            "format": "int32",
            "description": "How many members the server has now."
          },
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The same member as `userId`, under the name the rest of the API uses."
          }
        }
      },
      "MemberLeftData": {
        "type": "object",
        "description": "Who left.",
        "required": [
          "userId",
          "username",
          "leftAt",
          "memberCount",
          "discordId"
        ],
        "properties": {
          "userId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member who left or was removed."
          },
          "username": {
            "type": "string",
            "description": "Their Discord username."
          },
          "leftAt": {
            "type": "string",
            "format": "date-time",
            "description": "When they left."
          },
          "memberCount": {
            "type": "integer",
            "format": "int32",
            "description": "How many members the server has now."
          },
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The same member as `userId`, under the name the rest of the API uses."
          }
        }
      },
      "QuestCompletedData": {
        "type": "object",
        "description": "The completed quest.",
        "required": [
          "userId",
          "questId",
          "questTitle",
          "questType",
          "rewards",
          "completedAt",
          "discordId"
        ],
        "properties": {
          "userId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member who completed the quest."
          },
          "questId": {
            "type": "integer",
            "format": "int32",
            "description": "The quest."
          },
          "questTitle": {
            "type": "string",
            "description": "The quest's title."
          },
          "questType": {
            "type": "string",
            "description": "The kind of task, for example `api_check`."
          },
          "rewards": {
            "type": "object",
            "description": "What the completion paid.",
            "required": [
              "xp",
              "currency",
              "badge",
              "roleGranted"
            ],
            "properties": {
              "xp": {
                "type": "integer",
                "format": "int32",
                "description": "XP granted."
              },
              "currency": {
                "type": "integer",
                "format": "int32",
                "description": "Currency credited."
              },
              "badge": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The name of the badge awarded, if any."
              },
              "roleGranted": {
                "type": "boolean",
                "description": "Whether the quest's role reward was given."
              }
            }
          },
          "completedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When it was completed."
          },
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The same member as `userId`, under the name the rest of the API uses."
          }
        }
      },
      "TicketCreatedData": {
        "type": "object",
        "description": "The new ticket.",
        "required": [
          "ticketId",
          "ticketNumber",
          "panelName",
          "userId",
          "username",
          "channelId",
          "createdAt",
          "discordId"
        ],
        "properties": {
          "ticketId": {
            "type": "integer",
            "format": "int32",
            "description": "The ticket's ID."
          },
          "ticketNumber": {
            "type": "integer",
            "format": "int32",
            "description": "The ticket's number in your server (1, 2, 3 and so on)."
          },
          "panelName": {
            "type": "string",
            "description": "The name of the ticket panel it was opened from."
          },
          "userId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member who opened it."
          },
          "username": {
            "type": "string",
            "description": "Their Discord username."
          },
          "channelId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The channel (or thread) made for the ticket."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "When it was opened."
          },
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The same member as `userId`, under the name the rest of the API uses."
          }
        }
      },
      "MemberActionData": {
        "type": "object",
        "description": "What a member did in one of your apps. Every member-action event carries these fields.",
        "required": [
          "userId",
          "app",
          "action",
          "targetType",
          "targetId",
          "targetName",
          "itemId",
          "quantity",
          "occurredAt",
          "discordId"
        ],
        "properties": {
          "userId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member."
          },
          "app": {
            "type": "string",
            "description": "The app, for example `polls`."
          },
          "action": {
            "type": "string",
            "description": "What they did in it, for example `voted`."
          },
          "targetType": {
            "type": [
              "string",
              "null"
            ],
            "description": "What it was done to, for example `poll`. `null` when the action has no target."
          },
          "targetId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The ID of the target, as a string. `null` when the action has no target."
          },
          "targetName": {
            "type": [
              "string",
              "null"
            ],
            "description": "The target's name. CEEJAY does not fill it in yet, so it is always `null`."
          },
          "itemId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The ID of the item in that app that a campaign or category is put on (for a ticket, its panel). `null` when the action belongs to no item."
          },
          "quantity": {
            "type": [
              "number",
              "null"
            ],
            "description": "A number that belongs to the action, for example the level reached. `null` when it has none."
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time",
            "description": "When it happened."
          },
          "details": {
            "type": "object",
            "additionalProperties": true,
            "description": "More about the action. Only present when there is more to say."
          },
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The same member as `userId`, under the name the rest of the API uses."
          }
        }
      },
      "TestPingData": {
        "type": "object",
        "description": "The test message.",
        "required": [
          "message",
          "subscriptionId",
          "subscriptionName"
        ],
        "properties": {
          "message": {
            "type": "string",
            "description": "Always `This is a test webhook from CEEJAY`."
          },
          "subscriptionId": {
            "type": "integer",
            "format": "int32",
            "description": "The subscription that was tested."
          },
          "subscriptionName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Its name."
          }
        }
      },
      "ShopDeliveryPayload": {
        "type": "object",
        "description": "The request CEEJAY sends to an item's URL. Unlike subscription webhooks, the field names are snake_case and there is no `data` wrapper.",
        "required": [
          "event",
          "delivery_id",
          "purchase_id",
          "item",
          "member",
          "price_paid",
          "currency",
          "server_id",
          "created_at"
        ],
        "properties": {
          "event": {
            "type": "string",
            "description": "`shop.purchase` for a purchase, `reward.earned` for a reward a member earned, `shop.test` for a test delivery.",
            "enum": [
              "shop.purchase",
              "reward.earned",
              "shop.test"
            ]
          },
          "delivery_id": {
            "type": "string",
            "description": "The delivery's ID: `shop_<purchase id>`, the same on every retry of the same purchase. De-duplicate on it. A test delivery has a `test_` ID."
          },
          "purchase_id": {
            "type": [
              "integer",
              "string"
            ],
            "description": "The purchase. For a reward a member earned it is the ID of that reward's grant. A test delivery has a `test_` string."
          },
          "item": {
            "type": "object",
            "description": "The item.",
            "required": [
              "id",
              "name",
              "item_key"
            ],
            "properties": {
              "id": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "The item's ID."
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The item's name."
              },
              "item_key": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The item key you set on the item, so you know what to unlock."
              }
            }
          },
          "member": {
            "type": "object",
            "description": "The member.",
            "required": [
              "discord_id",
              "username",
              "display_name",
              "connections"
            ],
            "properties": {
              "discord_id": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/Snowflake"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "The member's Discord ID."
              },
              "username": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Their Discord username."
              },
              "display_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The name shown for them."
              },
              "connections": {
                "type": "object",
                "description": "Their linked account ID on each platform, or `null` when they have not linked it.",
                "required": [
                  "steam",
                  "xbox",
                  "playstation",
                  "epic",
                  "riot",
                  "roblox",
                  "battlenet",
                  "github",
                  "twitch"
                ],
                "properties": {
                  "steam": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The member's steam ID."
                  },
                  "xbox": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The member's xbox ID."
                  },
                  "playstation": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The member's playstation ID."
                  },
                  "epic": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The member's epic ID."
                  },
                  "riot": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The member's riot ID."
                  },
                  "roblox": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The member's roblox ID."
                  },
                  "battlenet": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The member's battlenet ID."
                  },
                  "github": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The member's github ID."
                  },
                  "twitch": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The member's twitch ID."
                  }
                }
              }
            }
          },
          "price_paid": {
            "type": "number",
            "description": "What the member paid, in your currency. `0` for a reward and for a test."
          },
          "currency": {
            "type": "string",
            "description": "The name of your currency, for example `Coins`."
          },
          "server_id": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Snowflake"
              },
              {
                "type": "null"
              }
            ],
            "description": "Your server."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the purchase was made."
          },
          "reward": {
            "type": "object",
            "description": "Where a reward came from. Only on `reward.earned`.",
            "required": [
              "id",
              "source_kind",
              "source_id",
              "source_label"
            ],
            "properties": {
              "id": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "The reward's ID."
              },
              "source_kind": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "What earned it, for example `level` or `quest`."
              },
              "source_id": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The ID of what earned it."
              },
              "source_label": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The name of what earned it."
              }
            }
          },
          "player": {
            "type": "object",
            "description": "The player of a game that the item was bought for, as it was when the member bought it. Only on a purchase made through a game's shop (the game's Buy a shop item), never on a purchase made in Discord and never on a test delivery. It names a player your server already knows from Connect, so you can give the item to the right player: the game's slug, the game's own ID for the player, and how far the player was proven. It is read from the purchase, never from the connection as it is now, so a player who moves to another ID between the purchase and a retry does not redirect the item. A studio that only trusts proven players checks `proof` and answers `failed` for a `basic` player; the coins go back. A player deleted with Delete a player has no `player` on a retry. The signature covers the whole body, so a receiver that ignores the field is unaffected.",
            "required": [
              "game",
              "player_id",
              "proof"
            ],
            "properties": {
              "game": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The game's slug, as in the `game` of Get a player."
              },
              "player_id": {
                "type": "string",
                "description": "The game's own ID for the player, as it was connected."
              },
              "proof": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "basic",
                  "verified",
                  null
                ],
                "description": "How far the connection was proven when the item was bought: `basic` or `verified`."
              }
            }
          },
          "answer": {
            "description": "The member's answer, when the item asks a question and they gave one."
          },
          "input": {
            "type": "object",
            "additionalProperties": true,
            "description": "Extra details the member filled in, when the item asks for them."
          }
        }
      },
      "ShopDeliveryAnswer": {
        "type": "object",
        "description": "What your endpoint may answer with in the body of a 2xx. Any other body, or none, counts as delivered.",
        "properties": {
          "status": {
            "type": "string",
            "description": "`delivered` (the same as no body), `pending` (you will finish later) or `failed` (you cannot deliver: the member is refunded).",
            "enum": [
              "delivered",
              "pending",
              "failed"
            ]
          },
          "message": {
            "type": "string",
            "description": "A note shown to the team when you answer `failed`. Up to 200 characters are read."
          }
        }
      },
      "ErrorCode": {
        "type": "string",
        "description": "The fixed code of an error. The list is closed for version 1: a code is never renamed or removed. Match on the code, not on the status or on the message text in `error`. Three codes are in the list and returned by no request yet (`x-not-answered-yet`): `field_name_taken` is for approving a field of a game when the community already has fields of both names it could take, and no request a key makes can approve one; `player_already_connected` and `banned` are today only values of `reason` on a connect session that ended.",
        "enum": [
          "missing_api_key",
          "invalid_api_key",
          "api_key_revoked",
          "api_key_expired",
          "insufficient_scope",
          "method_not_allowed",
          "rate_limited",
          "internal_error",
          "bad_request",
          "not_available",
          "unknown_platform",
          "member_not_found",
          "quest_not_found",
          "program_not_found",
          "member_not_enrolled",
          "purchase_not_found",
          "channel_not_found",
          "role_not_found",
          "role_not_allowed",
          "bot_missing_permission",
          "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",
          "program_not_active",
          "member_status_not_allowed",
          "linked_by_sign_in",
          "player_already_linked",
          "wrong_status",
          "idempotency_key_reused",
          "idempotency_in_progress",
          "idempotency_outcome_unknown",
          "idempotency_unavailable",
          "xp_limit_reached",
          "balance_limit_reached",
          "discord_rate_limited",
          "discord_unavailable",
          "discord_not_configured",
          "discord_error",
          "roles_unavailable",
          "dashboard_roles_unavailable",
          "member_roles_unavailable",
          "channels_unavailable",
          "bot_unavailable",
          "bot_timeout",
          "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_allowed",
          "game_not_found",
          "code_not_found",
          "session_not_found",
          "player_not_found",
          "game_paused",
          "session_locked",
          "session_changed",
          "nothing_to_confirm",
          "player_blocked",
          "player_already_connected",
          "player_taken",
          "member_disconnected",
          "banned",
          "result_gone",
          "too_many_attempts",
          "proof_service_unavailable",
          "too_many_players",
          "too_many_events",
          "body_too_large",
          "activity_not_shared",
          "event_rate_exceeded",
          "field_name_taken",
          "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",
          "community_required",
          "community_not_covered"
        ],
        "x-not-answered-yet": [
          "field_name_taken",
          "player_already_connected",
          "banned"
        ]
      },
      "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",
            "minimum": 0,
            "description": "The page size that was used. On a list that is always returned whole (List roles, List channels) it is the number of items."
          }
        }
      },
      "NotAvailableErrorResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ErrorResponse"
          },
          {
            "type": "object",
            "required": [
              "feature"
            ],
            "properties": {
              "feature": {
                "type": "string",
                "description": "What this instance does not have yet, under the name Community details reports it. `players` is Connect. `member_history` is the member history, which Log player events needs: it is disabled on this instance, or its table is missing.",
                "enum": [
                  "economy",
                  "quests",
                  "programs",
                  "linking",
                  "events",
                  "shopDelivery",
                  "players",
                  "member_history"
                ]
              }
            }
          }
        ],
        "description": "The body of a `404` with the code `not_available`."
      },
      "QuestRun": {
        "type": "object",
        "description": "The run of the quest this call completed.",
        "required": [
          "id",
          "status",
          "startedAt",
          "completedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The run's ID, as a string of digits."
          },
          "status": {
            "type": "string",
            "enum": [
              "completed"
            ],
            "description": "Always `completed` here."
          },
          "startedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the run was started: now for a run this call added, earlier for one the member had started in Discord."
          },
          "completedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it was completed."
          }
        }
      },
      "QuestTaskQuest": {
        "type": "object",
        "description": "A quest this call touched.",
        "required": [
          "questId",
          "questTitle"
        ],
        "properties": {
          "questId": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int32",
            "description": "The quest's ID."
          },
          "questTitle": {
            "type": [
              "string",
              "null"
            ],
            "description": "The quest's title."
          }
        }
      },
      "QuestTaskResult": {
        "type": "object",
        "description": "What the report did.",
        "required": [
          "matched",
          "completed",
          "sentForReview",
          "duplicate"
        ],
        "properties": {
          "matched": {
            "type": "integer",
            "description": "Always 1 here: CEEJAY found a member and passed the report on.",
            "const": 1
          },
          "completed": {
            "type": "array",
            "description": "The quests this call finished. Empty when it finished none: the task was completed but the quest has more tasks, the report went to review, a rule stopped the completion, or no task had the key.",
            "items": {
              "$ref": "#/components/schemas/QuestTaskQuest"
            }
          },
          "sentForReview": {
            "type": "array",
            "description": "The quests whose task went to the Review Queue with this call.",
            "items": {
              "$ref": "#/components/schemas/QuestTaskQuest"
            }
          },
          "duplicate": {
            "type": "boolean",
            "description": "`true` when this response replays an earlier request with the same `idempotencyKey`: nothing was changed this time. `false` otherwise, and always `false` without a key."
          }
        }
      },
      "Role": {
        "type": "object",
        "description": "A role of your server.",
        "required": [
          "roleId",
          "name",
          "color",
          "position",
          "managed",
          "grantable",
          "notGrantableReason"
        ],
        "properties": {
          "roleId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The role. Use it as `roleId` in Grant a role and Revoke a role."
          },
          "name": {
            "type": "string",
            "description": "The role's name."
          },
          "color": {
            "type": "integer",
            "description": "The role's colour, as the number Discord stores (`0` for none)."
          },
          "position": {
            "type": "integer",
            "description": "The role's place in the server's role list. A higher number is a higher role."
          },
          "managed": {
            "type": "boolean",
            "description": "`true` for a role Discord or another app manages."
          },
          "grantable": {
            "type": "boolean",
            "description": "`true` when a key may give and take away this role through the API, as far as CEEJAY can tell."
          },
          "notGrantableReason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "managed",
              "can_moderate",
              "can_mention_everyone",
              "dashboard_access",
              "above_bot",
              null
            ],
            "description": "Why not, or `null` for a role a key may change. `managed`: Discord or another app manages it. `can_moderate`: it can moderate or manage the server. `can_mention_everyone`: it can mention everyone, and your server's `@everyone` role cannot. `dashboard_access`: it is one of your community's bot master roles. `above_bot`: it sits at or above the bot's own highest role, so Discord would reject the change."
          }
        }
      },
      "ListRolesResponse": {
        "type": "object",
        "description": "Your server's roles.",
        "required": [
          "success",
          "data",
          "page"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "type": "array",
            "description": "Every role of the server but `@everyone`, highest first.",
            "items": {
              "$ref": "#/components/schemas/Role"
            }
          },
          "page": {
            "$ref": "#/components/schemas/Page"
          }
        }
      },
      "Channel": {
        "type": "object",
        "description": "A channel or category of your server.",
        "required": [
          "channelId",
          "name",
          "type",
          "parentId",
          "position",
          "private",
          "sendable"
        ],
        "properties": {
          "channelId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The channel. Use it as `channelId` in Send a message."
          },
          "name": {
            "type": "string",
            "description": "The channel's name."
          },
          "type": {
            "type": "string",
            "enum": [
              "text",
              "announcement",
              "forum",
              "category"
            ],
            "description": "The kind of channel. Voice, stage and thread channels are left out of the list."
          },
          "parentId": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Snowflake"
              },
              {
                "type": "null"
              }
            ],
            "description": "The category the channel is in, or `null`."
          },
          "position": {
            "type": "integer",
            "description": "The channel's place, as Discord orders channels."
          },
          "private": {
            "type": "boolean",
            "description": "`true` when the channel's own permission settings hide it from `@everyone`."
          },
          "sendable": {
            "type": "boolean",
            "description": "`true` for a `text` or `announcement` channel: the kinds Send a message can post to. It does not say that the bot has permission in this channel."
          }
        }
      },
      "ListChannelsResponse": {
        "type": "object",
        "description": "Your server's channels.",
        "required": [
          "success",
          "data",
          "page"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "type": "array",
            "description": "The server's text, announcement and forum channels and its categories, in Discord's order.",
            "items": {
              "$ref": "#/components/schemas/Channel"
            }
          },
          "page": {
            "$ref": "#/components/schemas/Page"
          }
        }
      },
      "ShopPurchaseResult": {
        "type": "object",
        "description": "The purchase after your update.",
        "required": [
          "purchaseId",
          "status",
          "alreadyDone",
          "refunded",
          "duplicate"
        ],
        "properties": {
          "purchaseId": {
            "type": "integer",
            "description": "The purchase.",
            "format": "int32"
          },
          "status": {
            "type": "string",
            "enum": [
              "fulfilled",
              "refunded",
              "failed"
            ],
            "description": "Where the purchase ended up: `fulfilled` after `delivered`, `refunded` after `failed` on a purchase, `failed` after `failed` on a reward a member earned."
          },
          "alreadyDone": {
            "type": "boolean",
            "description": "`true` when the purchase was already in the state you asked for. Nothing changed this time."
          },
          "refunded": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "How much currency went back to the member, or `null` when this call refunded nothing."
          },
          "duplicate": {
            "type": "boolean",
            "description": "`true` when this response replays an earlier request with the same `idempotencyKey`: nothing was changed this time. `false` otherwise, and always `false` without a key."
          }
        }
      },
      "WaitingPurchase": {
        "type": "object",
        "description": "A purchase, or a reward a member earned, that is delivered by your API and still waits on your server.",
        "required": [
          "purchaseId",
          "deliveryId",
          "event",
          "state",
          "item",
          "discordId",
          "pricePaid",
          "createdAt",
          "pendingSince",
          "completeBy",
          "attempts",
          "lastAttemptAt",
          "answer",
          "input",
          "player"
        ],
        "properties": {
          "purchaseId": {
            "type": "integer",
            "description": "The purchase. Use it in Complete a shop delivery."
          },
          "deliveryId": {
            "type": "string",
            "description": "The delivery ID every delivery of this purchase carried: `shop_<purchaseId>`.",
            "examples": [
              "shop_4411"
            ]
          },
          "event": {
            "type": "string",
            "enum": [
              "shop.purchase",
              "reward.earned"
            ],
            "description": "The event the delivery carried: `shop.purchase` for a bought item, `reward.earned` for a reward a member earned."
          },
          "state": {
            "type": "string",
            "enum": [
              "processing",
              "retrying"
            ],
            "description": "`processing`: your server answered pending, and CEEJAY is awaiting Complete a shop delivery. It is never sent again. `retrying`: the delivery failed and is tried again every hour."
          },
          "item": {
            "type": "object",
            "description": "The item, as the delivery named it.",
            "required": [
              "id",
              "name",
              "itemKey"
            ],
            "properties": {
              "id": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "The item's ID."
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The item's name."
              },
              "itemKey": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The item key you set on the item (`item.item_key` in the delivery)."
              }
            }
          },
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The member."
          },
          "pricePaid": {
            "type": "integer",
            "description": "What the member paid. `0` for a reward a member earned."
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the purchase was made."
          },
          "pendingSince": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When your server answered pending. `null` while `retrying`."
          },
          "completeBy": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "24 hours after `pendingSince`: a purchase still waiting then is refunded by CEEJAY. `null` while `retrying`."
          },
          "attempts": {
            "type": "integer",
            "description": "While `retrying`: how many deliveries have failed so far. While `processing`: `1` or more, the delivery your server answered pending.",
            "minimum": 0
          },
          "lastAttemptAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "While `retrying`: when the last failed delivery was sent. While `processing`: the same time as `pendingSince`."
          },
          "answer": {
            "description": "What the delivery carried under `answer` (what the member typed when the item asks a question), or `null`."
          },
          "input": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "What the delivery carried under `input`, or `null`."
          },
          "player": {
            "anyOf": [
              {
                "type": "object",
                "description": "The player of a game that the item was bought for, as it was when the member bought it.",
                "required": [
                  "game",
                  "playerId",
                  "proof"
                ],
                "properties": {
                  "game": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The game's slug."
                  },
                  "playerId": {
                    "type": "string",
                    "description": "The game's own ID for the player, as it was connected."
                  },
                  "proof": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "basic",
                      "verified",
                      null
                    ],
                    "description": "How far the connection was proven when the item was bought: `basic` or `verified`."
                  }
                }
              },
              {
                "type": "null"
              }
            ],
            "description": "What the delivery carried under `player` (`player.player_id` there), in the one spelling of the API, or `null` for a purchase that was not made through a game. A player deleted with Delete a player is no longer named. A key that is limited to some games is told the player only on a purchase made through one of its games: on the purchases of any other game it reads `null`. A key with no games listed sees every player."
          },
          "reward": {
            "type": "object",
            "description": "Only on a reward a member earned (`event` is `reward.earned`): where it came from, as the delivery's `reward` object named it.",
            "required": [
              "id",
              "sourceKind",
              "sourceId",
              "sourceLabel"
            ],
            "properties": {
              "id": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "The reward's ID."
              },
              "sourceKind": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "What the member earned it from, for example `level` or `quest`."
              },
              "sourceId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The ID of that source, as text."
              },
              "sourceLabel": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The name of that source."
              }
            }
          }
        }
      },
      "ListShopPurchasesResponse": {
        "type": "object",
        "description": "A page of purchases that wait on your server.",
        "required": [
          "success",
          "data",
          "page"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "type": "array",
            "description": "The waiting purchases on this page, oldest first.",
            "items": {
              "$ref": "#/components/schemas/WaitingPurchase"
            }
          },
          "page": {
            "$ref": "#/components/schemas/Page"
          }
        }
      },
      "Program": {
        "type": "object",
        "description": "A community program.",
        "required": [
          "programId",
          "name",
          "slug",
          "description",
          "status",
          "type",
          "memberCount",
          "maxMembers",
          "createdAt"
        ],
        "properties": {
          "programId": {
            "type": "integer",
            "description": "The program. Use it as `programId` in the program XP operations.",
            "format": "int32"
          },
          "name": {
            "type": "string",
            "description": "The program's name."
          },
          "slug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The program's URL slug."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the program is for."
          },
          "status": {
            "type": "string",
            "description": "The program's status: `active`, `draft`, `paused` or `archived`. The program XP operations work only while it is `active`."
          },
          "type": {
            "type": [
              "string",
              "null"
            ],
            "description": "The kind of program, as it is stored. `custom` for a program that was not made from a template."
          },
          "memberCount": {
            "type": "integer",
            "description": "How many members are in the program with the status `accepted` or `active`: the two statuses the program XP operations accept.",
            "minimum": 0
          },
          "maxMembers": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The most members the program takes, or `null` for no limit."
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the program was made."
          }
        }
      },
      "ListProgramsResponse": {
        "type": "object",
        "description": "A page of programs.",
        "required": [
          "success",
          "data",
          "page"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "type": "array",
            "description": "The programs on this page, by `programId`, lowest first.",
            "items": {
              "$ref": "#/components/schemas/Program"
            }
          },
          "page": {
            "$ref": "#/components/schemas/Page"
          }
        }
      },
      "Community": {
        "type": "object",
        "description": "The community a key belongs to, what the key may do, and what this instance can serve.",
        "required": [
          "serverId",
          "name",
          "apiVersion",
          "key",
          "features",
          "platforms"
        ],
        "properties": {
          "serverId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The Discord server the key belongs to."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The server's name, or `null` when it could not be read."
          },
          "apiVersion": {
            "type": "string",
            "description": "The version of the API this instance serves.",
            "examples": [
              "1.1.0"
            ]
          },
          "key": {
            "type": "object",
            "description": "The key that made this call. Never the key itself.",
            "required": [
              "name",
              "scopes",
              "rateLimitPerMinute",
              "expiresAt"
            ],
            "properties": {
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The name the key was given in the dashboard."
              },
              "scopes": {
                "type": "array",
                "description": "The scopes the key holds.",
                "items": {
                  "type": "string"
                }
              },
              "rateLimitPerMinute": {
                "type": [
                  "integer",
                  "null"
                ],
                "minimum": 1,
                "maximum": 600,
                "description": "The key's requests per minute, the same number as `X-RateLimit-Limit`."
              },
              "expiresAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "When the key stops working, or `null` for a key that does not expire."
              }
            }
          },
          "features": {
            "type": "object",
            "description": "What this instance can serve. A request for a feature that reads `available: false` returns `404` with the code `not_available` (for `roles` and `messages`: `500` with the code `discord_not_configured`).",
            "required": [
              "xp",
              "economy",
              "quests",
              "programs",
              "linking",
              "roles",
              "messages",
              "events",
              "shopDelivery",
              "retryKeys",
              "players",
              "gameLoop"
            ],
            "properties": {
              "xp": {
                "type": "object",
                "description": "The XP operations. Always available.",
                "required": [
                  "available"
                ],
                "properties": {
                  "available": {
                    "type": "boolean",
                    "description": "`true` when this instance can serve the operations. While `false` they return `404` with the code `not_available`."
                  }
                }
              },
              "economy": {
                "type": "object",
                "description": "The currency operations.",
                "required": [
                  "available",
                  "enabled"
                ],
                "properties": {
                  "available": {
                    "type": "boolean",
                    "description": "`true` when this instance can serve the operations. While `false` they return `404` with the code `not_available`."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "The community's own switch for its economy. While `false`, Complete a quest does not pay a quest's coins. Grant currency and Deduct currency work either way."
                  }
                }
              },
              "quests": {
                "type": "object",
                "description": "The quest operations.",
                "required": [
                  "available",
                  "dailyQuestLimit"
                ],
                "properties": {
                  "available": {
                    "type": "boolean",
                    "description": "`true` when this instance can serve the operations. While `false` they return `404` with the code `not_available`."
                  },
                  "dailyQuestLimit": {
                    "type": "integer",
                    "description": "How many quests a member may complete in one UTC day. 5 unless the community set another."
                  }
                }
              },
              "programs": {
                "type": "object",
                "description": "List programs and the program XP operations.",
                "required": [
                  "available"
                ],
                "properties": {
                  "available": {
                    "type": "boolean",
                    "description": "`true` when this instance can serve the operations. While `false` they return `404` with the code `not_available`."
                  }
                }
              },
              "linking": {
                "type": "object",
                "description": "Link an account, and finding a member by an account in Complete a quest task.",
                "required": [
                  "available"
                ],
                "properties": {
                  "available": {
                    "type": "boolean",
                    "description": "`true` when this instance can serve the operations. While `false` they return `404` with the code `not_available`."
                  }
                }
              },
              "roles": {
                "type": "object",
                "description": "List roles, Grant a role and Revoke a role.",
                "required": [
                  "available"
                ],
                "properties": {
                  "available": {
                    "type": "boolean",
                    "description": "`true` when this instance can serve the operations. While `false` they return `404` with the code `not_available`."
                  }
                }
              },
              "messages": {
                "type": "object",
                "description": "List channels and Send a message.",
                "required": [
                  "available"
                ],
                "properties": {
                  "available": {
                    "type": "boolean",
                    "description": "`true` when this instance can serve the operations. While `false` they return `404` with the code `not_available`."
                  }
                }
              },
              "events": {
                "type": "object",
                "description": "Emit an event.",
                "required": [
                  "available"
                ],
                "properties": {
                  "available": {
                    "type": "boolean",
                    "description": "`true` when this instance can serve the operations. While `false` they return `404` with the code `not_available`."
                  }
                }
              },
              "shopDelivery": {
                "type": "object",
                "description": "List shop purchases and Complete a shop delivery.",
                "required": [
                  "available"
                ],
                "properties": {
                  "available": {
                    "type": "boolean",
                    "description": "`true` when this instance can serve the operations. While `false` they return `404` with the code `not_available`."
                  }
                }
              },
              "retryKeys": {
                "type": "object",
                "description": "Whether this instance stores an `idempotencyKey`. While `false`, a request that sends one returns `503` with the code `idempotency_unavailable`, except Grant currency, Deduct currency and Complete a quest for a quest that grants coins, which store their key with the currency movement.",
                "required": [
                  "available"
                ],
                "properties": {
                  "available": {
                    "type": "boolean",
                    "description": "`true` when this instance can serve the operations. While `false` they return `404` with the code `not_available`."
                  }
                }
              },
              "players": {
                "type": "object",
                "description": "Connect: the requests a game makes with a public key, the Connect requests of a studio's server, and the player requests (Get a player, List players, Disconnect a player, Delete a player, Set player fields, Log player events, Get the field map and Propose to the field map). Connect is enabled per instance, so this also reads `false` where it is not enabled.",
                "required": [
                  "available"
                ],
                "properties": {
                  "available": {
                    "type": "boolean",
                    "description": "`true` when this instance can serve the operations. While `false` they return `404` with the code `not_available`."
                  }
                }
              },
              "gameLoop": {
                "type": "object",
                "description": "The game loop: the requests a game makes to show the community's quests, shop, locker, rank and daily reward and to sell from its shop (the **Game loop** tag of `openapi-connect.json`). It reads `available: true` when Connect is on, the loop's tables and columns are there, the key CEEJAY calls its bot with is set, and the economy, quest and retry-key tables are there: one call shows an instance that missed a step. Whether a game has the loop on is that game's own setting, set by the community's team; while it is off the game's requests return `409` with the code `hub_not_enabled`.",
                "required": [
                  "available"
                ],
                "properties": {
                  "available": {
                    "type": "boolean",
                    "description": "`true` when this instance can serve the operations. While `false` they return `404` with the code `not_available`."
                  }
                }
              }
            }
          },
          "platforms": {
            "type": "object",
            "description": "The platforms Link an account takes.",
            "required": [
              "builtIn",
              "custom"
            ],
            "properties": {
              "builtIn": {
                "type": "array",
                "description": "The platforms CEEJAY knows by name.",
                "items": {
                  "type": "string"
                }
              },
              "custom": {
                "type": "array",
                "description": "The platforms this community added under **Settings → Developers → Platforms**.",
                "items": {
                  "type": "object",
                  "required": [
                    "slug",
                    "name"
                  ],
                  "properties": {
                    "slug": {
                      "type": "string",
                      "description": "The name to send as `platform`."
                    },
                    "name": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The platform's display name."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "CommunityResponse": {
        "type": "object",
        "description": "The community a key belongs to.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/Community"
          }
        }
      },
      "PlayerId": {
        "type": "string",
        "pattern": "^[A-Za-z0-9][A-Za-z0-9._:@|+=-]{0,127}$",
        "description": "A 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. It is sent in URL paths, so no other character is accepted: encode an ID that contains one the same way every time (base64url, for example). Matched case-insensitively, stored as sent.",
        "examples": [
          "p_19"
        ]
      },
      "ConnectMemberName": {
        "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."
          }
        }
      },
      "ForbiddenErrorResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ErrorResponse"
          },
          {
            "type": "object",
            "properties": {
              "required": {
                "type": "array",
                "description": "The scopes the operation needs. Only with `insufficient_scope`.",
                "items": {
                  "type": "string"
                }
              },
              "granted": {
                "type": "array",
                "description": "The scopes the key has. Only with `insufficient_scope`.",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        ],
        "description": "The body of a `403` on an operation that can also reject a game: with the code `insufficient_scope` it adds `required` and `granted`, as ScopeErrorResponse does, and with any other code it adds nothing."
      },
      "CreateConnectPassRequest": {
        "type": "object",
        "required": [
          "playerId"
        ],
        "properties": {
          "playerId": {
            "$ref": "#/components/schemas/PlayerId",
            "description": "The player your server has signed in and vouches for. The pass works for this player only."
          },
          "playerName": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 80,
            "description": "The player's name in the game, at most 80 characters. When the pass carries a name, that name is the one the member is shown, and a name the game sends with the pass is ignored. Spaces at either end are removed, and an empty name counts as none."
          }
        }
      },
      "ConnectPass": {
        "type": "object",
        "description": "A connect pass: your server's word for one player.",
        "required": [
          "pass",
          "expiresAt"
        ],
        "properties": {
          "pass": {
            "type": "string",
            "pattern": "^ceejay_pass_[0-9a-f]{64}$",
            "description": "The pass. Send it to your game, which sends it as `proof` when it starts a connect session. It is shown once: CEEJAY stores only a hash. It works once, for 10 minutes, for this game and this player."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the pass stops working: 10 minutes after it was created."
          }
        }
      },
      "ConnectPassResponse": {
        "type": "object",
        "description": "A new connect pass.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/ConnectPass"
          }
        }
      },
      "CreateConnectSessionRequest": {
        "type": "object",
        "required": [
          "playerId"
        ],
        "properties": {
          "playerId": {
            "$ref": "#/components/schemas/PlayerId",
            "description": "The player your server has signed in. The secret key is the proof, so the session is at the verified level."
          },
          "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."
          },
          "code": {
            "type": [
              "string",
              "null"
            ],
            "description": "A code a member created in Discord and entered in the game. Send it to attach this player to that member's session: no new session is created, and the response names the member. 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`, stores nothing, and ends a connection this player has."
          },
          "confirmInGame": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "`true`, the default: the game confirms later, with the session secret or through your server with Confirm a connect session. `false` confirms on the game's behalf now, so the session finishes when the member approves. Send `false` only when the link is opened on the device the player is signed in on and the code is never shown where others can read it."
          }
        }
      },
      "StartedConnectSession": {
        "type": "object",
        "description": "A connect session your server started, or the session of a member's code your server attached a player to.",
        "required": [
          "sessionId",
          "url",
          "qrUrl",
          "code",
          "sessionSecret",
          "expiresAt",
          "status",
          "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."
          },
          "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 screen a player cannot click on. `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 a game needs to read this session's result and to confirm it. Returned once. Share it with your game only when the game should hold the player's tokens."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the session ends unless it is finished: 10 minutes after it was created."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "awaiting_player",
              "needs_game_confirmation",
              "needs_confirmation",
              "approved",
              "declined",
              "cancelled",
              "expired",
              "locked",
              "refused"
            ],
            "description": "The current state of the session. `pending`: waiting for the member to approve. `needs_game_confirmation`: the member approved, or the game attached a member's code, and the session is waiting for the game to confirm. `needs_confirmation`: a session a member started in Discord that the game has confirmed, waiting for the member to confirm there. `approved`: both approved and the connection is saved. `declined`, `cancelled`, `expired`, `locked` and `refused` are ends: see `reason`. `awaiting_player` is a session a member started in Discord before any game took its code. This list may grow: handle unknown values."
          },
          "alreadyConnected": {
            "type": "boolean",
            "description": "`true` when the session was approved immediately because this player is already connected at the verified level, with the member's own approval. `status` is then `approved` and no one is asked again."
          },
          "member": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ConnectMemberName"
              },
              {
                "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."
          }
        }
      },
      "StartedConnectSessionResponse": {
        "type": "object",
        "description": "The session.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/StartedConnectSession"
          }
        }
      },
      "ConnectSession": {
        "type": "object",
        "description": "A connect session as a studio's server reads it.",
        "required": [
          "sessionId",
          "status",
          "reason",
          "expiresAt",
          "member",
          "connection"
        ],
        "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",
              "awaiting_player",
              "needs_game_confirmation",
              "needs_confirmation",
              "approved",
              "declined",
              "cancelled",
              "expired",
              "locked",
              "refused"
            ],
            "description": "The current state of the session. `pending`: waiting for the member to approve. `needs_game_confirmation`: the member approved, or the game attached a member's code, and the session is waiting for the game to confirm. `needs_confirmation`: a session a member started in Discord that the game has confirmed, waiting for the member to confirm there. `approved`: both approved and the connection is saved. `declined`, `cancelled`, `expired`, `locked` and `refused` are ends: see `reason`. `awaiting_player` is a session a member started in Discord before any game took its code. This list may grow: handle unknown values."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "member_cancelled",
              "game_declined",
              "banned",
              "player_blocked",
              "player_already_connected",
              "expired",
              "too_many_tries",
              "under_age",
              null
            ],
            "description": "Why a session ended, or `null` while it is open, when it is approved, when it was superseded by a newer session for the same player, and when 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."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the session ends unless it is finished. A session past this time reads `expired`."
          },
          "member": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ConnectMemberName"
              },
              {
                "type": "null"
              }
            ],
            "description": "The Discord member, from `needs_game_confirmation` on. `null` before anyone said who they are, and on a session that ended without it."
          },
          "connection": {
            "anyOf": [
              {
                "type": "object",
                "required": [
                  "discordId",
                  "playerId",
                  "proof",
                  "connectedAt",
                  "moved",
                  "previousPlayerId"
                ],
                "properties": {
                  "discordId": {
                    "$ref": "#/components/schemas/Snowflake",
                    "description": "The member the player is connected to."
                  },
                  "playerId": {
                    "type": "string",
                    "description": "The player, as it is stored."
                  },
                  "proof": {
                    "type": "string",
                    "enum": [
                      "basic",
                      "verified"
                    ],
                    "description": "The level of the connection. A session your server starts is `verified`."
                  },
                  "connectedAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When the connection was made."
                  },
                  "moved": {
                    "type": "boolean",
                    "description": "`true` when the member held another player in this game and moved to this one."
                  },
                  "previousPlayerId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The player the member moved from, only when both connections were verified. `null` otherwise."
                  }
                }
              },
              {
                "type": "null"
              }
            ],
            "description": "The connection, once `status` is `approved`. `null` before, and when the connection has been removed since."
          }
        }
      },
      "ConnectSessionResponse": {
        "type": "object",
        "description": "One connect session.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/ConnectSession"
          }
        }
      },
      "ListConnectSessionsResponse": {
        "type": "object",
        "description": "The sessions that were asked for and belong to this game.",
        "required": [
          "success",
          "data",
          "page"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "type": "array",
            "description": "The sessions, oldest first. An ID of another game or community is left out.",
            "items": {
              "$ref": "#/components/schemas/ConnectSession"
            }
          },
          "page": {
            "$ref": "#/components/schemas/Page"
          }
        }
      },
      "ConfirmConnectSessionRequest": {
        "type": "object",
        "required": [
          "accept"
        ],
        "properties": {
          "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."
          }
        }
      },
      "PlayerMembership": {
        "type": "string",
        "enum": [
          "member",
          "pending",
          "not_member",
          "left"
        ],
        "description": "Whether the member is in the community's Discord server. `member`: in the server. `pending`: in the server, but Discord's membership screening is not complete. `not_member`: never joined. `left`: joined and left. CEEJAY reads this from the data its bot stores, never from Discord at the time of the request. This list may grow: handle unknown values."
      },
      "PlayerSharing": {
        "type": "object",
        "description": "What the member has consented to share with the game.",
        "required": [
          "agreed",
          "needed"
        ],
        "properties": {
          "agreed": {
            "type": "array",
            "description": "The share categories the game uses and the member has consented to. Empty for a connection your own server created with a secret key: the member was never asked, and your server is responsible for the consent.",
            "items": {
              "type": "string",
              "enum": [
                "progress",
                "activity",
                "purchases",
                "location"
              ]
            }
          },
          "needed": {
            "type": "array",
            "description": "Share categories the game added since the member consented. The member consents to them on their connections page. Values in them are not accepted until then.",
            "items": {
              "type": "string",
              "enum": [
                "progress",
                "activity",
                "purchases",
                "location"
              ]
            }
          }
        }
      },
      "Player": {
        "type": "object",
        "description": "A player of a game who is connected to a Discord member.",
        "required": [
          "playerId",
          "discordId",
          "playerName",
          "proof",
          "origin",
          "connectedAt",
          "membership"
        ],
        "properties": {
          "playerId": {
            "type": "string",
            "description": "The game's own ID for the player, as it is stored."
          },
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The Discord member the player is connected to."
          },
          "playerName": {
            "type": [
              "string",
              "null"
            ],
            "description": "The player's name in the game, or `null` when none was sent."
          },
          "proof": {
            "type": "string",
            "enum": [
              "basic",
              "verified"
            ],
            "description": "How far the player ID can be trusted. `verified`: the player was verified, by the game (a pass from your server or a sign-in token) or by your own server linking it. `basic`: the member connected with a player ID the game supplied and CEEJAY did not verify, so nothing confirms that the player is theirs."
          },
          "origin": {
            "type": "string",
            "enum": [
              "connect",
              "api"
            ],
            "description": "`connect`: the member created the connection themselves through Connect. `api`: your own server linked the player with Link an account."
          },
          "connectedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the connection was made."
          },
          "membership": {
            "$ref": "#/components/schemas/PlayerMembership"
          }
        }
      },
      "PlayerCard": {
        "type": "object",
        "description": "The member the player is connected to, as CEEJAY stores them. Read from the data the bot stores, never from Discord at the time of the request.",
        "required": [
          "displayName",
          "avatarUrl",
          "level",
          "xp",
          "coins"
        ],
        "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."
          },
          "level": {
            "type": "integer",
            "minimum": 0,
            "description": "The member's level in the community, `0` before the first."
          },
          "xp": {
            "type": "integer",
            "minimum": 0,
            "description": "The member's XP in the community."
          },
          "coins": {
            "type": "integer",
            "minimum": 0,
            "description": "The member's currency balance in the community."
          }
        }
      },
      "PlayerDetail": {
        "type": "object",
        "description": "One player: who they are connected to, the member summary, and the game's approved fields that are not sensitive.",
        "required": [
          "player",
          "member",
          "fields"
        ],
        "properties": {
          "player": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Player"
              },
              {
                "type": "object",
                "required": [
                  "sharing"
                ],
                "properties": {
                  "sharing": {
                    "$ref": "#/components/schemas/PlayerSharing"
                  }
                }
              }
            ],
            "description": "The player, with what the member shares."
          },
          "member": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PlayerCard"
              },
              {
                "type": "null"
              }
            ],
            "description": "The member summary. `null` when CEEJAY has no profile for the member in this community, and unless `player.membership` is `member` or `pending`: consent covers server members only."
          },
          "fields": {
            "type": "object",
            "description": "The values CEEJAY stores for this player, by the fixed key of each field in the field map (`std.level`, or the key of a field of the game's own). Only fields that are approved and not sensitive are here, and only those with a current value. Number fields return numbers and yes or no fields return `true` or `false`. A sensitive field is never returned to any key. Empty (`{}`) unless `player.membership` is `member` or `pending`.",
            "additionalProperties": {
              "type": [
                "string",
                "number",
                "boolean"
              ],
              "description": "The current value of one field."
            }
          }
        }
      },
      "PlayerDetailResponse": {
        "type": "object",
        "description": "One player.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/PlayerDetail"
          }
        }
      },
      "ListPlayersResponse": {
        "type": "object",
        "description": "A page of the game's connected players, newest connection first.",
        "required": [
          "success",
          "data",
          "page"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "type": "array",
            "description": "The players of this page.",
            "items": {
              "$ref": "#/components/schemas/Player"
            }
          },
          "page": {
            "$ref": "#/components/schemas/Page"
          }
        }
      },
      "PlayerNotFoundErrorResponse": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ErrorResponse"
          },
          {
            "type": "object",
            "properties": {
              "held": {
                "type": "boolean",
                "const": true,
                "description": "Only on `player_not_found`, and only when CEEJAY holds data for this player ID that is waiting for a connection (the game holds data for players who have not connected). It is held, not stored on any member, and not returned."
              }
            }
          }
        ],
        "description": "The body of a `404` on Get a player. With `player_not_found` it can add `held`."
      },
      "DisconnectPlayerResult": {
        "type": "object",
        "description": "The connection was ended.",
        "required": [
          "disconnected"
        ],
        "properties": {
          "disconnected": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          }
        }
      },
      "DisconnectPlayerResponse": {
        "type": "object",
        "description": "The connection was ended.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/DisconnectPlayerResult"
          }
        }
      },
      "DeletePlayerResult": {
        "type": "object",
        "description": "What was deleted for the player.",
        "required": [
          "deleted",
          "removed"
        ],
        "properties": {
          "deleted": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success, also when there was nothing to delete."
          },
          "removed": {
            "type": "object",
            "description": "How many of each were removed. All zeros when CEEJAY held nothing for the player.",
            "required": [
              "connection",
              "values",
              "events",
              "pending",
              "purchases"
            ],
            "properties": {
              "connection": {
                "type": "integer",
                "minimum": 0,
                "description": "Connections of this player in this game that were removed, the ended ones included."
              },
              "values": {
                "type": "integer",
                "minimum": 0,
                "description": "Values of the game's fields, current and older, that were removed from the member the player was connected to. It also counts the values of the game's player ID and player name fields that named this player before the connection was moved."
              },
              "events": {
                "type": "integer",
                "minimum": 0,
                "description": "Lines of the member history that were removed: the player's game events, and the lines that say the player connected or disconnected."
              },
              "pending": {
                "type": "integer",
                "minimum": 0,
                "description": "Rows removed from what the game held for a player who had not connected: the player and the events held for them."
              },
              "purchases": {
                "type": "integer",
                "minimum": 0,
                "description": "Purchases made through this game by the member linked to the player, from which the player ID, proof, connection and device were removed. The purchases and the coins they cost stay: they belong to the member. A delivery sent again after this names no player. Always present, and `0` on an instance where the game loop is not enabled."
              }
            }
          }
        }
      },
      "DeletePlayerResponse": {
        "type": "object",
        "description": "What was deleted.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/DeletePlayerResult"
          }
        }
      },
      "SetPlayerFieldsPlayer": {
        "type": "object",
        "description": "The values to set for one player. Name the player with exactly one of `playerId` or `discordId`.",
        "properties": {
          "playerId": {
            "$ref": "#/components/schemas/PlayerId",
            "description": "The player, by the game's own ID. Send exactly one of `playerId` or `discordId`."
          },
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The player, by the Discord member they are connected to. Send exactly one of `playerId` or `discordId`. A member with no connection to this game is `not_connected`."
          },
          "fields": {
            "type": "object",
            "description": "The values to set, as `{ \"<name>\": <value> }`, up to 40. A name is matched to the name the game uses for the field (the `sourceKey` of the field map) first, then to the field's key, case-insensitively. A value is text, a number, `true` or `false`, or `null` to clear the field. A field that is not in the object is left as it is. Required unless the entry says `underAge: true`.",
            "maxProperties": 40,
            "additionalProperties": {
              "type": [
                "string",
                "number",
                "boolean",
                "null"
              ],
              "description": "A value: text, a finite number, `true` or `false`. `null` clears the field. An object or a list is refused for that field."
            }
          },
          "underAge": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "`true` says the player is under the minimum age allowed to connect. Nothing is stored, the player's connection is ended with the reason `under_age`, and anything held for the player is removed. The player gets the status `under_age`."
          }
        }
      },
      "SetPlayerFieldsRequest": {
        "type": "object",
        "required": [
          "players"
        ],
        "properties": {
          "players": {
            "type": "array",
            "minItems": 1,
            "maxItems": 500,
            "description": "The players, 1 to 500. The body, with all of them, can be up to 512 KB.",
            "items": {
              "$ref": "#/components/schemas/SetPlayerFieldsPlayer"
            }
          },
          "dryRun": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "`true` runs every check and says what would happen to each value, and writes nothing: no value, no proposed field and no held player."
          }
        }
      },
      "SetPlayerFieldsResult": {
        "type": "object",
        "description": "What happened to each player, in the order you sent them, and the totals. A batch with problems still returns `200`.",
        "required": [
          "results",
          "summary"
        ],
        "properties": {
          "results": {
            "type": "array",
            "description": "One entry for each player you sent.",
            "items": {
              "type": "object",
              "required": [
                "index",
                "status",
                "written",
                "unchanged",
                "cleared",
                "problems"
              ],
              "properties": {
                "index": {
                  "type": "integer",
                  "minimum": 0,
                  "description": "Where the player stood in `players`, from 0."
                },
                "playerId": {
                  "type": "string",
                  "description": "The game's ID for the player. Only when you named the player by `playerId`, and then it is the only name given back. Missing on `bad_item`."
                },
                "discordId": {
                  "$ref": "#/components/schemas/Snowflake",
                  "description": "The Discord member. Only when you named the player by `discordId`, and then it is the only name given back."
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "written",
                    "unchanged",
                    "held",
                    "held_limit",
                    "not_connected",
                    "not_member",
                    "erased",
                    "under_age",
                    "bad_item"
                  ],
                  "description": "`written`: at least one value changed. `unchanged`: every value was already the current one. `held`: the player has not connected and the game holds data for players who have not, so the values are held until the player connects. `held_limit`: the held store is full for this game. `not_connected`: no active connection exists for this player. `not_member`: the player is connected, but the member is not in the server, and values are stored for members only. `erased`: the player was erased and is never held. `under_age`: you sent `underAge: true`. `bad_item`: the entry could not be read: see `message`. This list may grow: handle unknown values."
                },
                "message": {
                  "type": "string",
                  "description": "Only with `bad_item`: what was wrong with the entry."
                },
                "written": {
                  "type": "integer",
                  "minimum": 0,
                  "description": "Values that were new or changed. The older value stays as history."
                },
                "unchanged": {
                  "type": "integer",
                  "minimum": 0,
                  "description": "Values that were already the current one."
                },
                "cleared": {
                  "type": "integer",
                  "minimum": 0,
                  "description": "Values that were cleared with `null`."
                },
                "problems": {
                  "type": "array",
                  "description": "Fields that could not be set for this player. The other fields of the player were still set.",
                  "items": {
                    "type": "object",
                    "required": [
                      "field",
                      "code"
                    ],
                    "properties": {
                      "field": {
                        "type": "string",
                        "description": "The name you sent for the field."
                      },
                      "code": {
                        "type": "string",
                        "enum": [
                          "unknown_field",
                          "not_approved",
                          "unused",
                          "bad_value",
                          "not_a_choice",
                          "not_consented",
                          "needs_verified_proof",
                          "managed"
                        ],
                        "description": "`unknown_field`: the game already has 50 proposed fields, so this name was not added. `not_approved`: the field is proposed and waiting for approval by the community's team (or the name was new, and is now proposed). `unused`: the team marked the field unused. `bad_value`: the value does not fit the field's type, or is out of range. `not_a_choice`: the value is not one of the choices of a dropdown field (it is offered to the team). `not_consented`: the member has not consented to the field's category. `needs_verified_proof`: the player is connected at the basic level, and a field of the `purchases` or `location` category is not stored for a connection whose player ID the game supplied and CEEJAY did not verify; the player's other fields are still written. `managed`: the field is filled from the connection and cannot be set. This list may grow: handle unknown values."
                      }
                    }
                  }
                }
              }
            }
          },
          "summary": {
            "type": "object",
            "description": "Totals over the whole call.",
            "required": [
              "players",
              "written",
              "unchanged",
              "cleared",
              "held",
              "refused"
            ],
            "properties": {
              "players": {
                "type": "integer",
                "minimum": 0,
                "description": "Players in the call."
              },
              "written": {
                "type": "integer",
                "minimum": 0,
                "description": "Values written, over all players."
              },
              "unchanged": {
                "type": "integer",
                "minimum": 0,
                "description": "Values that were already the current one."
              },
              "cleared": {
                "type": "integer",
                "minimum": 0,
                "description": "Values cleared."
              },
              "held": {
                "type": "integer",
                "minimum": 0,
                "description": "Players whose values are held."
              },
              "refused": {
                "type": "integer",
                "minimum": 0,
                "description": "Players with a status that stored nothing: `not_connected`, `not_member`, `erased`, `under_age`, `bad_item` and `held_limit`."
              }
            }
          }
        }
      },
      "SetPlayerFieldsResponse": {
        "type": "object",
        "description": "What happened to each player.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/SetPlayerFieldsResult"
          }
        }
      },
      "LogPlayerEvent": {
        "type": "object",
        "description": "One thing a player did in the game. Name the player with exactly one of `playerId` or `discordId`.",
        "required": [
          "eventId",
          "event"
        ],
        "properties": {
          "eventId": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_.:-]{1,64}$",
            "description": "Your own ID for this event: 1 to 64 letters, digits and `. _ : -`. It is what makes a retry safe: events are idempotent on `eventId`, so a repeated ID is ignored. One event ID belongs to one player."
          },
          "event": {
            "type": "string",
            "maxLength": 80,
            "description": "What happened, such as `match_won`: up to 80 characters. It is matched to the name the game uses for the event (the `sourceKey` of the field map) first, then to the event's key, case-insensitively."
          },
          "playerId": {
            "$ref": "#/components/schemas/PlayerId",
            "description": "The player, by the game's own ID. Send exactly one of `playerId` or `discordId`."
          },
          "discordId": {
            "$ref": "#/components/schemas/Snowflake",
            "description": "The player, by the Discord member they are connected to. Send exactly one of `playerId` or `discordId`."
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time",
            "description": "When it happened. At most 5 minutes ahead of now and at most 7 days back. Without it, the time CEEJAY received the call."
          },
          "quantity": {
            "type": [
              "number",
              "null"
            ],
            "description": "A number that belongs to the event, such as a score. It is stored only where the event's entry in the field map keeps a quantity. Beyond 1e15 either way it is refused."
          },
          "underAge": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "`true` says the player is below the age your game may connect. Nothing is stored, and the player's connection is ended with the reason `under_age`."
          }
        }
      },
      "LogPlayerEventsRequest": {
        "type": "object",
        "required": [
          "events"
        ],
        "properties": {
          "events": {
            "type": "array",
            "minItems": 1,
            "maxItems": 500,
            "description": "The events, 1 to 500. The body, with all of them, can be up to 512 KB.",
            "items": {
              "$ref": "#/components/schemas/LogPlayerEvent"
            }
          },
          "dryRun": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "`true` runs every check and says what would happen to each event, and writes and counts nothing."
          }
        }
      },
      "LogPlayerEventsResult": {
        "type": "object",
        "description": "What happened to each event, in the order you sent them, and the totals. A batch with problems still returns `200`. The rewards an event earns are queued and granted by the bot asynchronously.",
        "required": [
          "results",
          "summary"
        ],
        "properties": {
          "results": {
            "type": "array",
            "description": "One entry for each event you sent.",
            "items": {
              "type": "object",
              "required": [
                "index",
                "eventId",
                "status",
                "reward"
              ],
              "properties": {
                "index": {
                  "type": "integer",
                  "minimum": 0,
                  "description": "Where the event stood in `events`, from 0."
                },
                "eventId": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "The event ID you sent, or `null` when the entry had none that could be read."
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "logged",
                    "duplicate",
                    "held",
                    "held_limit",
                    "not_connected",
                    "not_member",
                    "not_consented",
                    "erased",
                    "under_age",
                    "not_approved",
                    "unknown_event",
                    "too_old",
                    "in_future",
                    "bad_item"
                  ],
                  "description": "`logged`: the event is in the member history. `duplicate`: this event ID was logged before. `held`: the player has not connected and the game holds events for players who have not. `held_limit`: the held store is full, or the player already has 50 events held. `not_connected`: no active connection exists for this player. `not_member`: connected, but not in the server: events are stored for members only. `not_consented`: the member has not consented to sharing the `activity` category, because they connected before the game shared it and have not consented since on their connections page; a connection your own server created carries no consent and is not checked. `erased`: the player was erased. `under_age`: you sent `underAge: true`. `not_approved`: the event is not approved in the field map; it is now proposed, and can be sent again after approval, within 7 days. `unknown_event`: the game already has 50 proposed events. `too_old`: older than 7 days. `in_future`: more than 5 minutes ahead. `bad_item`: the entry could not be read: see `message`. This list may grow: handle unknown values."
                },
                "message": {
                  "type": "string",
                  "description": "Only with `bad_item`: what was wrong with the entry."
                },
                "reward": {
                  "type": "string",
                  "enum": [
                    "queued",
                    "none"
                  ],
                  "description": "`queued`: this event earns a reward that the bot grants asynchronously. `none`: it earns nothing."
                }
              }
            }
          },
          "summary": {
            "type": "object",
            "description": "Totals over the whole call.",
            "required": [
              "logged",
              "duplicate",
              "held",
              "refused",
              "rewardsQueued"
            ],
            "properties": {
              "logged": {
                "type": "integer",
                "minimum": 0,
                "description": "Events that were logged."
              },
              "duplicate": {
                "type": "integer",
                "minimum": 0,
                "description": "Events that were logged before."
              },
              "held": {
                "type": "integer",
                "minimum": 0,
                "description": "Events that are held for a player who has not connected."
              },
              "refused": {
                "type": "integer",
                "minimum": 0,
                "description": "Events with a status that stored nothing: not connected, not a member, not consented, erased, under age, not approved, unknown, too old, in the future, bad or held over the limit."
              },
              "rewardsQueued": {
                "type": "integer",
                "minimum": 0,
                "description": "Rewards that were queued."
              }
            }
          }
        }
      },
      "LogPlayerEventsResponse": {
        "type": "object",
        "description": "What happened to each event.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/LogPlayerEventsResult"
          }
        }
      },
      "FieldMapField": {
        "type": "object",
        "description": "One field in a game's field map.",
        "required": [
          "key",
          "sourceKey",
          "name",
          "type",
          "options",
          "unit",
          "scale",
          "category",
          "standard",
          "sensitive",
          "state"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "The fixed key CEEJAY knows the field by: `std.level` for a standard field, or a lower-case key of up to 40 characters for a field of the game's own. It is the key in `fields` of Get a player."
          },
          "sourceKey": {
            "type": [
              "string",
              "null"
            ],
            "description": "The name your game uses for the field. `null` for a standard field your game has not sent yet."
          },
          "name": {
            "type": "string",
            "description": "The field's name on CEEJAY's screens. The community's team can change it."
          },
          "type": {
            "type": "string",
            "enum": [
              "text",
              "number",
              "date",
              "choice",
              "yes_no",
              "ordered"
            ],
            "description": "What the field holds: `text`, `number`, `date`, `choice` (a dropdown), `yes_no` or `ordered` (choices that have an order). It never changes once the field is approved."
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The choices of a `choice` or `ordered` field, in order. Empty for every other type."
          },
          "unit": {
            "type": [
              "string",
              "null"
            ],
            "description": "The unit of a number, such as `hours`, or `null`."
          },
          "scale": {
            "type": [
              "number",
              "null"
            ],
            "description": "A number your game sends is multiplied by this before it is stored: `0.01` turns cents into dollars. `null` for none."
          },
          "category": {
            "type": "string",
            "enum": [
              "progress",
              "purchases",
              "location"
            ],
            "description": "The share category of the field. A member agrees to `progress` by connecting, and ticks `purchases` and `location` one by one."
          },
          "standard": {
            "type": "boolean",
            "description": "`true` for one of the 14 standard fields, which do not count towards the game's 25."
          },
          "sensitive": {
            "type": "boolean",
            "description": "`true` when the field is sensitive: it is never returned to a key, never used in a segment or a filter, and hidden from team members who are not admins."
          },
          "state": {
            "type": "string",
            "enum": [
              "proposed",
              "approved",
              "unused"
            ],
            "description": "`proposed`: waiting for the community's team (a standard field that carries no choices is approved by the first value). `approved`: values are written. `unused`: the team left it unused, and nothing is written."
          }
        }
      },
      "FieldMapEvent": {
        "type": "object",
        "description": "One event in a game's field map.",
        "required": [
          "key",
          "sourceKey",
          "label",
          "keepsQuantity",
          "state",
          "rewardMode"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "The fixed key of the event: lower case, up to 40 characters."
          },
          "sourceKey": {
            "type": [
              "string",
              "null"
            ],
            "description": "The name your game uses for the event."
          },
          "label": {
            "type": "string",
            "description": "What the event reads on CEEJAY's screens, such as \"Won a match\"."
          },
          "keepsQuantity": {
            "type": "boolean",
            "description": "`true` when the quantity you send with the event is stored. It is off by default."
          },
          "state": {
            "type": "string",
            "enum": [
              "proposed",
              "approved",
              "unused"
            ],
            "description": "`proposed`, `approved` or `unused`, as for a field. Only an approved event is logged."
          },
          "rewardMode": {
            "type": "string",
            "enum": [
              "once",
              "each"
            ],
            "description": "How an approved event's reward is granted, when it has one: `once` to each member, or `each` time it happens."
          }
        }
      },
      "FieldMap": {
        "type": "object",
        "description": "A game's field map. No value of a player is in it.",
        "required": [
          "fields",
          "events",
          "limits"
        ],
        "properties": {
          "fields": {
            "type": "array",
            "description": "Every field, the 14 standard ones included, with the state each is in.",
            "items": {
              "$ref": "#/components/schemas/FieldMapField"
            }
          },
          "events": {
            "type": "array",
            "description": "Every event, with the state each is in.",
            "items": {
              "$ref": "#/components/schemas/FieldMapEvent"
            }
          },
          "limits": {
            "type": "object",
            "description": "How many a game can have approved, besides the standard fields.",
            "required": [
              "ownFields",
              "events"
            ],
            "properties": {
              "ownFields": {
                "type": "integer",
                "description": "Approved fields of the game's own."
              },
              "events": {
                "type": "integer",
                "description": "Approved events."
              }
            }
          }
        }
      },
      "FieldMapResponse": {
        "type": "object",
        "description": "The field map.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/FieldMap"
          }
        }
      },
      "ProposeFieldMapRequest": {
        "type": "object",
        "description": "Send at least one field or event, in `sample`, `fields` or `events`.",
        "properties": {
          "sample": {
            "type": "object",
            "description": "Names with an example value. The type of each field is inferred from its value by fixed rules, and the value is stored as a sample for the team (truncated to 80 characters).",
            "properties": {
              "fields": {
                "type": "object",
                "description": "Field names with an example value each, up to 100: `{ \"league_position\": 3 }`.",
                "additionalProperties": {
                  "type": [
                    "string",
                    "number",
                    "boolean",
                    "null"
                  ],
                  "description": "An example value."
                }
              },
              "events": {
                "type": "array",
                "maxItems": 100,
                "description": "Event names, up to 100.",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "fields": {
            "type": "array",
            "maxItems": 100,
            "description": "Fields to propose, up to 100. An entry says more than a sample of the same name, and wins over it.",
            "items": {
              "type": "object",
              "required": [
                "sourceKey"
              ],
              "properties": {
                "sourceKey": {
                  "type": "string",
                  "maxLength": 120,
                  "description": "The name your game uses for the field: 1 to 120 characters."
                },
                "key": {
                  "type": "string",
                  "pattern": "^[a-z][a-z0-9_]{0,39}$",
                  "description": "The key to give the field: a lower-case letter, then up to 39 lower-case letters, digits or underscores. Generated from the name when left out."
                },
                "name": {
                  "type": "string",
                  "description": "The name for CEEJAY's screens, up to 60 characters. Generated from the name your game uses when left out."
                },
                "type": {
                  "type": "string",
                  "enum": [
                    "text",
                    "number",
                    "date",
                    "choice",
                    "yes_no",
                    "ordered"
                  ],
                  "description": "The type. Worked out from the value of a sample when left out, and `text` without one."
                },
                "unit": {
                  "type": "string",
                  "maxLength": 20,
                  "description": "The unit of a number, up to 20 characters."
                },
                "scale": {
                  "type": "number",
                  "description": "What a number is multiplied by when stored, above 0 and at most 1,000,000,000. A name that ends in `_cents` proposes `0.01`."
                },
                "options": {
                  "type": "array",
                  "maxItems": 50,
                  "description": "The choices of a `choice` or `ordered` field: 1 to 50 texts of up to 80 characters.",
                  "items": {
                    "type": "string"
                  }
                },
                "standard": {
                  "type": "string",
                  "pattern": "^std\\.[a-z_]+$",
                  "description": "The key of the standard field this one is, such as `std.level`, when the name alone would not say. A name that is a known alias of a standard field maps to it without this."
                }
              }
            }
          },
          "events": {
            "type": "array",
            "maxItems": 100,
            "description": "Events to propose, up to 100.",
            "items": {
              "type": "object",
              "required": [
                "sourceKey"
              ],
              "properties": {
                "sourceKey": {
                  "type": "string",
                  "maxLength": 80,
                  "description": "The name your game uses for the event: 1 to 80 characters."
                },
                "key": {
                  "type": "string",
                  "pattern": "^[a-z][a-z0-9_]{0,39}$",
                  "description": "The key to give the event, as for a field."
                },
                "label": {
                  "type": "string",
                  "maxLength": 80,
                  "description": "What the event reads on CEEJAY's screens, up to 80 characters. Generated from the name when left out."
                }
              }
            }
          }
        }
      },
      "ProposeFieldMapResult": {
        "type": "object",
        "description": "The entries your request named, as the map shows them now, and why any was left alone.",
        "required": [
          "fields",
          "events",
          "skipped"
        ],
        "properties": {
          "fields": {
            "type": "array",
            "description": "The fields your request named. An approved one shows as approved and is not changed.",
            "items": {
              "$ref": "#/components/schemas/FieldMapField"
            }
          },
          "events": {
            "type": "array",
            "description": "The events your request named.",
            "items": {
              "$ref": "#/components/schemas/FieldMapEvent"
            }
          },
          "skipped": {
            "type": "array",
            "description": "Names that were not proposed, and why.",
            "items": {
              "type": "object",
              "required": [
                "kind",
                "sourceKey",
                "reason"
              ],
              "properties": {
                "kind": {
                  "type": "string",
                  "enum": [
                    "field",
                    "event"
                  ],
                  "description": "Whether the name was a field or an event."
                },
                "sourceKey": {
                  "type": "string",
                  "description": "The name you sent."
                },
                "reason": {
                  "type": "string",
                  "enum": [
                    "approved",
                    "unused",
                    "limit",
                    "key_taken",
                    "standard_taken"
                  ],
                  "description": "`approved`: it is already approved and is not modified. `unused`: the team marked it unused. `limit`: the game already has 50 proposed entries of this kind. `key_taken`: the `key` is in use. `standard_taken`: the standard field you named already has a name from your game. This list may grow: handle unknown values."
                }
              }
            }
          }
        }
      },
      "ProposeFieldMapResponse": {
        "type": "object",
        "description": "The proposal.",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "data": {
            "$ref": "#/components/schemas/ProposeFieldMapResult"
          }
        }
      }
    }
  }
}
