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.
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 ordinarymessageCreate 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 thevoice_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.
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.
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
systemMessagewithtype: 'unknown', never asmessageCreate.
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 tosendCard().
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, wheremsg.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.