Skip to main content
Everything starts with a Client. You construct it with your token, attach event listeners, and call login().
Server-scoped methods take a serverId as their first argument. If a handler does several things in one server, guild(serverId) binds that id once so you can drop it from every call.
Message.serverId is string | null. It is null for a direct message. Guard with if (msg.isDM || !msg.serverId) return; before anything server-scoped, or your bot breaks on the first DM it receives. See Direct messages.

Constructor options

Identity and storage

string
required
Your bot token, in the form botid.tokenid.secret. Create it in the Cloak app under Settings > My Bots. Treat it like a password. Read it from an environment variable, never hardcode it.
string
Path to a file where the SDK persists your bot’s identity, its conversation keys, and its peer pins. Shorthand for keystore: fileKeystore(path). Mutually exclusive with keystore: passing both throws.A bot with no keystore of any kind is single-run only. Its second start fails permanently, because identity publication is write-once on the server. See Keys and the keystore.
KeystoreAdapter
Where to persist identity and keys when a file will not do. Use envKeystore() on a host with no writable volume, or supply your own adapter. Mutually exclusive with keystorePath. See Keystore backends.
string
A stable device id used for clean takeover on reconnect. Defaults to a value derived from your token. Set it only if you have a specific reason to.

Behavior

PermissionName[]
The permissions your bot declares it needs, sent to the server on login. Declaring is a request, not a grant. A human approves a subset per server, and the resolved grant arrives on permissionsUpdate. See Permissions.
string
default:"/"
The prefix the text lane of the slash-command parser matches. Only the text lane uses it. A structured invocation carries the command name outright and is prefix-independent. See Slash commands.
string
default:"Bot"
Display name for the webhook that sendEmbed() mints. Cosmetic only. Reuse is matched on creator plus channel, never on name, so renaming this does not orphan an existing webhook.
WebhookFetch
An injected fetch for the webhook ingress, for tests or a proxy. Defaults to globalThis.fetch. This is the only outbound HTTP the SDK performs on its own.
boolean
default:"false"
Logs every frame the SDK sends and receives, with your bot token redacted. Useful while developing, noisy in production.

Endpoints and transport

Bots against the hosted Cloak service rarely set any of these.
string
The connection endpoint. Defaults to the hosted service on the lane implied by transport: DEFAULT_URL for 'ws', DEFAULT_WEBTRANSPORT_URL for 'webtransport'. A wss:// url speaks the WebSocket lane, an https:// url the WebTransport lane.
'ws' | 'webtransport'
default:"ws"
Which wire to speak. The default WebSocket lane rides the pure-JavaScript ws package, so npm install compiles nothing. 'webtransport' needs the optional Quiche packages installed. See Installing.
boolean
default:"false"
Permit a plain ws:// url, which sends the bot token in cleartext. Local development stacks only.
number
default:"60000"
Treat the connection as dead after this long with no inbound data, then reconnect. Defaults to 60000 on the WebSocket lane and is disabled on WebTransport, which has its own QUIC idle timeout. 0 disables it.
{ algorithm: 'sha-256'; value: Uint8Array }[]
WebTransport lane only: a self-signed QUIC certificate pin for a self-hosted or local stack. Throws on the WebSocket lane. Not needed for the hosted service.
string
The REST base that client.rest talks to. Defaults to DEFAULT_REST_URL. HTTPS only, except loopback. A bad value throws on the first client.rest call.
string
The LiveKit signalling url handed back on VoiceConnection.url. Defaults to livekitUrlFor(<transport url>), which is the same host on port 7880. The SDK never connects to it. See Joining voice.

Properties

