API reference
Messages
Read, send, edit and delete messages, with replies, mentions, files, reactions and pins.
All paths below start with https://baff.gg/api and need Authorization: Bot <token>. Your bot needs View Channel to read a channel and Send Messages to post in it.
The message object
{
"id": "103745999999999999",
"channelId": "103700000000000001",
"author": { "id": "103600000000000000", "username": "andre", "displayName": "Andre", "avatarUrl": null },
"content": "Hey <@103712345678901248>, **nice** work",
"createdAt": "2026-10-14T03:12:45.120Z",
"editedAt": null,
"attachments": [],
"embeds": [],
"components": [],
"flags": 0,
"reactions": [{ "emoji": "🎉", "count": 2, "me": false }],
"mentions": ["103712345678901248"],
"mentionRoles": [],
"mentionEveryone": false,
"replyDeleted": false,
"pinned": false,
"crossposted": false
}| Field | Notes |
|---|---|
author.tag | "bot", "verified_bot", "official" or "system" when the author has one |
author.clanTag | The clan tag they wear, or null: { serverId, name, badge, color } (see Users) |
attachments | { id, filename, contentType, size, width, height, url }. Images are converted to WebP (GIFs stay GIFs) |
baffles | A priced post: { price, unlocked, files, closed? }. Its files are sold for Baffles, so unless your bot posted it, attachments is [] and files only lists { kind, size } (image, video, audio or file). closed: true means it can't be bought any more (its server stopped being an official baff.gg server). Bots can't buy posts. Send one with baffles: <price> (1 to 1,000,000) next to your files; this only works in official baff.gg servers (elsewhere 403 baffles_official_only) and needs Baffle Upload in the channel |
embeds | Rich embeds from bots and webhooks, then link previews. See Embeds |
stickers | Stickers sent with the message, in order ([] when none): { id, name, url, animated, serverId }. A sticker deleted from its server since stays in the list as { id, name: "", animated: false, deleted: true } with no url. See Stickers |
components | Buttons, select menus and layout components from bots and webhooks ([] when none). See Message Components |
flags | 32768 (IS_COMPONENTS_V2) when the message is built only from components; 64 on ephemeral answers |
reply | When it replies to another message: { id, author, content, hasAttachments, forwarded? } (first 200 characters; for a message built from components, its first text display; forwarded: true when the original is a forward, whose own content is empty) |
forward | A forwarded message: a snapshot of the original, see Forwarded messages |
replyDeleted | It was a reply, but the original is gone |
interaction | A bot's answer to a slash command: { user, name } |
serverId | Only on MESSAGE_CREATE events, so you know which server it's from |
type | Left out for normal messages. System notices in DMs and group DMs use Discord's numbers: 1 someone was added to the group (target = who), 2 someone left or was removed (target; the author is who removed them, or the person who left), 3 a call (call: { participants, endedAt }, endedAt null while it's going), 4 the group was renamed (content = the new name), 5 the group picture changed. Only baff.gg posts these; they can't be edited or deleted |
nonce | Echoed back if you sent one |
Text
Messages are 1-2000 characters (or empty, with files or embeds). They're shown with Markdown-like formatting:
| Write | Shows |
|---|---|
**bold**, *italic*, __underline__, ~~strike~~ | Formatting |
`code` and ```blocks``` | Code |
||spoiler|| | Hidden until clicked |
<https://example.com> | A link without a preview |
<@userId> | Mentions (pings) a person |
<@&roleId> | Mentions everyone with a role |
@everyone, @here | Pings everyone who can see the channel (needs Mention Everyone) |
<:name:id>, <a:name:id> | A custom emoji (still or animated) |
<#channelId> | A link to a channel (never pings) |
# Big, ## Medium, ### Small at the start of a line | Headings |
-# text at the start of a line | Small grey text |
- item or * item, 1. item | Bulleted and numbered lists |
> text, or >>> to quote everything after it | Quotes |
Put a backslash before a character to show it as-is: \*not italic\*.
Reading
GET /api/channels/{channelId}/messages?limit=50&before={messageId}Returns up to limit messages (1-100, default 50), oldest first. Without before you get the newest page; pass the oldest ID you have to go further back. IDs grow over time, so they also sort messages.
Pinned messages: GET /api/channels/{channelId}/pins (newest pin first).
Sending
POST /api/channels/{channelId}/messages
Content-Type: application/json
{ "content": "Hello!", "replyTo": "103745999999999999", "mentionReply": false }| Field | Notes |
|---|---|
content | The text (may be empty when you send files, embeds, components or stickers) |
embeds | Up to 10 embeds (bots only) |
components | Up to 5 action rows of buttons and select menus, or a layout with flags (bots only). See Message Components |
flags | 32768 (IS_COMPONENTS_V2): the message is built only from components, with no content or embeds (bots only) |
stickerIds | Up to 3 sticker IDs (sticker_ids works too) |
replyTo | Message ID in the same channel to reply to |
mentionReply | Whether the reply pings its author. Defaults to true |
nonce | Up to 64 characters; returned in the response and the MESSAGE_CREATE event |
Answers 201 with the message.
Files
Send multipart/form-data with a payload_json part (the JSON above) and up to 10 files parts of up to 10 MB each. Needs Attach Files.
curl https://baff.gg/api/channels/$CHANNEL_ID/messages \
-H "Authorization: Bot $BOT_TOKEN" \
-F 'payload_json={"content": "Weekly chart"}' \
-F 'files=@chart.png'Stickers
Every server can have up to 30 stickers: 320 x 320 images (WebP, or GIF when animated) with a name, a description and a related emoji. Anyone in a server can send its stickers in any channel or DM, so your bot can send the stickers of every server it's in:
POST https://baff.gg/api/channels/{channelId}/messages
Authorization: Bot bfx_4f1c...
Content-Type: application/json
{ "content": "", "stickerIds": ["104100000000000000"] }A message carries up to 3 stickers, and content may be empty when it has one. Stickers can't go with a poll or flags: 32768 (400 invalid_stickers). A sticker from a server your bot isn't in, or one that was deleted, answers 400 unknown_sticker; more than 3 answers 400 too_many_stickers.
| Route | Does |
|---|---|
GET /api/servers/{serverId}/stickers | The server's stickers: { id, serverId, name, description, emoji, animated, url, uploader } |
GET /api/users/@me/stickers | Stickers from every server you're in |
GET /api/stickers/{stickerId} | One sticker with its server (usable = you're in it) |
POST /api/servers/{serverId}/stickers | Upload one: multipart/form-data with name (2 to 30 characters), description (up to 100), emoji (a Unicode emoji, or a custom one as name:id) and file (PNG, APNG, WebP or GIF, up to 512 KB). Needs Manage Expressions |
PATCH /api/servers/{serverId}/stickers/{stickerId} | Change name, description or emoji. Needs Manage Expressions |
DELETE /api/servers/{serverId}/stickers/{stickerId} | Delete it. Needs Manage Expressions |
Members get SERVER_STICKERS_UPDATE ({ serverId, stickers }) whenever the list changes.
Polls (experimental)
Polls are still being tried out and may change. Most people's apps show a poll as its question and answers in plain text for now.
POST /api/channels/{channelId}/messages
{
"poll": {
"question": "Next game night?",
"answers": [{ "text": "Friday", "emoji": "🎲" }, { "text": "Saturday" }],
"duration": 24,
"allowMultiselect": false
}
}| Field | Notes |
|---|---|
question | 1-300 characters, one line |
answers | 2-10 answers of 1-55 characters. emoji is optional: a Unicode emoji or a custom one as name:id |
duration | Hours it stays open, 1-168 (default 24) |
allowMultiselect | Whether people may pick more than one answer (default false) |
content may be empty; polls can't have files and can't be edited. Messages with a poll carry poll: { question, answers: [{ id, text, emoji, count }], allowMultiselect, expiresAt, ended, total } (total = how many people voted). Votes arrive as POLL_VOTE events ({ channelId, messageId, userId, answerIds, poll }). End your poll early with POST /api/channels/{channelId}/polls/{messageId}/expire (or anyone's with Manage Messages). When a poll ends, its author posts a short result reply; it carries pollResult ({ messageId, question, winners, total }). Bots can't vote.
Editing and deleting
PATCH /api/channels/{channelId}/messages/{messageId} { "content": "Fixed typo" }
DELETE /api/channels/{channelId}/messages/{messageId}Bots can edit only their own messages. An edit can also carry embeds and components (each replaces the old ones, [] removes them), and flags; leave content out to change only those. A message sent with IS_COMPONENTS_V2 keeps it (invalid_flags). Bots can delete their own, or anyone's with Manage Messages. Deleting a message deletes its files.
Reactions
PUT /api/channels/{channelId}/messages/{messageId}/reactions/{emoji}
DELETE /api/channels/{channelId}/messages/{messageId}/reactions/{emoji}
GET /api/channels/{channelId}/messages/{messageId}/reactions/{emoji}{emoji} is the URL-encoded emoji (%F0%9F%8E%89 for 🎉) or a custom one as name:id. GET lists who reacted (first 100). Adding a new emoji needs Add Reactions; joining one that's already there doesn't. Messages take up to 20 different reactions.
Pins and typing
PUT /api/channels/{channelId}/pins/{messageId}
DELETE /api/channels/{channelId}/pins/{messageId}
POST /api/channels/{channelId}/typingPinning needs Manage Messages (up to 50 pins per channel). The typing call shows "Bot is typing..." for about 9 seconds; repeat it every few seconds while you work.
Threads
POST /api/channels/{channelId}/threads
{ "name": "Patch notes discussion", "messageId": "103745999999999999", "autoArchiveMinutes": 1440 }Starts a thread in a text or announcement channel, optionally from one of its messages (one thread per message). autoArchiveMinutes is when it hides after the last message: 60, 1440, 4320 or 10080. A thread is a channel with kind: "thread"; send messages to it like any channel. GET /api/channels/{channelId}/threads lists a channel's threads, newest activity first.
A locked thread (threadLocked: true) only takes messages from people with Manage Threads; everyone else gets 403 thread_locked.
Forum posts
A forum channel (kind: "forum") has no chat of its own: its conversations are posts. A post is a thread under the forum with a title, up to 5 of the forum's tags and a first message, which has the same id as the post. Start one on the same route as a thread:
POST /api/channels/{forumId}/threads
{
"name": "Crash when opening settings",
"appliedTags": ["104546295275847680"],
"message": { "content": "Steps to reproduce: ...", "embeds": [{ "title": "Build 1.2" }] }
}nameis the title (1-100 characters).appliedTags(orapplied_tags) are ids from the forum'savailableTags: up to 5, and at least one when the forum hasrequireTag.autoArchiveMinutes(orauto_archive_duration) is optional, as for threads.messageis the first message, with the same fields as sending a message:content, and for botsembedsandcomponents. It needs text or a file. To attach files, sendmultipart/form-datawith the JSON inpayload_jsonand the files asfiles.- It needs Send Messages in the forum. The forum's
slowmodeSecslimits how often one person starts a post (429 slowmode); Manage Threads, Manage Messages and Manage Channels skip it. - The answer (
201) is the post (a channel) with the first message inmessage.
Reply by sending messages to the post's id. GET /api/channels/{forumId}/posts?sort=&q=&tags=&offset=&limit= lists posts as the app's cards show them: { "posts": [{ "channel": <post>, "message": <first message or null>, "following": true }], "hasMore": false }. sort is activity (newest message first) or created; without it the forum's defaultSortOrder applies. q matches titles and first messages, tags is a comma-separated list of tag ids (posts with any of them), limit is up to 50.
People follow a post when they start it or reply; PUT / DELETE /api/channels/{postId}/follow follow or unfollow it for the caller.
Forwarded messages
People can forward a message to up to five channels or DMs. The forward is a new message by the person who forwarded it: its own content is "", its own attachments is [], and it carries forward, a snapshot taken when it was forwarded. Later edits and deletes of the original don't change it.
"forward": {
"sourceChannelId": "103700000000000001",
"sourceServerId": "103690000000000000",
"sourceMessageId": "103745999999999999",
"sourceKind": "text",
"sourceChannelName": "general",
"sourceServerName": "Baff Fans",
"author": { "id": "103600000000000000", "username": "andre", "name": "Andre" },
"content": "Hey <@103712345678901248>, **nice** work",
"embeds": [],
"attachments": [],
"timestamp": "2026-10-14T03:12:45.120Z",
"editedAt": null
}| Field | Notes |
|---|---|
sourceServerId | null when it came from a DM (sourceKind: "dm", and sourceChannelName is null too) |
sourceChannelName, sourceServerName | The names when it was forwarded |
author.name | The original author's display name (or username) at the time |
content | The original text. For a message built from components, its text displays; buttons and select menus never come along |
embeds, attachments | The original's embeds and files, in the same shapes as on a message |
timestamp | When the original was sent |
Forwards never ping anyone and never get link previews, and they can't be edited (400 forward_not_editable). The person's optional note arrives as a separate, normal message right after the forward. A forward of a forward passes on the first snapshot. Polls, priced posts, posts in paid channels and ephemeral answers can't be forwarded.
Bots can read forwards but can't forward messages (403 bots_cant_forward).
Publishing announcements
In an announcement channel, POST /api/channels/{channelId}/messages/{messageId}/crosspost copies a message to every channel that follows it (once per message). Your own messages need Send Messages; anyone's need Manage Messages. Copies keep their components, with buttons and select menus switched off (only link buttons still work there).