Skip to main content
Your bot encrypts and decrypts inside its own process. The Cloak service relays ciphertext for message content and never holds an unwrapped key. That is true of message bodies. It is not true of everything a bot can send. A few lanes are plaintext to the server on purpose, because the server has to route or render something without a key. This page is the map, so you can pick a lane knowing what it costs.
Never tell your users that “nobody can read anything the bot sends”. Mention targets, your published slash-command menu, and the entire webhook lane are readable server-side by design.

The short version

What is encrypted

Message bodies

send(), msg.reply(), msg.channel.send(), and guild().send() all encrypt the text with the conversation key for that server before a frame leaves your process. The content is AES-256-GCM. The server stores ciphertext it cannot read.

Card embeds

sendCard() is the encrypted embed lane. The title, description, fields, and colors are encrypted with the same conversation key and the same epoch as the message body, and ride the same send. The card posts under your bot’s own member identity with the BOT badge.
One constraint follows from the encryption: the four slots a card fetches on its own (image.url, thumbnail.url, author.icon_url, footer.icon_url) must have origin exactly https://media.cloak.chat. The server cannot rewrite a URL it cannot read, so the SDK refuses a third-party host locally instead. See Rich cards.

Direct messages

A 1:1 DM between your bot and a human is encrypted the same way, under a key exchanged once per peer. Inbound DMs arrive on the ordinary messageCreate event. See Direct messages.

Voice media frames

Audio frames are encrypted with a voice key that is a separate slot from conversation keys. Your bot never derives that key: a human member seals one to the bot’s identity and the SDK fetches it. What the bot can decrypt depends on which key it was given, which depends on the voice_listen permission. See Voice keys and scopes.

What is not encrypted

Mention targets

A mention is two independent halves, and you supply both. The ping array rides the send frame in plaintext. Cloak’s server has to fan out notifications without decrypting anything, so it needs to see who a message pings. Anyone with server-side visibility can see who your bot mentioned, but not what it said.
If a given message must not reveal who it addresses, do not mention. Send the text without an opts.mentions array and the ping never happens.

The slash-command menu

login() publishes whatever commands are registered at that moment so a human’s / picker can list them. That menu is stored and served in plaintext, because the picker has to render it before any key exchange has happened.
Command names, descriptions, option names, and choice labels are readable server-side and are served to every member of every server your bot is in. Never put a secret, an internal hostname, or a customer name in a description.
Invocations are a separate matter. A slash-command invocation is an ordinary encrypted message, so the server cannot read it. It is also durable and not ephemeral: every member holding that epoch key can read every invocation of your bot’s commands. There is no “only you can see this” in v1, and deleting the message afterwards does not fake one. See Slash commands.

The webhook lane

client.sendEmbed() and client.webhooks.post() are not end-to-end encrypted at all. The payload type is called UnencryptedWebhookPost and requires a literal unencrypted: true, which is re-checked at runtime so a plain-JavaScript caller cannot skip it. Three things are true of every webhook post:
  • The message is written to the database as readable text, by design. Nothing about that is recoverable after the fact.
  • It is attributed to a webhook persona, not to your bot. Shipped Cloak clients render it with a webhook badge and a “not encrypted” notice.
  • Your bot never sees it come back. Webhook messages arrive as systemMessage with type: 'unknown', never as messageCreate.
If the content is sensitive, use sendCard() or send().

The REST lane

client.rest is ordinary HTTPS against Cloak’s REST API. The request path, the query string, and the headers are visible to the server, and ids ride query strings. The credential is short-lived and scoped, minted over the already-authenticated realtime session, so the long-lived bot token never rides an HTTP header. The lane is disabled server-side by default. See The REST lane.

Routing metadata

The server stores and relays the metadata it needs to deliver a message: who sent it, which channel it belongs to, when it was sent, and which message a reply points at. Reaction and pin activity is server-visible the same way. In voice, a participant in a channel learns who speaks and for how long even while holding no usable key. End-to-end encryption protects content. It does not hide that a conversation happened.

Choosing an embed lane

Two paths render a rich embed, with opposite properties. Default to sendCard(). Reach for the webhook lane only when you need one of the three things it alone does: many personas from one identity, posting with no bot account at all, or reaching clients older than the card rollout. The two are hard to confuse in either direction. sendEmbed() requires unencrypted: true, and CardMessage declares unencrypted, username, and avatar_url as never, so a webhook payload is a compile error on sendCard().

Guarding server-scoped work

Every sample that touches a server id has to survive a DM, where msg.serverId is null. Guard first, then act.

Next

Keys and the keystore

Where the keys that do the encrypting live, and how to keep them.

Choosing an embed lane

The full comparison, with runnable samples on both sides.

Mentions and replies

The two halves of a mention, and the plaintext one.

How bots work

The encryption model your bot is a member of.