CEEJAYDocs

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

SendDeprecated alias, still accepted
idempotencyKeyidempotency_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 sentSame value as
progressThe 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

Request
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.

200 OK
{
  "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.

Rulecode when it is not metStatusThe body includes
The quest's status is activequest_not_active400Nothing. The message names the status
Its start date has passedquest_not_started409startsAt
Its end date has not passedquest_ended409endsAt
The member has the role it requiresquest_requires_role409requiredRoleId
The member has reached the level it requiresquest_requires_level409requiredLevel and level, the member's level now
The member has earned the season XP it requiresquest_requires_season_xp409requiredSeasonXp, seasonXp and seasonRunning
The member is in the program it belongs toquest_requires_program409programId
Its overall completion limit is not reachedquest_full409maxCompletions
It can be repeated, or the member has not completed itquest_already_completed400Nothing
The member is under its per-member limitquest_limit_reached400Nothing
The wait between two runs has passedquest_cooling_down409availableAt, when the member can complete it again
The member has completed the quest that comes firstquest_requires_quest409prerequisiteQuestId
The member is under your community's daily quest limitquest_daily_limit_reached409dailyQuestLimit, 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 404 with the code member_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 503 with the code discord_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.completed webhook to your subscribers.
  • If the XP levelled the member up, it grants that level's rewards and roles, and subscribers to member.level_up get an event. A badge the member did not have yet sends badge.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: 0 and no balance. XP, the badge and the role are still granted. Community details reports this as features.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.programXp gives 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: true and the header Idempotent-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:coins and match-9:xp. A key that was used for a different request, here or on any other request of the API, returns 409 with the code idempotency_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

StatuscodeerrorWhen
400bad_requestThe request body must be a JSON objectThe 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
400bad_requestdiscordId 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
400bad_requestquestId must be a numberquestId is missing, or is neither a number nor a string of digits
400bad_requestquestId must be a positive integerquestId is zero, negative, has a fraction, or is a string of digits with a leading zero
400bad_requestquestId cannot exceed 2147483647questId is above 2,147,483,647
400bad_requestidempotencyKey must be a string of 1-128 charactersThe key is empty, longer than 128 characters or not text, or contains an unpaired surrogate, such as the JSON "\ud800"
400bad_requestidempotencyKey cannot contain a null characterThe key contains a null character (U+0000)
400bad_requestidempotency_key must be a string of 1-128 charactersThe same check, when you sent the key as idempotency_key
400bad_requestproof cannot contain a null characterproof contains a null character (U+0000) somewhere, in a value or in a field name
400bad_requestproof is nested too deeplyproof nests lists and objects more than 32 levels deep
400bad_requestproof must be valid Unicode textproof contains an unpaired surrogate somewhere, such as the JSON "\ud800"
400quest_not_activeQuest is not active (status: draft)The quest is a draft, paused, ended or archived. The message names its status
400quest_already_completedQuest already completed and is not repeatableAlready completed
400quest_limit_reachedMax completions per user reachedPer-member limit reached
404quest_not_foundQuest not foundNo quest with this ID on your server
404member_not_foundMember not foundCEEJAY 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
404not_availableThis 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
409quest_not_startedQuest has not started yetBefore the quest's start date. The body includes startsAt
409quest_endedQuest has endedAfter the quest's end date. The body includes endsAt
409quest_requires_roleThe member does not have the role this quest requiresRequired role missing. The body includes requiredRoleId
409quest_requires_levelThe member has not reached the level this quest requiresRequired level not reached. The body includes requiredLevel and level
409quest_requires_season_xpThe member has not earned the season XP this quest requiresSeason XP not earned. The body includes requiredSeasonXp and seasonXp and seasonRunning
409quest_requires_programThe member is not in the program this quest belongs toNot in the quest's program. The body includes programId
409quest_fullQuest has reached its completion limitOverall limit reached. The body includes maxCompletions
409quest_cooling_downThe member has to wait before completing this quest againToo soon after the last run. The body includes availableAt
409quest_requires_questThe member has not completed the quest that comes before this oneEarlier quest not completed. The body includes prerequisiteQuestId
409quest_daily_limit_reachedThe member has reached this community's daily quest limitDaily quest limit reached. The body includes dailyQuestLimit and completedToday and resetsAt
409xp_limit_reachedThis grant would take the member's XP past the largest number CEEJAY can store, so nothing was changedThe quest's XP would take the member's XP past the largest number CEEJAY can store. Nothing was completed
409balance_limit_reachedThis grant would take the member's total past the largest number CEEJAY can store, so nothing was changedThe quest's coins would take the member's lifetime total past the largest number CEEJAY can store. Nothing was completed
409idempotency_key_reusedThis idempotencyKey was already used for a different requestYour API key already used this idempotencyKey for a different request: a request of another kind, or this one with other values. Nothing was changed
409idempotency_in_progressThe 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
503discord_unavailableCould not check with Discord that this ID is a member of the server, so nothing was changedCEEJAY 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
503member_roles_unavailableCould not read this member's roles from Discord, so the quest was not completedThe quest requires a role, and CEEJAY could not read the member's roles from Discord. Nothing was completed. Retry the request
503idempotency_unavailableidempotencyKey cannot be used on this community yet, so nothing was changedYou sent a key for a quest that grants coins, and this instance does not support idempotency keys yet. Nothing was completed
409 Conflict
{
  "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 questComplete a quest task
Path/v1/quests/complete/v1/quests/tasks/complete
You sendA discordId and a questIdA 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 decidesYou, within the quest's rules. The quest is completed on your instruction, whatever its tasks areCEEJAY. 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 APINoYes
The quest's rulesApplied, and a rule that is not met is an error with a codeApplied, and a quest that a rule blocks is skipped silently
A Discord ID CEEJAY has no profile for, that is not in your serverRejected with 404Does not match a member: 202 with matched: 0
Review by a moderatorNoOptional
Role rewardAssigned by CEEJAY's bot after the responseAssigned by CEEJAY's bot
Program XP, for a quest in a programAdded with the completionAdded
Extra rewards from the Rewards EngineGranted by CEEJAY's bot after the responseGranted
quest.completed webhookSent after the responseSent when the quest completes
Retrying the callidempotencyKey covers the whole completionidempotencyKey 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.

On this page

Community StudiosPrivacyTerms