ceejay.quests
List quests, complete one for a member, or report a quest task your own platform checked.
Every function takes the same last argument, options: signal, timeoutMs and
maxRetries, for that one call. A write also takes idempotencyKey there. Every function
can also raise the errors any request can raise, such as a bad key or the rate limit. See
Errors and retries.
Which of the two to call
| You want to | Call |
|---|---|
| Complete a whole quest for a member, and vouch that they did it | ceejay.quests.complete |
| Report one thing a member did, for a quest task of the type Completed by your API | ceejay.quests.reportTask |
ceejay.quests.complete accepts the quest's ID. ceejay.quests.reportTask accepts the task
key the quest builder shows on the task, and CEEJAY determines which quests it finishes.
ceejay.quests.list
Lists the quests set up on your server, a page at a time, with their rewards and rules.
GET /v1/quests/list · requires quests:read · API reference: List quests
Retried by the SDK: Yes. It is a read
Parameters: one object, which can be left out, then options.
Prop
Type
There are two ways to paginate. Use one of them. With cursor, each quest is returned
exactly once, even when quests are added while you page. With offset, you skip a number
of quests.
Returns the full response, typed as Schemas['ListQuestsResponse']. The quests are in
data, and page, total, limit and offset describe the pagination.
Prop
Type
Each quest, typed as Schemas['Quest']:
Prop
Type
And the rules ceejay.quests.complete applies to it:
Prop
Type
The community's daily quest limit is not on a quest: ceejay.community.get reports it.
Each quest also includes these legacy field names, in lower case with underscores, which
are still sent: icon_emoji, quest_type, xp_reward,
currency_reward, role_reward_id, badge_id, is_repeatable, max_completions,
max_completions_per_user (now maxCompletionsPerMember), starts_at, ends_at,
verification_type, program_id, created_at, repeat_cooldown_hours, required_level,
required_role_id, prerequisite_quest_id and required_season_xp.
Errors
| Status | code | When |
|---|---|---|
400 | bad_request | limit or offset is not a whole number of 0 or more, or cursor is not one this function returned |
404 | not_available | Quests are not available on this instance |
Example
const page = await ceejay.quests.list({ category: 'integrations', limit: 20 })
console.log(`${page.data.length} of ${page.total} quests`)
for (const quest of page.data) {
console.log(quest.id, quest.title, quest.xpReward, quest.currencyReward)
}To read every quest, follow page.nextCursor until it is null:
let cursor: string | null = null
do {
const page = await ceejay.quests.list({ limit: 100, ...(cursor ? { cursor } : {}) })
for (const quest of page.data) console.log(quest.id, quest.title)
cursor = page.page.nextCursor
} while (cursor)ceejay.quests.complete
Marks a quest completed for a member and grants its rewards. CEEJAY applies the quest's own rules first, the ones it applies when a member does the quest in Discord. The quest's own verification and review are not run: when you call, you vouch that the member did it.
POST /v1/quests/complete · requires quests:complete · API reference: Complete a quest
Retried by the SDK: Only with idempotencyKey
Parameters: one object, then options.
Prop
Type
Options
Prop
Type
Without a key, a second request is processed as a new completion and checked like one. With
a key, the SDK retries automatically after a 429, a 5xx or no response. The API also
accepts the key as idempotency_key, for code that calls it without the SDK.
Returns Schemas['CompleteQuestResult'].
Prop
Type
The response also includes the run under its legacy name, progress, with its stored
field names. It is still sent. Use run.
rewards shows what was granted at the time of the response:
Prop
Type
What happens after the response. CEEJAY then assigns the quest's role, grants the extra
rewards set on the quest in the Rewards Engine, and sends the quest.completed webhook.
The call does not wait for this, and still returns a response when CEEJAY's bot is down:
these are processed when the bot is back online.
If you call ceejay.roles.grant, ceejay.xp.program.grant or ceejay.economy.grant
yourself after a completion, to add what this function used to leave out, stop doing that:
the member would now receive it twice.
Errors
If a rule of the quest is not met, the request is rejected and nothing is completed. The
code identifies the rule. The rules are checked in the order of this table, and the first
one that is not met is returned. Three of them return 400, as they always have, and the
others return 409: test the code, not the status. Some errors include the state that
caused them in error.body.
| Status | code | When | Also in error.body |
|---|---|---|---|
400 | quest_not_active | The quest is not active | |
409 | quest_not_started | The quest has not started yet | startsAt |
409 | quest_ended | The quest has ended | endsAt |
409 | quest_requires_role | The member lacks the role the quest requires | requiredRoleId |
409 | quest_requires_level | The member has not reached the level the quest requires | requiredLevel, level |
409 | quest_requires_season_xp | The member has not earned the season XP the quest requires | requiredSeasonXp, seasonXp, seasonRunning |
409 | quest_requires_program | The member is not in the program the quest belongs to | programId |
409 | quest_full | The quest has reached its overall completion limit | maxCompletions |
400 | quest_already_completed | The member completed it, and it cannot be repeated | |
400 | quest_limit_reached | The member reached the quest's limit for one member | |
409 | quest_cooling_down | The member has to wait before completing it again | availableAt |
409 | quest_requires_quest | The member has not completed the quest that comes first | prerequisiteQuestId |
409 | quest_daily_limit_reached | The member has reached the community's daily quest limit | dailyQuestLimit, completedToday, resetsAt |
The daily quest limit counts the quests a member completes in one UTC day, across all quests. It is 5 unless the community set another. It is a rule about starting: a run the member started in Discord before they reached the limit still completes.
The other errors:
| Status | code | When |
|---|---|---|
400 | bad_request | A field is missing or not valid, or proof is nested more than 32 levels deep |
404 | quest_not_found | There is no quest with this ID on your server |
404 | member_not_found | The ID is not a member of your server. A member who has just joined may not be available for up to 15 seconds: retry after 15 seconds |
404 | not_available | Quests are not available on this instance |
409 | xp_limit_reached, balance_limit_reached | A reward would exceed the maximum value CEEJAY can store. Nothing was completed |
503 | discord_unavailable | CEEJAY could not check this ID with Discord. Nothing was completed |
503 | member_roles_unavailable | The quest requires a role, and CEEJAY could not read the member's roles from Discord. Nothing was completed |
Idempotency errors are covered in Errors and retries.
Example
import { CeejayError } from '@communitystudios/ceejay'
try {
const done = await ceejay.quests.complete(
{ discordId: '409876543210987654', questId: 42, proof: { matchId: 'm_8841', score: 12 } },
{ idempotencyKey: 'quest-42-match-m_8841' },
)
console.log(done.questTitle, done.rewards.xp, done.rewards.currency, done.run?.id)
if (done.rewards.currencySkipped) console.log('The economy is switched off, so no coins were paid')
} catch (error) {
if (error instanceof CeejayError && error.code === 'quest_already_completed') {
console.log('The member already has this quest')
} else if (error instanceof CeejayError && error.code === 'quest_daily_limit_reached') {
const refusal = error.body as { resetsAt: string }
console.log(`The member can complete another quest after ${refusal.resetsAt}`)
} else {
throw error
}
}ceejay.quests.reportTask
Tells CEEJAY that a member did something your own platform checks. CEEJAY completes the quest task that has your task key, and the quest when that was its last task.
POST /v1/quests/tasks/complete · requires quests:complete · API reference: Complete a quest task
Retried by the SDK: Only with idempotencyKey
The request also has a legacy endpoint, /api/quests/external/complete, which still
works. The SDK uses the endpoint above.
Parameters: one object, then options.
Prop
Type
Options
Prop
Type
Identify the member with one of the six identifiers. TypeScript rejects an object that identifies nobody. If you send more than one, CEEJAY uses the first in the order of the table. The five that are not a Discord ID must match an account the member has linked in your community.
The API also accepts three of the fields under their legacy names, task_key,
discord_id and x_handle, for code that calls it without the SDK. The SDK sends the
names in the table.
A discordId CEEJAY has no member profile for is checked against Discord first. One that is
not in your server returns no member, and the response has matched: 0.
CEEJAY completes only the tasks whose key is your taskKey. A quest that requires a role
is completed only for a member who has it.
Returns the full response. It has one of two shapes, distinguished by matched.
Prop
Type
data contains:
Prop
Type
The key. A key is stored only when the call did something: at least one quest was
completed or sent for review. A response with matched: 0, or with both lists empty,
stores nothing, so the same request with the same key is processed as a new request, for
example after the player has linked their account.
Errors
| Status | code | When |
|---|---|---|
400 | bad_request | The task key is missing or not valid, no member is identified, playerId was sent without platform, evidence is not a string, evidence or the name you sent contains a null character, or evidence contains half of a surrogate pair, such as an emoji split in two |
400 | unknown_platform | The platform you sent with playerId is not one your community knows |
403 | game_not_allowed | 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. Nothing happened |
404 | not_available | The member is identified by an account, and account linking is not available on this instance |
502 | bot_unavailable | CEEJAY's bot, which completes quests, could not be reached or rejected the request. Nothing happened, and the idempotency key is released |
502 | bot_timeout | CEEJAY's bot did not respond in time, or its response was lost: it failed part way, or the connection to it dropped. The task may or may not have been completed |
503 | discord_unavailable | CEEJAY could not check with Discord whether the ID is a member of your server, or a matching quest requires a role and the member's roles could not be read. Nothing happened, so retry shortly |
After bot_timeout. For a quest a member can complete once, retrying the task is safe.
For a repeatable quest it can complete the quest again. With a key, CEEJAY does not report
a second time: while the first attempt may still be running the error is
idempotency_in_progress, which the SDK waits for, and after a minute it is
idempotency_outcome_unknown. Idempotency errors are covered in
Errors and retries.
Example
const report = await ceejay.quests.reportTask(
{
taskKey: 'tk_4k2m9x7qzb',
discordId: '409876543210987654',
evidence: 'https://acme.example/players/nova/tutorial',
},
{ idempotencyKey: 'tutorial-done-409876543210987654' },
)
if (report.matched === 0) {
console.log('CEEJAY found no member for that ID')
} else {
for (const quest of report.data.completed) {
console.log(`Finished quest ${quest.questId}: ${quest.questTitle}`)
}
for (const quest of report.data.sentForReview) {
console.log(`Quest ${quest.questId} waits for a moderator`)
}
}To identify the member by their ID in your game, send playerId with its platform:
const report = await ceejay.quests.reportTask({
taskKey: 'tk_4k2m9x7qzb',
playerId: '88213004',
platform: 'roblox',
})
console.log(report.matched)When platform is a game set up for Connect, a member who connected the player themselves
at the basic level is not found this way: nothing confirmed that the player is theirs, so
matched is 0. A player connected at the verified level is found, and so is one your own
server linked with ceejay.members.link. A game can be set up so that basic connections
earn too, and then they are found.

