Skip to main content
Sometimes your bot needs messages it did not see live: the last few in a channel, everything before a date, or the context around a single message. client.fetchMessages() reads them back from the server, always ordered oldest to newest. This guide covers the cursors, the paging behavior, and what you get back.
fetchMessages reads server-channel history only. There is no DM history read in this SDK. A bot sees direct messages as they arrive on messageCreate and cannot page back through them. See Direct messages.

Read the newest messages

With no cursor, fetchMessages(serverId, channelId, opts?) returns the newest messages in a channel, oldest to newest. limit defaults to 50.
Reads are auto-paginated for you, so a larger limit fetches across as many pages as it takes.
The wire page sizes are fixed: the first page holds up to 50 rows, and each page after it up to 25. A limit of 100 therefore costs several round-trips. The read stops early when a short page proves the history is exhausted in that direction.

Cursors are a Date or a Message

You point a read at a moment in time with a cursor. A cursor is always a Date or a Message, never a message id.
Cloak message ids are random UUIDv4 values, so they do not sort by time. There is no id cursor. When you pass a Message as a cursor, the read uses that message’s createdAt. A Message with no createdAt throws.
Three options place the window relative to the cursor: before, after, and around.

Page backward with before

before returns messages strictly older than the cursor. The newest limit of them are kept, and the result is still ordered oldest to newest.
Pass a Message to walk backward from one you already have. This is the usual way to load older history a page at a time.
before is auto-paginated.

Page forward with after

after returns messages strictly newer than the cursor. The limit closest to the cursor are returned, so the oldest matching messages are the ones you keep.
after is auto-paginated.

Fetch context around a message

around returns a single window of about 50 messages straddling the cursor. Reach for it when you want the context on either side of one message.
around is the one read that is not auto-paginated. It returns a single window and ignores limit for paging.

Options

Date | Message
Return messages strictly older than the cursor. Auto-paginated.
Date | Message
Return messages strictly newer than the cursor. Auto-paginated.
Date | Message
Return a single window of about 50 messages straddling the cursor. Not auto-paginated.
number
default:"50"
How many messages to keep. Applies to before, after, and a cursorless read. around ignores it for paging.
string
The channel’s group, where you already know it.
The whole paged read runs as one serialized action, so the channel selection cannot move mid-read. You get a consistent slice even when several pages are fetched under the hood.

Returned messages are live

Every message from fetchMessages is fully actionable, exactly like one you received on the firehose. Each carries reply, channel.send, react, unreact, edit, delete, pin, and unpin, plus a pinned boolean. The same fields are populated too. mentions, mentionsMe, and repliedTo are computed for history exactly as they are for a live message, because both paths run through the same builder.
History rows always carry a non-null serverId, because this read is server-channel only. That is why the loop above can call msg.pin() without a guard. A live messageCreate message is different: serverId is null for a DM, and the mutation methods reject there. Guard with if (msg.isDM || !msg.serverId) return; before you carry this pattern into a messageCreate handler.
Message.command is never set on fetched history. Replaying history must not re-execute old slash commands, so the SDK deliberately leaves the field undefined on this path. If you want to audit past invocations, parse the text yourself.
See the Message type for the full shape.

Undecryptable messages

Each message decrypts with the key for its own epoch. If a row cannot be decrypted, its content is the empty string ''. You never receive raw ciphertext, so a simple check is enough to skip a message you cannot read.
A bot only holds keys from the epochs it was present for, so history from before it joined, or from before a key cycle, commonly reads as ''.

Requires the message_read_history permission

fetchMessages requires the message_read_history permission. If the server has not granted it, the promise rejects with a CloakActionError whose .permission is 'message_read_history'.
You can pre-gate the call with client.can('message_read_history', serverId) to skip it when the grant is missing. The check is advisory, and the server stays the authority. See Permissions.

Find where a message lives

client.messageLocation(messageId) tells you where a message the bot has seen lives, whether it arrived live or through fetchMessages. It returns a MessageLocation, or undefined when the message is not in the cache.
string | null
The server the message lives in, or null when the remembered message is a direct message. In that case channelId holds the dm id and is the whole location.
string
The channel within that server, or the dm id when serverId is null.
string
The channel’s group, where known.
string
The id of the message’s author.
number | null
When the message was created, in milliseconds, or null if unknown.
The lookup is a bounded, best-effort cache of about the last 5000 messages the bot has seen. Older messages fall out, and messageLocation returns undefined for them.

Next

The Message object

Everything a returned message can do.

Direct messages

Why DM history is out of scope, and what you get instead.

Mentions and replies

How mentions and mentionsMe are derived.

Permissions

What message_read_history grants and how.