Deduct XP
Remove XP from a member. Never drops below zero.
POST /v1/xp/deduct · requires xp:deduct
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 |
Example
curl -X POST https://ceejay.communitystudios.xyz/api/v1/xp/deduct \
-H "Authorization: Bearer $CEEJAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"discordId": "409876543210987654",
"amount": 200,
"reason": "Reversed a duplicate award",
"idempotencyKey": "reversal-10442"
}'The TypeScript example uses ceejay.xp.deduct from the TypeScript SDK. It returns the data of the response below.
{
"success": true,
"data": {
"discordId": "409876543210987654",
"previousXP": 200,
"xpDeducted": 200,
"totalXP": 0,
"previousLevel": 1,
"currentLevel": 0,
"reason": "Reversed a duplicate award",
"auditId": "10443",
"xp": 0,
"previousXp": 200,
"level": 0,
"duplicate": false
}
}Behaviour
Deducting more than the member has sets their total to zero rather than returning an
error. xpDeducted reports what was actually removed, which may be less than amount.
A member CEEJAY has no profile for returns 404 with the code member_not_found. Unlike Grant XP,
this request does not create a profile, and it does not check with Discord.
When something was removed, the change goes into the member's XP history as a negative entry. The XP, its history entry and the idempotency key are committed in a single transaction. A deduction does not change the member's season XP.
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.
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 2147483647 | More than 2,147,483,647 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 this ID on your server. Nothing was deducted |
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 | 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.

