Client is an event emitter. You subscribe with client.on(event, listener). The event map is fully typed, so your listener’s arguments are inferred.
Overview
On an older Cloak server, the
serverId and channelId on the message, reaction, pin, and lifecycle events can fall back to the bot’s currently selected server or channel. An up-to-date server carries them on every event.Messages
messageCreate
Fires for every message your bot can see. Because delivery is a firehose, that covers every visible text channel across every server, the threads under those channels, and the posts inside every visible forum channel. It also fires for direct messages. A DM arrives on this same event withisDM: true and a null serverId, and its channelId holds the dm id. On a backend that honors wire version 3, your bot’s own DM sends echo back here too, with authorId === client.user.id, and a DM message carries createdAt, repliedTo, and card. See Direct messages.
Forum posts arrive here too. A message inside a forum post has msg.postId set, msg.channelId equal to that post id, and msg.forumChannelId carrying the parent forum. The post’s first message never rides the firehose on its own: the SDK emits it from the post’s create event as a messageCreate with starter: true, once, right before the matching forumPostUpdate. See Forums.
msg.reply() is a true reply. It renders a quote header and fires a “replied to you” notification that pierces mutes. For a plain answer, like a ping bot, use msg.channel.send(). See Mentions and replies.systemMessage
Fires for system events rather than user messages. Thetype field is member_join, bot_add, or unknown. See SystemMessageEvent.
messageUpdate
Fires when a message is edited.content is the decrypted new text, best-effort. It is '' when the key is missing, never raw ciphertext.
card when the edit came through editCard(). The three states mean different things:
serverId is null for a DM-lane edit, with channelId the dm id. The same is true of messageDelete and reactionUpdate. On a backend that honors wire version 3, a DM card that arrives after the message itself was already emitted also surfaces here, as a messageUpdate carrying card, so it is not lost.
messageDelete
Fires when a message is deleted.sendRejected
Fires when the server denies asend(). For a plain send this is the only way a denial becomes visible: the promise already resolved when the frame went out. A send made with ack: true also rejects with a CloakActionError when the denial is pinned to it.
{ code, message, permission?, serverId, channelId, attribution, frame }.
'exact' | 'ambiguous' | 'none'
How
serverId and channelId were chosen. Every send carries a nonce, and a plan-136 backend echoes it on the deny, which names the send exactly no matter how many are outstanding. Without the echo the SDK falls back to its queue of sends still awaiting their firehose echo. 'exact': one send was outstanding, it supplied the route, and it was retired. 'ambiguous': several were, the oldest supplied the route as a hint, and nothing was retired. 'none': nothing was outstanding, and both ids are null. A bot that awaits each ack: true send before the next one always sees 'exact'.sendRejected does not also fire error. A backend denial is an expected outcome, not a plumbing failure. If you want one funnel, forward it yourself.
Reactions and pins
reactionUpdate
Fires when a reaction is added or removed on a visible message.count is the new total for that reaction. The wire does not say whether it was an add or a remove, so operation is inferred by comparing count with previousCount, the last total this session saw for the same message and reaction; it reads 'unknown' when there is none (the first time a reaction is seen after a start). userId is the reactor, so filter with userId === client.user?.id to ignore your bot’s own reactions. See Reactions.
{ messageId, reactionId, count, userId, serverId, channelId, operation, previousCount, containerKind, parentChannelId }. On a plan-136 backend operation is authoritative (the hook carries an add/remove flag) and containerKind / parentChannelId name the thread or post the message lives in; on older backends operation is inferred and the two container fields are null.
pinUpdate
Fires when a message is pinned or unpinned.pinned is a boolean.
Polls and threads
New in 0.3.0. See Polls and Threads.pollUpdate
Fires when a visible, non-hidden poll’s tally changes, and when a poll is closed and its final numbers broadcast.counts is null for a hidden poll that has not revealed yet, and counts are always absolute server totals, never deltas.
threadUpdate
Fires with a decoded thread row (title decrypted best-effort) on thread creation, activity, flag changes, and membership moves.threadDelete
Fires when a thread is deleted. For a private thread, being removed from its member list also arrives as athreadDelete, because the thread stops existing from the bot’s point of view. The payload is { serverId, parentChannelId, threadId }.
Forums
New in 0.4.0. See Forums.forumPostUpdate
Fires with a decodedForumPost when a post is created, when a reply bumps its activity, when its archived, locked, pinned, or answered state flips, or when its tags are edited. It reaches every member who can view the parent forum, your bot included, and it fires for your bot’s own posts too.
The title is decrypted best-effort and is '' when the key is missing. starterContent is the decrypted first message when the row carried it (the create event and fetchForumPosts() pages do; a later activity bump does not). following is the bot’s own follow state where the row said, and false otherwise.
messageCreate with starter: true, then this event fires. A redelivered create event never emits the starter twice.
forumPostDelete
Payload{ serverId, forumChannelId, postId }. Fires when a post is deleted.
Interactions
New in 0.4.0. See Components and Modals.interaction
Fires when someone clicks a button, picks from a select, or submits a modal on a card your bot posted. The payload is anInteraction. It is addressed to your bot alone: it never rides the firehose, and a click on another bot’s card in the same channel never reaches your handler.
ack(), showModal(), or ephemeral(). The clicker’s control shows a pending state for up to 3 seconds while it waits. A second call rejects locally with a CloakClientError whose source is 'interaction', and nothing goes on the wire. An interaction the SDK cannot decrypt (no key for that server and epoch, or for that DM peer) emits error and is dropped.
This event never fires for your bot’s own actions. Bots do not click, and the server refuses a click from a bot session before reading it.
Typing
typing
Fires when someone starts or stops typing. A firehose across every channel your bot can see, exactly likemessageCreate.
serverId (null in a DM) and channelId (the thread or post id when typing inside one) are set, and containerKind / parentChannelId name the container. On an older backend all four are null.
The event is raw and un-debounced by design, with no aggregation and no expiry timer. A bot that wants “who is typing right now” should hold its own map and expire an entry about ten seconds after its last true, which is what the shipped Cloak clients do. The outbound side is not limited the same way: sendTyping() targets threads and posts, and sendTypingDM() targets a DM. See Typing, presence, and profile.
Channels, groups, and servers
These fire as channels, groups, and the server itself change, and they fire for your bot’s own mutations too. See Channels and groups.channelCreate
Payload{ channelId, name, groupId, type, serverId }.
channelUpdate
Payload{ channelId, name, serverId }. Fires when a channel is renamed.
channelDelete
Payload{ channelId, serverId }.
groupCreate
Payload{ groupId, name, serverId }.
groupUpdate
Payload{ groupId, name, serverId }. Fires when a group is renamed.
groupDelete
Payload{ groupId, serverId }.
serverUpdate
Payload{ serverId, name }. Fires when the server is renamed by its owner. There is no method to rename a server, so this is how you learn about it.
serverDelete
Payload{ serverId }. Fires when the server is deleted by its owner.
Lifecycle
ready
Fires once, afterlogin() finishes. Permission grants are already populated, so can() works inside this handler.
guildCreate
Fires when your bot is added to a server, and once per existing server during login. Payload is aGuild.
guildDelete
Fires when your bot is removed from a server. The SDK drops that server’s cached conversation key, its voice key, and its permission state.permissionsUpdate
Fires when a server resolves or changes your bot’s grant. The payload is theserverId plus the full PermissionSet, spread flat.
Commands
commandError
Fires when a slash command could not be run.reason: 'parse' means the readable text matched a registered command but did not fit its declaration, for example a missing required option or an uncoercible number. The handler is not invoked, and the SDK does not auto-reply, because it never speaks unprompted.
reason: 'handler' means your own handler threw or rejected.
An unregistered command emits nothing at all. See Slash commands.
Voice
voiceKeyUpdate
Fires when the bot’s voice key for a server is provisioned again: a new key version, a new scope, or both.voice_listen can now hear everyone, and one that lost it cannot. Never poll for this. See Voice keys.
voiceJoin
Fires when someone joins a voice channel, including your own bot. Filter withp.userId === client.user?.id.
The payload is the live roster entry, so later updates mutate it in place. Copy it if you need a snapshot.
voiceLeave
Payload{ channelId, userId }. Fires for your bot’s own leave too.
voiceStateUpdate
Payload{ channelId, userId, state, value }. Fires when a member’s mute, deafen, webcam, or stream flag changes. state is a VoiceStateFlag.
The
VoiceStateFlag union still lists 'p2pWebcam' and 'p2pStream'. Peer-to-peer video is deleted from the Cloak platform, so those two values can no longer arrive. Ignore them.voiceConnectionState
Payload{ channelId, userId, reconnecting }. Fires when a member’s voice session enters or leaves the backend’s 60 second reconnect grace window.
voiceDisconnect
Payload{ channelId, reason: 'moved' | 'forced', toChannelId? }. Fires when a moderator moves or disconnects your bot. toChannelId is set only for a move.
voiceConnection() is already cleared when this fires, and the SDK does not auto-rejoin. Rejoin yourself if that is what you want.
Connection
These let you observe the automatic reconnect. You do not usually need to act on them. See Connection lifecycle.disconnect
reconnecting
reconnected
draining
Payload{ delayMs }. Fires when the server lane your connection is on is being drained for a deploy. The client reconnects on its own after delayMs, so you do not need to act on it.
Diagnostics
error
Fires when the SDK catches a failure that would otherwise have been silent: one of your handlers threw or returned a rejecting promise, a key pull failed, or an inbound frame could not be dispatched. The payload is aCloakClientError.
Server denials do not come through here. An awaited action rejects with
CloakActionError, and a denied send fires sendRejected.raw
Every inbound frame, verbatim, including the ones the SDK consumes itself. A frame is a positional array, typed asunknown[].
client.raw.
Next
Types
The shape of each event payload.
Client
The methods you call in response.
Errors
Every error class, and what reaches which channel.
Direct messages
Why messageCreate needs a guard.