REST API endpoints for Discord messages.
All functions return {:ok, result} or {:error, reason}.
Summary
Functions
Bulk deletes messages (2-100, not older than 14 days).
Creates a message in a channel.
Publishes a message in an announcement channel to every channel following it.
Deletes a message.
Edits a message.
Forwards a message to another channel.
Gets a message by ID.
Retrieves message history from a channel with automatic pagination.
Gets messages from a channel.
Pins a message in a channel.
Gets pinned messages in a channel, paginating automatically.
Gets one page of a channel's pins.
Purges messages from a channel with automatic chunking and 14-day filtering.
Replies to a message, automatically setting message_reference.
Searches a guild's message history.
Returns a lazy Stream that yields messages from a channel, page by page.
Unpins a message from a channel.
Functions
Bulk deletes messages (2-100, not older than 14 days).
@spec create(String.t() | integer(), String.t() | map() | keyword()) :: {:ok, map()} | {:error, term()}
Creates a message in a channel.
Parameters
channel_id- The ID of the channelcontent- Message content (string), full message payload (map), or keyword options
Examples
EDA.API.Message.create(channel_id, "Hello!")
EDA.API.Message.create(channel_id, content: "Look!", embeds: [embed])
EDA.API.Message.create(channel_id, content: "File!", files: [file])
Publishes a message in an announcement channel to every channel following it.
POST /channels/{channel_id}/messages/{message_id}/crosspost. Needs SEND_MESSAGES for the
bot's own message, and MANAGE_MESSAGES as well for anyone else's. A message is published
once; publishing it again fails with 40033 (EDA.Error.message_already_crossposted/0).
Deletes a message.
@spec edit(String.t() | integer(), String.t() | integer(), map() | keyword()) :: {:ok, map()} | {:error, term()}
Edits a message.
Accepts a keyword list of options (which may upload files) or a raw payload map.
The attachments array replaces, it does not merge
Discord treats attachments as the complete list the message should end up with, so an
attachment left out of it is deleted. Uploading a file without naming the existing ones
therefore removes them:
# keeps only the newly uploaded file — the message's other attachments are gone
EDA.API.Message.edit(channel_id, message_id, files: [EDA.File.from_path("new.png")])Name them with EDA.Attachment.keep/2 to hold on to them, and the upload is appended:
EDA.API.Message.edit(channel_id, message_id,
attachments: Enum.map(message.attachments, &EDA.Attachment.keep/1),
files: [EDA.File.from_path("new.png")]
):attachments also takes bare ids and raw attachment maps. EDA.Message.edit/2 has the
shorter attachments: :keep for a message struct you already hold.
Omitting :attachments entirely leaves the message's attachments untouched — it is only
sending the array that is destructive.
Changing an existing attachment
EDA.Attachment.keep/2 carries the two fields Discord lets an edit update, so a spoiler
can be applied after the fact without re-uploading the file:
EDA.API.Message.edit(channel_id, message_id,
attachments: [EDA.Attachment.keep(attachment, is_spoiler: true, description: "Ending")]
)
@spec forward(String.t() | integer(), String.t() | integer(), String.t() | integer()) :: {:ok, map()} | {:error, term()}
Forwards a message to another channel.
Creates a message reference with type: 1 (forward) pointing to the original message.
The bot must be able to read the message it forwards
Forwarding is not a way to relay a message out of a channel the bot cannot see. Discord
checks read access to the source message's content at forward time and rejects the
request with code 160014 otherwise — typically a missing view_channel or
read_message_history in the source channel, or a source channel the bot is no longer in:
case EDA.API.Message.forward(target_id, source_id, message_id) do
{:ok, message} -> message
{:error, %{code: 160_014}} -> :cannot_read_source
endEDA.Error.cannot_forward_unreadable_message/0 names that code.
Examples
EDA.API.Message.forward(target_channel_id, source_channel_id, message_id)
Gets a message by ID.
@spec history(String.t() | integer(), pos_integer() | :infinity, keyword()) :: {:ok, [map()]} | {:error, term()}
Retrieves message history from a channel with automatic pagination.
Handles Discord's 100-message-per-request limit transparently.
Supports :infinity to retrieve all messages.
Options
:before- snowflake ID, get messages before this:after- snowflake ID, get messages after this:around- snowflake ID, get messages around this (single page, max 100)
Examples
{:ok, msgs} = EDA.API.Message.history(channel_id, 250)
{:ok, msgs} = EDA.API.Message.history(channel_id, 500, before: msg_id)
{:ok, msgs} = EDA.API.Message.history(channel_id, :infinity)
Gets messages from a channel.
Pins a message in a channel.
Options
:reason- Audit log reason
Gets pinned messages in a channel, paginating automatically.
Returns message objects. Use pins/2 if you need the pinned_at timestamps
or want to drive the pagination yourself.
Options
:limit- maximum number of messages to return (default:infinity)
Examples
{:ok, msgs} = EDA.API.Message.pinned(channel_id)
{:ok, msgs} = EDA.API.Message.pinned(channel_id, limit: 10)
Gets one page of a channel's pins.
Returns Discord's raw envelope: %{"items" => [pin], "has_more" => boolean},
where each pin is %{"pinned_at" => iso8601, "message" => message}.
Options
:before- ISO8601 timestamp, get pins pinned before this (use thepinned_atof the last item of the previous page):limit- pins per page (1-50, default 50)
@spec purge( String.t() | integer(), keyword() ) :: {:ok, non_neg_integer()} | {:error, term()}
Purges messages from a channel with automatic chunking and 14-day filtering.
Options
:limit- max messages to purge (default 100, max:infinity):before- purge messages before this ID:after- purge messages after this ID:filter- predicatefn(message) -> booleanto select which messages to purge:filter_old- filter out messages older than 14 days (defaulttrue)
Examples
{:ok, count} = EDA.API.Message.purge(channel_id, limit: 200)
{:ok, count} = EDA.API.Message.purge(channel_id,
limit: 500,
filter: fn msg -> msg["author"]["id"] == user_id end
)
Replies to a message, automatically setting message_reference.
Accepts a message struct (with :channel_id and :id) or a raw map
(with "channel_id" and "id").
Examples
EDA.API.Message.reply(msg, "Got it!")
EDA.API.Message.reply(msg, content: "Reply with embed", embeds: [embed])
Searches a guild's message history.
GET /guilds/{guild_id}/messages/search. Requires READ_MESSAGE_HISTORY in the channels
searched, and is gated on the MESSAGE_CONTENT privileged intent.
Returns Discord's raw payload. EDA.Message.search/2 returns parsed structs and flattens
the nesting described below.
The messages key is a list of lists
Each entry is a context group, not a message: Discord returns the matching message
surrounded by its neighbours, and marks the match itself with "hit" => true. A group of
one is the usual case, but nothing promises it.
{:ok, %{"messages" => groups, "total_results" => total}} =
EDA.API.Message.search(guild_id, content: "deploy")
hits = Enum.map(groups, fn group -> Enum.find(group, & &1["hit"]) end)Options
Multi-valued filters take a list and become repeated query keys.
:content- free text, max 1024 characters:channel_id- restrict to these channels, max 500:author_id- max 100 ·:author_type-:user,:botor:webhook:mentions- messages mentioning these users, max 100:mentions_role_id- max 100 ·:mention_everyone- boolean:replied_to_user_id,:replied_to_message_id- max 100:has-:image,:sound,:video,:file,:sticker,:embed,:link,:pollor:snapshot:embed_type,:embed_provider,:link_hostname,:attachment_filename,:attachment_extension- max 100 each:pinned- boolean ·:include_nsfw- boolean, defaultfalse:min_id/:max_id- snowflake bounds:sort_by-:timestamp(default) or:relevance·:sort_order-:descor:asc:limit- 1–25, default 25 ·:offset- max 9975:slop- words allowed between matching tokens, max 100, default 2
Examples
EDA.API.Message.search(guild_id,
content: "incident",
channel_id: [ops_channel, alerts_channel],
has: [:link],
sort_by: :relevance,
limit: 10
)An option this endpoint does not define is refused, not forwarded: Discord ignores a
query parameter it does not recognise, so contnet: would quietly return the guild's whole
history while looking like a filtered search.
Caveats Discord documents
- Sort order is ignored when sorting by relevance.
total_resultsis approximate while messages are being created or deleted, and a page may come back slightly shorter than:limit.:offsetcaps at 9975, so at most ten thousand results are reachable — narrow the query rather than paging to the end.- A guild whose history is still being indexed answers HTTP 202 rather than results.
That is a success status carrying no messages, so it is reported as
{:error, {:index_pending, retry_after_seconds}}instead of an empty search.
@spec stream( String.t() | integer(), keyword() ) :: Enumerable.t()
Returns a lazy Stream that yields messages from a channel, page by page.
Options
:before- start before this message ID:after- start after this message ID:per_page- messages per request (1-100, default 100)
Examples
EDA.API.Message.stream(channel_id) |> Stream.take(50) |> Enum.to_list()
EDA.API.Message.stream(channel_id) |> Enum.find(&(&1["author"]["id"] == user_id))
Unpins a message from a channel.
Options
:reason- Audit log reason