Complete a quest
Mark a quest completed for a member and grant its configured rewards.
POST /v1/quests/complete · requires quests:complete
Body parameters
Prop
Type
Deprecated names that still work
| Send | Deprecated alias, still accepted |
|---|---|
idempotencyKey | idempotency_key |
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.
Returns
Prop
Type
The response also includes the original field names. They contain the same values, and remain for as long as version 1 exists.
| Deprecated field, still sent | Same value as |
|---|---|
progress | The run as stored, with its snake_case field names. Use run. On a retry it is the run as it is now, and null when the run no longer exists. |
run contains id (text), status, startedAt and completedAt. progress is the same run as CEEJAY stores it, with lowercase field names such as id, quest_id, status, submission_data (your proof) and completed_at: it is the deprecated form, and is still returned.
Rewards
Completing a quest grants everything configured on it. rewards reports what the response itself granted:
Prop
Type
Example
curl -X POST https://ceejay.communitystudios.xyz/api/v1/quests/complete \
-H "Authorization: Bearer $CEEJAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"discordId": "409876543210987654",
"questId": 42,
"proof": {
"matchId": "m-88213",
"score": 14
},
"idempotencyKey": "match-m-88213"
}'The TypeScript example uses ceejay.quests.complete from the TypeScript SDK. It returns the data of the response below.
{
"success": true,
"data": {
"discordId": "409876543210987654",
"questId": 42,
"questTitle": "Join the weekend tournament",
"status": "completed",
"rewards": {
"xp": 500,
"currency": 250,
"roleId": "982371004827100394",
"badgeId": 7,
"currencyRequested": 250,
"currencyCapped": false,
"balance": 2700,
"programXp": 0
},
"progress": {
"id": 9012,
"server_id": "721104938271003842",
"user_id": "409876543210987654",
"quest_id": 42,
"status": "completed",
"progress_data": {},
"submission_data": {
"matchId": "m-88213",
"score": 14
},
"review_note": null,
"reviewed_by": null,
"completed_at": "2026-10-01T09:14:22.118Z",
"created_at": "2026-10-01T09:14:22.118Z",
"updated_at": "2026-10-01T09:14:22.118Z"
},
"duplicate": false,
"run": {
"id": "9012",
"status": "completed",
"startedAt": "2026-10-01T09:14:22.118Z",
"completedAt": "2026-10-01T09:14:22.118Z"
}
}
}What the call checks
The quest must exist on your server. The call then applies the quest's own rules, the ones CEEJAY applies when a member does the quest in Discord. A rule that is not met returns a code that identifies it, and nothing is completed or granted.
| Rule | code when it is not met | Status | The body includes |
|---|---|---|---|
The quest's status is active | quest_not_active | 400 | Nothing. The message names the status |
| Its start date has passed | quest_not_started | 409 | startsAt |
| Its end date has not passed | quest_ended | 409 | endsAt |
| The member has the role it requires | quest_requires_role | 409 | requiredRoleId |
| The member has reached the level it requires | quest_requires_level | 409 | requiredLevel and level, the member's level now |
| The member has earned the season XP it requires | quest_requires_season_xp | 409 | requiredSeasonXp, seasonXp and seasonRunning |
| The member is in the program it belongs to | quest_requires_program | 409 | programId |
| Its overall completion limit is not reached | quest_full | 409 | maxCompletions |
| It can be repeated, or the member has not completed it | quest_already_completed | 400 | Nothing |
| The member is under its per-member limit | quest_limit_reached | 400 | Nothing |
| The wait between two runs has passed | quest_cooling_down | 409 | availableAt, when the member can complete it again |
| The member has completed the quest that comes first | quest_requires_quest | 409 | prerequisiteQuestId |
| The member is under your community's daily quest limit | quest_daily_limit_reached | 409 | dailyQuestLimit, completedToday and resetsAt |
The rules are checked in that order, and the first one that is not met is the one returned.
A rule that is not met returns 409. Three of them return 400, as they did before the codes existed, and keep that status for as long as version 1 exists. Match on the code, not the status.
The daily quest limit is your community's Daily Quest Limit Per User setting of Quests: 5 unless the community set another, and Community details reports it. It counts the quests a member completes in one day, across all quests, and the day is the UTC day. It is a rule about starting: it is applied when the member has no open run of this quest, so a run they started in Discord before they reached the limit still completes.
The level comes from the member's XP and your community's level curve, as in Get a member. The required role is read from Discord at the time of the call: when Discord cannot be reached, the call returns 503 with the code member_roles_unavailable and completes nothing.
The call does not run the quest's own verification or review. By calling it, you confirm that the member completed the quest. List quests returns each of the quest's rules, if you want to check them before you call.
The member
For a member CEEJAY already has a profile for, the call goes straight to the quest's rules. An ID CEEJAY has no profile for is looked up in Discord first:
- A member of your server passes. When the quest is completed, a profile is created when XP is granted and a balance is created when currency is granted.
- An ID 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. - 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. Retry shortly.
Nothing is written in the last two cases. A successful membership lookup is cached for five minutes.
A completion is a single transaction
The completion, the XP, the currency, the badge, the program XP and the idempotency key are committed in a single transaction. A call that fails part of the way, with a 500 for example, leaves nothing behind: retry it and everything is granted once.
Concurrent requests for the same member and quest are processed one at a time. The second request sees the result of the first, so the two cannot both pass the checks. A completion the member makes in Discord at the same time is processed the same way.
When the member has an open run of the quest, one they started in Discord that is in progress or awaiting review, the call completes that run. A new run is created only when the member has none open.
A completion that takes the last place of a quest with an overall limit ends the quest: its status becomes ended, as it does when the last place is taken in Discord.
What happens after the response
The response is returned as soon as the completion is written. CEEJAY's bot then completes the remaining steps, usually within a minute:
- It assigns the quest's role, the one in
rewards.roleId. - It grants the extra rewards set on the quest in the Rewards Engine.
- It sends the
quest.completedwebhook to your subscribers. - If the XP levelled the member up, it grants that level's rewards and roles, and subscribers to
member.level_upget an event. A badge the member did not have yet sendsbadge.earned.
The call returns 200 when the bot is down: these steps run once the bot is back online. A role Discord could not assign at that moment is retried for about 25 minutes, and quest.completed can say roleGranted: false for a role that is assigned shortly after.
Stop adding these yourself
If you call Grant a role, Grant program XP or Grant currency yourself after a completion to add what this request used to leave out, stop doing that: the member would now receive it twice.
Also part of a completion:
- Coins while the economy is disabled. While your community's economy is disabled, the quest's coins are not granted, as they are not when the same quest is completed in Discord. The response then has
rewards.currencySkipped: true,rewards.currency: 0and nobalance. XP, the badge and the role are still granted. Community details reports this asfeatures.economy.enabled. - Program XP. A quest in a program adds its XP reward to the member's program XP, in the same write as the completion.
rewards.programXpgives the amount. - The completion is logged in the member's activity history, and the currency shows in Commerce → Economy → Transactions as a Quest reward.
Retrying safely
Send an idempotencyKey with every call you might need to retry, such as after a timeout. It is a string of 1 to 128 characters that you choose, sent in the JSON body: an Idempotency-Key header is not read.
- Retrying with the same key returns the original response. When the same request is sent again with its key, CEEJAY returns the original response, with
duplicate: trueand the headerIdempotent-Replayed: true. - Each key is tied to one request payload. The same request means the same member, quest and proof, and the key covers the whole completion: its XP, badge and program XP as well as its currency. Two requests for one event need two keys, for example
match-9:coinsandmatch-9:xp. A key that was used for a different request, here or on any other request of the API, returns409with the codeidempotency_key_reused, and nothing is changed. - A rejected request does not use up the key. A call that was rejected can be retried with the same key once the cause is fixed.
- A key belongs to the API key that sent it. Another API key of your community can send the same text, and it is a different key.
- Keys are retained for 30 days. Use a value you can regenerate after a crash, such as an order ID or a match ID.
- This request accepted a key before version 1.0.0, as
idempotency_key. That name still works, and the key keeps working on an instance that has not been updated yet. - Without a key, a retried call is a new call.
A retry returns 200 with the original response and duplicate: true even when the quest has ended or reached a limit since, so a retry of the call that took the last place still gets its response. progress in a retry is the run as it is now, and null when the run no longer exists.
A completion that granted coins is also recognised from the member's transaction history when CEEJAY no longer stores its response: a key older than 30 days, or one sent before version 1.0.0. The response then includes the coins and the balance of the first call, the quest as it is now, and rewards.programXp: 0.
Without a key, a second call is a new completion and is checked like one: a quest that cannot be repeated returns 400 with the code quest_already_completed.
Errors
| Status | code | error | When |
|---|---|---|---|
400 | bad_request | The request body must be a JSON object | The body is null, text, a number or a list. A request with no Content-Type header is read as text, so it gets this error too |
400 | bad_request | discordId must be a valid Discord snowflake (17-20 digits) | discordId is missing or is not a string of 17 to 20 digits. A JSON number is rejected |
400 | bad_request | questId must be a number | questId is missing, or is neither a number nor a string of digits |
400 | bad_request | questId must be a positive integer | questId is zero, negative, has a fraction, or is a string of digits with a leading zero |
400 | bad_request | questId cannot exceed 2147483647 | questId is above 2,147,483,647 |
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 | bad_request | idempotency_key must be a string of 1-128 characters | The same check, when you sent the key as idempotency_key |
400 | bad_request | proof cannot contain a null character | proof contains a null character (U+0000) somewhere, in a value or in a field name |
400 | bad_request | proof is nested too deeply | proof nests lists and objects more than 32 levels deep |
400 | bad_request | proof must be valid Unicode text | proof contains an unpaired surrogate somewhere, such as the JSON "\ud800" |
400 | quest_not_active | Quest is not active (status: draft) | The quest is a draft, paused, ended or archived. The message names its status |
400 | quest_already_completed | Quest already completed and is not repeatable | Already completed |
400 | quest_limit_reached | Max completions per user reached | Per-member limit reached |
404 | quest_not_found | Quest not found | No quest with this ID on your server |
404 | member_not_found | Member not found | CEEJAY has no profile for the ID, and it is not a member of your server. Nothing was changed. A member who has only just joined can take up to 15 seconds to be found: wait 15 seconds before retrying |
404 | not_available | This is not available on this instance yet. | This feature is not available on this instance yet. Community details reports the same under features. The body includes feature |
409 | quest_not_started | Quest has not started yet | Before the quest's start date. The body includes startsAt |
409 | quest_ended | Quest has ended | After the quest's end date. The body includes endsAt |
409 | quest_requires_role | The member does not have the role this quest requires | Required role missing. The body includes requiredRoleId |
409 | quest_requires_level | The member has not reached the level this quest requires | Required level not reached. The body includes requiredLevel and level |
409 | quest_requires_season_xp | The member has not earned the season XP this quest requires | Season XP not earned. The body includes requiredSeasonXp and seasonXp and seasonRunning |
409 | quest_requires_program | The member is not in the program this quest belongs to | Not in the quest's program. The body includes programId |
409 | quest_full | Quest has reached its completion limit | Overall limit reached. The body includes maxCompletions |
409 | quest_cooling_down | The member has to wait before completing this quest again | Too soon after the last run. The body includes availableAt |
409 | quest_requires_quest | The member has not completed the quest that comes before this one | Earlier quest not completed. The body includes prerequisiteQuestId |
409 | quest_daily_limit_reached | The member has reached this community's daily quest limit | Daily quest limit reached. The body includes dailyQuestLimit and completedToday and resetsAt |
409 | xp_limit_reached | This grant would take the member's XP past the largest number CEEJAY can store, so nothing was changed | The quest's XP would take the member's XP past the largest number CEEJAY can store. Nothing was completed |
409 | balance_limit_reached | This grant would take the member's total past the largest number CEEJAY can store, so nothing was changed | The quest's coins would take the member's lifetime total past the largest number CEEJAY can store. Nothing was completed |
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 |
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 ID and could not confirm with Discord whether it is a member of your server. Retry with a pause between attempts. If it keeps failing, CEEJAY's bot may no longer be in your server |
503 | member_roles_unavailable | Could not read this member's roles from Discord, so the quest was not completed | The quest requires a role, and CEEJAY could not read the member's roles from Discord. Nothing was completed. Retry the request |
503 | idempotency_unavailable | idempotencyKey cannot be used on this community yet, so nothing was changed | You sent a key for a quest that grants coins, and this instance does not support idempotency keys yet. Nothing was completed |
{
"error": "The member has not reached the level this quest requires",
"code": "quest_requires_level",
"requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
"requiredLevel": 10,
"level": 7
}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.
Which request to use
Two requests complete quests for a partner platform. They do different jobs.
| Complete a quest | Complete a quest task | |
|---|---|---|
| Path | /v1/quests/complete | /v1/quests/tasks/complete |
| You send | A discordId and a questId | A taskKey, and the member as a Discord ID, their ID in your game, an X handle, a wallet, a GitHub login or a Twitch login |
| Who decides | You, within the quest's rules. The quest is completed on your instruction, whatever its tasks are | CEEJAY. It completes the tasks that have your key, and the quest when the last one is done |
| Needs a task of the type Completed by your API | No | Yes |
| The quest's rules | Applied, and a rule that is not met is an error with a code | Applied, and a quest that a rule blocks is skipped silently |
| A Discord ID CEEJAY has no profile for, that is not in your server | Rejected with 404 | Does not match a member: 202 with matched: 0 |
| Review by a moderator | No | Optional |
| Role reward | Assigned by CEEJAY's bot after the response | Assigned by CEEJAY's bot |
| Program XP, for a quest in a program | Added with the completion | Added |
| Extra rewards from the Rewards Engine | Granted by CEEJAY's bot after the response | Granted |
quest.completed webhook | Sent after the response | Sent when the quest completes |
| Retrying the call | idempotencyKey covers the whole completion | idempotencyKey is stored only for a call that changed something |
Use Complete a quest when your platform decides on its own that a member earned a quest, and there is no task to match. Use Complete a quest task when the quest has a task that your platform checks, and you want CEEJAY to track the tasks and run the quest's review. The quest builder shows that request's endpoint URL, with the task key, on a task of the type Completed by your API.

