CEEJAYDocs

API changelog and versioning

What changed in the API, what you can rely on staying the same, and how to build so a change does not break you.

The API is versioned in its URL path: everything in this reference lives under /api/v1, except the older Complete a quest task endpoint at /api/quests/external/complete. The version of the API itself is 1.2.0, and Community details reports it as apiVersion. This page lists what changed, newest first, and says what you can count on.

What we promise

  • Event names are permanent. A webhook event name such as poll.voted is never renamed. Other systems are written against these names, so a new kind of event gets a new name.
  • Within version 1 nothing is renamed or removed. A request can gain fields, and a response or an event can gain fields. Every name a request accepts stays accepted, every field a response carries stays where it is, and every status stays. Where a field was renamed, the older name is still accepted beside the new one: each reference page lists these names.
  • Error codes are permanent. Every error carries a code from a closed list, on the Errors page. A code is never renamed or removed. The text of an error message can change.
  • One prefix, with one exception. Everything in this reference lives under /api/v1, except the older endpoint of Complete a quest task, which still works. New requests and events are added beside what is there.

Deprecation policy

We have not yet published a deprecation policy: how long a deprecated behaviour stays available alongside its replacement if something ever had to change, and how we would notify you in advance. Until it is published here, rely only on the promises above and on what this reference describes.

Building so changes do not break you

  • Ignore fields you do not know, in responses and in event payloads.
  • Do not rely on the order of fields, or on the exact text of an error message. Use the code, which every error has. Do not branch on the status alone: four refusals keep the 400 they have always had where newer ones return 409. See Errors.
  • Treat the values of fields such as status as open: a new value can appear.
  • Use the X-CEEJAY-Delivery header of a webhook event (the same value as its id), and an idempotencyKey on your own requests, to make a retry harmless.

Upcoming

These are built and will ship in the next release. They are not live yet, and each will get a date here when it ships.

Organisation keys

  • New: one key for several communities. An organisation's owner or an admin can create a key that covers some or all of the organisation's communities. Each request names its community in the CEEJAY-Community header. See An organisation's key.
  • New: two error codes, community_required (400) and community_not_covered (403). Only requests made with an organisation's key return them. See Errors.

2026-11-12

Surveys in games. It is version 1.2.0 of the API, and the two requests it adds carry x-since: 1.2.0. Nothing a caller could send or read before changes.

2026-11-09

  • Changed: a key limited to some games can complete only purchases made through those games or outside any game. Complete a shop delivery returns 404 purchase_not_found for a purchase made through another of the community's games, the same as for a purchase on another server. A key with no games listed is unchanged.
  • Changed: Steam tickets are single-use. Start connect returns 401 proof_rejected with reason: "used" for a Steam ticket CEEJAY has accepted before. Use a new ticket for each request; see the Connect guide.
  • Changed: when CEEJAY switches off a webhook after 10 failed deliveries in a row, including one that receives events sent with Emit an event, the community's team gets a notification in the dashboard, as for any other webhook.
  • Changed: each webhook's delivery history is retained for 30 days, then deleted. See Webhooks.

2026-11-05

  • New: Steam proof on Start connect and Get connection status. A game on Steam sends proof: { type: "steam", token }, with the hex ticket of GetAuthTicketForWebApi("ceejay") and the player's SteamID64 as playerId. CEEJAY checks the ticket with the community's publisher key. Use each ticket once. See Connect.
  • Changed: the message of unknown_platform names Settings → Developers → Platforms, where a custom platform is added today. The code and the status are the same.

2026-10-29

Fixes to what a key can read and change. Nothing a caller could send before is refused now, except where a line below says so.

  • Changed: a secret key limited to some games (listed in the dashboard) reads only those games. Get a member lists the connections to those games and the accounts that belong to no game, and Complete a quest task returns 403 with the code game_not_allowed for a playerId on a platform that is another game of the community. A key with no games listed is unchanged.
  • Changed: a role with View Audit Log is refused like the other roles that can moderate the server. Grant a role, Revoke a role and List roles return role_not_allowed for it.
  • Changed: a call CEEJAY makes to Discord for Send a message, Grant a role and Revoke a role times out after 10 seconds and returns 503 with the code discord_unavailable. A change or a post that timed out may or may not have been applied, so retry with the same idempotencyKey; a read that timed out changed nothing.
  • Changed: Send a message cuts its text at a whole character, so an emoji is never cut in two, and returns 400 with the code bad_request for text or embeds that contain half of a surrogate pair.

