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.votedis 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
codefrom 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 the400they have always had where newer ones return409. See Errors. - Treat the values of fields such as
statusas open: a new value can appear. - Use the
X-CEEJAY-Deliveryheader of a webhook event (the same value as itsid), and anidempotencyKeyon 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-Communityheader. See An organisation's key. - New: two error codes,
community_required(400) andcommunity_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.
- New: two requests a game makes with a player session, in the Game loop group of
/openapi-connect.json: List surveys and Answer a survey. A survey is a form with In game switched on, for one game or every game, and can wait until the game has logged an event a number of times. See Surveys in the game. - New: two error codes,
survey_not_foundandsurvey_unavailable. See Errors. - Changed: Community details reports
apiVersion1.2.0.
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
404purchase_not_foundfor 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
401proof_rejectedwithreason: "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 ofGetAuthTicketForWebApi("ceejay")and the player's SteamID64 asplayerId. CEEJAY checks the ticket with the community's publisher key. Use each ticket once. See Connect. - Changed: the message of
unknown_platformnames 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
403with the codegame_not_allowedfor aplayerIdon aplatformthat 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_allowedfor 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
503with the codediscord_unavailable. A change or a post that timed out may or may not have been applied, so retry with the sameidempotencyKey; 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
400with the codebad_requestfor 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
404with the codenot_availableandfeature: "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.jsoncovers 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.connectedandplayer.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
rewardfield 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
409with the codehub_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
idempotencyKeyand theexpectedPricethe 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.spendHoldUntilin the connection is part of that hold period, andspending.holdUntilin 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:
playerin 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 isnullin List shop purchases for any other purchase, and left out of the delivery. See A purchase made in a game. - New:
features.gameLoopin Community details,hub.enabledandhubinsharingin Get the player's profile, andremoved.purchasesin 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 is503, and the bot being too slow is504. A body over 16 KB returns413with the codebody_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:readfor Get a player, List players and Get the field map, and for what Get a member lists;players:writefor Set player fields, Log player events and Propose to the field map; andplayers:delete(Delete a player's data) for Delete a player. - A
*key never includesplayers:connectorplayers:delete. A key has them only when they are ticked by name. A Full Access key returns403with the codeinsufficient_scopeon the requests that need them. Every other scope is part of*as before,players:readandplayers:writeincluded. - 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_connectedandbanned. - New:
feature: "member_history"on a404with the codenot_available. Log player events returns it where the member history is switched off. - New: Community details reports Connect as
features.players, and itsapiVersionis1.1.0.playersisavailable: falsewhere 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:
playerIdmust be one a game can use (400with the codebad_player_idotherwise); a paused game returns409withgame_paused, a key limited to other games403withgame_not_allowed, a game directed at children403withaudience_not_allowed, a game that was removed404withgame_not_found, and a community that removed CEEJAY403withcommunity_not_active; a member who ended their connection to the game themselves cannot be linked to it again by a key (409withmember_disconnected), and neither can a member whose connection to it ended because the player is under age (403withunder_age); a player the community blocked cannot be linked (409withplayer_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 sendsplayer.connected, and where the connection meets the proof level the game requires, it adds theaccount.linkedentry, 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
playerIdreturns409with the codelinked_by_sign_inwhile the connection is active. - Changed: Complete a quest task does not find a member by
playerIdthrough a connection the member made themselves at the basic level, where the game supplied the player ID without verification. Such a task returns202withmatched: 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
connectionsin Get a member carriesproof. For a connection to a game it isverifiedwhen the player was verified andbasicwhen the game supplied the player ID without verification; it isnullfor every other account. Abasicconnection 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
403withaudience_not_allowedfor such a game. underAge: trueon a player or an event needs the key to haveplayers:connectas well asplayers:write. Without it the whole call returns403withinsufficient_scopeandrequired: ["players:connect"]. A call can carry at most 50 of them, or it returns400with the codebad_request.- A player connected at the basic level does not accept fields in the
purchasesorlocationcategory: each is a problem with the codeneeds_verified_proof, and the player's other fields are still written. - Log player events returns
not_consentedfor a member who consented to share without theactivitycategory. - Get a player returns
member: nullandfields: {}unless the member is in the server. - The result of Set player fields names the player by the name you used,
playerIdordiscordId, and only that one.
- 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
- 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 withduplicate: trueand the headerIdempotent-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 anX-Request-Idheader. Successful responses used to include no rate limit headers. - New: every error has a
codeand arequestId, besideerror. A validation error also names the field infield. The list of codes is on the Errors page. - New: deliveries carry
X-CEEJAY-Signature-V2with 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 addspage, withnextCursorandlimit. Complete a quest task and Complete a shop delivery gaindataand keep the fields they had besidesuccess. - New: List quests pages by
cursoras well as byoffset. - Renamed fields, older names still accepted:
playerId(wasplatformUserId) andplayerName(wasplatformUsername) on Link an account,reason(wasdescription) on the two currency requests,idempotencyKey(wasidempotency_key), andtaskKey,discordIdandxHandle(weretask_key,discord_idandx_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,playerIdandxHandleon Complete a quest task,reasonon Grant currency and Deduct currency,playerIdandplayerNameon Link an account,idempotencyKeyon every request that grants, deducts or sends, andcursoron 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 returns400withplatform is required with playerId, becauseplayerIdis read before the other accounts; and{ "task_key": …, "x_handle": …, "discordId": "…" }used to find the member by the X handle and now finds them bydiscordId. Where an older name is sent too, it still wins: a body with bothdiscord_idanddiscordIdis 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,previousXpandlevelon the XP requests,programXpandpreviousProgramXpon the program XP requests,reasonon the currency requests,runon Complete a quest,connectionId,discordId,playerId,playerName,activeandconnectedAton Link an account,playerIdandplayerNamein a member'sconnections, and a camelCase twin of every snake_case field of a quest in List quests. - New: a
questIdand aprogramIdcan be sent as a string of digits as well as a JSON number. - New: Complete a quest task finds a member by
playerIdwithplatform, the ID you linked with Link an account, and its response lists the quests whose task went to review indata.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.programXpis 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
409with the codequest_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 refusesembedsthat is not a list with400. Such a call used to reach Discord, whose own400was passed back. Withcontentthe 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 returns404with the codemember_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
503with the codediscord_unavailableand does nothing. - Changed: Grant a role and Revoke a role refuse a role that can mention everyone, in a server whose
@everyonerole cannot, with403and the coderole_not_allowed. Where the@everyonerole 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
409with the codexp_limit_reachedorbalance_limit_reached, and nothing is changed. It used to fail with500. - 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
500after the XP had changed. - Changed: on Grant currency and Deduct currency, the same key sent again with another amount returns
409with the codeidempotency_key_reused. It used to return200withduplicate: trueand the figures of the first amount. A repeat of a grant that hit the balance cap now returnscapped: 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
429with the codediscord_rate_limited, and withretryAfterandRetry-Afterwhen 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
404with the codenot_available. It used to fail with500. - 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 namedplatformUserIdnow use the new names when you send them. The statuses are the same. Act on thecode. - 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
idempotencyKeywith each task. The two codes now tell the two cases apart.bot_unavailablemeans the bot never received the task or rejected it, so nothing happened.bot_timeoutmeans 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
userIdalso carrydiscordId, 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-Deliveryas the first attempt, the nextX-CEEJAY-Attemptand a newX-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,roleIdorchannelIdsent as a JSON number or as a list is refused with400and 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 adiscord_idreturns202, as any other malformed one does, and adiscord_idthat isnullor empty now counts as not sent, so the next identifier you sent is used. - Counts are always JSON numbers.
balance,totalEarnedandtotalSpentin Get currency balance, Get a member, Grant currency and Deduct currency could be returned as text.xp,totalMessagesandtotalVoiceMinutesare returned as numbers in the same way. - A
POSTbody that is not a JSON object returns400withThe request body must be a JSON object. A body ofnullused to fail with500, and text, a number or a list was treated as if a field were missing. - Deduct XP and Deduct currency refuse an
amountabove 2,147,483,647 with400. An XP deduction that large used to fail with500. - Complete a quest refuses a
questIdthat is negative, has a fraction or is above 2,147,483,647 with400. A fraction or a number that large used to fail with500, and a negative one returned404. - Grant program XP and Deduct program XP refuse a
programIdabove 2,147,483,647 with400, and Complete a shop delivery does the same for the purchase ID. Both used to fail with500. - List quests refuses a
limitoroffsetthat is negative, has a fraction or is not a number with400. A negative one used to fail with500, and text was read as the default. - Link an account refuses a
platformover 50 characters, an ID or a name over 255 characters, and a name that is not text, with400. A value that long used to fail with500. - Emit an event takes a name of 1 to 93 letters, digits,
.,_,:or-, with or withoutcustom.in front. Any other name is refused with400and 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
allowedRoleIdsfield, up to 20 role IDs, and@everyoneand@herenever notify anyone through the API. Until now every mention incontentworked as in any bot message. allowedRoleIdson Send a message refuses your server's own ID with400. It is the ID of the@everyonerole.- Send a message refuses a
contentthat is not a string with400andcontent must be a string. A number used to be sent as text, and an object could fail with500. - Complete a quest task takes
task_keyandevidenceas strings only. Atask_keythat is not a string returns400like a missing one, and anevidencethat is not a string returns400withevidence must be a string. A number used to be turned into text, and an object could fail with500. - Complete a quest task reads an
x_handle,wallet,githubortwitchsent 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 andtruefind no member and return202without a lookup. They used to be turned into text and looked up, and an object could fail with500. - A null character (U+0000) in text or JSON that CEEJAY stores returns
400, with a message that names the field, such asreason cannot contain a null character, and nothing is changed or sent. This coversreasonon Grant XP, Deduct XP, Grant program XP and Deduct program XP, the note and the idempotency key on Grant currency and Deduct currency,proofand the idempotency key on Complete a quest, the platform, the ID and the name on Link an account,dataon Emit an event,statusandcategoryon List quests,evidenceand the identifier a member is looked up by on Complete a quest task, andmessageon Complete a shop delivery. These used to fail with500, except on Grant XP and Deduct XP, where the call returned200and 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. Inevidenceand in the shopmessageit returned502, which invites a retry that would never succeed. reasonon the four XP endpoints, and the note on Grant currency and Deduct currency, must be strings. A number, a list or an object returns400withreason must be a stringordescription must be a string, andnullcounts as left out. Any value used to be accepted, and a deeply nested list could fail with500, 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, returns400in an idempotency key, inproofon Complete a quest, indataon Emit an event, inevidenceon Complete a quest task, inmessageon Complete a shop delivery, and inreasonon Grant program XP and Deduct program XP withreason 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 areasonfailed with500after the program XP had changed. - An endpoint that fails in a way it did not expect always returns
500with the codeinternal_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 gets429from an endpoint it has no scope for, where it used to get403. - 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
@everyonerole, a role managed by Discord or another app, and a role that can moderate or manage the server return403with the coderole_not_allowed. A role that is not in your server returns404with the coderole_not_found. Until now a key withroles:managecould 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
403and the coderole_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 return503with the codedashboard_roles_unavailableand change nothing. A key withroles:managecould 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
503with the coderoles_unavailableand change nothing. That is also the response when CEEJAY's bot is no longer in your server, where Discord's own404or403used 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
platformof letters, digits and underscores, and trims it as well as lowercasing it. Any other character returns400. 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
400with the codeunknown_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
409with the codelinked_by_sign_inwhen the member linked the platform by signing in, the link is active and you send a different account, and409with the codeplayer_already_linkedwhen 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
409with the codeplayer_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.joinedandmember.leftare 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
202or{ "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
409and acode, 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_completedandquest_limit_reached) keep the status400and their messages. The rules that are new return409. - Complete a quest reads the role a quest requires from Discord. When Discord cannot be reached for it, the call returns
503with the codemember_roles_unavailableand 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
200with the original response and the new fieldduplicate: 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
proofthat nests lists and objects more than 32 levels deep, with400andproof is nested too deeply. A deeper one used to fail with500, in some cases after the completion and its rewards were written. Emit an event refusesdatanested 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,prerequisiteQuestIdandrequiredSeasonXp, each also under its snake_case name. - Fixed: List quests returned
500on an instance without programs. It now lists the quests there, withprogram_idasnull. - 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
404with the codemember_not_found. When Discord cannot be queried, or its response does not say whether the ID is a member, the call returns503with the codediscord_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 returns202withmatched: 0, and when Discord cannot be queried the call returns503with the codediscord_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
409with the codeidempotency_key_reusedon Grant currency, Deduct currency and Complete a quest, and nothing is changed. It used to return200withduplicate: trueand 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
idempotencyKeyreturns503with the codeidempotency_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, asfeatures.retryKeys.
Webhooks
- One new event for each member action, 21 in all, from
poll.votedtomember.invited. See Webhooks. - A failed delivery is retried up to four more times over about ten minutes, with the same
idandX-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, alongsidehttpStatus. - 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 sendsmember.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.earnedandmember.level_up. - Complete a quest task adds each completion to the member's activity too, so the badge it grants can send
badge.earnedand the XP it grants can sendmember.level_up. - Link an account sends
account.linkedand 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_campaignandceejay_get_connections.ceejay_search_memberscan filter by what members did in the community's apps. See Connect an AI agent over MCP.
Released
2026-09-16
- Shop delivery by API. A shop item set to Delivered by your API sends a signed request when a member buys it, and Complete a shop delivery completes it. New scope
shop:fulfil. See Deliver shop items from your own platform.
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,cappedandduplicate. - A deduction no longer counts as the member's spending, and a member with no balance gets
Insufficient balancelike 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.completedandticket.created.
2026-06-08
- Program XP. Grant program XP and Deduct program XP, with the scopes
xp:program:grantandxp:program:deduct.
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.