BotUser | null
Your bot’s identity after login, or null before login() completes. Its id is normalized, so you can compare it against msg.authorId to detect your bot’s own messages. See BotUser.
CommandRegistry
The bot’s slash-command registry. register(decl, handler) is synchronous.Register before login(). Whatever is registered at that moment is published as the bot’s menu. A command added afterwards still dispatches locally, but it does not appear in the published menu until the next login, and the SDK never re-publishes on its own. See Slash commands.
WebhookApi
The incoming-webhook surface: list, create, edit, regenerate, delete, and post. The five CRUD calls require manage_server. post(url, payload) requires nothing at all and is the recommended path. See Webhooks.
Everything posted through this lane is not end-to-end encrypted and renders under a webhook identity rather than your bot’s.
RestApi
The authenticated REST lane: request, json, and binary. Every call transparently mints a short-lived credential over the realtime session, so your long-lived bot token never rides an HTTP header.This is off by default server-side, so the first call usually rejects with a CloakActionError carrying code -7. See REST.
RawApi
The escape hatch to opcodes the SDK does not map: send(frame, opts?) and request(frame, replyOpcode, opts?). Raw frames run on the same serialized chain as every mapped action.request() refuses reply opcodes the SDK correlates internally unless you pass { allowReserved: true }, because awaiting one can claim a reply the SDK was waiting for. See Raw frames.
The voice-key lane, and nothing else: getVoiceKey, acquireVoiceKey, voiceFrameKeyFor, and forgetVoiceKey. Hand this to a media session so it can install frame keys without reaching your conversation keys. See Voice keys.This accessor’s interface type is intentionally not exported, so annotate with typeof client.voiceKeys if you need a name for it.

Lifecycle

login

Checks the Node runtime, loads the keystore, connects, authenticates, publishes your bot’s identity if it has never been published, declares the permission manifest, publishes the command menu, emits guildCreate for existing servers, then emits ready. Call it once, after attaching your listeners and registering your commands.
Two login failures are terminal and stop the automatic reconnect loop: -2 (the token was revoked or regenerated) and -12 (this SDK is below the backend’s minimum wire version). Both emit disconnect rather than throwing from a call you can await. -8 is transient and keeps retrying. See Connection lifecycle.

watch

Optionally pre-selects a channel, for example to warm its key before your first send. You rarely need it, because the firehose already delivers every visible channel. groupId is auto-resolved from the server’s channel list when omitted. The target is remembered and re-established after a reconnect.

destroy

Closes the connection, stops reconnecting, and flushes any queued keystore writes. The clean way to shut a bot down. See Connection lifecycle.

Sending

send

Sends an encrypted message to a channel. Sends are serialized internally so they never race each other. opts.groupId is auto-resolved for any channel the bot has already seen, so you usually omit it.
SendOptions is { groupId?, mentions?, replyTo? }.
The fourth argument is an options object. It was a positional groupId before 0.2.0, and that form is gone. send(s, c, text, groupId) becomes send(s, c, text, { groupId }).
send() is fire-and-forget. The wire has no success acknowledgement for it, so the promise resolves once the frame is out. A server-side denial does not reject the promise: it arrives later on the sendRejected event. Silence does not mean delivered.
The one local throw is a plain Error when your bot does not hold the server’s conversation key yet, which happens in the moments right after a first join. Most handlers use msg.reply() or msg.channel.send() instead, which already know where the message came from. See Sending messages.

sendDM

Sends an encrypted direct message, creating the DM and exchanging its conversation key on first contact. Both the dm id and the key are persisted in the keystore, so that cost is paid once per person. Eligibility is the server’s decision and it is deny-by-default: a human may hold a DM with a bot only if they own it, or they share at least one server with it. An ineligible target rejects with a CloakActionError before anything is encrypted. Inside a DM, replyTo works, and role, @everyone, and @here mention pairs are dropped server-side. See Direct messages.

sendCard

Posts a rich card. This is the encrypted embed lane and the one to reach for by default: the card is encrypted with the same conversation key and epoch as the message body, and it renders under your bot’s own identity. It requires message_send and nothing else. card.content is required and must be non-blank. Card media must be Cloak-hosted, and anything else throws a CloakEmbedError locally before a frame leaves your process. See Rich cards and Embeds.

sendEmbed

Posts a rich embed by minting or reusing a webhook for the channel. Requires manage_server.
This lane is not end-to-end encrypted. The message is stored as plaintext server-side and renders under a webhook identity, not your bot’s. Your bot does not receive its own post back on messageCreate. Prefer sendCard() unless you need per-post personas, no bot account at all, or clients older than the card rollout. See Webhook embeds.

sendCommand

