Everything on this page is a server-operator setting or a server deployment fact. None of it is SDK configuration, and your bot cannot probe for any of it. If you run against Cloak’s hosted service, treat this page as a lookup table for “why did that not work”. If you self-host, treat it as a checklist.
Server environment variables
BOT_REST_CREDENTIALS
client.rest is off by default server-side, and cloak-backend ships the flag unset. Until an operator turns it on, the first call rejects with a message that says so by name. That is the backend’s own gate, not a bug in your bot, so do not read a -7 as your problem.
Two things stay true even once it is on:
- The credential is scoped behind a default-deny allowlist. File upload, delete and permissions are classified as bot-usable. The two obvious read surfaces, custom emoji and the audit log, are classified as unauthorized because neither route does per-resource authorization today.
- That is why the SDK ships no
fetchCustomEmojis()and nofetchAuditLog(). Named methods that always fail would be worse than none. (client.fetchEmojis()is unrelated and works: it returns the static unicode catalog, never per-server custom emoji.)
The webhook ingress variables
The webhook embed lane needs an ingress the server actually runs and a public base it can build absolute URLs from. IfWEBHOOK_PUBLIC_BASE is empty, the backend returns a relative path and the SDK refuses to post to it rather than guessing an origin. The SDK also refuses plain http:// to anything but localhost, 127.0.0.1 and ::1, because the webhook secret is a path component and would travel in cleartext.
The image proxy variables, and the silent strip
This is the one that costs people an afternoon. Every URL a viewer’s client would auto-fetch is rewritten through the server’s signed image proxy so viewers never hit a third-party origin:image.url, thumbnail.url, video.url, author.icon_url, footer.icon_url, provider.icon, and a per-post avatar_url. When the proxy is unconfigured, or a URL is not plain http(s), the field is stripped. The POST still returns success. There is no error, no warning, and nothing in the response to read.
Quick check against a live deployment: set avatar_url on a webhook post. If the avatar does not change, the proxy is off, and your image will vanish too.
One more consequence: a proxied URL is longer than the original, and the 6000-byte embed budget is measured after the rewrite. Your real budget is smaller than 6000. The SDK deliberately does not pre-check it, so the server answers 400 embeds too large.
This applies to the webhook embed lane only. The encrypted card lane cannot be proxied at all, because the server cannot read the payload, which is why it restricts media to Cloak-hosted origins instead. See Choosing an embed lane.
The force-update gate and -12
The SDK sends a wire/crypto capability version as the trailing slot of its login frame. The backend’s force-update gate, CLOAK_MIN_CLIENT_VERSION, refuses anything below its floor with the shared -12 “update required” code.
A -12 is terminal. The SDK emits disconnect and stops the reconnect loop, because no amount of retrying makes an old build newer. The fix is to update @cloak-software/bot-sdk and redeploy. See Login rejections.
The mirror of this gate is that -2 (a revoked or regenerated token) is also terminal, while -8 (a transient backend error) keeps retrying.
Both sides must be on the HPKE wrap format
Your bot never generates a server’s conversation key. A human member’s client wraps it and uploads it. This SDK produces and accepts only the HPKE (v2) wrap. So the humans in your bot’s servers have to be on a client build that ships HPKE, or every key acquire fails. When the SDK receives a pre-HPKE wrap it names the offending peer in a warning, and it appends that peer’s id to theTimed out acquiring key for server … message so you are not left guessing which member is stale.
This is a fleet property of the servers your bot joins, not something you configure.
Backend deployment level
These are not settings. They are whether the server you are talking to is recent enough.Server member lists: fetchMembers() is broken everywhere today
Server member lists: fetchMembers() is broken everywhere today
A backend change made op-30 direct-message-only, so a server-scoped request produces no reply frame of any kind, not even an error code.
fetchMembers() neither resolves nor rejects. It hangs until the transport timeout. This affects every bot on every server, and it is blocked on a backend rebuild. There is no version of the server today where it works. See Known limitations.timeout(): needs a backend that routes op-160
timeout(): needs a backend that routes op-160
timeout() and removeTimeout() require member_timeout and a server carrying the timeout plan. Older servers drop the opcode, so the call times out rather than resolving. A working server acknowledges it.Reaction and pin acks: need the numeric bot ack
Reaction and pin acks: need the numeric bot ack
react(), unreact(), pin() and unpin() await a numeric ack that a backend has to send. Without it the awaited action cannot resolve, because the same opcode also carries the broadcast hook and the SDK will not accept a broadcast as its own acknowledgement.Cross-server events: need membership-routed hooks
Cross-server events: need membership-routed hooks
Reaction, channel, group and server lifecycle events reaching a bot for a server it has not selected requires a backend with membership-routed server hooks. On an older server,
serverId and channelId on those events fall back to the bot’s current selection cursor, which is a less useful answer.What none of this changes
The wire format is stable and backward-compatible. A backend that is missing one of the features above does not break your bot’s messaging: sending, receiving, history, moderation and the firehose all work. What you lose is the specific feature, and usually the loss is quiet, which is the reason this page exists.Next
Known limitations
The client-side half of the same honesty.
When a bot will not start
Symptom-indexed triage for the failures you can see.
The REST lane
What
client.rest is for, once an operator enables it.Webhook embeds
The lane the image proxy applies to.