Deduct currency
Deduct currency from a member's balance. A deduction larger than the balance is rejected.
POST /v1/economy/deduct · requires economy:deduct
Body parameters
Prop
Type
Deprecated names that still work
| Send | Deprecated alias, still accepted |
|---|---|
reason | description |
idempotencyKey | idempotency_key |
Send one of the two. If both are sent, the deprecated one is used. Deprecated names stay supported for as long as version 1 exists, and an error message names the field name you sent.
Returns
Prop
Type
The response also includes the deprecated field names. They hold the same values, and stay for as long as version 1 exists.
| Deprecated field, still sent | Same value as |
|---|---|
description | reason |
Example
curl -X POST https://ceejay.communitystudios.xyz/api/v1/economy/deduct \
-H "Authorization: Bearer $CEEJAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"discordId": "409876543210987654",
"amount": 200,
"reason": "Season pass",
"idempotencyKey": "order-88213-deduct"
}'The TypeScript tab calls ceejay.economy.deduct from the TypeScript SDK. It returns the data of the response below.
{
"success": true,
"data": {
"discordId": "409876543210987654",
"amountDeducted": 200,
"duplicate": false,
"balance": 2250,
"description": "Season pass",
"reason": "Season pass"
}
}Insufficient balance
Unlike XP, a deduction larger than the balance is rejected, not reduced to zero. If the member's balance is less than amount, the
call returns 400 with the code insufficient_balance and nothing is deducted. The response includes the current
balance, so you can decide what to do.
{
"error": "Insufficient balance",
"code": "insufficient_balance",
"requestId": "req_5f0c2a9b7d3e41f8a6b0c1d2",
"currentBalance": 120,
"requested": 500,
"balance": 120
}balance and currentBalance contain the same number: currentBalance is the deprecated field name, and is still sent. A member with no balance at all gets the same response, with balance at 0, and no balance is created for them.
This error returns 400, where newer errors for a rule return 409. It has always returned 400, and keeps that status for as long as version 1 exists: match on code.
Retrying safely
Send an idempotencyKey with every call you might have 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.
- A repeat changes nothing. When the same request is sent again with its key, CEEJAY returns the response it returned the first time, with
duplicate: trueand the headerIdempotent-Replayed: true. - One key is one request. Here the same request is the same member and the same amount. 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 its key. A call that was rejected can be sent again 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 something you can rebuild 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 is still accepted, and the key still works on an instance that is still being updated. - Without a key, a repeated call is a new call.
A deduction that was refused for an insufficient balance does not use up its key.
For a deduction, the same request is the same member and the same amount. The note is not compared: a retry may send its reason again, and the response then includes the note the deduction was first made with.
A key older than 30 days, and a key sent before version 1.0.0, is still recognised from the member's transaction history, permanently: a repeat then returns 200 with duplicate: true, the amount and the balance of the first call, and the note you sent this time. Only the member is compared for such a key, so a second deduction with the same key and another amount is treated as a repeat and nothing is deducted.
In the dashboard
Each deduction appears in Commerce → Economy → Transactions as a Partner API deduction, and your team can reverse it there. A deduction does not count as the member's spending: it leaves their totalSpent unchanged, which only grows when they buy from the shop. Currency deductions do not send a webhook event. A deduction works whether or not the community's economy is enabled.
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 response 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 refused |
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. No balance is that large |
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 half of a surrogate pair, such as the JSON "\ud800" or an emoji cut in two |
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 | 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 | description cannot contain a null character | The same check, when you sent the note as description |
400 | bad_request | description must be a string | The same check, when you sent the note as description |
400 | insufficient_balance | Insufficient balance | The member's balance is less than amount. The body adds currentBalance, requested and balance |
404 | not_available | This is not available on this instance yet. | This feature is not available on this instance yet. An update is rolling out. Community details reports the same under features. The body adds feature |
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, then 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 carries requestId, and a 400 with the code bad_request names the invalid field in field. Match on code, not on the message text, which can change.
The errors every request shares (401, 403, 405, 429, 500) are on the Errors page.

