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, 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.
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
required
Exactly one embed. Clients render one card per message.
string
The channel’s group, when it is not already cached from an inbound frame.
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. Desktop only today, see the warning below.
video is rejected by name.
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: shape, then content, then unknown keys, then caps, then the media rule, then link schemes, then “nothing renderable”.
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
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.
Next
Rich cards
The guide: lane choice, media rules, and a complete bot.
Webhooks reference
The plaintext lane’s types, caps and errors.
Errors
Every error class the SDK raises.
Client
Constructor options and the full method surface.