Messages¶
A message is one post in a channel, together with the text, attachments, embeds, stickers, and reactions stored with it. Channel metadata and membership are defined by the Channels resource.
A text-bearing channel is a channel type that stores messages: every type except a guild category and a guild link channel. The message history cutoff is a time stored on the guild; a member without READ_MESSAGE_HISTORY reads nothing created before it.
Unless a route says user-only, it accepts both a user session (Authorization: Bearer <token>) and a bot token (Authorization: Bot <token>). Error bodies follow Errors.
Channel resolution¶
TEBHOS resolves the channel before it applies any per-route authorisation:
- A channel that does not exist, a guild the caller is not a member of, and a private channel the caller is not a recipient of all return 404
NOT_FOUND. - A guild member without VIEW_CHANNEL in the channel receives 403
FORBIDDEN. - A guild caller without
READ_MESSAGE_HISTORYsees only messages created at or after the guild's message history cutoff, and nothing when the guild has no cutoff. Older messages behave as if they do not exist (an empty list or 404), not as a 403. - A timed-out member cannot send, type, react, or upload in that guild (403
FORBIDDEN).
Permission bits are evaluated only in guild channels.
Message object¶
Structure¶
| Field | Type | Description |
|---|---|---|
| id | snowflake | The ID of the message |
| channel_id | snowflake | The ID of the channel |
| author | partial user object | The user credited with the message |
| webhook_id? | snowflake | The originating webhook ID, present only for a webhook-authored message |
| type | integer | Message type |
| flags | integer | Message flags |
| content | string | The text of the message, empty when the message has only media |
| timestamp | ISO8601 timestamp | Creation time derived from the message snowflake |
| edited_timestamp | ?ISO8601 timestamp | Most recent edit time, or null when the message has never been edited |
| pinned | boolean | Whether the message is pinned |
| mention_everyone | boolean | Whether the content contains @everyone or @here |
| tts | boolean | Whether the message was sent as text-to-speech |
| mentions | array[partial user object] | The users mentioned in the content |
| mention_roles | array[snowflake] | The IDs of the roles mentioned in the content |
| mention_channels? | array[channel mention object] | The channels the content links by ID |
| embeds | array[embed object] | The link previews and rich embeds on the message |
| attachments | array[message attachment object] | The files attached to the message |
| stickers | array[sticker item object] | The stickers sent with the message |
| reactions? | array[reaction object] | Reaction summaries, omitted when the message has none |
| message_reference? | message reference object | Reply or forward reference |
| message_snapshots? | array[message snapshot object] | The copies captured for a forward |
| nonce? | string | The caller-supplied nonce, echoed only in the create response and its Gateway event |
| call? | message call object | Call state attached to a CALL message |
| referenced_message? | ?message object | The resolved reply target, without its own referenced_message |
mentions, mention_roles, embeds, attachments and stickers are always present and can be empty. A field marked ? is omitted when it has no value.
referenced_message has three states: the key is absent when the message is not a reply, present and null when the reply target no longer exists, and present with the message when it does. A client must tell absent apart from null by key presence.
A webhook-authored message has no user author. TEBHOS builds its author from the webhook: id is the webhook ID, username is the name the message was posted under, discriminator is "0", avatar is the webhook avatar hash, and bot is true. A message whose author account can no longer be resolved carries an author object with only a real id.
A forward has immutable copies in message_snapshots
A reply resolves into referenced_message; a forward has none. TEBHOS captures the snapshot at forward time and it never follows later edits to the source.
Example¶
{
"id": "1501320000000000000",
"channel_id": "1501314428688998182",
"author": {
"id": "1501314428688990000",
"username": "ada",
"discriminator": "0",
"global_name": "Ada",
"avatar": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
"avatar_color": null,
"flags": 0
},
"type": 0,
"flags": 0,
"content": "shipping it",
"timestamp": "2026-08-31T09:14:02.000Z",
"edited_timestamp": null,
"pinned": false,
"mention_everyone": false,
"tts": false,
"mentions": [],
"mention_roles": [],
"embeds": [],
"attachments": [],
"stickers": []
}
Message types¶
| Value | Name | Description |
|---|---|---|
| 0 | DEFAULT | Ordinary message (also used for forwards and webhook messages) |
| 1 | RECIPIENT_ADD | Group direct message recipient-add system message |
| 2 | RECIPIENT_REMOVE | Group direct message recipient-remove system message |
| 3 | CALL | Private call system message |
| 4 | CHANNEL_NAME_CHANGE | Group direct message name-change system message |
| 5 | CHANNEL_ICON_CHANGE | Group direct message icon-change system message |
| 6 | CHANNEL_PINNED_MESSAGE | Pin system message, referencing the pinned message |
| 7 | USER_JOIN | Guild join system message |
| 19 | REPLY | Message with a reply reference |
Only DEFAULT and REPLY messages can be pinned or unpinned; any other type returns 400 BAD_REQUEST.
Message flags¶
| Value | Name | Description |
|---|---|---|
| 1 << 2 | SUPPRESS_EMBEDS | Suppress rendering of embeds (link previews are still resolved but not broadcast) |
| 1 << 12 | SUPPRESS_NOTIFICATIONS | Do not generate push or desktop notifications |
| 1 << 13 | VOICE_MESSAGE | The message is a voice message |
| 1 << 17 | COMPACT_ATTACHMENTS | Render attachments in a compact layout |
These bits are the complete sendable set. A create keeps only these bits of the supplied value, and a modify replaces only these bits and keeps every other stored bit. Any other bit is discarded without an error.
Message attachment object¶
An attachment is one file stored against a message and served from the TEBHOS CDN.
Structure¶
| Field | Type | Description |
|---|---|---|
| id | snowflake | The ID of the attachment |
| filename | string | The stored filename |
| title | ?string | Display title, or null |
| description | ?string | Alternative text, or null |
| content_type | ?string | The media type recorded at upload |
| content_hash | ?string | SHA-256 hash of the stored bytes, or null when it was never computed |
| size | integer | The stored size in bytes |
| url | ?string | The public file URL: https://cdn.tebhos.com/attachments/{channel_id}/{attachment_id}/{filename} |
| proxy_url | ?string | For images (except SVG): a resizable URL of the form https://tebhos.com/api/v1/files/attachments/{channel_id}/{attachment_id}/{filename}. null for other files |
| width? | integer | Pixel width for visual media |
| height? | integer | Pixel height for visual media |
| placeholder? | string | Encoded media placeholder |
| flags | integer | Attachment flags |
| nsfw? | boolean | Set by TEBHOS media safety scanning; true means the file was flagged and clients hide it behind a click-through |
| duration? | integer | Audio or video duration in seconds |
| waveform? | string | Base64-encoded voice waveform supplied by the uploader |
| expires_at | ?ISO8601 timestamp | The recorded storage expiry of the attachment, or null |
| expired? | boolean | Whether expires_at has passed |
Attachment URLs are not signed and do not expire on their own. proxy_url accepts width, height and format query parameters; see Media proxy.
Example¶
{
"id": "1501320000000000001",
"filename": "diagram.png",
"title": null,
"description": "Sequence diagram",
"content_type": "image/png",
"content_hash": null,
"size": 18422,
"url": "https://cdn.tebhos.com/attachments/1501314428688998182/1501320000000000001/diagram.png",
"proxy_url": "https://tebhos.com/api/v1/files/attachments/1501314428688998182/1501320000000000001/diagram.png",
"width": 1280,
"height": 720,
"flags": 0,
"expires_at": null
}
Attachment flags¶
| Value | Name | Description |
|---|---|---|
| 1 << 3 | IS_SPOILER | Attachment is presented as a spoiler |
| 1 << 4 | CONTAINS_EXPLICIT_MEDIA | Media inspection marked the attachment explicit |
| 1 << 5 | IS_ANIMATED | Attachment is animated |
Emoji object¶
| Field | Type | Description |
|---|---|---|
| id | ?snowflake | The ID of a custom emoji, or null for a Unicode emoji |
| name | string | The custom emoji name, or the Unicode emoji itself |
| animated? | boolean | Whether a custom emoji is animated |
Reaction object¶
A reaction object summarises one emoji group on a message.
| Field | Type | Description |
|---|---|---|
| emoji | emoji object | The emoji this group counts |
| count | integer | The number of users who added this emoji |
| me | boolean | Whether the caller added this emoji |
Groups are listed in the order their first reaction was added. TEBHOS applies no per-message ceiling on reaction groups or users.
Example¶
{
"emoji": {"id": "1501314428688991234", "name": "party_parrot", "animated": true},
"count": 7,
"me": true
}
Message reference object¶
| Field | Type | Description |
|---|---|---|
| channel_id | snowflake | The ID of the referenced channel |
| message_id | snowflake | The ID of the referenced message |
| guild_id? | snowflake | The ID of the referenced guild |
| type | integer | Message reference type |
Message reference types¶
| Value | Name | Description |
|---|---|---|
| 0 | DEFAULT | Reply reference (the default when type is omitted) |
| 1 | FORWARD | Forward reference represented by message_snapshots |
Message snapshot object¶
A snapshot is a flat, immutable copy of one forwarded message. It has no message ID, channel ID, or author.
| Field | Type | Description |
|---|---|---|
| content? | string | Snapshotted text |
| timestamp | ISO8601 timestamp | Original creation time |
| edited_timestamp? | ISO8601 timestamp | Original edit time |
| mentions? | array[snowflake] | Snapshotted mentioned user IDs |
| mention_roles? | array[snowflake] | Snapshotted mentioned role IDs |
| embeds? | array[embed object] | Snapshotted embeds (omitted when the source had SUPPRESS_EMBEDS) |
| attachments? | array[message attachment object] | Snapshotted attachments. They are copied into the destination channel, so deleting the source leaves them intact |
| stickers? | array[sticker item object] | Snapshotted stickers |
| type | integer | Original message type |
| flags | integer | Original message flags |
Message call object¶
| Field | Type | Description |
|---|---|---|
| participants | array[snowflake] | Participant user IDs |
| ended_timestamp | ?ISO8601 timestamp | Call end time, or null while the call is active |
Channel mention object¶
| Field | Type | Description |
|---|---|---|
| id | snowflake | The ID of the mentioned channel |
| name | string | The channel name |
| type | integer | Channel type |
Sticker item object¶
| Field | Type | Description |
|---|---|---|
| id | snowflake | The ID of the sticker |
| name | string | The sticker name at send time |
| animated | boolean | Whether the sticker is animated |
| image_url? | string | The current image URL of the sticker, omitted when the sticker no longer exists |
| guild_id? | snowflake | The ID of the guild that owns the sticker |
| legacy_id? | string | An older identifier of the sticker, present on some stickers |
Embed object¶
An embed is a rendered preview on a message: either resolved from a link in the content or supplied as a rich embed input.
Structure¶
| Field | Type | Description |
|---|---|---|
| type | string | Embed type |
| title? | string | Embed title |
| description? | string | Embed description |
| url? | string | Embed destination URL |
| timestamp? | ISO8601 timestamp | Embed timestamp |
| color? | integer | Embed colour as a 24-bit RGB integer |
| author? | embed author object | Embed author |
| provider? | embed provider object | External provider (link previews only) |
| thumbnail? | embed media object | Thumbnail image |
| image? | embed media object | Main image |
| video? | embed media object | Video media (link previews only) |
| audio? | embed media object | Audio media (link previews only) |
| footer? | embed footer object | Embed footer |
| fields? | array[embed field object] | Embed fields |
| html? | string | Sanitised oEmbed markup (link previews only) |
| children? | array[embed object] | Nested link-preview embeds |
Every optional field is omitted when it has no value.
Example¶
{
"type": "rich",
"title": "Release 2.4.0",
"description": "Voice reconnects are now instant.",
"url": "https://example.com/releases/2.4.0",
"color": 5793266,
"footer": {"text": "example.com"},
"fields": [{"name": "Platform", "value": "Desktop", "inline": true}]
}
Embed types¶
| Value | Description |
|---|---|
| rich | A rich embed supplied by a bot, user or webhook |
| link | A generic link preview, and the type of a preview that has not been resolved yet |
| image | An image preview resolved from a link |
| video | A video preview resolved from a link |
| gifv | An animated looping video preview (for example a KLIPY GIF link) |
| article | An encyclopaedia article preview |
| social | A social-network post preview |
Embed author object¶
| Field | Type | Description |
|---|---|---|
| name | string | Author name |
| url? | string | Author link |
| icon_url? | string | Author icon URL |
Embed media object¶
| Field | Type | Description |
|---|---|---|
| url | string | Media URL |
| proxy_url | string | Always equal to url |
| width? | integer | Pixel width |
| height? | integer | Pixel height |
| duration? | integer | Duration in seconds |
| description? | string | Media description |
| content_type? | string | Media type |
| placeholder? | string | Encoded media placeholder |
| flags? | integer | Media flags using CONTAINS_EXPLICIT_MEDIA and IS_ANIMATED from attachment flags |
Embed footer object¶
| Field | Type | Description |
|---|---|---|
| text | string | Footer text |
| icon_url? | string | Footer icon URL |
Embed field object¶
| Field | Type | Description |
|---|---|---|
| name | string | Field name |
| value | string | Field value |
| inline | boolean | Whether the field can render inline |
Embed provider object¶
| Field | Type | Description |
|---|---|---|
| name | string | Provider name |
| url? | string | Provider URL |
Allowed mentions object¶
An allowed mentions object selects which mentions written in the message text notify anyone. The mentions still render and are still stored on the message; only notifications are filtered.
| Field | Type | Description |
|---|---|---|
| parse? | array[string] | Mention categories that notify: users, roles, everyone |
| users? | array[snowflake] | User IDs allowed to be notified (used instead of parse: ["users"]) |
| roles? | array[snowflake] | Role IDs allowed to be notified (used instead of parse: ["roles"]) |
| replied_user? | boolean | Whether the author of the replied-to message is notified (default true) |
When the object is omitted, every mention notifies. When it is present, parse defaults to all three categories unless users or roles is given; a category then notifies only when parse names it or its explicit ID list allows the ID. An empty object {} suppresses every notification, including the reply notification. A non-empty parse together with a non-empty users or roles list is rejected with 400 PARSE_AND_USERS_OR_ROLES_CANNOT_BE_USED_TOGETHER.
A reply (outside one-to-one DMs) adds the author of the replied-to message to the message's mentions and notifies them, unless replied_user is false or the object is {}. Replying to your own message never notifies you.
Independently of this object, @everyone and @here notify only when the sender holds MENTION_EVERYONE in a guild channel, and a role mention notifies the role's members only when the role is mentionable or the sender holds MENTION_EVERYONE.
Rich embed input object¶
The shape a bot or user supplies in embeds on Create message. TEBHOS stores every supplied embed with the rich type.
| Field | Type | Description |
|---|---|---|
| url? | string | Embed destination URL, http or https |
| title? | string | Embed title, up to 256 characters |
| description? | string | Embed description, up to 4,096 characters |
| color? | integer | Embed colour as a 24-bit RGB integer, 0 to 16777215 (0xFFFFFF) |
| timestamp? | ISO8601 timestamp or integer | Embed timestamp, as an ISO 8601 string or as milliseconds since the Unix epoch |
| author? | object | {name, url?, icon_url?}. name is up to 256 characters. Dropped when name is missing or empty |
| image? | object | {url, description?}. Dropped when url is missing or empty. url may be attachment://{filename}, see Embed images from attachments |
| thumbnail? | object | {url, description?}. Dropped when url is missing or empty. url may be attachment://{filename} |
| footer? | object | {text, icon_url?}. text is up to 2,048 characters. Dropped when text is missing or empty |
| fields? | array[object] | Up to 25 {name, value, inline?} entries. name is 1-256 characters and value 0-1,024 characters |
Every URL (url, author.url, author.icon_url, image.url, thumbnail.url, footer.icon_url) must use http or https. image.url and thumbnail.url may instead be attachment://{filename}.
Embed limits¶
| Limit | Value | When exceeded |
|---|---|---|
| Embeds supplied per message | 10 | 400 INVALID_FORM_BODY, error code TOO_MANY_EMBEDS at path embeds |
| Total characters across all supplied embeds | 6,000 | 400 INVALID_FORM_BODY, error code EMBEDS_EXCEED_MAX_CHARACTERS at path embeds |
| Title | 256 characters | Schema violation, see below |
| Description | 4,096 characters | Schema violation |
| Fields per embed | 25 | Schema violation |
| Field name / value | 1-256 / 0-1,024 characters | Schema violation |
| Footer text | 2,048 characters | Schema violation |
| Author name | 256 characters | Schema violation |
| Colour | 0 to 16777215 | Schema violation |
| Link previews resolved from content | 3 URLs | Later URLs get no preview |
The 6,000-character total counts the title, description, footer.text, author.name, and every field name and value of every supplied embed.
Supplied embeds come first, and link previews for URLs in the content are added after them up to 10 embeds in all; previews beyond that are dropped.
How a schema violation is reported depends on the body:
- JSON body: 400
INVALID_FORM_BODYwith one error at pathmessage_data, codeINVALID_MESSAGE_DATA. The response does not say which field failed. - Multipart body (
payload_json): 400INVALID_FORM_BODYwith one error per failing field, for example pathembeds.0.titlewith codeSTRING_LENGTH_INVALID.
The same limits apply to Modify message and to Execute webhook.
Embed images from attachments¶
An embed's image.url or thumbnail.url can point at a file sent with the same message as attachment://{filename}, on Create message and Modify message. The reference is resolved when the message is stored:
- the URL is replaced by the attachment's file URL, and the attachment's width, height, content type and placeholder are copied into the embed media;
- the referenced attachment is removed from the message's
attachmentslist, so the file appears only inside the embed.
{filename} is the attachment's file name. TEBHOS stores file names with unsafe characters replaced by _ (for example my photo.png becomes my_photo.png); either spelling matches. Only image files (png, jpg, jpeg, webp, gif) can be referenced. The request fails with 400 INVALID_FORM_BODY before anything is stored when:
errors[].code |
Condition |
|---|---|
CANNOT_REFERENCE_ATTACHMENTS_WITHOUT_ATTACHMENTS |
The message carries no attachments |
REFERENCED_ATTACHMENT_NOT_FOUND |
No attachment of the message has that file name |
ATTACHMENT_MUST_BE_IMAGE |
The referenced file is not an image |
Attachment uploads¶
A message references files that were uploaded first. The flow is:
POST /channels/{channel_id}/attachmentswith{"attachments": [{"id", "filename", "file_size", "content_type?"}]}(1-10 entries). Each result hasupload_filename(the upload key) andupload_mode. A file of at most 10 MiB getsupload_mode: "singlepart"and oneupload_url(a presignedPUTURL valid for 300 seconds); a larger file getsupload_mode: "multipart"withupload_id,part_sizeand apartslist of per-part upload URLs.PUTeach file to itsupload_url, or each part of a multipart file to its part URL.- For multipart files, call Complete attachment uploads (
POST /channels/{channel_id}/attachments/complete). - Send the message with the upload keys in
attachment_ids(or, on the channel-scoped route, inattachments[].upload_filename).
A message can also carry its files directly as multipart/form-data; see Multipart body.
Uploading requires SEND_MESSAGES and ATTACH_FILES in a guild channel. Bot tokens are accepted. The per-file ceiling is 10 MiB, 200 MiB with TEBHOS Pro, or the guild's boost ceiling (50 MiB at level 1, 100 MiB at level 2), whichever is larger. The route allows 10 requests per 10 seconds for each user and channel (channel:attachment:upload). Uploads describes the upload routes in full.
List channel messages¶
GET /channels/{channel_id}/messages
Returns an array of message objects, newest first. Requires VIEW_CHANNEL.
Query parameters¶
| Field | Type | Description |
|---|---|---|
| limit? | integer | Number of messages to return (at most 100, default 50) |
| before? | snowflake | Return messages before this message ID |
| after? | snowflake | Return messages after this message ID |
| around? | snowflake | Return a window centred on this message ID, overriding before and after. With limit 50 the window holds 25 newer messages, the target and 24 older messages |
before and after can be combined. A page can hold fewer than limit messages when some fall outside the caller's message history cutoff. A caller without READ_MESSAGE_HISTORY in a guild with no cutoff receives an empty array.
Rate limit¶
100 requests per 10 seconds for each user and channel, on the channel:messages:read bucket.
curl "https://tebhos.com/api/v1/channels/1501314428688998182/messages?limit=20" \
-H "Authorization: Bot $TEBHOS_BOT_TOKEN"
List messages from multiple channels¶
POST /channels/messages/bulk
Returns bounded message windows from several channels in one request. User-only.
Each channel is authorised exactly like List channel messages. An inaccessible channel fails the whole request.
JSON body¶
| Field | Type | Description |
|---|---|---|
| requests | array[object] | 1-25 entries, each {channel_id, limit, before?, after?, around?}. limit must be 1-25, and the limit values must sum to at most 250 |
Response body¶
| Field | Type | Description |
|---|---|---|
| channels | array[object] | One {channel_id, messages} entry per request entry, in request order. messages is newest first |
Rate limit¶
20 requests per 10 seconds for each user, on the channel:messages:bulk_read bucket.
Get message¶
GET /channels/{channel_id}/messages/{message_id}
Returns one message object. Requires VIEW_CHANNEL. A message that does not belong to the channel, does not exist, or lies outside the caller's message history cutoff returns 404 NOT_FOUND.
Rate limit¶
100 requests per 10 seconds for each user and channel, on the channel:message:read bucket.
Create message¶
POST /channels/{channel_id}/messages
Creates a message and returns 200 with the created message object. Emits a Message Create Gateway event. The body is JSON or, to send files in the same request, multipart/form-data (see Multipart body).
Limitations¶
- The channel must be text-bearing; a link channel returns 400
BAD_REQUEST. - A guild channel requires
VIEW_CHANNELand SEND_MESSAGES, and the caller must be a member who is not timed out. - A locked channel refuses every sender with 403
FORBIDDEN. - Attachments require ATTACH_FILES and embeds require EMBED_LINKS in a guild channel (403
FORBIDDENotherwise). WithoutEMBED_LINKS, links in the content get no preview. - A custom emoji or sticker from another guild requires USE_EXTERNAL_EMOJIS or USE_EXTERNAL_STICKERS and TEBHOS Pro (403
FORBIDDEN). - In a one-to-one direct message, a block in either direction returns 403
FORBIDDEN, and a recipient whose privacy settings refuse the sender returns 400CANNOT_SEND_MESSAGES_TO_USER. - Slowmode applies unless the caller holds BYPASS_SLOWMODE; see Slowmode.
JSON body¶
| Field | Type | Description |
|---|---|---|
| content? | string | Message text. See content length |
| embeds? | array[rich embed input object] | Rich embeds (see embed limits) |
| attachment_ids? | array[string] | Upload keys from Attachment uploads, at most 10 |
| attachments? | array[object] | Metadata for the uploads: {upload_filename, width?, height?, description?, title?, duration?, waveform?, flags?}. On this route an entry's upload_filename also adds the upload to attachment_ids, and a flags value with IS_SPOILER marks it as a spoiler |
| spoiler_attachment_ids? | array[string] | Upload keys to mark as spoilers |
| sticker_ids? | array[snowflake] | At most 3 sticker IDs |
| message_reference? | object | {message_id, channel_id?, guild_id?, type?}. See replies and forwards |
| reply_to_id? | snowflake | Shorthand for a reply reference to a message in the same channel |
| allowed_mentions? | allowed mentions object | Notification policy |
| flags? | integer | Message flags, masked to the sendable set |
| tts? | boolean | Text-to-speech request. Forced to false in a guild channel when the caller lacks SEND_TTS_MESSAGES |
| nonce? | string | integer | A caller-generated nonce: a string of 1-32 characters or a non-negative integer (turned into a string). It is echoed as nonce on the created message and its Gateway event. Any other value fails with 400 (message_data / INVALID_MESSAGE_DATA) |
| _optimistic_id? | string | Older name for nonce, used when nonce is absent |
content, sticker_ids and an embed's fields may be null, which counts as absent. A message must have visible content, an attachment, a sticker or an embed. An otherwise empty request fails with 400 CANNOT_SEND_EMPTY_MESSAGE.
The smallest body that works:
A reused nonce replays the first result
A nonce is remembered for the sender for five minutes. Reusing it in the same channel returns the message the first request created (200) instead of creating a duplicate. Reusing it in a different channel returns 404 NOT_FOUND. A client that retries a send after a timeout should reuse the same nonce.
Multipart body¶
With Content-Type: multipart/form-data the request carries:
| Part | Description |
|---|---|
payload_json |
The JSON body described above, as a string |
files[n] |
A file, where n is 0-9. The legacy names file and file{n} are also accepted |
content, nonce, tts, flags, sticker_ids |
Optional plain fields; each overrides the same key of payload_json |
An attachments entry whose id equals a file's index supplies that file's filename, description and flags (IS_SPOILER marks a spoiler). Every file passes the same checks as an uploaded one (permissions, size limit, blocked-file list) and the whole body may be at most 100 MiB. Embeds can reference the files with attachment://{filename}.
curl -X POST https://tebhos.com/api/v1/channels/1501314428688998182/messages \
-H "Authorization: Bot $TEBHOS_BOT_TOKEN" \
-F 'payload_json={"content":"Chart","embeds":[{"title":"Today","image":{"url":"attachment://chart.png"}}],"attachments":[{"id":0,"filename":"chart.png"}]}' \
-F "files[0][email protected]"
Content length¶
| Sender | Maximum characters |
|---|---|
| User without TEBHOS Pro | 2,000 |
| User with TEBHOS Pro | 4,000 |
| Bot | 4,000 |
| Webhook (Execute webhook) | 4,000 |
Content above 4,000 characters fails validation with 400 INVALID_FORM_BODY; content between the two limits from a user without TEBHOS Pro fails with 400 BAD_REQUEST.
Replies and forwards¶
A reply (message_reference.type omitted or 0, or reply_to_id) must target a message in the same channel; otherwise the request fails with 400 BAD_REQUEST. The created message has type REPLY (19) and a resolved referenced_message.
A forward (message_reference.type 1) requires both channel_id and message_id and must not carry content, attachments or stickers (400 BAD_REQUEST). The caller must be able to view the source channel, and a source message that does not exist returns 404. The created message has type DEFAULT and a message_snapshots entry; the source's attachments are copied into the destination channel.
Slowmode¶
In a guild channel with a non-zero rate_limit_per_user, each member may send one message per interval. A send inside the interval returns 400 SLOWMODE_RATE_LIMITED (not 429), with a Retry-After header in whole seconds and a top-level retry_after member in decimal seconds:
{
"error": "400 Bad Request",
"code": "SLOWMODE_RATE_LIMITED",
"message": "...",
"retry_after": 4.2
}
The owner, administrators, and members with BYPASS_SLOWMODE are exempt; MANAGE_MESSAGES or MANAGE_CHANNELS alone does not exempt a member. Private channels have no slowmode.
Side effects¶
The operation stores the message, binds the referenced uploads, advances the sender's read state (except for bots), reopens a closed direct message for its recipients, indexes the message for search, and emits Message Create to every session that can see the channel. Up to 3 links in the content are resolved into previews asynchronously; each resolution emits Message Update unless SUPPRESS_EMBEDS is set.
Rate limit¶
20 requests per 10 seconds for each user and channel, on the channel:message:create bucket.
const res = await fetch(
"https://tebhos.com/api/v1/channels/1501314428688998182/messages",
{
method: "POST",
headers: {
Authorization: `Bot ${process.env.TEBHOS_BOT_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ content: "Build finished" }),
},
);
const message = await res.json();
Modify message¶
PATCH /channels/{channel_id}/messages/{message_id}
Modifies a message and returns the updated message object. Emits a Message Update Gateway event. Accepts user sessions and bot tokens. The body is JSON or multipart/form-data (payload_json plus files[n], as in Multipart body).
JSON body¶
| Field | Type | Description |
|---|---|---|
| content? | string | Replacement text (at most 4,000 characters, and at most 2,000 without TEBHOS Pro). "" together with embeds clears the text. Only the author can change content |
| flags? | integer | Replacement message flags; only the sendable bits change. The author, or a member with MANAGE_MESSAGES, can change flags |
| embeds? | array[rich embed input object] | Replaces the message's supplied embeds ([] removes them). Only the author; requires EMBED_LINKS in a guild channel. Link previews are kept. Images may use attachment://{filename} |
| attachments? | array[object] | The complete new attachment list, see Editing attachments. Omit it to keep the attachments unchanged |
At least one field is required, and a blank content needs non-empty embeds, attachments or flags beside it (400 CANNOT_SEND_EMPTY_MESSAGE otherwise). A caller who is not the author and supplies content or embeds receives 403 FORBIDDEN.
A content change advances edited_timestamp, recalculates mentions, replaces the link previews with previews of the links in the new content, and re-indexes the message for search. A flags-only or attachments-only change does not mark the message edited.
Editing attachments¶
Each entry of attachments is one of:
| Entry | Effect |
|---|---|
{id, title?, description?} |
Keeps an existing attachment. id is the attachment ID, or its position in the current list. title and description replace the stored values (null clears them) |
{upload_filename, filename?, description?, title?, width?, height?, duration?, waveform?, flags?} |
Adds a file uploaded through Attachment uploads by the caller in this channel. Requires ATTACH_FILES in a guild channel; the file passes the blocked-file check and the size limit |
An existing attachment that is not listed is removed from the message; [] removes them all. At most 10 entries. In a multipart request, entries whose id equals a file index become new attachments; when files are sent, the new list is the listed existing attachments plus the files. An entry that names a filename without a matching file fails with 400 INVALID_FORM_BODY (errors[].code NO_FILE_FOR_ATTACHMENT_METADATA).
A member who is not the author may send attachments only in a guild channel and with MANAGE_MESSAGES: they can drop attachments or change title/description, and any other key fails with 400 INVALID_FORM_BODY (errors[].code CANNOT_EDIT_ATTACHMENT_METADATA). Without the permission the request fails with 403 CANNOT_EDIT_OTHER_USER_MESSAGE.
| Status | Condition |
|---|---|
400 INVALID_FORM_BODY (errors[].code FILE_NOT_FOUND) |
An upload_filename is unknown, already used, or belongs to another account or channel |
400 FILE_SIZE_TOO_LARGE |
A new file exceeds the caller's size limit |
403 CONTENT_BLOCKED |
A new file is on the blocked-file list |
403 MISSING_PERMISSIONS |
New files without ATTACH_FILES |
Not supported on TEBHOS
allowed_mentions cannot be changed through this route.
Rate limit¶
20 requests per 10 seconds for each user and channel, on the channel:message:update bucket.
Delete message¶
DELETE /channels/{channel_id}/messages/{message_id}
Deletes one message and returns 200 with a JSON body containing a message string. Emits a Message Delete Gateway event.
The author can delete their own message. Deleting another user's message requires MANAGE_MESSAGES (403 FORBIDDEN otherwise).
Side effects¶
The message is removed and deleted from search. Message Delete carries the channel ID, message ID, the deleted content and the author ID. When the message was pinned, Channel Pins Update is also emitted with the channel's unchanged last-pin timestamp.
Rate limit¶
20 requests per 10 seconds for each user and channel, on the channel:message:delete bucket.
Delete message attachment¶
DELETE /channels/{channel_id}/messages/{message_id}/attachments/{attachment_id}
Removes one attachment from the caller's own message and returns 204. Accepts user sessions and bot tokens. Only the author can use this route; MANAGE_MESSAGES is not enough (403 MISSING_PERMISSIONS). The caller must be able to view the channel.
attachment_id is the attachment's id from the message object.
When the attachment is the message's last one and the message has no content, embeds or stickers, the whole message is deleted and Message Delete is emitted. Otherwise the file is deleted from storage (and queued for CDN cache purge), the attachment is removed from the message, edited_timestamp advances, and Message Update is emitted.
| Status | Condition |
|---|---|
| 204 | The attachment (or the whole message) was deleted |
403 MISSING_PERMISSIONS |
The caller is not the author |
404 UNKNOWN_MESSAGE |
The message does not exist in this channel, has no attachments, or has no attachment with this ID |
| 404 / 403 | The caller cannot access the channel (see Channel resolution) |
DELETE /messages/{message_id}/attachments/{attachment_id} is an older TEBHOS form of the same operation. It resolves the channel from the message and answers 200 with the updated message object, or 200 with a message string when the whole message was deleted.
Rate limit¶
20 requests per 10 seconds for each user and channel, on the channel:message:delete bucket.
Bulk delete messages¶
POST /channels/{channel_id}/messages/bulk-delete
Deletes 1-100 messages in one channel and returns 204. Requires MANAGE_MESSAGES. The alias POST /channels/{channel_id}/messages/bulk_delete behaves identically.
JSON body¶
| Field | Type | Description |
|---|---|---|
| message_ids? | array[snowflake] | Message IDs to delete (1-100) |
| messages? | array[snowflake] | Alias for message_ids, used when message_ids is absent |
An empty list or more than 100 IDs fails with 400 BAD_REQUEST. IDs that are not valid, or that name no message in the channel, are skipped. Messages of any age can be selected.
Side effects¶
Every matching message is removed and deleted from search. One Message Delete Bulk with the deleted IDs is emitted to sessions that can see the channel.
Rate limit¶
10 requests per 10 seconds for each user and channel, on the channel:message:bulk_delete bucket.
Bulk delete own messages¶
POST /channels/{channel_id}/messages/bulk-delete-mine
Deletes every message the caller authored in one channel and returns 202. Requires VIEW_CHANNEL and sudo mode: a user session must have obtained a sudo grant through POST /auth/sudo within the last few minutes, otherwise the request fails with 403 SUDO_MODE_REQUIRED. A bot token always satisfies sudo mode.
The deletion runs inside the request, so the messages are gone when the response arrives. One Message Delete Bulk is emitted. The same bucket as Bulk delete messages applies.
There is no restore
Every message the caller authored in the channel is deleted, without an age boundary.
Two wider variants exist with the same sudo requirement: POST /users/@me/guilds/{guild_id}/messages/bulk-delete-mine deletes the caller's messages in every channel of a guild (10 requests per 10 seconds per user and guild), and POST /users/@me/messages/bulk-delete-mine deletes across the caller's direct messages, group direct messages and guilds (user-only, 5 requests per 30 minutes per user; the body accepts include_dms, include_group_dms, include_guilds (each default true), and start_date/end_date as ISO8601 timestamps).
Purge personal notes¶
POST /channels/{channel_id}/messages/purge
Deletes every message in the caller's personal notes channel and returns {"deleted_count": N}. channel_id must equal the caller's user ID; any other channel returns 400 BAD_REQUEST. Emits Message Delete Bulk when anything was deleted.
Rate limit¶
2 requests per minute for each user and channel, on the channel:message:purge bucket.
List pinned messages¶
GET /channels/{channel_id}/pins
Returns one page of pinned messages, most recently pinned first. Requires VIEW_CHANNEL. The alias GET /channels/{channel_id}/messages/pins behaves identically.
A guild caller without READ_MESSAGE_HISTORY sees only pins whose message falls on or after the history cutoff.
Query parameters¶
| Field | Type | Description |
|---|---|---|
| limit? | integer | Number of pins to return (at most 50, default 50) |
| before? | snowflake | The ID of the last pinned message of the previous page |
Response body¶
| Field | Type | Description |
|---|---|---|
| items | array[message object] | Pinned messages, most recently pinned first |
| has_more | boolean | Whether another page may exist |
Rate limit¶
20 requests per 10 seconds for each user and channel, on the channel:pins bucket.
Pin message¶
PUT /channels/{channel_id}/pins/{message_id}
Pins a DEFAULT or REPLY message and returns 204. Requires PIN_MESSAGES in a guild channel; any recipient can pin in a private channel. A system message returns 400 BAD_REQUEST, and a message that is not in the channel returns 404.
Side effects¶
The message's pin time and the channel's last-pin timestamp are set to now, and a CHANNEL_PINNED_MESSAGE (6) system message referencing the pinned message is created. TEBHOS emits Message Update for the message (pinned: true), Channel Pins Update with channel_id, message_id, guild_id and last_pin_timestamp, and Message Create for the system message.
Note
Pinning an already pinned message refreshes its pin time and creates another system message.
Rate limit¶
20 requests per 10 seconds for each user and channel, on the channel:pins bucket.
Unpin message¶
DELETE /channels/{channel_id}/pins/{message_id}
Unpins a message and returns 204. Authorisation is the same as Pin message. Emits Message Update (pinned: false) and Channel Pins Update with the channel's unchanged last-pin timestamp. No system message is created, and the original CHANNEL_PINNED_MESSAGE stays. The operation is idempotent. Same bucket as pinning.
Acknowledge pins¶
POST /channels/{channel_id}/pins/ack
Records the channel's current last-pin timestamp in the caller's read state and returns 204. User-only. The request has no body.
When the channel has never held a pin, nothing is written and nothing is emitted. Otherwise TEBHOS writes the channel's last-pin timestamp and emits Channel Pins ACK with channel_id and last_pin_timestamp to the caller's own sessions.
Rate limit¶
20 requests per 10 seconds for each user and channel, on the channel:pins bucket.
Reaction emoji path value¶
The emoji path value is URI-encoded and takes one of these forms:
| Form | Example | Meaning |
|---|---|---|
| Unicode emoji | %F0%9F%91%8D |
A standard emoji |
name:id |
party_parrot:1501314428688991234 |
A custom emoji |
a:name:id |
a:party_parrot:1501314428688991234 |
An animated custom emoji |
<:name:id> / <a:name:id> |
%3C%3Aparty_parrot%3A1501314428688991234%3E |
A custom emoji in message-token form |
All custom forms address the same stored reaction. The add, remove-own and remove-user routes accept an optional session_id query parameter (1-64 characters) that is echoed in the Gateway event so the originating session can skip its own echo.
List reaction users¶
GET /channels/{channel_id}/messages/{message_id}/reactions/{emoji}/users
Returns one page of the users who added a reaction. Requires VIEW_CHANNEL; a message outside the caller's history cutoff returns 404.
Query parameters¶
| Field | Type | Description |
|---|---|---|
| limit? | integer | Number of users to return (1-100, default 100) |
| after? | snowflake | Return users with an ID after this user ID |
Response body¶
| Field | Type | Description |
|---|---|---|
| items | array[object] | Users in ascending user ID order, each {id, name, image_url} where image_url is the avatar hash (empty string when none) |
| has_more | boolean | Whether another page exists |
| next_after | ?snowflake | The cursor for the next page (the last user ID), or null |
Rate limit¶
30 requests per 10 seconds for each user and channel, on the channel:reactions bucket.
List reaction users unpaged¶
GET /channels/{channel_id}/messages/{message_id}/reactions/{emoji}
Returns {emoji, users}, where emoji is an emoji object and users is every user who added the reaction, in the same {id, name, image_url} shape. Same authorisation and bucket as List reaction users.
Add own reaction¶
PUT /channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me
Adds the caller's reaction and returns 204. Emits Message Reaction Add.
- The message must belong to the channel (404 otherwise), and the caller must be able to view it under the history cutoff.
- A timed-out member cannot react (403
FORBIDDEN). - Creating a new emoji group in a guild channel requires ADD_REACTIONS; joining an existing group does not.
- A new group with a custom emoji from another guild requires USE_EXTERNAL_EMOJIS and TEBHOS Pro (403
FORBIDDEN).
The operation is idempotent, and every successful call emits the event. Message Reaction Add carries channel_id, message_id, user_id, emoji and session_id when supplied.
Rate limit¶
30 requests per 10 seconds for each user and channel, on the channel:reactions bucket.
Remove own reaction¶
DELETE /channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me
Removes the caller's reaction and returns 204. Emits Message Reaction Remove whether or not the reaction existed. Same bucket as adding.
Remove another user's reaction¶
DELETE /channels/{channel_id}/messages/{message_id}/reactions/{emoji}/{user_id}
Removes one user's reaction and returns 204. Requires MANAGE_MESSAGES, including when user_id is the caller. Emits Message Reaction Remove with the target user ID. Same bucket as adding.
Remove all reactions for emoji¶
DELETE /channels/{channel_id}/messages/{message_id}/reactions/{emoji}
Removes every reaction for one emoji and returns 204. Requires MANAGE_MESSAGES. Emits one Message Reaction Remove Emoji. Same bucket as adding.
Remove all reactions¶
DELETE /channels/{channel_id}/messages/{message_id}/reactions
Removes every reaction from a message and returns 204. Requires MANAGE_MESSAGES. Emits one Message Reaction Remove All. Same bucket as adding.
Indicate typing¶
POST /channels/{channel_id}/typing
Publishes typing activity and returns 204. Requires VIEW_CHANNEL and SEND_MESSAGES in a guild channel, and the caller must not be timed out; a private channel needs only recipient access.
Emits Typing Start with channel_id, user_id and a Unix timestamp in seconds. Nothing is stored; clients expire the indicator themselves.
Rate limit¶
20 requests per 10 seconds for each user and channel, on the channel:typing bucket.
Acknowledge message¶
POST /channels/{channel_id}/messages/{message_id}/ack
Advances the caller's read state in the channel through message_id. User-only. TEBHOS resolves neither the channel nor the message.
The stored mention count is reset to 0, and a message_id below the stored watermark leaves the entry unchanged. There is no request body; use Acknowledge read states to store a mention count or to move the watermark backwards.
Response¶
200 with {"read_states": [read state], "read_state_proto": string}; see Read state object. Emits Message ACK to the caller's own sessions.
Rate limit¶
100 requests per 10 seconds for each user and channel, on the channel:message:ack bucket.
Clear channel read state¶
DELETE /channels/{channel_id}/messages/ack
Deletes the caller's read state entry for one channel and returns 204. User-only. The channel is not resolved, the operation is idempotent, and no Gateway event is emitted.
Rate limit¶
40 requests per 10 seconds for each user and channel, on the channel:read_state:delete bucket.