Invokes a slash command, your bot’s own or another bot’s. opts.botId names the target bot and has no default. Targeting a foreign bot also requires opts.decl, because the SDK cannot know another bot’s option order. See Slash commands.

sendTyping

Shows or clears the typing indicator in a channel. isTyping defaults to true. The SDK never sends this implicitly, not even around send(). Receivers clear the indicator themselves about ten seconds after the last true, so re-send it every few seconds during long work. A repeat of the same boolean for the same channel inside about a second is dropped without touching the wire. It moves the server-side selection cursor exactly like a send does, and it is fire-and-forget. See Typing, presence, and profile.

Presence and profile

setStatus

Sets the bot’s presence. BotStatus is 'invisible' | 'online' | 'away' | 'busy' | 'dnd'. dnd also suppresses push notifications on every device. This writes an account-level status to a database column, so it survives reconnects, restarts, and redeploys. Set it once on ready, never on a timer. Rejects with a CloakActionError if the server does not acknowledge it.
online means “defer to liveness” rather than “show a green dot”. A bot with no live session displays as offline whatever its stored base says. The other four values pin the display outright.

setAvatar

Sets or clears the bot’s avatar from an already-hosted image url. miniIconUrl defaults to iconUrl. Passing null clears both. Urls are capped at 512 characters server-side. The SDK never uploads images, because file upload is closed to bots.

History and reads

fetchMessages

Reads message history for a channel, returned oldest to newest and auto-paginated. Requires message_read_history. Cursors are a Date or a Message, never a message id, because Cloak message ids are random UUIDs and not time-sortable. around returns a single window and is not auto-paged. Content decrypts per row against the key for that row’s own epoch, and is '' (never raw ciphertext) when the key is missing. Returned messages are fully actionable and carry pinned. See Message history.

messageLocation

Returns where a message the bot has seen lives, or undefined. This is a bounded, best-effort cache. See MessageLocation.

fetchEmojis

Returns the global emoji catalog. It takes no serverId because the catalog is server-agnostic, and it does not move the selection cursor. Its ids are what msg.react() accepts. react() itself stays an opaque pass-through with no client-side validation. See Reactions.

fetchRoles

Reads a server’s roles on demand, with no caching. Returns each role’s color, position, and member count. See Role.

fetchBans

Returns the server’s ban list. Requires member_ban, and some older Cloak servers also require owner or admin standing. Usernames are resolved server-side and may be null. See BanInfo.

fetchMembers

fetchMembers() does not work against any current Cloak backend. Do not call it.It awaits an opcode the backend made DM-only. A server-scoped request produces no reply frame of any kind, not even an error code, so the returned promise neither resolves nor rejects. await hangs until the transport timeout, and try/catch does not save you. This is not flakiness and not a permission problem, and there is no workaround in the SDK.The rebuild is planned and the signature will survive it, so code written against this method stays valid. It just cannot run today. guild().fetchMembers() binds the same broken call. fetchRoles() is unaffected.
Enumerates a server’s members. See Member and Members and roles.

Moderation

All of these are server-scoped and reject with CloakActionError on a server deny. See Moderation.

kick

Removes a member. Requires member_kick, a rank above the target, and the target not being the owner or the bot itself.

ban

Kicks a member and records the ban. Requires member_ban. reason is capped at 256 characters server-side.

unban

Lifts a ban. Requires member_ban. Some older Cloak servers also require owner or admin standing.

timeout

Times a member out for durationSeconds. A timed-out member cannot send or react in that server. Passing 0 lifts the timeout. Requires member_timeout.
timeout() needs an up-to-date Cloak server. An older server drops the opcode, so the call times out rather than resolving.

removeTimeout

Lifts a member’s timeout. Equivalent to timeout() with 0 seconds.

Channels and groups

All of these require manage_channels and reject with CloakActionError on a server deny. See Channels and groups.

createChannel

Creates a channel in a group and returns the new channel’s id. channelType defaults to 0, a text channel.

editChannel

Renames a channel.

deleteChannel

Deletes a channel along with its messages and pins.

moveChannel

Moves a channel to a group and position. newOrder is clamped to the range 0 to 126. Moving to the same group and order is a no-op the server rejects.