2026-10-08

Connect: linking a game player to their Discord account in a community, and what follows once they are linked, including the game loop. It is version 1.1.0 of the API, and every request it adds carries x-since: 1.1.0. Nothing a caller could send or read before changes, except where a line below says so, and each of those needs a game to exist first.

  • New: Connect. A player presses Connect in a game, authorises the connection with their Discord account on a page CEEJAY serves, and the game shows the member's name, avatar, level, coins and shared roles. Nothing is saved until the member has authorised the connection in Discord and the game has confirmed it. Connect is switched on per instance: where it is off, its requests return 404 with the code not_available and feature: "players".
  • New: seven requests a game makes itself, with a public key (ceejay_pk_) that ships inside the game: Start connect, Get the result, Confirm connect, Get connection status, Renew a player session, Get the player's profile and Disconnect. They are rate limited per IP address and can be called from a browser game on an origin saved for the game.
  • New: five requests for a studio's own server, with a secret key: Create a connect pass, Create a connect session, Get a connect session, List connect sessions and Confirm a connect session. They are limited to 600 requests a minute for each game and are not counted against the key's own requests per minute.
  • New: a second API file, /openapi-connect.json, for the requests a game makes. /openapi.json covers the requests a secret key makes.
  • New: eight requests about the players of a game and its field map, with a secret key: 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. They are counted against the key's own requests per minute, and a call that carries up to 500 players or events is one request. Player data and The field map explain them.
  • New: two webhook events, player.connected and player.disconnected, sent when a connection is made, moved or ended. They need the member history to be switched on for your instance.
  • New: rewards for connecting and for game events. A community can grant a reward when a player connects, and for each event it approved in the field map. CEEJAY's bot grants it shortly afterwards, once, and sends the member no direct message about it. The reward field a game reads shows its status. See Game rewards.
  • New: the button a member presses in Discord to start a connection there. The member gets a code, types it into the game, and confirms in Discord.
  • New: the game loop. A game shows the community's quests, shop, locker, rank and daily reward for a connected player, and sells from its shop with the player's coins. It is switched on per instance, as Connect is, and then per game by the community's team: a game that has it off returns 409 with the code hub_not_enabled. The member consents to it as part of connecting, and the consent text states what the game may spend. Nothing a game does sends a member a direct message.
  • New: eleven requests a game makes with a player session, in the Game loop group of /openapi-connect.json: List the quests, Start a quest, Verify a quest task, Answer a quest task, List the shop, Buy a shop item, Read the locker, Claim an item, Get the ranks, Get the balance and Check in. A player session can make 140 requests a minute of any kind, 120 reads and 20 writes among them, and Get the player's profile counts toward the same 140.
  • New: rules for spending a member's coins. A game sells only the items the community's team put on its list. Buy a shop item requires an idempotencyKey and the expectedPrice the player saw. A device that connected without verification cannot buy or claim unless the team allows a basic connection to spend. A new connection and a new device have a hold period before they can spend. spendHoldUntil in the connection is part of that hold period, and spending.holdUntil in Get the balance is the time the hold ends. Each member has a daily spending limit for each game. The rules about money explain each one.
  • New: player in the signed shop delivery and in List shop purchases for an item bought in a game's shop: the game's slug, the game's own ID for the player, and the connection's verification level. It is null in List shop purchases for any other purchase, and left out of the delivery. See A purchase made in a game.
  • New: features.gameLoop in Community details, hub.enabled and hub in sharing in Get the player's profile, and removed.purchases in Delete a player.
  • New: 14 error codes for the loop, on top of the 42 above, listed under Codes of the game loop. The requests of the loop return the 18 codes that existed before with their own statuses: a request that conflicts with a rule is 409, the bot being unreachable is 503, and the bot being too slow is 504. A body over 16 KB returns 413 with the code body_too_large.
  • New scopes: players:connect (Act for a player in the Generate API Key dialog) for the five requests above, Disconnect a player, and for flagging a player as under age in Set player fields and Log player events; players:read for Get a player, List players and Get the field map, and for what Get a member lists; players:write for Set player fields, Log player events and Propose to the field map; and players:delete (Delete a player's data) for Delete a player.
  • A * key never includes players:connect or players:delete. A key has them only when they are ticked by name. A Full Access key returns 403 with the code insufficient_scope on the requests that need them. Every other scope is part of * as before, players:read and players:write included.
  • New: 42 error codes, listed under Codes of Connect and Codes of player data. No request returns three of them yet: field_name_taken, player_already_connected and banned.
  • New: feature: "member_history" on a 404 with the code not_available. Log player events returns it where the member history is switched off.
  • New: Community details reports Connect as features.players, and its apiVersion is 1.1.0. players is available: false where Connect is not switched on for your instance.
  • Changed: Link an account recognises a game's slug as a platform, from the day the game is set up. For such a platform, and only for it: playerId must be one a game can use (400 with the code bad_player_id otherwise); a paused game returns 409 with game_paused, a key limited to other games 403 with game_not_allowed, a game directed at children 403 with audience_not_allowed, a game that was removed 404 with game_not_found, and a community that removed CEEJAY 403 with community_not_active; 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); a player the community blocked cannot be linked (409 with player_blocked); and the community's own reward for linking an account is not granted. CEEJAY's bot processes the connection instead, as it processes any connection to the game: it sends player.connected, and where the connection meets the proof level the game requires, it adds the account.linked entry, releases rewards held until the account is linked, and grants the game's own reward for connecting. A name that was linked before it became a game's slug keeps its links.
  • Changed: on Link an account, a connection a member made themselves through Connect is protected like a sign-in. A call that names a different playerId returns 409 with the code linked_by_sign_in while the connection is active.
  • Changed: Complete a quest task does not find a member by playerId through a connection the member made themselves at the basic level, where the game supplied the player ID without verification. Such a task returns 202 with matched: 0, unless the game is set up so that basic connections earn. A player connected at the verified level, and one your own server linked, is found as before.
  • Changed: Get a member lists a game account the member connected themselves through Connect only for a key with players:read, which a * key includes. An account your own server linked is always listed.
  • New: each entry of connections in Get a member carries proof. For a connection to a game it is verified when the player was verified and basic when the game supplied the player ID without verification; it is null for every other account. A basic connection does not verify that the player belongs to the member.
  • Changed in the dashboard: the Generate API Key dialog offers the four new scopes, and the line under Full Access says what it leaves out. A custom platform cannot be added under the slug of a game.
  • What the player requests refuse, and what they do not.
    • Disconnect a player and Delete a player work for a game directed at children too: a studio can always end a connection and delete a player's data. Every other request returns 403 with audience_not_allowed for such a game.
    • underAge: true on a player or an event needs the key to have players:connect as well as players:write. Without it the whole call returns 403 with insufficient_scope and required: ["players:connect"]. A call can carry at most 50 of them, or it returns 400 with the code bad_request.
    • A player connected at the basic level does not accept fields in the purchases or location category: each is a problem with the code needs_verified_proof, and the player's other fields are still written.
    • Log player events returns not_consented for a member who consented to share without the activity category.
    • Get a player returns member: null and fields: {} unless the member is in the server.
    • The result of Set player fields names the player by the name you used, playerId or discordId, and only that one.
  • Not in this release: Unity, Unreal and web SDKs, coming later. The game loop leaves out cosmetics, using a perk, skipping a quest task and earned rewards in a game's locker.

2026-10-07

Version 1.0.0 of the API. One contract for every request, and stricter checks on what each accepts. Each line says what a call got before.

  • New: Community details, List roles, List channels, List programs and List shop purchases. List programs needs the new scope programs:read (Read programs in the Generate API Key dialog). The other four use scopes that exist, and Community details needs none.
  • New: Complete a quest task has an endpoint under /v1, /v1/quests/tasks/complete. The older endpoint still works.
  • New: every request that grants, deducts or sends takes an idempotencyKey. A repeat returns the original response with duplicate: true and the header Idempotent-Replayed: true. One key is one request, so two requests for one event need two keys. Keys are retained for 30 days; a currency key never expires.
  • New: every response includes the key's limit and the remaining requests (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) and an X-Request-Id header. Successful responses used to include no rate limit headers.
  • New: every error has a code and a requestId, beside error. A validation error also names the field in field. The list of codes is on the Errors page.
  • New: deliveries carry X-CEEJAY-Signature-V2 with a time, a delivery ID and an attempt number (X-CEEJAY-Timestamp, X-CEEJAY-Delivery, X-CEEJAY-Attempt), on webhooks and on shop deliveries. The legacy signature is still sent, unchanged.
  • New: a successful response always includes data. A list adds page, with nextCursor and limit. Complete a quest task and Complete a shop delivery gain data and keep the fields they had beside success.
  • New: List quests pages by cursor as well as by offset.
  • Renamed fields, older names still accepted: playerId (was platformUserId) and playerName (was platformUsername) on Link an account, reason (was description) on the two currency requests, idempotencyKey (was idempotency_key), and taskKey, discordId and xHandle (were task_key, discord_id and x_handle) on Complete a quest task. When both names are sent, the older one is used.
  • Changed: these names were ignored until now and are read from this release. taskKey, discordId, playerId and xHandle on Complete a quest task, reason on Grant currency and Deduct currency, playerId and playerName on Link an account, idempotencyKey on every request that grants, deducts or sends, and cursor on List quests. A body that already carried one of them for a purpose of its own is now read differently. Two examples on Complete a quest task: { "task_key": …, "x_handle": …, "playerId": "your own value" } used to find the member by the X handle and now returns 400 with platform is required with playerId, because playerId is read before the other accounts; and { "task_key": …, "x_handle": …, "discordId": "…" } used to find the member by the X handle and now finds them by discordId. Where an older name is sent too, it still wins: a body with both discord_id and discordId is read as before. If your bodies carry any of these names for your own use, rename or remove yours before this release.
  • New response fields beside the older ones, which are still returned: xp, previousXp and level on the XP requests, programXp and previousProgramXp on the program XP requests, reason on the currency requests, run on Complete a quest, connectionId, discordId, playerId, playerName, active and connectedAt on Link an account, playerId and playerName in a member's connections, and a camelCase twin of every snake_case field of a quest in List quests.
  • New: a questId and a programId can be sent as a string of digits as well as a JSON number.
  • New: Complete a quest task finds a member by playerId with platform, the ID you linked with Link an account, and its response lists the quests whose task went to review in data.sentForReview.
  • Changed: a quest completed through the API now grants the quest's role, issues its reward list from the Rewards Engine, adds program XP for a quest in a program, and sends quest.completed. If you added any of these yourself after each completion (Grant a role, Grant program XP, Grant currency), stop: the member would receive it twice. rewards.programXp is new in the response.
  • Changed: Complete a quest applies the community's daily quest limit, 5 unless the community set another. A member over it receives 409 with the code quest_daily_limit_reached. The call used to ignore the limit.
  • Changed: Complete a quest does not grant a quest's coins while the community's economy is switched off. The response includes currencySkipped: true. XP, the badge and the role are still granted. The coins used to be granted.
  • Changed: a key's limit is at most 600 requests a minute. A key set to a higher number is capped at 600.
  • Changed: with no content, Send a message refuses embeds that is not a list with 400. Such a call used to reach Discord, whose own 400 was passed back. With content the text is posted and the embeds are left out, as before.
  • Changed: Link an account needs the member to be in your server, and trims playerId. An ID CEEJAY has no profile for that is not in your server returns 404 with the code member_not_found; any ID of 17 to 20 digits used to be linked. An ID stored with a space at an end is the same account as its trimmed form.
  • Changed: on Link an account, a link a member makes by signing in is protected while it is active, on every platform, and the protection ends when the member disconnects. It used to depend on whether the sign-in left CEEJAY a token, and stayed after a disconnect. An ID is compared case-insensitively. A link the API points at a different account no longer retains the old account's sign-in, and neither does a link the API makes again after the member disconnected it: it used to be restored with the member's sign-in.
  • Changed: Complete a quest task only touches quests whose task key matches, and completes a quest that needs a role only for a member who has it. A call used to start every live quest with a Completed by your API task, whatever key it sent, and did not check the role. When the member's roles cannot be read, the call returns 503 with the code discord_unavailable and does nothing.
  • Changed: Grant a role and Revoke a role refuse a role that can mention everyone, in a server whose @everyone role cannot, with 403 and the code role_not_allowed. Where the @everyone role can mention everyone itself, which is how Discord sets up a new server, such a role adds nothing and is still granted and revoked as before.
  • Changed: a grant that would exceed a limit returns 409 with the code xp_limit_reached or balance_limit_reached, and nothing is changed. It used to fail with 500.
  • Changed: Grant XP, Deduct XP and the two program XP requests write the change and its history together or not at all. A program XP call could return 500 after the XP had changed.
  • Changed: on Grant currency and Deduct currency, the same key sent again with another amount returns 409 with the code idempotency_key_reused. It used to return 200 with duplicate: true and the figures of the first amount. A repeat of a grant that hit the balance cap now returns capped: true, as the original response did.
  • Changed: a Discord ID that Discord says is not in your server is cached for 15 seconds. A member who has only just joined can take that long to be found: wait 15 seconds before retrying.
  • Changed: Discord's rate limit returns 429 with the code discord_rate_limited, and with retryAfter and Retry-After when Discord gave a wait time. It used to include neither. While Discord is limiting the bot, CEEJAY returns this without calling Discord again.
  • Changed: a request for a feature your instance does not have yet returns 404 with the code not_available. It used to fail with 500.
  • Changed: the text of some error messages. The message for an instance with no connection to Discord, the messages for CEEJAY's bot not responding on Complete a quest task and Complete a shop delivery, the message for an invalid ID on Get a member (it now says discordId), and the messages that named platformUserId now use the new names when you send them. The statuses are the same. Act on the code.
  • Changed: when CEEJAY's bot does not respond in time, Complete a quest task no longer asks you to retry the task: for a quest a member can repeat, a retry can complete it twice. Send an idempotencyKey with each task. The two codes now tell the two cases apart. bot_unavailable means the bot never received the task or rejected it, so nothing happened. bot_timeout means the response was delayed or lost, including when the bot failed partway or the connection dropped, so the task may or may not have been completed.
  • Changed: webhook events that carry userId also carry discordId, with the same value. Events you send with Emit an event are passed on as you sent them.
  • Changed: a retry of a webhook delivery carries the same X-CEEJAY-Delivery as the first attempt, the next X-CEEJAY-Attempt and a new X-CEEJAY-Timestamp.
  • Changed in the dashboard: the Generate API Key dialog closes once the key is made. Adding a platform under Settings → Integrations → Platforms with a slug that is taken is refused; it used to replace the platform that had it. Deleting a platform asks first. A key name over 100 characters is refused when you save it.
  • One rule from now on: within version 1 nothing is renamed or removed.

The checks that were already waiting for this release:

  • Discord IDs must be strings. A discordId, roleId or channelId sent as a JSON number or as a list is refused with 400 and the endpoint's own invalid-ID message. A number used to be accepted, and could name a different member. On Complete a quest task such a discord_id returns 202, as any other malformed one does, and a discord_id that is null or empty now counts as not sent, so the next identifier you sent is used.
  • Counts are always JSON numbers. balance, totalEarned and totalSpent in Get currency balance, Get a member, Grant currency and Deduct currency could be returned as text. xp, totalMessages and totalVoiceMinutes are returned as numbers in the same way.
  • A POST body that is not a JSON object returns 400 with The request body must be a JSON object. A body of null used to fail with 500, and text, a number or a list was treated as if a field were missing.
  • Deduct XP and Deduct currency refuse an amount above 2,147,483,647 with 400. An XP deduction that large used to fail with 500.
  • Complete a quest refuses a questId that is negative, has a fraction or is above 2,147,483,647 with 400. A fraction or a number that large used to fail with 500, and a negative one returned 404.
  • Grant program XP and Deduct program XP refuse a programId above 2,147,483,647 with 400, and Complete a shop delivery does the same for the purchase ID. Both used to fail with 500.
  • List quests refuses a limit or offset that is negative, has a fraction or is not a number with 400. A negative one used to fail with 500, and text was read as the default.
  • Link an account refuses a platform over 50 characters, an ID or a name over 255 characters, and a name that is not text, with 400. A value that long used to fail with 500.
  • Emit an event takes a name of 1 to 93 letters, digits, ., _, : or -, with or without custom. in front. Any other name is refused with 400 and nothing is sent. Such a name used to be accepted, and could make every delivery fail.
  • Send a message sends every message so that only member mentions notify anyone. A role mention notifies the role only when you list it in the new allowedRoleIds field, up to 20 role IDs, and @everyone and @here never notify anyone through the API. Until now every mention in content worked as in any bot message.
  • allowedRoleIds on Send a message refuses your server's own ID with 400. It is the ID of the @everyone role.
  • Send a message refuses a content that is not a string with 400 and content must be a string. A number used to be sent as text, and an object could fail with 500.
  • Complete a quest task takes task_key and evidence as strings only. A task_key that is not a string returns 400 like a missing one, and an evidence that is not a string returns 400 with evidence must be a string. A number used to be turned into text, and an object could fail with 500.
  • Complete a quest task reads an x_handle, wallet, github or twitch sent as a whole number as its digits, as it always has, up to 9,007,199,254,740,991. A larger number, a number with a fraction, a list, an object and true find no member and return 202 without a lookup. They used to be turned into text and looked up, and an object could fail with 500.
  • A null character (U+0000) in text or JSON that CEEJAY stores returns 400, with a message that names the field, such as reason cannot contain a null character, and nothing is changed or sent. This covers reason on Grant XP, Deduct XP, Grant program XP and Deduct program XP, the note and the idempotency key on Grant currency and Deduct currency, proof and the idempotency key on Complete a quest, the platform, the ID and the name on Link an account, data on Emit an event, status and category on List quests, evidence and the identifier a member is looked up by on Complete a quest task, and message on Complete a shop delivery. These used to fail with 500, except on Grant XP and Deduct XP, where the call returned 200 and the change was left out of the audit log. On the two program XP endpoints the XP had already changed by the time the call failed, on Emit an event the first webhook had already been delivered, and on Complete a quest, for a quest that grants currency, a null character in the key failed after the completion and its XP were in. In evidence and in the shop message it returned 502, which invites a retry that would never succeed.
  • reason on the four XP endpoints, and the note on Grant currency and Deduct currency, must be strings. A number, a list or an object returns 400 with reason must be a string or description must be a string, and null counts as left out. Any value used to be accepted, and a deeply nested list could fail with 500, on the XP endpoints after the XP had changed.
  • Half of a surrogate pair, such as the JSON "\ud800" or an emoji cut in two, returns 400 in an idempotency key, in proof on Complete a quest, in data on Emit an event, in evidence on Complete a quest task, in message on Complete a shop delivery, and in reason on Grant program XP and Deduct program XP with reason must be valid Unicode text. Evidence longer than 2,000 characters is cut at a whole character, so an emoji at the cut is left out and not cut in two. Two such keys used to be stored as one, so the second request was treated as a repeat of the first, and such a reason failed with 500 after the program XP had changed.
  • An endpoint that fails in a way it did not expect always returns 500 with the code internal_error, as JSON.
  • A request refused for a missing scope (403) now counts toward the key's rate limit and is logged against the key. It used to be neither counted nor logged. The limit is checked before the scope, so a key that is over its limit gets 429 from an endpoint it has no scope for, where it used to get 403.
  • Of the requests refused by the rate limit (429), the first in each one-minute window is logged against the key. None used to be.
  • Grant a role and Revoke a role only change an ordinary role of your own server. The @everyone role, a role managed by Discord or another app, and a role that can moderate or manage the server return 403 with the code role_not_allowed. A role that is not in your server returns 404 with the code role_not_found. Until now a key with roles:manage could grant or revoke every role below the bot's own, administrator and moderator roles included.
  • The two role endpoints refuse a bot master role as well, with 403 and the code role_not_allowed. That is a role your community added under Settings → Team Management: anyone with that role has full access to the CEEJAY dashboard. When those roles cannot be read the endpoints return 503 with the code dashboard_roles_unavailable and change nothing. A key with roles:manage could grant such a role to any member.
  • The two role endpoints read your server's role list from Discord before they change anything, and cache it for up to a minute. When the list cannot be read they return 503 with the code roles_unavailable and change nothing. That is also the response when CEEJAY's bot is no longer in your server, where Discord's own 404 or 403 used to be passed on.
  • A role change made through the API shows in your server's audit log in Discord with the reason API key: and the name of the key. It used to show no reason.
  • Link an account takes a platform of letters, digits and underscores, and trims it as well as lowercasing it. Any other character returns 400. A name with a space, a hyphen or a dot used to be stored as a platform of its own.
  • Link an account only links a known platform: one CEEJAY recognises by name, a custom platform of your community, or one your community already has links for. Any other name returns 400 with the code unknown_platform. Any name used to be accepted.
  • A custom platform can be added in the dashboard again: Add Platform under Settings → Integrations → Platforms saves it in API Link mode, and its Slug is the value to send as platform. The button was switched off.
  • Link an account returns 409 with the code linked_by_sign_in when the member linked the platform by signing in, the link is active and you send a different account, and 409 with the code player_already_linked when another member of your server already has that account linked. Both used to be accepted: the first replaced the member's own link, and the second left one account linked to two members.
  • Link an account matches the ID case-insensitively when it looks for the member an account is linked to, as Complete a quest task does when it finds a member by a linked account. A wallet sent in lower case is the same account as one stored in mixed case.
  • Two calls to Link an account that send one account for two members at the same moment are processed one after the other: the first links it, and the second returns 409 with the code player_already_linked.
  • Link an account still accepts a link the member already has. Where two members were linked to one account before this release, a repeat call for either of them is not refused: it updates the display name and delivers pending rewards, as it does today.
  • A failure while Link an account writes the link's activity entry now undoes the link. The link used to stay, and the call returned 500.
  • member.joined and member.left are no longer sent for bot accounts. Until now adding or removing a bot sent them too.
  • Switching a webhook back on after it was switched off resets its failure counter to zero and clears its last error. It used to retain the count, so one more failed delivery switched it off again.
  • A webhook name over 100 characters, or a URL over 500 characters, is refused with a message that says so when you save the webhook. It used to fail without one.
  • A 202 or { "status": "pending" } response to a retry of a shop delivery, the hourly one or a Retry clicked in Redemptions, now moves the purchase to Being delivered and awaits Complete a shop delivery for up to 24 hours. It used to count as a failed attempt, and the third attempt refunded the member. No further request is sent after a pending response, so complete the purchase with the callback.
  • A new API key needs at least one scope: in the Generate API Key dialog, Generate stays switched off until one is ticked.
  • The API Keys tab no longer lists AI agent tokens, and one cannot be revoked from it. They are listed and revoked on their own tab, as before. The list of keys on a Completed by your API quest task leaves them out too.
  • Complete a quest applies the quest's own rules. Complete a quest refuses, with 409 and a code, a quest that has not started or has ended, a quest that has reached its overall completion limit, a member who lacks the role, level, season XP, program or earlier quest it requires, and a member who completed it too recently. It used to check only that the quest was active, not already completed and under its per-member limit.
  • Every refusal of Complete a quest by one of the quest's rules carries a code, and some add the state that caused them. The three refusals it already had (quest_not_active, quest_already_completed and quest_limit_reached) keep the status 400 and their messages. The rules that are new return 409.
  • Complete a quest reads the role a quest requires from Discord. When Discord cannot be reached for it, the call returns 503 with the code member_roles_unavailable and completes nothing.
  • Complete a quest completes the run a member has open (one they started in Discord, in progress or pending review) and adds a new run only when there is none. It used to add a new completed run every time and leave the open one behind, where it could be completed and rewarded a second time.
  • Complete a quest writes the completion, the XP, the currency, the badge and the program XP together or not at all. A call that failed partway used to leave the quest completed with only some of its rewards granted.
  • Two calls to Complete a quest for the same member and quest at the same moment are processed one after the other, and so are a call and a completion the member makes in Discord at that moment. Both used to pass the checks, so a quest that can be done once could be completed and rewarded twice.
  • A completion through Complete a quest that takes the last place of a quest with an overall limit ends the quest, as it does in Discord. The quest used to stay active.
  • The idempotency key protects the whole completion on Complete a quest. A repeat of the same call returns 200 with the original response and the new field duplicate: true, and grants nothing. The key used to guard the currency only: a repeat completed a repeatable quest again and granted its XP again.
  • Complete a quest refuses a proof that nests lists and objects more than 32 levels deep, with 400 and proof is nested too deeply. A deeper one used to fail with 500, in some cases after the completion and its rewards were written. Emit an event refuses data nested that deeply in the same way.
  • List quests returns five more rules with each quest, the rest of the rules Complete a quest applies: repeatCooldownHours, requiredLevel, requiredRoleId, prerequisiteQuestId and requiredSeasonXp, each also under its snake_case name.
  • Fixed: List quests returned 500 on an instance without programs. It now lists the quests there, with program_id as null.
  • Members must be in your server. Grant XP, Grant program XP, Grant currency, Complete a quest and Link an account look up an ID CEEJAY has no profile for in Discord before they write anything. An ID that is not a member of your server returns 404 with the code member_not_found. When Discord cannot be queried, or its response does not say whether the ID is a member, the call returns 503 with the code discord_unavailable. Any ID of 17 to 20 digits used to be accepted, and got a profile or a balance. A repeat of a call that was already applied, sent again with its idempotency key, is handled first, without this lookup: it returns the original response even when the member has since left your server.
  • Complete a quest task makes the same check on a discordId. An ID that CEEJAY has no profile for and that is not a member of your server finds no member and returns 202 with matched: 0, and when Discord cannot be queried the call returns 503 with the code discord_unavailable. The task used to be completed, and its rewards granted, for any ID of 17 to 20 digits.
  • An idempotency key is scoped to the API key that sent it. Two API keys of one community used to share one set of idempotency keys, which they also shared with CEEJAY's own. A currency key sent before this release remains valid after it: a grant, a deduction or a quest completion that granted coins, made before the release and retried after it, is still treated as a repeat.
  • A key that was already used for a different request (another member, another amount, another quest, or another kind of change) returns 409 with the code idempotency_key_reused on Grant currency, Deduct currency and Complete a quest, and nothing is changed. It used to return 200 with duplicate: true and the figures of the first request, as if the second had been applied.
  • On an instance that cannot store idempotency keys yet, a call that sends an idempotencyKey returns 503 with the code idempotency_unavailable, and nothing is changed. Grant currency, Deduct currency and Complete a quest for a quest that grants coins are the exception: their key is stored with the currency movement, as it always was. Community details reports whether keys are stored, as features.retryKeys.

Webhooks

  • One new event for each member action, 21 in all, from poll.voted to member.invited. See Webhooks.
  • A failed delivery is retried up to four more times over about ten minutes, with the same id and X-CEEJAY-Delivery.
  • A webhook URL must be a public https:// URL. It is checked when you save the webhook and again before every delivery, and redirects are never followed.
  • Emit an event reports why a delivery failed in error, alongside httpStatus.
  • A new webhook's signing secret is shown once, in a box above the list, when you create it. Until now the dashboard did not show it.

XP

  • Grant XP and Deduct XP use your community's own level curve, put the change in the member's XP history, and for a grant add it to the season XP. The responses gain auditId. A grant that levels the member up grants that level's rewards and roles and sends member.level_up.
  • Fixed: Get a member and Get XP balance returned an error on every call. They now return the member, with the level worked out from your level curve.

Quests and members

  • Complete a quest grants its XP the same way Grant XP does, and a quest that has a badge no longer fails after the XP and currency were granted. The completion is added to the member's activity, so it can send badge.earned and member.level_up.
  • Complete a quest task adds each completion to the member's activity too, so the badge it grants can send badge.earned and the XP it grants can send member.level_up.
  • Link an account sends account.linked and grants your community's reward for linking an account, when it has one.

Messages

  • Send a message only posts to channels in the key's own server. A channel anywhere else returns 404.

AI agents

  • Three new reading tools: ceejay_list_campaigns, ceejay_get_campaign and ceejay_get_connections. ceejay_search_members can filter by what members did in the community's apps. See Connect an AI agent over MCP.

Released

2026-09-16

2026-09-15

  • Currency changes are safe to retry. Grant currency, Deduct currency and Complete a quest take an idempotency_key.
  • A currency grant stops at the community's balance cap, and reports it with amountRequested, capped and duplicate.
  • A deduction no longer counts as the member's spending, and a member with no balance gets Insufficient balance like any other member who has too little.
  • AI agents' changes are limited to the dashboard: messages, polls and giveaways are saved as drafts, and nothing is posted in Discord.
  • Quest tasks completed by your API. A quest task of the type Completed by your API is completed by a call to POST /api/quests/external/complete, the endpoint the quest builder shows. It is the one route in this reference outside /api/v1. See Complete a quest task.

2026-09-11

  • MCP for AI agents. An endpoint that MCP clients such as Claude Code can connect to, to read a community and, where it allows it, make changes after a confirmation. See Connect an AI agent over MCP.

2026-08-19

  • Events from CEEJAY itself. Webhooks start receiving member.joined, member.left, quest.completed and ticket.created.

2026-06-08

2026-03-28

  • The first version of /api/v1. API keys with scopes and a per-key rate limit; members, XP, currency, quests, roles, messages and custom events; and signed webhooks for the custom events you emit.

On this page

Community StudiosPrivacyTerms