Grant currency
Credit a member's currency balance and log the transaction, safely repeatable with an idempotency key.
POST /v1/economy/grant · requires economy:grant
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/grant \
-H "Authorization: Bearer $CEEJAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"discordId": "409876543210987654",
"amount": 500,
"reason": "Weekly tournament prize",
"idempotencyKey": "tournament-2026-40-winner"
}'The TypeScript tab calls ceejay.economy.grant from the TypeScript SDK. It returns the data of the response below.
{
"success": true,
"data": {
"discordId": "409876543210987654",
"amountGranted": 500,
"amountRequested": 500,
"capped": false,
"duplicate": false,
"balance": 2950,
"totalEarned": 9600,
"description": "Weekly tournament prize",
"reason": "Weekly tournament prize"
}
}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 repeat is handled before the member is looked up. It returns the first grant's response even when the member has left your server since, or when Discord is unavailable.
For a grant, 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 grant 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 grant with the same key and another amount is treated as a repeat and nothing is credited.
The balance cap
A balance cannot go above the community's Balance cap, a setting in Shop & Economy (1,000,000 unless the community has set another figure). When a grant would take the member over it, CEEJAY credits only what fits: amountGranted is smaller than amountRequested and capped is true. A member already at the cap gets nothing credited, and the call still returns 200.
A grant that would take the member's lifetime total past the largest number CEEJAY can store returns 409 with the code balance_limit_reached, and nothing is credited.
In the dashboard
Each grant appears in Commerce → Economy → Transactions as a Partner API grant, with your reason. Your team can reverse it there. Currency grants do not send a webhook event. A grant works whether or not the community's economy is enabled: Community details reports the switch as features.economy.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 1000000 | More than 1,000,000 in one call |
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 |
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 |
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 |
409 | balance_limit_reached | This grant would take the member's total past the largest number CEEJAY can store, so nothing was changed | The grant would take the member's lifetime total past the largest number CEEJAY can store. Nothing was credited |
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 learn from Discord whether it is a member of your server. Retry with a pause between attempts. If the error persists, 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 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.
Members CEEJAY has not seen
If the member has no CEEJAY profile yet, Grant currency creates a balance for them rather than failing. Before it does, CEEJAY checks with Discord whether the ID is a member of your server:
- A member of your server is credited.
- 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 Discord cannot be reached, 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 positive membership check is cached for five minutes, a negative one for 15 seconds. A member CEEJAY already has a profile for is never checked with Discord, and neither is the member of a call that was already applied and is sent again with its idempotencyKey.

