baff.gg Docs Developer Portal

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

json
{
	"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
}
FieldNotes
author.tag"bot", "verified_bot", "official" or "system" when the author has one
author.clanTagThe 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)
bafflesA 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
embedsRich embeds from bots and webhooks, then link previews. See Embeds
stickersStickers 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
componentsButtons, select menus and layout components from bots and webhooks ([] when none). See Message Components
flags32768 (IS_COMPONENTS_V2) when the message is built only from components; 64 on ephemeral answers
replyWhen 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)
forwardA forwarded message: a snapshot of the original, see Forwarded messages
replyDeletedIt was a reply, but the original is gone
interactionA bot's answer to a slash command: { user, name }
serverIdOnly on MESSAGE_CREATE events, so you know which server it's from
typeLeft 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
nonceEchoed back if you sent one

Text

Messages are 1-2000 characters (or empty, with files or embeds). They're shown with Markdown-like formatting:

WriteShows
**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, @herePings 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 lineHeadings
-# text at the start of a lineSmall grey text
- item or * item, 1. itemBulleted and numbered lists
> text, or >>> to quote everything after itQuotes

Put a backslash before a character to show it as-is: \*not italic\*.

Reading

http
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

http
POST /api/channels/{channelId}/messages
Content-Type: application/json

{ "content": "Hello!", "replyTo": "103745999999999999", "mentionReply": false }
FieldNotes
contentThe text (may be empty when you send files, embeds, components or stickers)
embedsUp to 10 embeds (bots only)
componentsUp to 5 action rows of buttons and select menus, or a layout with flags (bots only). See Message Components
flags32768 (IS_COMPONENTS_V2): the message is built only from components, with no content or embeds (bots only)
stickerIdsUp to 3 sticker IDs (sticker_ids works too)
replyToMessage ID in the same channel to reply to
mentionReplyWhether the reply pings its author. Defaults to true
nonceUp 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.

bash
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:

http
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.

RouteDoes
GET /api/servers/{serverId}/stickersThe server's stickers: { id, serverId, name, description, emoji, animated, url, uploader }
GET /api/users/@me/stickersStickers from every server you're in
GET /api/stickers/{stickerId}One sticker with its server (usable = you're in it)
POST /api/servers/{serverId}/stickersUpload 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.

http
POST /api/channels/{channelId}/messages

{
	"poll": {
		"question": "Next game night?",
		"answers": [{ "text": "Friday", "emoji": "🎲" }, { "text": "Saturday" }],
		"duration": 24,
		"allowMultiselect": false
	}
}
FieldNotes
question1-300 characters, one line
answers2-10 answers of 1-55 characters. emoji is optional: a Unicode emoji or a custom one as name:id
durationHours it stays open, 1-168 (default 24)
allowMultiselectWhether 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

http
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

http
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

http
PUT    /api/channels/{channelId}/pins/{messageId}
DELETE /api/channels/{channelId}/pins/{messageId}
POST   /api/channels/{channelId}/typing

Pinning 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

http
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:

http
POST /api/channels/{forumId}/threads
{
  "name": "Crash when opening settings",
  "appliedTags": ["104546295275847680"],
  "message": { "content": "Steps to reproduce: ...", "embeds": [{ "title": "Build 1.2" }] }
}
  • name is the title (1-100 characters). appliedTags (or applied_tags) are ids from the forum's availableTags: up to 5, and at least one when the forum has requireTag. autoArchiveMinutes (or auto_archive_duration) is optional, as for threads.
  • message is the first message, with the same fields as sending a message: content, and for bots embeds and components. It needs text or a file. To attach files, send multipart/form-data with the JSON in payload_json and the files as files.
  • It needs Send Messages in the forum. The forum's slowmodeSecs limits 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 in message.

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.

json
"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
}
FieldNotes
sourceServerIdnull when it came from a DM (sourceKind: "dm", and sourceChannelName is null too)
sourceChannelName, sourceServerNameThe names when it was forwarded
author.nameThe original author's display name (or username) at the time
contentThe original text. For a message built from components, its text displays; buttons and select menus never come along
embeds, attachmentsThe original's embeds and files, in the same shapes as on a message
timestampWhen 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).