Skip to main content
Your bot can change a message after it sends it, remove one, and pin or unpin it. It can also hear when any of that happens, including its own changes. This page covers msg.edit(), msg.delete(), msg.pin(), and msg.unpin(), plus the events that report each change.
pin() and unpin() reject with a plain Error on a direct message, unconditionally and regardless of permissions. edit(), editCard(), and delete() work on a DM message that carries createdAt, which needs a backend that honors wire version 3; on an older backend a DM message has no timestamp and those reject too, with a message saying why. Direct messages arrive on the same messageCreate event as server messages, so guard with if (msg.isDM || !msg.serverId) return; before anything server-scoped. See Direct messages.

Edit a message the bot sent

msg.edit(newContent) replaces the text of a message and returns Promise<void>. The server allows only the message’s author to edit it, so a bot can edit its own messages and never another member’s. The bot receives its own messages on the firehose, so it can hold one and edit it later.
An edit is re-encrypted through the same lane as a fresh send, under the channel’s current key. If the channel’s key cycled between the original send and the edit, the edited text is stored under the newer key. Members who hold that key read it normally.
edit() touches the text only. To replace a card’s embed or its buttons, use msg.editCard(card), which re-encrypts the text and the card together. See Buttons and selects.

Delete a message

msg.delete() removes a message and returns Promise<void>. The server allows the author to delete their own message, or a member holding message_manage to delete another member’s. This is the method a moderation bot reaches for. Pre-gate with an advisory can() check so the bot skips a delete the server would only refuse.
The msg.isDM || !msg.serverId guard is what narrows msg.serverId from string | null to string, which is what can() requires. Skip it and TypeScript rejects the call, while plain JavaScript silently treats every message as unpermitted.

Pin and unpin

msg.pin() and msg.unpin() both return Promise<void>. Pinning is allowed for the message’s author, or for a member holding the channel’s pin permission. Both work on any server Message the bot holds, whether it came from the firehose or from fetchMessages.
Pinning is gated by a channel-level pin permission that is not one of the 31 named permissions (codes 0 to 30). Because it has no name in that vocabulary, a pin denial’s CloakActionError carries no .permission attribution, though .message still explains what happened.
Message.pinned is a boolean that is available when known, from history via fetchMessages or from a pinUpdate event. Treat undefined as unknown rather than as unpinned.

Every action needs createdAt

edit(), delete(), pin(), and unpin() all need the message’s createdAt, because the server locates the row by its own send time. Live messages from the firehose and messages loaded with fetchMessages both carry it, so you rarely have to think about this.
If createdAt is missing, the call rejects with a plain Error before it ever reaches the server. On a DM message that is the only gate for edit(), editCard(), and delete(): a DM that arrived through the legacy notify signal alone (a backend below wire version 3) carries no timestamp and rejects with a message saying so. pin() and unpin() reject on a DM even when createdAt is present.

Handle a denied action

All four methods reject with a CloakActionError when the server refuses. Catch it, read .message, and branch on .code when you need the specific reason.
Confirming a pin needs a Cloak server new enough to send the bot a success acknowledgement. Against an older backend the call cannot confirm and times out even though the pin landed. See What the server must support. See Handling errors for the full pattern.

React to changes

Three events report changes to existing messages. messageUpdate and messageDelete fire for the bot’s own edits and deletes too, so filter where you only care about other members.
{ messageId, content, serverId, channelId, card? }
content is the decrypted new text, best-effort. When the key is missing it is an empty string, never raw ciphertext. card is present only when the edit came through editCard() and replaced the card. serverId is null for a DM edit, with channelId the dm id. See messageUpdate.
{ messageId, serverId, channelId }
Fires when a message is removed. serverId is null for a DM.
{ messageId, pinned, serverId, channelId }
pinned is a boolean: true when the message was pinned, false when it was unpinned. The acknowledgement the server sends back for the bot’s own pin() call is a separate reply and does not fire this event.
Because messageUpdate and messageDelete fire for the bot’s own changes as well, remember the message ids the bot just acted on and skip them if you only want other members’ changes.
On an older Cloak server, serverId and channelId on these events can fall back to the current selection. An up-to-date Cloak server always reports the message’s own location.

Next

Reactions

The other message action, under the same DM rule.

Message history

Load past messages with fetchMessages so you have one to act on.

Moderation

Kicks, bans, and timeouts alongside message removal.

Handling errors

What CloakActionError carries and how to catch it.