> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cloak.chat/llms.txt
> Use this file to discover all available pages before exploring further.

# Events

> All 32 events a Client emits, when they fire, and what they carry.

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.

```ts theme={null}
client.on('messageCreate', (msg) => {
  // msg is typed as Message
});
```

<Warning>
  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:

  ```ts theme={null}
  client.on('typing', (e) => {
    if (e.userId === client.user?.id) return; // our own indicator
  });
  ```
</Warning>

## Overview

| Event                  | Payload                                                         | Fires when                                                           |
| ---------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------- |
| `ready`                | none                                                            | Login has completed and the bot is online.                           |
| `error`                | [`CloakClientError`](/api-reference/errors#cloakclienterror)    | The SDK caught a failure that would otherwise be silent.             |
| `messageCreate`        | [`Message`](/api-reference/types#message)                       | A message arrives, from a channel **or a DM**.                       |
| `systemMessage`        | [`SystemMessageEvent`](/api-reference/types#systemmessageevent) | A system event happens, such as a member joining.                    |
| `messageUpdate`        | `{ messageId, content, serverId, channelId }`                   | A message was edited.                                                |
| `messageDelete`        | `{ messageId, serverId, channelId }`                            | A message was deleted.                                               |
| `reactionUpdate`       | `{ messageId, reactionId, count, userId, serverId, channelId }` | A reaction was added or removed.                                     |
| `pinUpdate`            | `{ messageId, pinned, serverId, channelId }`                    | A message was pinned or unpinned.                                    |
| `typing`               | `{ userId, isTyping, serverId, channelId }`                     | Someone started or stopped typing.                                   |
| `sendRejected`         | `{ code, message, permission?, serverId, channelId, frame }`    | The server denied a `send()`.                                        |
| `channelCreate`        | `{ channelId, name, groupId, type, serverId }`                  | A channel was created.                                               |
| `channelUpdate`        | `{ channelId, name, serverId }`                                 | A channel was renamed.                                               |
| `channelDelete`        | `{ channelId, serverId }`                                       | A channel was deleted.                                               |
| `groupCreate`          | `{ groupId, name, serverId }`                                   | A group was created.                                                 |
| `groupUpdate`          | `{ groupId, name, serverId }`                                   | A group was renamed.                                                 |
| `groupDelete`          | `{ groupId, serverId }`                                         | A group was deleted.                                                 |
| `serverUpdate`         | `{ serverId, name }`                                            | The server was renamed by its owner.                                 |
| `serverDelete`         | `{ serverId }`                                                  | The server was deleted by its owner.                                 |
| `guildCreate`          | [`Guild`](/api-reference/types#guild)                           | The bot is added to a server, or is already in one at login.         |
| `guildDelete`          | [`Guild`](/api-reference/types#guild)                           | The bot is removed from a server.                                    |
| `permissionsUpdate`    | `{ serverId } & PermissionSet`                                  | A server resolves or changes the bot's grant.                        |
| `commandError`         | `{ name, error, message, reason }`                              | A slash command could not be run.                                    |
| `voiceKeyUpdate`       | `{ serverId, version, scope }`                                  | The bot's voice key was provisioned again.                           |
| `voiceJoin`            | [`VoiceParticipant`](/api-reference/types#voiceparticipant)     | Someone joined a voice channel.                                      |
| `voiceLeave`           | `{ channelId, userId }`                                         | Someone left a voice channel.                                        |
| `voiceStateUpdate`     | `{ channelId, userId, state, value }`                           | A member's mute, deafen, webcam, or stream flag changed.             |
| `voiceConnectionState` | `{ channelId, userId, reconnecting }`                           | A member's voice session entered or left the reconnect grace window. |
| `voiceDisconnect`      | `{ channelId, reason, toChannelId? }`                           | A moderator moved or disconnected **this bot**.                      |
| `disconnect`           | `unknown`                                                       | The connection drops.                                                |
| `reconnecting`         | `{ attempt, delayMs }`                                          | A reconnect attempt is scheduled.                                    |
| `reconnected`          | none                                                            | A reconnect succeeds.                                                |
| `raw`                  | `unknown[]`                                                     | Any frame arrives, verbatim.                                         |

<Note>
  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.
</Note>

## Messages

### messageCreate

Fires for every message your bot can see. Because delivery is a [firehose](/concepts/message-delivery), 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.

```ts theme={null}
client.on('messageCreate', async (msg) => {
  if (msg.authorId === client.user?.id) return; // ignore our own messages

  if (msg.isDM || !msg.serverId) {
    await msg.channel.send('thanks, I got that');
    return;
  }

  // server-scoped work is safe from here on
  if (!client.can('message_send', msg.serverId)) return;
  if (msg.content.trim() === '!ping') await msg.channel.send('pong');
});
```

<Warning>
  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](/guides/direct-messages).
</Warning>

<Note>
  `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](/guides/mentions-and-replies).
</Note>

### systemMessage

Fires for system events rather than user messages. The `type` field is `member_join`, `bot_add`, or `unknown`. See [SystemMessageEvent](/api-reference/types#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.

```ts theme={null}
client.on('messageUpdate', ({ messageId, content }) => {
  console.log(`message ${messageId} is now: ${content}`);
});
```

### messageDelete

Fires when a message is deleted.

```ts theme={null}
client.on('messageDelete', ({ messageId, channelId }) => {
  console.log(`message ${messageId} deleted in ${channelId}`);
});
```

### 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.

```ts theme={null}
client.on('sendRejected', (r) => {
  console.warn(`send denied (${r.code}): ${r.message}`);
  if (r.permission) console.warn(`missing ${r.permission}`);
});
```

<Warning>
  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.
</Warning>

`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](/guides/reactions).

```ts theme={null}
client.on('reactionUpdate', ({ messageId, reactionId, count }) => {
  console.log(`${reactionId} on ${messageId} is now ${count}`);
});
```

### pinUpdate

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

```ts theme={null}
client.on('pinUpdate', ({ messageId, pinned }) => {
  console.log(`message ${messageId} ${pinned ? 'pinned' : 'unpinned'}`);
});
```

## Typing

### typing

Fires when someone starts or stops typing. A firehose across every channel your bot can see, exactly like `messageCreate`.

```ts theme={null}
client.on('typing', ({ userId, isTyping }) => {
  if (userId === client.user?.id) return; // our own sendTyping() echoes back
  console.log(`${userId} is ${isTyping ? 'typing' : 'done'}`);
});
```

<Warning>
  `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.
</Warning>

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](/guides/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](/guides/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()`](/api-reference/client#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`](/api-reference/types#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](/api-reference/permissions#permissionset), spread flat.

```ts theme={null}
client.on('permissionsUpdate', ({ serverId, effectiveRank, ...perms }) => {
  console.log(`send in ${serverId}: ${perms.message_send}, rank ${effectiveRank}`);
});
```

## Commands

### commandError

Fires when a slash command could not be run.

```ts theme={null}
client.on('commandError', ({ name, reason, error }) => {
  console.error(`/${name} failed (${reason})`, error);
});
```

`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](/api-reference/commands).

## Voice

<Warning>
  `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()`](/api-reference/client#voiceroster).
</Warning>

### voiceKeyUpdate

Fires when the bot's voice key for a server is provisioned again: a new key version, a new scope, or both.

```ts theme={null}
client.on('voiceKeyUpdate', ({ serverId, version, scope }) => {
  console.log(`voice key for ${serverId} is now v${version}, scope ${scope}`);
});
```

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](/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`.

<Note>
  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.
</Note>

### 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()`](/api-reference/client#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](/concepts/connection-lifecycle).

### disconnect

```ts theme={null}
client.on('disconnect', (err) => console.warn('connection dropped', err));
```

<Warning>
  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.
</Warning>

### reconnecting

```ts theme={null}
client.on('reconnecting', ({ attempt, delayMs }) =>
  console.log(`reconnecting (attempt ${attempt}) in ${delayMs}ms`),
);
```

### reconnected

```ts theme={null}
client.on('reconnected', () => console.log('back online'));
```

## 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`](/api-reference/errors#cloakclienterror).

```ts theme={null}
import type { CloakClientError } from '@cloak-software/bot-sdk';

client.on('error', (e: CloakClientError) => {
  console.error(e.source, e.context, e.message);
  console.error(e.cause); // the original failure
});
```

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.

<Note>
  Server denials do not come through here. An awaited action rejects with [`CloakActionError`](/api-reference/errors#cloakactionerror), and a denied send fires [`sendRejected`](#sendrejected).
</Note>

### raw

Every inbound frame, verbatim, including the ones the SDK consumes itself. A frame is a positional array, typed as `unknown[]`.

```ts theme={null}
client.on('raw', (frame) => {
  console.log('opcode', frame[0]);
});
```

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`](/guides/raw-frames).

## Next

<CardGroup cols={2}>
  <Card title="Types" icon="brackets-curly" href="/api-reference/types">
    The shape of each event payload.
  </Card>

  <Card title="Client" icon="book" href="/api-reference/client">
    The methods you call in response.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/api-reference/errors">
    Every error class, and what reaches which channel.
  </Card>

  <Card title="Direct messages" icon="envelope" href="/guides/direct-messages">
    Why messageCreate needs a guard.
  </Card>
</CardGroup>
