Complete a quest task
Report that a member finished something your own platform checks, so CEEJAY can complete the quest task that has your key.
POST /v1/quests/tasks/complete · requires quests:complete
Use this when the thing a member has to do happens on your own platform: finishing a tutorial, reaching a rank, signing up. You add a task of the type Completed by your API to a quest. When a member has done it, your server calls this request and CEEJAY completes the task. When it was the quest's last task, CEEJAY completes the quest and grants its rewards.
To complete a whole quest for a member without matching a task, use Complete a quest. That page compares the two requests.
This request is also available at a legacy endpoint, /api/quests/external/complete, which keeps working: see Complete a quest task (legacy endpoint).
Set up the task
Open Gamification → Quests. Open a quest, or press New quest, then press Add task and choose Completed by your API under Integrations.
Copy the key from the Task key field. CEEJAY fills it in as tk_ and ten lowercase letters and digits, such as tk_4k2m9x7qzb, and you can change it. Your platform sends this key to say which task a member finished. A duplicated task gets a new key. List quests does not return it, so copy it from here.
Choose whether completions go to the Review Queue with Send completions to the Review Queue instead of completing straight away. Off, which is the default, the member completes the task as soon as CEEJAY receives your request. On, a moderator approves each one first, with the evidence your platform sent.
Add a link button to the member's quest card with Where members do it (optional), and name it with Link label (optional). The card also has a Check status button, which says whether the task is done, waiting for review or rejected.
Make an API key with Quests Complete under Settings → Developers → API Keys. The task's editor lists the keys that have that scope under API key. Under What your platform calls it shows the endpoint URL and the JSON body, and Ready-made curl gives a curl command with a placeholder for the key, which Copy curl copies.
Give each task its own key, and use only letters, digits, _ and -, from 4 to 64 characters. The builder asks for one key per task but does not check it, and it accepts keys this request rejects.
Body parameters
Prop
Type
Deprecated names that still work
| Send | Deprecated alias, still accepted |
|---|---|
taskKey | task_key |
discordId | discord_id |
xHandle | x_handle |
Send one of the two. If both are sent, the deprecated one is used. Deprecated names remain supported for as long as version 1 exists, and an error message names the one you sent.
Who did it
Send one of discordId, playerId with platform, xHandle, wallet, github and twitch. If you send more than one, CEEJAY uses the first in that order and ignores the rest. A discordId that is null or an empty string is skipped, and the next one in that order is used.
| Field | CEEJAY finds the member by |
|---|---|
discordId | The ID itself. An ID CEEJAY has no member profile for must be a member of your server: see below |
playerId with platform | The account you linked with Link an account: the member's ID in your game, on that platform, ignoring capitals and spaces at its ends |
xHandle | The member's connected X account: its ID or its username, ignoring capitals. A leading @ is dropped |
wallet | The member's connected web3 wallet: its address, ignoring capitals |
github | The member's connected GitHub account: its ID or its login |
twitch | The member's connected Twitch account: its ID or its login |
platform is read only when playerId is the identifier in use. With a discordId, or with no playerId, whatever the body contains under that name is ignored. With playerId, a missing platform returns 400, and a platform your community does not know returns 400 with the code unknown_platform.
When platform is a game set up for Connect and the key is limited to other games of your community, the task returns 403 with the code game_not_allowed. Otherwise, a member who connected the player themselves at the basic level is not found by playerId: nothing confirmed that the player is theirs, so the task returns 202 as it does for a player nobody is linked to. A player connected at the verified level is found, and so is one your own server linked with Link an account. A game can be set up so that basic connections earn too, and then they are found.
Send xHandle, wallet, github and twitch as strings. A whole number is read as well, as its digits, so a GitHub or Twitch ID sent as a JSON number finds the same account as that ID in quotes. That applies up to 9,007,199,254,740,991, the largest whole number a JSON number can hold with every digit intact. An X user ID is larger, so send it in quotes. A larger number, a number with a fraction, a list, an object and true or false do not match a member and return 202. They are never converted to text.
An account counts when the member connected it in your community and has not disconnected it. Members connect accounts themselves, for example through the Connect X, Connect Web3 Wallet, Connect GitHub and Connect Twitch quest tasks, or you link them with Link an account under the platform twitter, metamask, github or twitch. When two members connected the same account, the one who connected it last is used.
A discordId CEEJAY already has a member profile for is used as it is. One it has no profile for is looked up in Discord first:
- A member of your server passes, and that result is cached for five minutes.
- An ID that is not in your server does not match a member. The call returns
202withmatched: 0, like any identifier CEEJAY cannot match. - When the Discord API is unavailable, or its response does not say whether the ID is a member, the call returns
503with the codediscord_unavailable. No changes were made: retry shortly.
Returns
Prop
Type
Each entry of completed and sentForReview contains questId and questTitle. These fields are inside data. Alongside success, the response also includes the two original fields, which remain for as long as version 1 exists: matched, and completed with each quest's title under title.
Example
curl -X POST https://ceejay.communitystudios.xyz/api/v1/quests/tasks/complete \
-H "Authorization: Bearer $CEEJAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"taskKey": "tk_4k2m9x7qzb",
"discordId": "409876543210987654",
"evidence": "https://acme.example/players/nova/tutorial",
"idempotencyKey": "tutorial-88213004"
}'The TypeScript example uses ceejay.quests.reportTask from the TypeScript SDK. It returns the full response below.
{
"success": true,
"matched": 1,
"completed": [
{
"questId": 42,
"title": "Finish the Acme tutorial"
}
],
"data": {
"matched": 1,
"completed": [
{
"questId": 42,
"questTitle": "Finish the Acme tutorial"
}
],
"sentForReview": [],
"duplicate": false
}
}When CEEJAY does not match a member, the response is 202:
{
"success": true,
"matched": 0,
"data": {
"matched": 0,
"completed": [],
"sentForReview": []
}
}What a call does
- CEEJAY checks the key, then finds the member.
- It looks at the quests in your community that are active and inside their start and end dates, and at their Completed by your API tasks whose key is the one you sent. Any number of tasks can match, in any quest. A quest whose task key does not match is never started.
- For each match it applies the quest's own rules, listed below. A quest the member cannot take part in is skipped without an error.
- It completes the task. With review on, it files the report in the Review Queue instead, and
data.sentForReviewlists the quest. - When every task of a quest is done, the quest completes, and
data.completedlists it.
What CEEJAY checks, and what it does not
The quest's own rules apply. If one of them blocks a quest, the call still returns 200 with that quest omitted, and it does not say which rule applied:
- The quest is not active, or today is outside its dates.
- The quest requires a role the member does not have. A member who already has a run of that quest open is not checked again.
- The member is below the level, or short of the season XP, that the quest asks for.
- The quest belongs to a program the member has not joined.
- The quest's prerequisite quest is not done.
- The wait between runs of a repeatable quest has not passed.
- The member has reached the quest's limit per member, or the quest has reached its overall limit.
- The member has reached your community's Daily Quest Limit Per User, in your Quest settings.
When a matching quest requires a role and CEEJAY's bot cannot read the member's roles from Discord, the call returns 503 with the code discord_unavailable. Nothing was started or completed: retry shortly.
CEEJAY does not check:
- That a member it already knows is still in your server. Only a
discordIdit has no member profile for is looked up in Discord. - That what you report is true. Unless review is on, you are confirming that the member did it.
- That a key belongs to one task. Every live task that has the key is completed, in every quest.
- Which API key made the call. Any key with the scope can report any task key in your community.
Review
With review on, nothing completes yet. CEEJAY files the report in the Review Queue, under Gamification → Quests, and your team gets a Quest submission needs review notification. The moderator sees evidence as the proof, or Reported done by your platform when you sent none. Approving completes the task, and the quest when it was the last task. Rejecting does not, and the next call from your platform files it again.
Review is on when the task's switch is on or the call sends review: true. A call can ask for review, but it cannot take it away. While a report is awaiting review, the quest's other automatic tasks do not advance for that member.
Without review, evidence is stored with the member's progress on the task.
Retrying safely
Send an idempotencyKey with every call you might need to retry. It is a string of 1 to 128 characters that you choose, sent in the JSON body.
- A key is stored only when the call changed something: a
200in which at least one quest was completed or sent for review. A retry then returns the original response, withdata.duplicate: trueand the headerIdempotent-Replayed: true, and CEEJAY's bot is not called again. - No key is stored for a call that made no changes. A
202, and a200with both lists empty, do not use up the key. A retry with the same key is processed as a new request, for example after the player has linked the account. - Each key is tied to one request payload. The same request means the same task key, member, evidence and
review. A key that was used for a different request returns409with the codeidempotency_key_reused. - After a lost response, the key prevents a second attempt. When CEEJAY's bot does not respond in time, or its response is lost because it failed part way or the connection dropped, the call returns
502with the codebot_timeout, and the task may or may not have been completed. With a key, the same request returns409with the codeidempotency_in_progressfor a minute, andidempotency_outcome_unknownafter that: it is never sent to the bot a second time. Check the member's quests in the dashboard, then use a new key if the task has to be sent again. bot_unavailableis the response that is safe to retry. It means the bot never received the task or rejected it: no changes were made, and the key is released, so a retry with the same key is processed as a new request.- On an instance that does not support idempotency keys yet, a call that sends one returns
503with the codeidempotency_unavailable.idempotency_keyis not accepted on this endpoint; useidempotencyKey.
Without a key, what a retry does depends on the quest:
- A quest a member can complete once: after the first completion a retry does nothing and returns
200with empty lists. It is safe to retry. - A report awaiting review: a retry does nothing until a moderator decides.
- A task already done in a quest with other tasks still open: a retry does nothing.
- A quest members can repeat: once the wait between runs is over, a retry starts a new run and completes the task in it, and the quest grants its rewards again when that was its last task. With no wait set, every call starts a new run. So after a
bot_timeout, sending the task again without a key can complete a repeatable quest a second time. Send one call for each thing the member did, with a key.
Side effects
- When a quest completes, CEEJAY grants everything configured on it: XP, coins while your community's economy is enabled, the badge, the role and any extra rewards set on the quest in the Rewards Engine. A quest in a program also adds its XP to the member's program XP.
- Subscribers to the
quest.completedwebhook get an event when the quest completes. - The completion is added to the member's activity history. A badge the member did not have yet can send
badge.earned, and XP that levels the member up grants that level's rewards and roles and can sendmember.level_up. - A call starts the quest for the member, if they have not started it, for every live quest that has a Completed by your API task with your key. A quest that one of the rules above blocks is not started.
- CEEJAY posts no completion message and no achievement card in Discord for this request. The member sees the task's status when they press Check status.
Errors
| Status | Response | When |
|---|---|---|
200 | data.matched: 1, with data.completed and data.sentForReview | The report reached CEEJAY's bot. The lists name the quests it finished and the ones it sent for review, and are empty when it did neither |
202 | data.matched: 0 | CEEJAY did not match a member. An identifier it cannot match, a discordId that is not a string of 17 to 20 digits, a discordId CEEJAY has no member profile for that is not in your server, and an identifier that is empty, null, or neither a string nor a whole number up to 9,007,199,254,740,991 all return this. No changes were made |
| Status | code | error | When |
|---|---|---|---|
400 | bad_request | taskKey is required (4–64 letters, digits, _ or -) | taskKey is missing, is not a string, or is not 4 to 64 letters, digits, _ or -. A body that is not a JSON object returns this too |
400 | bad_request | task_key is required (4–64 letters, digits, _ or -) | The same check, when you sent the key as task_key |
400 | bad_request | Send one of discordId, playerId with platform, xHandle, wallet, github or twitch to say who did it | The body names nobody: none of discordId, playerId, xHandle, wallet, github and twitch is in it |
400 | bad_request | evidence must be a string | evidence was sent and is not a string. null counts as not sent |
400 | bad_request | evidence cannot contain a null character | evidence contains a null character (U+0000), which cannot be stored |
400 | bad_request | evidence must be valid Unicode text | evidence contains an unpaired surrogate, such as the JSON "\ud800" |
400 | bad_request | xHandle cannot contain a null character | The identifier the member is looked up by contains a null character. The message names the field, using the name you sent |
400 | bad_request | playerId must be a string of 1 to 255 characters | playerId is not a string, or is empty once its spaces are removed |
400 | bad_request | playerId cannot exceed 255 characters | playerId over 255 characters |
400 | bad_request | playerId cannot contain a null character | A null character in playerId |
400 | bad_request | platform is required with playerId | playerId was sent without a platform |
400 | bad_request | platform cannot exceed 50 characters | platform is longer than 50 characters |
400 | bad_request | platform cannot contain a null character | A null character in the platform |
400 | bad_request | platform can only contain letters, digits and underscores | platform contains a space, a hyphen or any other character |
400 | bad_request | idempotencyKey must be a string of 1-128 characters | The key is empty, longer than 128 characters or not text, or contains an unpaired surrogate, such as the JSON "\ud800" |
400 | bad_request | idempotencyKey cannot contain a null character | The key contains a null character (U+0000) |
400 | unknown_platform | Unknown platform. Add it as a custom platform under Settings, Developers, Platforms, then send the task again | The platform is not one CEEJAY knows by name, not a custom platform of your community, and one your community has no link for |
403 | game_not_allowed | This API key is limited to other games | The member is named by a playerId on a platform that is a game of your community, and the key is limited to other games. Nobody is looked up and nothing is completed |
404 | not_available | This is not available on this instance yet. | The member is named by an account, and this instance does not support account linking yet. The body includes feature |
409 | idempotency_key_reused | This idempotencyKey was already used for a different request | Your API key already used this idempotencyKey for a different request: a request of another kind, or this one with other values. Nothing was changed |
409 | idempotency_in_progress | The first request with this idempotencyKey is still running. Try again in a few seconds. | The first request with this key is still being processed. Wait retryAfter seconds 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. | A first request with this key was started more than a minute ago and never finished. CEEJAY cannot know whether it took effect, and does not do it again. Check what happened, then use a new key if it has to be sent again |
502 | bot_unavailable | The quest engine could not be reached | CEEJAY's bot, which completes quests, could not be reached or rejected the request. No changes were made, and the idempotency key is released |
502 | bot_timeout | The quest engine did not answer in time, so the task may or may not have been completed | CEEJAY's bot did not respond within 12 seconds. The task may or may not have been completed, and the idempotency key stays reserved: see Retrying safely before you retry |
502 | bot_timeout | The quest engine did not give an answer, so the task may or may not have been completed | CEEJAY's bot received the task and its response was lost: it failed part way, or the connection to it dropped. The task may or may not have been completed, and the idempotency key stays reserved: see Retrying safely before you retry |
503 | discord_unavailable | Could not check with Discord that this ID is a member of the server, so nothing was changed | CEEJAY has no profile for the discordId and could not confirm with Discord whether it is a member of your server. No changes were made: retry with a pause between attempts |
503 | discord_unavailable | Could not read this member's roles from Discord, so no quest was completed | A matching quest requires a role, and the member's roles could not be read from Discord. Nothing was started or completed: retry with a pause between attempts |
503 | idempotency_unavailable | idempotencyKey cannot be used on this community yet, so nothing was changed | You sent a key, and this instance does not support idempotency keys yet. Nothing was changed. Community details reports it as features.retryKeys |
A body that is not valid JSON, or is over 1 MB, is rejected before the key is checked.
CEEJAY returns 202, not an error, when it does not match a member, and the response says nothing else about the member. This is deliberate, so that nobody can work out who is in your community by trying names. It is not a full guarantee: 0 against 1 still shows whether an X handle, wallet or login is connected, and whether a Discord ID is one CEEJAY knows or a member of your server.
Every error body also includes requestId, and a 400 with the code bad_request identifies the invalid field in field. Match on code; message text can change.
The errors every request shares (401, 403, 405, 429, 500) are on the Errors page.
Send Discord IDs as strings
A JSON number cannot represent a Discord ID accurately: its last digits change, and it would identify someone else. CEEJAY never converts a number into an ID, so a discordId sent as a JSON number does not match a member and returns 202, even when you sent another identifier alongside it. Always send it as a string.
Related
- Complete a quest completes a whole quest for a member, with no task.

