Skip to main content
The 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.
Most events fire for your bot’s own actions too. When your bot deletes a message, your own messageDelete handler runs. The same is true of messageCreate, reactionUpdate, pinUpdate, the channel and group lifecycle events, typing (your own sendTyping() echoes back), and voiceJoin / voiceLeave.Filter with the actor id:

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 all visible channels across all servers. It also fires for direct messages. A DM arrives on this same event with isDM: true and a null serverId, and its channelId holds the dm id.
Guard before anything server-scoped. Passing a null msg.serverId into client.can(), client.guild(), or client.send() is the single most common bug in Cloak bots. See Direct messages.
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. The type 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.

messageDelete

Fires when a message is deleted.

sendRejected

Fires when the server denies a send(). This is the only way a denied send becomes visible: send() is fire-and-forget, so its promise already resolved when the frame went out.
The deny frame carries no routing, so serverId and channelId are attributed best-effort from the most recent send inside a 30 second window and are null otherwise. Treat them as a hint, not a fact.This is also not an acknowledgement channel. Silence does not mean delivered.
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 event does not say whether it was an add or a remove. userId is the reactor, so filter with userId === client.user?.id to ignore your bot’s own reactions. See Reactions.

pinUpdate

Fires when a message is pinned or unpinned. pinned is a boolean.

Typing

typing

Fires when someone starts or stops typing. A firehose across every channel your bot can see, exactly like messageCreate.
serverId and channelId on this event are always null. That is a wire fact, not a decode gap: the relay frame carries no routing tail, so there is nothing to read. They are typed nullable so filling them in later stays additive.Never substitute your bot’s own selection cursor for them. It points wherever the bot last sent, which has nothing to do with who is typing.
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. 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, after login() 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 a Guild.

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 the serverId 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

voiceJoin, voiceLeave, voiceStateUpdate, and voiceConnectionState are cursor-gated, not firehose. Your bot receives them only for the server it currently has selected or is in voice in. Do not build on them as a firehose. The same applies to the roster snapshot behind voiceRoster().

voiceKeyUpdate

Fires when the bot’s voice key for a server is provisioned again: a new key version, a new scope, or both.
A live media session must re-pin on either change. A scope change matters as much as a version change: a bot that just gained 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 with p.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

Two login failures are terminal and stop the reconnect loop, and both arrive here rather than throwing: code -2 (the token was revoked or regenerated) and code -12 (this SDK is below the backend’s minimum wire version). If your bot goes quiet and no reconnecting follows a disconnect, that is why. Code -8 is transient and keeps retrying.

reconnecting

reconnected

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 a CloakClientError.
Registering this listener changes nothing about control flow. It only makes failures visible. Dispatch continues either way, and a throwing handler never stops the next frame.
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 as unknown[].
This is a firehose and it is emitted only when something is listening, so it costs nothing if you never subscribe. It is the read side of 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.