Grant XP
Award XP to a member and find out whether it levelled them up.
POST /v1/xp/grant · requires xp:grant
Body parameters
Prop
Type
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 |
|---|---|
totalXP | xp |
previousXP | previousXp |
currentLevel | level |
Levels follow your community's own level curve, so level is the level members see in Discord and in the dashboard.
Example
curl -X POST https://ceejay.communitystudios.xyz/api/v1/xp/grant \
-H "Authorization: Bearer $CEEJAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"discordId": "409876543210987654",
"amount": 250,
"reason": "Completed the tutorial island",
"idempotencyKey": "tutorial-409876543210987654"
}'The TypeScript example uses ceejay.xp.grant from the TypeScript SDK. It returns the data of the response below.
{
"success": true,
"data": {
"discordId": "409876543210987654",
"previousXP": 8450,
"xpGranted": 250,
"totalXP": 8700,
"previousLevel": 12,
"currentLevel": 12,
"leveledUp": false,
"reason": "Completed the tutorial island",
"auditId": "10442",
"xp": 8700,
"previousXp": 8450,
"level": 12,
"duplicate": false
}
}What a grant does
- Adds the XP to the member's total and puts it in their XP history, which feeds the XP pages in the dashboard. The XP, its history entry and the idempotency key are committed in a single transaction.
- Counts toward the member's season XP while XP seasons are enabled and a season is running.
- When the member levels up, the CEEJAY bot grants that level's rewards and assigns level roles shortly after the response. These are not included in the response.
- When the member levels up, subscribers to the
member.level_upwebhook get an event.
A total cannot pass 2,147,483,647. A grant that would take it there returns 409 with the code xp_limit_reached, and nothing is granted.
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, amount and reason. 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.
- On an instance that does not support idempotency keys yet, a call that sends one returns
503with the codeidempotency_unavailable, and nothing is changed. Community details reports whether keys are supported, underfeatures.retryKeys.idempotency_keyis not accepted on this endpoint; useidempotencyKey. - Without a key, a retried call is a new call.
A retry is processed before the member is looked up, so it returns the original response even when the member has left your server since.
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 | amount must be a positive integer | amount is missing, zero, negative, fractional or sent as text |
400 | bad_request | amount cannot exceed 100000 | More than 100,000 in one call |
400 | bad_request | reason cannot contain a null character | reason contains a null character (U+0000), which cannot be stored |
400 | bad_request | reason must be a string | reason was sent and is not a string: a number, a list, an object, true or false. null counts as left out |
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) |
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 |
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 | xp_limit_reached | This grant would take the member's XP past the largest number CEEJAY can store, so nothing was changed | The change would take the member's XP past 2,147,483,647, the largest number CEEJAY can store. Nothing was changed |
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 | 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 |
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.
Members without a CEEJAY profile
Granting XP to a member of your server CEEJAY has no profile for creates the profile rather than failing. This lets you award XP to someone who joins your game before they have said anything in Discord. The new profile has no display name, avatar or join date. Before creating the profile, CEEJAY checks with Discord whether the ID is a member of your server:
- A member of your server gets the XP and the profile.
- 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 changed in the last two cases. A successful membership check is cached for five minutes, a failed one for 15 seconds. A member CEEJAY already has a profile for is never looked up, and neither is the member of a call that was already applied and is retried with its idempotencyKey.

