Send a message
Post a message or embed to a channel in your Discord server.
POST /v1/messages/send · requires messages:send
Body parameters
Prop
Type
At least one of content or embeds must be present. List channels gives you each channel's channelId.
Returns
Prop
Type
Example
curl -X POST https://ceejay.communitystudios.xyz/api/v1/messages/send \
-H "Authorization: Bearer $CEEJAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channelId": "718230049827100384",
"content": "Tournament results are in. Congratulations to everyone who played."
}'The TypeScript example uses ceejay.messages.send from the TypeScript SDK. It returns the data of the response below.
{
"success": true,
"data": {
"messageId": "1178230049827100384",
"channelId": "718230049827100384",
"duplicate": false
}
}How it works
CEEJAY first checks that the channel belongs to your server, then posts the message as the bot. A channel in another server, a channel that does not exist and a channel the bot cannot see all return the same 404.
The text of a message is not filtered or changed, so read the next section before you pass on text written by players. CEEJAY does not store a copy of the message.
Embeds
embeds must be a list of Discord embed objects, and only the first 10 are sent.
- With no
content, the embeds are the whole message: a value that is not a list returns400with the messageembeds must be a list. - With
content, a value that is not a list is left out and the text is posted.
Embeds go to Discord unchanged, so Discord decides whether they are valid. An embed Discord does not accept is returned with Discord's own status and the code discord_error.
Mentions
Every message is sent so that only mentions of members notify anyone.
In content | Notifies |
|---|---|
A member mention, <@409876543210987654> | That member, always |
A role mention, <@&718230049827100399> | The role, only when its ID is in allowedRoleIds. Otherwise nobody |
@everyone or @here | Nobody. The API can never ping everyone |
To ping a role, name it in allowedRoleIds and mention it in content:
{
"channelId": "718230049827100384",
"content": "Sign-ups are open, <@&718230049827100399>.",
"allowedRoleIds": ["718230049827100399"]
}A role in allowedRoleIds that content does not mention pings nobody. Discord's own rules still apply: the bot can only ping a role that is set to be mentionable, unless the bot has the Mention @everyone, @here, and All Roles permission.
Your server's own ID is also the ID of its @everyone role, so allowedRoleIds cannot contain it. A list that does is rejected with 400.
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 channel, text, embeds and
allowedRoleIds, as they would be posted. Two requests for one event need two keys, for examplematch-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.
CEEJAY cannot undo a posted message, so it never posts twice with one key:
- A rejection from Discord (any
4xx, its rate limit included) does not use up the key. The same key can be reused. - After a timeout, a dropped connection or a
5xxfrom Discord, the message may or may not have been posted. The same key then returns409with the codeidempotency_in_progressfor a minute, andidempotency_outcome_unknownafter that. Check the channel, then use a new key if the message has to be sent again. - With a key, embeds nested more than 32 levels deep are rejected with
400.
Without a key, sending the same request twice posts the message twice.
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 | channelId must be a valid Discord snowflake (17-20 digits) | channelId is missing or is not a string of 17 to 20 digits |
400 | bad_request | content must be a string | content was sent and is not a string. A number is rejected, not converted to text |
400 | bad_request | content must be valid Unicode text | content contains an unpaired surrogate, such as the JSON "\ud800". Nothing is sent to Discord |
400 | bad_request | embeds must be valid Unicode text | The embeds contain an unpaired surrogate anywhere in their text or their keys. Nothing is sent to Discord |
400 | bad_request | Either content or embeds is required | The body has no content, and embeds is missing or an empty list |
400 | bad_request | embeds must be a list | There is no content, and embeds is not a list |
400 | bad_request | allowedRoleIds must be a list of up to 20 valid Discord snowflakes (17-20 digits) | allowedRoleIds is not a list, has more than 20 entries, contains something that is not a role ID, or contains your server's own ID |
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) |
400 | bad_request | embeds is nested too deeply | You sent an idempotencyKey, and the embeds nest lists and objects more than 32 levels deep |
400 | discord_error | Discord API error: 400 | Discord rejected the message, for example an embed it does not accept. details contains Discord's response |
403 | bot_missing_permission | Bot lacks permission to send messages in this channel | The bot has no Send Messages permission in this channel |
404 | channel_not_found | Channel not found or bot has no access | The channel does not exist, is in another server, or the bot cannot see it |
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 | idempotency_outcome_unknown | An earlier request with this idempotencyKey may or may not have been applied. It was not sent again. Use a new key to send it again. | A first request with this key was started more than a minute ago and never finished. CEEJAY cannot know whether it took effect, and does not do it again. Check what happened, then use a new key if it has to be sent again |
429 | discord_rate_limited | Discord API rate limited, try again later | Discord is rate limiting the bot. retryAfter and the Retry-After header contain the wait Discord requested, in whole seconds, if it gave one. Do not retry in a tight loop |
500 | discord_not_configured | This instance cannot reach Discord yet | This instance has no connection to Discord yet |
503 | discord_unavailable | Discord did not answer in time, so the channel could not be checked and nothing was posted | Discord did not respond to the channel check within 10 seconds. Nothing was posted, and no idempotency key is used up: retry after a pause |
503 | discord_unavailable | Discord did not answer in time, so the message may or may not have been posted | Discord did not respond to the post within 10 seconds. The message may or may not be in the channel, and the idempotency key stays reserved: see Retrying safely before you 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 |
| Discord's | discord_error | Discord API error: 503 | Any other status Discord returns is passed on, with Discord's own status. details contains Discord's response. After a 5xx the message may or may not have been posted |
Discord's own rate limit returns 429 with the code discord_rate_limited. A limit Discord sets for one channel stops calls for that channel only, and the response does not mean your key's own limit is used up: X-RateLimit-Remaining still shows what the key has left.
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.
Bot permissions
The bot needs View Channel and Send Messages in the target channel, and Embed Links for
embeds to show. Without Send Messages the call returns 403; a channel the bot cannot see returns 404.

