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.
A direct message’s history is read with fetchDmMessages(userId, opts), or with fetchMessages(null, dmId, opts) for a DM this session has already seen traffic in. Both need a backend that honors wire version 3. See Read a DM’s history.

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.
string
Read a thread’s history rather than the channel’s. channelId is then the thread’s parent text channel. See Read a thread’s history.
string
Read a forum post’s history rather than the channel’s. channelId is then the parent forum channel. Rows come back with postId and forumChannelId set and channelId equal to the post, so acting on one stays inside the post. Mutually exclusive with threadId. See Forums.
AbortSignal
Cancel the read. The SDK checks it before each page and cancels the wait for the page in flight; the promise rejects with the signal’s reason.
Each wire page is its own serialized action. The SDK re-establishes the channel (or thread, post, or DM) selection before every page, which costs nothing when nothing moved it, and releases the chain in between, so a long read no longer holds every send and reaction behind it. Each page is a consistent slice; between pages another action may interleave.

Read a thread’s history

Pass threadId to read a thread instead of a channel. Address it the way a send does: the thread’s parent text channel as channelId, the thread id in the options.
This matters most after a restart. Only live thread messages arrive on messageCreate, so a bot that derives state from thread replies (a bridge tracking reactions, for example) has no other way to rebuild it. Everything on this page applies unchanged: the same cursors, the same auto-paging, the same message_read_history permission, the same per-row decryption. What differs is what comes back. Rows carry threadId and threadParentId, and their channelId is the thread rather than the parent, exactly as live thread messages report themselves. That is what makes acting on a fetched row stay inside the thread.
Thread history needs a Cloak server that supports it. On an older backend the read falls back to the parent channel’s messages, and the SDK rejects rather than returning them under the thread’s name. See What the server must support.
See Threads for the rest of the thread surface.

Read a DM’s history

A 1:1 DM is paged the same way, on a backend that honors wire version 3. There are two forms.
fetchDmMessages(userId, opts) takes the peer’s user id and the same before, after, around, and limit options as a channel read. fetchMessages(null, dmId, opts) is the same read addressed by dm id; a null server id is what selects the DM form. It rejects, naming fetchDmMessages() as the alternative, when the DM’s peer is not yet known to this session, and it rejects threadId and postId, because a DM has no containers. Rows decrypt under the DM key and come back with content, repliedTo, card, poll, and createdAt from the row’s own send time, plus isDM: true, serverId: null, and channelId equal to the dm id. Your bot’s own messages are included, and because each row carries createdAt, edit() and editCard() work on them. That is how a bot recovers a card it sent before a restart; see the Approval bot. No message_read_history gate applies in a DM. On a backend that denies the DM read, the call resolves empty, exactly like an empty channel. Message.command is never set on a DM history row either.

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.
Rows from a server-channel read carry a non-null serverId, which is why the loop above can call msg.pin() without a guard. Rows from a DM read, and live DM messages, have serverId: null, and pin() rejects 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

A server-channel 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'. A DM read has no permission gate.
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.