client.sendCard(), the end-to-end encrypted embed lane. For how and when to use it, see rich cards. The plaintext lane has its own webhooks reference.
sendCard
message_send. The card is encrypted with the same conversation key and epoch as card.content and rides the ordinary message frame.
The returned promise rejects with CloakEmbedError when the card is invalid, or CloakComponentError when its rows are, before anything is sent and before the connection cursor moves. It does not reject on a server-side denial: sending is fire-and-forget, and denials arrive on the sendRejected event.
editCard
edit(). CardEdit is Pick<CardMessage, 'content' | 'embed' | 'components'>: the same three fields, without the routing keys, which the edit takes from the message.
opts.createdAt is required because the edit addresses the row by its send time. A received Message carries it, which is why msg.editCard(card) is the convenient form. opts.pinned should mirror the message’s current pin state (false when unknown). threadId and postId are mutually exclusive.
The text and the card are encrypted from one context, so pass the message’s current content to leave the text as it was. Pass components: [] to strip the rows after a decision. The replacement arrives on messageUpdate as card. A server that predates cards ignores the card slot and edits only the text.
Rejects locally with a plain Error when createdAt is missing, with CloakEmbedError or CloakComponentError on an invalid card, and with CloakActionError on a server deny. See Buttons and selects.
CardMessage
The payload passed tosendCard().
string
required
Genuine human-readable text, non-blank. It is what search indexes, what push notifications and channel previews are built from, what a reply quoting this card renders, and what any surface not wired to the card renderer shows. The SDK never synthesizes it.
embedFallbackText() derives one line if you want it.BotEmbed
Exactly one embed. Clients render one card per message. Required unless
components is present: a components-only card is a message with rows under its text and no embed at all.ActionRow[]
Up to 5 rows of buttons and string selects rendered under the card. Their presence is what selects the v2 envelope. Validated by
assertComponents(). Clicks arrive on the interaction event. See Buttons and selects.string
The channel’s group, when it is not already cached from an inbound frame.
string
Post the card into a thread of
channelId, the thread’s parent. Mutually exclusive with postId.string
Post the card into a forum post of
channelId, the parent forum channel. Mutually exclusive with threadId. See Forums.MentionTarget[]
Who to notify. These targets ride the wire in plaintext, exactly as on
send(), because the server routes notification fanout without decrypting anything.ReplyTarget
Make the card a true reply. It renders the quote header and fires a “replied to you” notification. A received
Message satisfies ReplyTarget structurally, so replyTo: msg works.never
Declared as
never on purpose. This lane is end-to-end encrypted, so a webhook-shaped payload is a compile error here. assertCardMessage() re-checks it at runtime and points you at sendEmbed().never
Declared as
never. Per-post personas are a webhook-lane feature. A card posts under the bot’s own identity.never
Declared as
never, for the same reason as username.BotEmbed
Field names are wire names, insnake_case. Do not camelCase them.
'rich'
Stamped by the SDK. Setting it to anything else is rejected.
string
Up to 256 characters.
string
Up to 2048 characters. Plain text, not markdown: v1 renders it literally.
string
Click-through target for the card. A link slot: any
http(s) URL up to 2048 characters, and nothing is fetched for it at render, so the Cloak-hosted rule does not apply.number | string
A decimal integer in
0..0xffffff (for example 0x22c55e) or a #rrggbb string. Exactly six hex digits, no shorthand and no alpha. Anything else is rejected.string
ISO-8601, up to 64 characters. Rendered in the footer.
{ name?: string; url?: string; icon_url?: string }
name up to 256. url is a link slot. icon_url is a media slot and must be Cloak-hosted.{ name?: string; url?: string }
name up to 256. url is a link slot. There is no provider.icon on this lane, and passing one is rejected.{ url: string }
A media slot. Must be Cloak-hosted.
{ url: string }
A media slot. Must be Cloak-hosted.
text up to 256. icon_url is a media slot and must be Cloak-hosted.BotEmbedField[]
Up to 8 rows, in a three-column grid.
inline: false takes a full row. See the warning below about older mobile builds.video is rejected by name.
Components
The rows a card may carry under its embed. Every type here is pure data: no client state, no I/O.ActionRow
Button
'button'
required
ButtonStyle
required
'primary', 'secondary', 'success', 'danger', or 'link'.string
required
1 to 80 characters.
string
1 to 100 characters. Required for every style but
'link', where it must be absent. What the interaction handler switches on. Opaque to everyone but the bot, and readable by every member holding the conversation key.string
'link' style only: an absolute https URL, up to 2048 characters. The client opens it through its trusted-link path and the click never reaches the bot. Nothing is fetched for it at render.string
Exactly one unicode emoji, rendered ahead of the label.
isSingleEmoji() is the check.boolean
StringSelect
'select'
required
string
required
1 to 100 characters.
string
Up to 150 characters.
number
1 to 25, default 1. Never above
maxValues.number
1 to 25, default 1. Never above the option count.
boolean
SelectOption[]
required
1 to 25 options with unique
values.SelectOption
string
required
1 to 100 characters.
string
required
1 to 100 characters, unique within the select. What arrives in
Interaction.values.string
Up to 100 characters.
string
Exactly one unicode emoji.
boolean
ModalDefinition
WhatInteraction.showModal() opens. It travels only on the response lane, never inside a card. See Modals.
string
required
1 to 45 characters.
string
required
1 to 100 characters. Comes back as the modal interaction’s
customId.ModalInput[]
required
1 to 5 inputs with unique
customIds.ModalInput
string
required
1 to 100 characters. The key in
Interaction.fields.string
required
1 to 45 characters.
'short' | 'paragraph'
required
string
Up to 100 characters.
boolean
Default
true.number
0 to 4000, never above
maxLength.number
1 to 4000.
string
Pre-filled text, up to 4000 characters.
EphemeralMessage
WhatInteraction.ephemeral() shows the clicker alone.
content with no embed and no rows is refused with reason: 'empty'. Rows inside it are clickable, and their clicks carry the original message id plus a parentId naming the interaction that produced the reply.
MessageCard
The parsed card a received message carries onMessage.card, and the card field on messageUpdate.
null on the message, never a partial card.
The two envelopes
The plaintext inside the encrypted card slot has two shapes, and the SDK picks between them by whether the card carries rows.
The rule exists so that a component-free card keeps working on every client that ever rendered one. It also means a card with rows degrades to its
content on an older client, with no embed and no buttons, which is one more reason content must be genuine text.
encodeCardEnvelope({ embed?, components? }) makes the pick and is what sendCard(), createForumPost(), and editCard() call. parseCardEnvelope(plain) is the read side: it accepts both shapes and returns a MessageCard or null, never throwing. Both are exported for raw-frame authors.
BotEmbedField
string
required
Non-empty, up to 128 characters.
string
required
Non-empty, up to 512 characters.
boolean
true renders the row side by side. false is equivalent to omitting it: the renderer keeps only a literal true.Media rules
Four slots are fetched with no user action and must have origin exactlyhttps://media.cloak.chat:
This is not configurable. See rich cards for why.
Helpers
embedFallbackText
title, else the first non-blank line of the description, else the embed url, capped at MAX_FALLBACK_TEXT (256). It never concatenates title and description.
Throws CloakEmbedError with reason: 'content' when the embed carries none of the three. An image-only card must be given real text, because blank content is invisible to search, notifications, previews and quoting.
isCloakMediaUrl
true only when raw is a string that parses as a URL, carries no userinfo, and whose origin is exactly CLOAK_MEDIA_ORIGIN. Comparison is exact string equality on the origin, which pins scheme, host and port together.
assertCardMessage
CardMessage type. sendCard() calls it for you. Call it yourself to validate a card built from untrusted input before you queue a send. It throws on the first problem it finds, in a deliberate order: rows (when present), then shape, then content, then unknown keys, then caps, then the media rule, then link schemes, then “nothing renderable”. A components-only card is refused for a bad row, never for a missing embed.
assertComponents
ActionRow[]. Throws CloakComponentError on the first violation of any cap in the Components section. sendCard(), createForumPost(), editCard(), and Interaction.ephemeral() call it for you. Everything it accepts renders: it is a strict refinement of the client parser, which drops a card whole on a violation.
assertModal
ModalDefinition. Interaction.showModal() calls it before anything is encrypted.
parseComponents
assertComponents(): the validated rows, or null on any violation. Never throws.
encodeCardEnvelope
MAX_EMBED_JSON. Validate first; this only serializes and caps.
parseCardEnvelope
MessageCard, or null on any violation. Strict caps on both shapes. Never throws.
encodeEphemeral
Interaction.ephemeral() calls it for you.
encodeBotEmbed
type: 'rich'. Enforces MAX_EMBED_JSON on the UTF-8 byte length and throws reason: 'cap' when over. sendCard() calls it for you. Use it directly only to size a card before sending.
CloakEmbedError
Thrown locally, before anything reaches the wire. It is the only place a bot author can be told the card is wrong: the server cannot read the payload, and the renderer strips bad input silently by design.string
Always
'CloakEmbedError'.CloakEmbedErrorReason
Why the embed was refused. See the table below.
string | undefined
The exact offending path, for example
'embed.image.url' or 'embed.fields[3].value'. Absent when the whole payload is at fault.string
A remedy-carrying explanation, prefixed with
[cloak-sdk] sendCard:.CloakEmbedErrorReason
CloakComponentError
Thrown locally when a component row, a modal definition, or an ephemeral reply’s rows are refused, before anything reaches the wire. The same reasoning asCloakEmbedError: the server cannot read the envelope, and the client parser drops a bad card silently, so this is the only place you can be told.
string
Always
'CloakComponentError'.CloakComponentErrorReason
'shape' for a wrong type, an unknown key, a missing required key, or a duplicate id. 'cap' for a value over one of the pinned caps.string | undefined
The offending path, for example
'components[1].components[0].label' or 'modal.inputs[2].customId'.string
A remedy-carrying explanation, prefixed with
[cloak-sdk].Constants
Every cap mirrors a client renderer constant. Where the renderer truncates or strips a value,sendCard() rejects it.
MAX_EMBED_JSON sits below the server’s own 16 KiB sidecar cap on the ciphertext, because base64 expands by roughly four thirds. MAX_EMBED_WIRE is reachable only by a payload that already failed MAX_EMBED_JSON. Both exist so an oversize card is a local synchronous throw rather than a sendRejected event with code -11 arriving later. MAX_EMBED_JSON applies to the whole envelope, embed and rows together.
Component constants
Next
Rich cards
The guide: lane choice, media rules, and a complete bot.
Buttons and selects
Rows, clicks, the three answers, and
editCard() in practice.Errors
Every error class the SDK raises.
Client
Constructor options and the full method surface.