Errors
One error shape for every request: a message, a fixed code and the ID of the request.
Every failed request returns a non-2xx status and the same body: a message for a person, a fixed code for your program, and the ID of the request.
{
"error": "discordId must be a valid Discord snowflake (17-20 digits)",
"code": "bad_request",
"requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
"field": "discordId"
}| Field | What it is |
|---|---|
error | A message saying what was wrong. Its text can change: do not test it |
code | One of the codes in the table below. The list is closed for version 1: a code is never renamed or removed |
requestId | The ID CEEJAY gave this request: req_ and 24 hexadecimal characters. The header X-Request-Id carries the same value on every response, the successful ones included. Quote it when you ask us about a request |
field | Only with bad_request, and only when one field is at fault: the name of that field, as you sent it |
Some codes add extra fields, such as required and granted, retryAfter, balance or feature. The table below names them, and each reference page lists the errors of its own request.
Act on the code, not on the status
The status follows one rule for everything new since version 1.0.0:
| Status | Meaning | What to do |
|---|---|---|
200 | Success | — |
202 | Complete a quest task found no member for the identifier you sent, so nothing happened. It is not an error | Check the identifier. The body has data.matched: 0 |
308 | The path ends in a slash. The redirect points at the same path without it | Write paths without a trailing slash |
400 | Invalid input: a missing or malformed field | Fix the request. Do not retry unchanged |
401 | Missing, malformed, unknown, revoked or expired key. On a request a game makes: a public key, a session secret, a refresh token or a player session that was not accepted, or a proof that was rejected | Check the header, then the key's status in the dashboard. For a game, read the code |
403 | Key lacks the required scope, the CEEJAY bot lacks a Discord permission, or the role is one the API never grants or revokes. For Connect: the game cannot be used by this key or from this origin, or the player cannot be connected | Reissue the key with the scope named in required, fix the bot's permission, or choose another role. For Connect, read the code |
404 | Member, quest, program, purchase, channel, role, game, connect session or code not found, or this instance does not have the feature yet (not_available). A path that does not exist also returns 404, as an HTML page | Verify the ID and that it belongs to your server, and check the path |
405 | Wrong HTTP method | Check the method for that request |
409 | The request conflicts with a rule: a quest's rule, a program that is not active, an account that is already linked, a game that is paused, a connect session that is not waiting for this, or an idempotencyKey that cannot be used this way | Read the code. The body names the state that blocked the call |
410 | The result of a connect session can no longer be read (result_gone) | Start a new session |
413 | The body is larger than 1 MB. On Set player fields and Log player events the limit is 512 KB, and on the requests of the game loop it is 16 KB. Both return the usual error shape with the code body_too_large | Send less in one request |
429 | Rate limited, by CEEJAY (rate_limited, too_many_attempts on Start connect, or event_rate_exceeded on Log player events) or by Discord (discord_rate_limited) | Wait retryAfter seconds when it is there |
500 | Something failed on our side, or this instance has no connection to Discord (discord_not_configured) | Retry with backoff. If it persists, send us the requestId |
502, 504 | CEEJAY's bot could not be reached or did not respond in time, on Complete a quest task and Complete a shop delivery | Read the request's page: a shop delivery is safe to retry, a quest task is not always |
503, 504 | The same two codes on the requests of the game loop: 503 with bot_unavailable when nothing was done, 504 with bot_timeout when it is not known whether the request was done | Retry the request. Every request of the loop is safe to retry, apart from Check in, which returns checkin_cooldown once the daily reward is claimed |
503 | CEEJAY had to query a service first and could not, so nothing was changed: Discord, or your community's list of bot master roles. Or this instance cannot store an idempotencyKey yet. Or a game's sign-in service could not be queried to verify a proof (proof_service_unavailable) | For the first kind, retry shortly. If it persists, check that CEEJAY's bot is still in your server |
Four refusals do not follow that rule. They returned 400 before the codes existed, and they keep 400 for as long as version 1 exists, where a request that conflicts with a rule now returns 409:
code | Status | Request |
|---|---|---|
quest_not_active | 400 | Complete a quest |
quest_already_completed | 400 | Complete a quest |
quest_limit_reached | 400 | Complete a quest |
insufficient_balance | 400 | Deduct currency |
So branch on code. A program that treats every 400 as "fix the request" and every 409 as "the request conflicts with a rule" gets these four wrong.
Three more statuses are named exceptions, because a caller could already read them: 502 for bot_unavailable and bot_timeout on Complete a quest task, 502 and 504 for the same two codes on Complete a shop delivery, and Discord's own status passed on with the code discord_error. The requests of the game loop are newer, and follow the rule: they return 503 and 504 for those two codes, and 409 for the quest rules and insufficient_balance that return 400 above. See the table below.
Every code
These are all the codes of version 1.0.0.
code | Status | When |
|---|---|---|
missing_api_key | 401 | No Authorization header, or it does not start with Bearer |
invalid_api_key | 401 | The key does not start with ceejay_sk_, or there is no such key |
api_key_revoked | 401 | The key was revoked in the dashboard |
api_key_expired | 401 | The key had an expiry date and it has passed |
insufficient_scope | 403 | The key does not have the scope the request needs. The body adds required and granted |
method_not_allowed | 405 | The request does not take this method |
rate_limited | 429 | The key is over its limit. The body adds retryAfter: see Rate limits |
internal_error | 500 | Something failed on our side |
bad_request | 400 | A field is missing or malformed. The body adds field when one field is at fault |
not_available | 404 | This instance does not have what the request needs yet. The body adds feature, the name Community details reports it under. For Connect and the player requests it is players, and for Log player events, when the member history is off, it is member_history |
unknown_platform | 400 | Link an account and Complete a quest task, for a platform that is not known |
member_not_found | 404 | The ID is not a member: CEEJAY has no profile for it, and where Discord is queried, Discord reports that it is not in your server |
quest_not_found | 404 | Complete a quest, for a quest that is not on your server |
program_not_found | 404 | The program XP requests, for a program that is not in your community |
member_not_enrolled | 404 | The program XP requests, for a member who is not in the program |
purchase_not_found | 404 | Complete a shop delivery, for a purchase that is not yours or is not delivered by your API |
channel_not_found | 404 | Send a message, for a channel that does not exist, is in another server, or that the bot cannot see |
role_not_found | 404 | Grant a role and Revoke a role, for a role that is not in your server |
role_not_allowed | 403 | The same two requests, for @everyone, a managed role, a role that can moderate or manage the server, a role that can mention everyone where @everyone cannot, or a bot master role |
bot_missing_permission | 403 | The CEEJAY bot lacks the Discord permission, or its role sits too low |
quest_not_active | 400 | Complete a quest: the quest's status is not active |
quest_already_completed | 400 | Complete a quest: the member completed it, and it cannot be repeated |
quest_limit_reached | 400 | Complete a quest: the member reached the quest's per-member limit |
insufficient_balance | 400 | Deduct currency: the member has less than the amount. The body adds balance, currentBalance and requested |
quest_not_started | 409 | Complete a quest: before the quest's start date. The body adds startsAt |
quest_ended | 409 | Complete a quest: after the quest's end date. The body adds endsAt |
quest_requires_role | 409 | Complete a quest: the member does not have the role the quest requires. The body adds requiredRoleId |
quest_requires_level | 409 | Complete a quest: the member is below the level. The body adds requiredLevel and level |
quest_requires_season_xp | 409 | Complete a quest: the member is short of the season XP. The body adds requiredSeasonXp, seasonXp and seasonRunning |
quest_requires_program | 409 | Complete a quest: the member is not in the quest's program. The body adds programId |
quest_requires_quest | 409 | Complete a quest: the quest that comes first is not completed. The body adds prerequisiteQuestId |
quest_full | 409 | Complete a quest: the quest reached its overall limit. The body adds maxCompletions |
quest_cooling_down | 409 | Complete a quest: the cooldown between two runs has not passed. The body adds availableAt |
quest_daily_limit_reached | 409 | Complete a quest: the member reached your community's daily quest limit. The body adds dailyQuestLimit, completedToday and resetsAt |
program_not_active | 409 | The program XP requests: the program is not active. The body adds programStatus |
member_status_not_allowed | 409 | The program XP requests: the member's status in the program does not allow XP changes. The body adds memberStatus |
linked_by_sign_in | 409 | Link an account: the member linked the platform by signing in, and you sent a different account |
player_already_linked | 409 | Link an account: another member already has that account linked |
wrong_status | 409 | Complete a shop delivery: the purchase has already moved on. The body adds status |
idempotency_key_reused | 409 | The idempotencyKey was already used for a different request |
idempotency_in_progress | 409 | The first request with this key is still being processed. The body adds retryAfter |
idempotency_outcome_unknown | 409 | A first request with this key never finished, and CEEJAY cannot know whether it took effect. It was not retried |
idempotency_unavailable | 503 | You sent a key, and this instance cannot store idempotency keys yet. Nothing was changed |
xp_limit_reached | 409 | The change would take XP past the largest number CEEJAY can store. Nothing was changed |
balance_limit_reached | 409 | The grant would take the member's lifetime total past the largest number CEEJAY can store. Nothing was changed |
discord_rate_limited | 429 | Discord is limiting the bot. The body adds retryAfter when Discord provided a retry time |
discord_unavailable | 503 | CEEJAY could not query Discord about whether an ID is a member of your server, so nothing was changed. On Complete a quest task, also when a member's roles could not be read |
discord_not_configured | 500 | This instance has no connection to Discord yet |
discord_error | Discord's own | Discord refused the request. The body adds details with Discord's own text |
roles_unavailable | 503 | Your server's role list could not be read from Discord. Nothing was changed |
dashboard_roles_unavailable | 503 | Your community's bot master roles could not be read. Nothing was changed |
member_roles_unavailable | 503 | Complete a quest: the quest requires a role, and the member's roles could not be read from Discord. Nothing was completed |
channels_unavailable | 503 | List channels: the channel list could not be read from Discord |
bot_unavailable | 502 | CEEJAY's bot could not be reached or refused the request. On Complete a quest task nothing happened; on Complete a shop delivery, also when the bot could not finish |
bot_timeout | 502, 504 | CEEJAY's bot did not respond in time: 502 on Complete a quest task, where it is also the response when the bot's response was lost and the task may or may not have been completed, and 504 on Complete a shop delivery |
Codes of an organisation's key
An organisation's key covers several communities, and each request names one of them in the CEEJAY-Community header. These 2 codes are part of the same closed list, and only requests made with an organisation's key return them.
code | Status | When |
|---|---|---|
community_required | 400 | The CEEJAY-Community header is missing, or its value is not a server ID |
community_not_covered | 403 | 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 |
Codes of Connect
The requests of Connect add these codes. They are part of the same closed list, and they were added in version 1.1.0 of the API.
code | Status | When |
|---|---|---|
bad_player_id | 400 | A game's ID for a player is missing, is not text, is longer than 128 characters, or contains a character outside the allowed set (letters, digits and . _ : @ | + = -, starting with a letter or a digit). On Link an account only for a platform that is a game, where the body adds field |
bad_proof | 400 | Start connect and Get connection status: proof cannot be read, or the game has no project saved for that sign-in service |
age_answer_required | 400 | Start connect and Create a connect session: the game is set up for a mixed audience and underAge was not sent |
unknown_return | 400 | Start connect and Create a connect session: returnTo is not the key of a return URL saved for the game |
wrong_key_kind | 401 | A request a game makes was sent a different kind of key or token than the one it accepts |
invalid_public_key | 401 | A request a game makes carries no public key, or one CEEJAY does not recognise |
key_retired | 401 | The public key was retired |
key_suspended | 401 | The public key was suspended |
proof_rejected | 401 | Start connect and Get connection status: the pass or the token was not accepted. The body adds reason |
invalid_session_secret | 401 | Get the result and Confirm connect: no session of this game has this secret |
invalid_refresh_token | 401 | Renew a player session and Disconnect: the value is not a refresh token this instance issued for this game, or it has not been used for 90 days |
refresh_reused | 401 | Renew a player session: the token was replaced and has been used again, so the device is signed out |
invalid_player_session | 401 | Get the player's profile and the requests of the game loop: no player session, or one that cannot be read or has expired |
connection_ended | 401 | Renew a player session, Get the player's profile and the requests of the game loop: the connection or the device was ended. On a renewal the body adds reason |
community_not_active | 403 | The community removed CEEJAY from its server |
audience_not_allowed | 403 | The game is set up as directed at children. Every Connect and player request returns it except Disconnect a player and Delete a player, which a studio can always make |
under_age | 403 | Start connect and Create a connect session: underAge is true. Nothing is stored. On Link an account, for a platform that is a game: the member's connection to it ended because the player is under age |
origin_not_allowed | 403 | A request a game makes carries an Origin header that is not on the game's list |
proof_required | 403 | Start connect: the game connects verified players only. Get connection status: the token is of an anonymous sign-in |
game_not_allowed | 403 | The secret key is limited to other games of the community |
game_not_found | 404 | The community has no game with that slug, or the game was removed |
code_not_found | 404 | Start connect and Create a connect session: no pending request matches the code you sent |
session_not_found | 404 | Get a connect session and Confirm a connect session: the game has no session with that ID |
player_not_found | 404 | Get a player and Disconnect a player: no active connection has that player ID. On Get a player the body adds held: true when CEEJAY stores data for the ID that is waiting for a connection |
game_paused | 409 | The game is paused or suspended |
session_locked | 409 | Start connect and Create a connect session: the code belongs to a pending request that was locked after five failed attempts |
session_changed | 409 | Confirm connect and Confirm a connect session: the session changed between the member's consent and the game's confirmation |
nothing_to_confirm | 409 | Confirm connect and Confirm a connect session: the session is not awaiting the game's confirmation |
player_blocked | 409 | The community blocked the player. On the two confirm requests and on Link an account |
player_already_connected | 409 | No request returns it as a code yet. It is a value of reason on a connect session that ended |
player_taken | 409 | Confirm connect and Confirm a connect session: another Discord account is connected to the player and it cannot be moved |
member_disconnected | 409 | Link an account, for a platform that is a game: the member ended their connection themselves |
banned | 409 | No request returns it as a code yet. It is a value of reason on a connect session that ended |
result_gone | 410 | Get the result and Confirm connect: the result of the session can no longer be read. Start again |
too_many_attempts | 429 | Start connect: too many starts or codes. The body adds retryAfter |
proof_service_unavailable | 503 | Start connect and Get connection status: the keys of the sign-in service could not be fetched to check a token. Retry |
Codes of player data
The player requests add these codes. They are part of the same closed list, and they were added in version 1.1.0 of the API.
code | Status | When |
|---|---|---|
too_many_players | 400 | Set player fields: players contains more than 500 players |
too_many_events | 400 | Log player events: events contains more than 500 events |
body_too_large | 413 | Set player fields and Log player events: the body is larger than 512 KB. A request of the game loop that takes a body: it is larger than 16 KB. It is refused before it is parsed |
activity_not_shared | 409 | Log player events: the game does not share the activity category, so no event can be logged. The whole call is refused |
event_rate_exceeded | 429 | Log player events: the game sent more events this minute than it is allowed (3,000 unless another number was set for the game). The body adds retryAfter, and the Retry-After header carries the same number |
field_name_taken | 409 | No request returns it yet. The community's team sees this error when approving a game's field and the community already has fields with both names it could take |
A batch that has problems is not an error: Set player fields and Log player events return 200 with a status for each player or event. The statuses and the problems are values of those responses, not codes of this list.
Three codes are in the list and no request returns them yet: field_name_taken, player_already_connected and banned. The API file names them under x-not-answered-yet on its ErrorCode schema.
Codes of the game loop
The requests of the game loop add these 14 codes. They are part of the same closed list, and they were added in version 1.1.0 of the API. The message of each is on the reference page of the request that returns it.
code | Status | When |
|---|---|---|
hub_not_enabled | 409 | Any request of the loop: the game does not show the community's quests, shop and rank, because the community's team has not switched the loop on for it |
sharing_needed | 409 | Any request of the loop: the member has not consented to share their quests, rewards, shop and rank with the game, or has to consent again. The body adds manageUrl |
membership_required | 409 | Any request of the loop: the Discord account is not in the server. The body adds membership, which is pending, left or not_member, and joinUrl for the last two |
verified_proof_required | 403 | Buy a shop item and Claim an item: the device connected without verification, and the game only lets verified players spend |
spend_on_hold | 409 | Buy a shop item and Claim an item: a new connection or device is still within its hold period and cannot spend yet. The body adds holdUntil and retryAfter |
daily_spend_cap_reached | 409 | Buy a shop item: the purchase would exceed the daily limit. The body adds cap, spentToday, price, resetsAt and retryAfter |
item_not_found | 404 | Buy a shop item: the item does not exist, is switched off, or is not on the game's list. The three cases return the same error |
purchase_refused | 409 | Buy a shop item: the item cannot be bought now. The body adds reason and message |
price_changed | 409 | Buy a shop item: the item's price differs from expectedPrice. The body adds price and expectedPrice |
not_claimable | 409 | Claim an item: the purchase is in a state that cannot be claimed. The body adds status |
quest_task_not_done | 409 | Verify a quest task and Answer a quest task: the verification failed, or the answer is incorrect. The body adds message |
quest_task_unavailable | 409 | Verify a quest task and Answer a quest task: the task cannot be done this way. The body adds reason and, for needs_connect, platform |
checkin_cooldown | 409 | Check in: the daily reward was already claimed. The body adds nextCheckInAt and retryAfter |
economy_disabled | 409 | List the shop, Buy a shop item, Get the balance and Check in: the community has switched its economy off |
survey_not_found | 404 | Answer a survey: the survey does not exist in this community |
survey_unavailable | 409 | Answer a survey: the survey cannot be answered now. The body adds reason (not_open, closed, paused, role, already_answered, not_due, answer_in_discord or not_here) and a message to show the player |
In the SDK, spend_on_hold and daily_spend_cap_reached are thrown as a CeejaySpendBlockedError: see The game loop, inside a game.
Statuses the game loop gives to codes that existed
Eighteen codes were in the list before the game loop. The requests of the loop return them with the status below, which follows the rule that a request that conflicts with a rule is 409. The older requests keep the status they have always had and never change it.
code | Status in the game loop | Message |
|---|---|---|
quest_not_found | 404 | Quest not found |
quest_not_active | 409 | Quest is not active |
quest_not_started | 409 | Quest has not started yet |
quest_ended | 409 | Quest has ended |
quest_already_completed | 409 | Quest already completed and is not repeatable |
quest_limit_reached | 409 | Max completions per user reached |
quest_requires_role | 409 | The member does not have the role this quest requires |
quest_requires_level | 409 | The member has not reached the level this quest requires |
quest_requires_season_xp | 409 | The member has not earned the season XP this quest requires |
quest_requires_program | 409 | The member is not in the program this quest belongs to |
quest_requires_quest | 409 | The member has not completed the quest that comes before this one |
quest_full | 409 | Quest has reached its completion limit |
quest_cooling_down | 409 | The member has to wait before completing this quest again |
quest_daily_limit_reached | 409 | The member has reached this community's daily quest limit |
insufficient_balance | 409 | Insufficient balance |
purchase_not_found | 404 | Purchase not found |
bot_unavailable | 503 | This could not be done right now and nothing was changed. Try again in a few seconds |
bot_timeout | 504 | The answer did not arrive in time, and the request may or may not have been done. Send the same request again: it is safe to repeat |
A refusal about a quest also includes message, text the game can show to the player.
Errors with extra fields
{
"error": "Insufficient scopes",
"code": "insufficient_scope",
"requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
"required": ["economy:grant"],
"granted": ["xp:grant", "xp:read"]
}{
"error": "Insufficient balance",
"code": "insufficient_balance",
"requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
"currentBalance": 120,
"requested": 500,
"balance": 120
}{
"error": "The proof of the player was not accepted",
"code": "proof_rejected",
"requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
"reason": "expired"
}{
"error": "Program is not active",
"code": "program_not_active",
"requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
"programStatus": "paused"
}Key and limit errors
These come before any request runs, and every request can return them.
| Status | code | error | Cause |
|---|---|---|---|
401 | missing_api_key | Missing or invalid Authorization header. Expected: Bearer ceejay_sk_xxx | No Authorization header, or it does not start with Bearer (capital B, then a space). A key in the query string is not read, and a browser preflight (OPTIONS) carries no key, so both get this response too |
401 | invalid_api_key | Invalid API key format. Expected: ceejay_sk_xxx | The key does not start with ceejay_sk_ |
401 | invalid_api_key | Invalid API key | No such key |
401 | api_key_revoked | API key has been revoked | The key was revoked in the dashboard |
401 | api_key_expired | API key has expired | The key had an expiry date and it has passed |
400 | community_required | This is an organisation’s key: name the community in the CEEJAY-Community header | An organisation's key without the CEEJAY-Community header, or with a value that is not a server ID. The call still counts toward the key's rate limit |
403 | community_not_covered | This key doesn’t cover that community | The community is not in the organisation, is not covered by the key, or has turned off the organisation's keys. The call still counts toward the key's rate limit |
403 | insufficient_scope | Insufficient scopes | The key does not have the scope the request needs. The body adds required and granted. The call still counts toward the rate limit |
405 | method_not_allowed | Method not allowed | The request does not take this method. The key is checked first, so the call still counts toward the rate limit |
429 | rate_limited | Rate limit exceeded | Over the key's limit. The body adds retryAfter, and the Retry-After header carries the same number of seconds. The limit is checked before the scope, so a key that is over its limit gets 429 from a request it has no scope for |
429 | rate_limited | Rate limit exceeded | The IP address sent 60 secret keys CEEJAY does not recognise within a minute. Every key from that IP address is refused for the rest of the minute, before it is looked up, except one CEEJAY recognised moments ago. The body adds retryAfter, and the Retry-After header carries the same number. This response carries no X-RateLimit headers: no key is known. A caller using its own key from its own IP address is not affected |
500 | internal_error | Internal server error | Something failed on our side |
A 401 for a secret key carries X-Request-Id and no rate limit headers: no key is known, so there is no limit to report.
Errors every request of a game shares
The requests a game makes with its public key or a player session ("Connect: from the game" in the reference) do not take a secret key, so the table above does not apply to them. These come before any of them runs, in this order:
| Status | code | error | Cause |
|---|---|---|---|
429 | rate_limited | Rate limit exceeded | More than 600 requests in a minute from one IP address, across every request a game makes. The body adds retryAfter, and the Retry-After header carries the same number |
404 | not_available | This is not available on this instance yet. | This instance does not offer Connect or the game loop yet. The body adds feature, which is players. Hide the button |
401 | invalid_public_key | Invalid public key | No Authorization header, a value that does not have the form of a public key, or a key CEEJAY does not know |
401 | wrong_key_kind | This request takes another kind of key | The bearer value is another kind of key or token than the request takes: a secret key sent from a game, a public key sent where a player session belongs |
401 | invalid_player_session | Invalid or expired player session | On Get the player's profile and the requests of the game loop: no player session, or one that cannot be read or has expired. Renew it |
403 | community_not_active | This community is no longer active on CEEJAY | The community removed CEEJAY from its server |
401 | key_retired | This public key has been retired | The public key was retired. Ship the new key before you retire the old one |
401 | key_suspended | This public key has been suspended | The public key was suspended |
404 | game_not_found | Game not found | The game was removed |
403 | audience_not_allowed | A game made for children cannot connect players or send player data | The game is set up as directed at children. Such a game cannot connect players |
409 | game_paused | This game is paused | The game is paused or suspended |
403 | origin_not_allowed | This origin is not on the list saved for this game | The request carries an Origin header that is not on the game's list. A request with no Origin header passes |
405 | method_not_allowed | Method not allowed | The request does not take this method |
500 | internal_error | Internal server error | Something failed on our side |
Every one of these responses includes X-Request-Id and the three rate limit headers, a 401 included: a game's request is counted against its IP address before its key is read.
The five Connect requests a server makes with a secret key share the key and limit errors above, with two differences. A Full Access key gets 403 with the code insufficient_scope, because players:connect is never part of *. And their 429 is about the game's own limit of 600 a minute, not the key's: see Rate limits.
The player requests (Get a player to Propose to the field map) share the key and limit errors too, and use the key's own limit. A Full Access key has players:read and players:write, and gets 403 with the code insufficient_scope on Disconnect a player (players:connect) and Delete a player (players:delete), and on a call to Set player fields or Log player events that sets underAge: true (players:connect).
Errors about an idempotency key
Every request that changes data accepts an idempotencyKey. Four responses are about the key itself:
| Status | code | error | What to do |
|---|---|---|---|
409 | idempotency_key_reused | This idempotencyKey was already used for a different request | Use a key of its own for each request. One key is one request, across every request of the API |
409 | idempotency_in_progress | The first request with this idempotencyKey is still running. Try again in a few seconds. | Wait retryAfter seconds (the Retry-After header says the same) and retry |
409 | idempotency_outcome_unknown | 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. | Sent by Send a message, Emit an event, Complete a quest task and Buy a shop item. Check what happened, then use a new key if it has to be retried |
503 | idempotency_unavailable | idempotencyKey cannot be used on this community yet, so nothing was changed | Waiting does not change this response. Community details reports it as features.retryKeys: ask us to complete the update |
Buy a shop item, which a game sends with a player session, is the one request of the game loop that takes a key, and it requires one. A key sent there belongs to the member, the game and the connection, and is retained for 30 days.
Responses that come before your key is read
CEEJAY returns these before any of its own code runs, so they are not JSON objects: they have no error, no code and no requestId, and they carry no X-Request-Id header.
| Status | Response | Cause |
|---|---|---|
400 | The plain text Invalid JSON | The Content-Type is application/json and the body is not valid JSON |
413 | The plain text Body exceeded 1mb limit | The body is larger than 1 MB |
404 | An HTML page | The path does not exist under /api/v1. Check it against its reference page |
308 | A redirect | The path ends in a slash. The redirect goes to the same path without it, so write paths without a trailing slash |
Calls that go through Discord
Grant a role, Revoke a role and Send a message ask Discord to do the work, and pass its response on:
| Status | code | error | Cause |
|---|---|---|---|
403 | bot_missing_permission | Bot lacks permission to manage this role or Bot lacks permission to send messages in this channel | The bot's Discord permissions or role position |
404 | member_not_found | Member or role not found in this server | Discord found no such member in your server |
404 | channel_not_found | Channel not found or bot has no access | The channel does not exist in your server, or the bot cannot see it |
429 | discord_rate_limited | Discord API rate limited, try again later | Discord is limiting the bot. The body adds retryAfter, and the response includes Retry-After, when Discord provided a retry time |
| the status Discord sent | discord_error | Discord API error: and that status | Anything else Discord refused. The body adds details with Discord's own text |
500 | discord_not_configured | This instance cannot reach Discord yet | This instance has no connection to Discord |
The two role requests first look the role up in your server's role list, which CEEJAY reads from Discord and caches for up to a minute. Then they check that it is not one of your community's bot master roles, which CEEJAY reads on every call. These responses come from those checks, before Discord is asked to change anything:
| Status | code | error | Cause |
|---|---|---|---|
403 | role_not_allowed | A message saying which kind of role it is | The role is @everyone, is managed by Discord or another app, can moderate or manage the server, can mention everyone in a server whose @everyone role cannot, or is a bot master role |
404 | role_not_found | Role not found in this server | Your server has no role with that ID |
503 | roles_unavailable | Could not read this server's roles from Discord, so nothing was changed | The role list could not be read. Nothing was changed |
503 | dashboard_roles_unavailable | Could not check whether this role gives access to the CEEJAY dashboard, so nothing was changed | The bot master roles could not be read. Nothing was changed |
Validation rules worth knowing
- Send
Content-Type: application/jsonwith every JSON body. Without it the body is not read as JSON, and the response is a400. With no content type, ortext/plain, it isThe request body must be a JSON object. With a form content type, whichcurl -dsends when you set none, it names a field as missing, as if you had left the field out. For examplediscordId must be a valid Discord snowflake (17-20 digits), even though the body contains a validdiscordId. - The body of a
POSTmust be a JSON object.null, a string, a number and a list return400withThe request body must be a JSON object. Complete a shop delivery and Complete a quest task return400with their own first message instead. - Each field has one current name, such as
discordId,playerId,reasonandidempotencyKey. Every name a request accepted before is still accepted: each reference page lists the older names. When both names of one field are sent, the older one is used, and nothing is refused for sending both. discordIdmust be a string of 17–20 digits. A JSON number cannot represent all the digits: its last ones would change, so it could name a different member. It is refused with400, like any other value that is not a string, and so is aroleId, achannelIdor an ID inallowedRoleIdssent that way. On Complete a quest task adiscordIdthat is not a string of 17 to 20 digits returns202, not400.- Text is sent as a JSON string.
reasonon the XP and currency requests,contenton Send a message, andtaskKeyandevidenceon Complete a quest task return400for a number, a list or an object. On Complete a quest task anxHandle,wallet,githubortwitchcan also be a whole number, which is read as its digits. Any other value that is not a string returns202. - Text and JSON that CEEJAY stores cannot contain a null character (U+0000). It returns
400with the name of the field, such asreason cannot contain a null character, and nothing is changed or sent. This coversreason, everyidempotencyKey,proofon Complete a quest, the platform, ID and 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. - Text that CEEJAY stores as a key or inside JSON must be well-formed Unicode. Half of a surrogate pair, such as the JSON
"\ud800"or an emoji cut in two, returns400in anidempotencyKey, inreasonon Grant program XP and Deduct program XP, inproofon Complete a quest, indataon Emit an event, inevidenceon Complete a quest task and inmessageon Complete a shop delivery. proofon Complete a quest anddataon Emit an event can nest lists and objects up to 32 levels deep. A deeper value returns400.amountmust be a positive whole number, sent as a JSON number. XP grants cap at 100,000 per call, and so do program XP grants and deductions. Currency grants cap at 1,000,000. XP deductions and currency deductions cap at 2,147,483,647, the largest whole number the database stores. An amount above the cap returns400.- A
questIdand aprogramIdare whole numbers from 1 to 2,147,483,647, sent as a JSON number or as a string of digits written without leading zeros. A fraction, a negative number or a larger one returns400. So does such a purchase ID in a path. - Balances, totals and other counts in a response are always JSON numbers, never text. CEEJAY's own IDs of quests, programs and purchases are numbers in every response.
- Deducting more currency than a member has returns
400with their current balance. A balance never goes negative. - Deducting XP below zero floors at zero rather than failing.
- A currency grant that would take a member over the community's balance cap credits only the amount that fits and reports that in the response. It is not an error.
- The requests that change a member's XP, currency, quests or linked accounts (Grant XP,
Grant program XP, Grant currency,
Complete a quest and Link an account) only write to a member of your server. An ID
CEEJAY has no profile for is looked up in Discord first, and one that is not in your server returns
404with the codemember_not_found. A member who has only just joined can take up to 15 seconds to be found: wait 15 seconds before retrying. Complete a quest task makes the same check on adiscordId, and returns202for one that is not in your server. When CEEJAY cannot confirm with Discord whether an ID is a member, the response is503with the codediscord_unavailableand nothing is written. The requests that read a member behave as before.
Each reference page lists the errors its own request can send.