createGroup

Creates a channel group. The acknowledgement carries no id, so the new group’s id is captured best-effort from the server’s create broadcast within a short window. Handle the undefined case.

editGroup

Renames a group.

deleteGroup

Deletes a group and its channels.
There are no methods to edit or delete a server. Those actions are owner-level and surface only as the serverUpdate and serverDelete events.

Voice

This is the control plane. It mints a LiveKit credential and stops there. The SDK sends no media of its own. See Voice overview.

joinVoice

Joins a voice channel and returns the VoiceConnection the backend minted, including the LiveKit token and url. Requires voice_connect server-wide plus channel-level view. opts.groupId must be the channel’s real group and must never equal the channel id. That shape is Cloak’s DM-call form, and the SDK refuses it locally rather than round-tripping a rejection. The group is resolved from the server’s channel list when omitted.
voice_connect gets your bot into the channel. Hearing anyone is a separate grant, voice_listen. Without it the bot joins and publishes normally, receives no media, and is never given a key that could decrypt anyone. joinVoice() deliberately does not gate on it, so the failure mode is total silence with no error. See Voice keys.

leaveVoice

Leaves voice. A safe no-op server-side when the bot is not in a channel, so you may call it defensively.

voiceConnection

The bot’s own voice membership, or null. A synchronous cache read.

setVoiceSelfState

Sets the bot’s own mute and deafen flags. Requires a live join. The server never acknowledges this, so local state is updated optimistically. Deafened implies muted, mirroring both shipped Cloak clients.
selfDeafened is a broadcast flag, not an answer to “can I hear”. Whether the bot receives audio is decided by voice_listen and the scope of the voice key it was handed.

voiceRoster

The cached roster of a voice channel, read synchronously. The array is a fresh copy, but its entries are the live cache objects, so copy them if you need a snapshot.

refreshVoiceRoster

Re-seeds the roster for a server by re-selecting it, because the roster snapshot is a side effect of that selection.
Voice roster events are cursor-gated, not firehose. Your bot sees them only for the server it currently has selected or is in voice in. Do not build on them as a firehose.

Permissions

These are advisory. They reflect the last grant the server sent, and the server re-checks every action. See Permissions.

can

Whether your bot holds permission in serverId per the last grant. administrator implies every permission here. Returns false when no grant has arrived yet, so a false means “not permitted, or not known yet”.

permissions

The full resolved capability set for a server, or undefined when no grant has arrived. See PermissionSet.

visibleChannelIds

The channel ids your bot may view in a server. Mirrors the firehose read-gate. Empty array if unknown.

Encryption helpers

You need these only when you are building a frame by hand through client.raw.

encryptFor

Encrypts text for a server’s current epoch key, producing exactly what send() would put on the wire. Throws when the bot holds no key for that server.

decryptFrom

Decrypts a wire ciphertext with the key for its own epoch. Returns '', never raw ciphertext, when the key is missing or stale.

Events

Client extends Node’s EventEmitter, and on, once, off, addListener, prependListener, prependOnceListener, removeListener, and emit all carry typed overloads over ClientEvents. Your listener arguments are inferred.
Listeners are wrapped at registration so a handler that throws surfaces on the error event instead of vanishing. That means client.listeners(event) returns the wrappers rather than your original functions. off() and removeListener() still work normally: pass the same function you registered.

guild

Returns a lightweight handle that binds serverId onto every server-scoped method. Pure sugar over the flat methods, with no cache and no state.
The handle exposes id, plus:
guild().fetchMembers() binds the same broken call as fetchMembers() and hangs the same way.
See The guild() handle.

Exported constants

string
Cloak’s hosted WebSocket endpoint, used when you omit url.
string
Cloak’s hosted WebTransport endpoint, used when you omit url and set transport: 'webtransport'.
string
The default origin for client.rest.
(transportUrl: string) => string
Derives the LiveKit signalling url from a transport url: same host, port 7880. Returns '' for an unparseable url. This is what livekitUrl defaults to.

Next

Events

All 32 events a Client emits.

Types

Message, SendOptions, Member, and the rest.

Errors

Every error class an action can reject with.

The guild() handle

Bind a server id once.