Skip to main content
Everything on this page posts messages that are not end-to-end encrypted and are attributed to a webhook identity rather than to your bot. The payload type is called UnencryptedWebhookPost for that reason. For the encrypted alternative, see cards and embeds.
For when to use this lane at all, read webhook embeds first.

sendEmbed

Finds or mints a webhook for the channel and posts through it. Requires manage_server, because minting is a server management action. The payload is validated before anything is minted, so a post that will be refused does not burn one of the 10 creations per account per hour. On a 401 the SDK drops its cached URL, re-mints, and retries exactly once. Reuse only ever adopts a webhook the bot itself created.

webhooks

client.webhooks implements WebhookApi. client.guild(serverId).webhooks exposes the same methods with the serverId argument dropped.

list

Every webhook on a server. Requires manage_server. Never includes a token, by design.

create

Mints a webhook on a channel and returns its one-time delivery URL. Requires manage_server. Capped at 15 per channel and 10 creations per account per hour. icon and miniIcon are already-hosted image URLs. The SDK never uploads, because file upload is closed to bots. The server sanitizes them on write: a non-Cloak URL is rewritten through the image proxy, or dropped when the proxy is unconfigured.

edit

Rename, re-icon, or move a webhook to another channel. Requires manage_server. Omitted fields are left alone. icon and miniIcon are written as a pair, so passing miniIcon without icon does nothing.

regenerate

Mints a fresh token for an existing webhook and returns its new URL. Requires manage_server.
This permanently invalidates the previous URL. Anything still posting to it starts getting 401. It is also the only way to obtain a URL for a webhook you did not just create, because list() never returns a token.

delete

Deletes a webhook. Requires manage_server. Messages it already posted keep rendering, because the identity is stamped on each row.

post

POSTs to a delivery URL. Requires no permissions at all, which makes it the recommended path: a human creates the webhook in the Cloak app, you put the URL in an environment variable, and your bot never needs manage_server. Rejects with CloakWebhookError on anything but a 204. The URL never appears in the error.

postToWebhook

The standalone function behind webhooks.post(). Use it when you have no Client at all.
Pass undefined for fetchImpl to use globalThis.fetch. It runs no retries and holds no state: the mint, reuse and re-mint-on-401 policy lives in sendEmbed(). Before the request it checks the unencrypted: true acknowledgement, requires an absolute URL, and refuses plain http:// to anything but localhost, 127.0.0.1 or ::1, because the token secret is a path component of the URL. Its own POST timeout is 15 seconds.

UnencryptedWebhookPost

true
required
Required acknowledgement that this message is not end-to-end encrypted. There is no default and no way to omit it. It is re-checked at runtime, and it never reaches the wire.
string
Plain text body, up to 2000 characters. Either this or a non-empty embeds is required.
Embed[]
First 10 kept. The serialized JSON is capped at 6000 bytes measured after the server rewrites every media URL through its image proxy, so your real budget is smaller. The SDK does not pre-check it.
string
Per-post display-name override, honored at 1 to 80 characters. Out-of-range values fall back to the webhook’s stored name. This is the real win of the lane: one webhook can speak as many personas.
string
Per-post avatar, as an already-hosted image URL. The SDK never uploads. Honored only when the server’s image proxy is configured, and otherwise the webhook’s stored icon is used while the POST still returns success.

Embed

A Discord-shaped rich embed. Field names are wire names in snake_case, so do not camelCase them. Unknown keys are passed through untouched. This type is deliberately not the same as BotEmbed on the encrypted lane. The caps differ, this one has video and provider.icon, and its media is proxied rather than restricted to a single origin.
string
Truncated server-side to 256 characters.
string
Truncated server-side to 4096 characters. Markdown links render here.
string
Click-through target for the title. Never proxied, never stripped.
number | string
A decimal int (0x24292f) or a CSS color string ('#24292f'). The renderer accepts both.
string
ISO-8601, rendered in the footer.
'rich'
Optional. The server does not require it.
{ name?: string; url?: string; icon_url?: string }
name truncated to 256. icon_url is proxied, or stripped when the proxy is unconfigured.
text truncated to 2048. icon_url is proxied or stripped.
{ name?: string; icon?: string }
icon is proxied or stripped.
{ name: string; value: string; inline?: boolean }[]
First 25 kept. name truncated to 256, value to 1024. inline defaults to true on this backend, unlike Discord’s default of false, so pass it explicitly if you care.
{ url: string }
Proxied or stripped.
{ url: string }
Proxied or stripped.
{ url: string }
Proxied or stripped.
Every URL a viewer auto-fetches is rewritten through the server’s signed image proxy, and the field is stripped when IMGPROXY_SECRET and IMGPROXY_PUBLIC_BASE are unset or the URL is not plain http(s). The POST still returns 204. Text and click-through links always survive.
The server normalizes leniently: out-of-spec payloads are truncated rather than rejected, except for the hard 400s on content length, embed size, request body size, and an empty post.

WebhookInfo

One row from webhooks.list().
string
The webhook id. Not a secret.
string
The display name it posts under.
string
The channel it posts into.
string | null
The small avatar it posts under, or null. Cloak-hosted or proxied, because the server sanitizes stored icons on write.
string | null
The user who created it, or null on a row with no creator recorded. The reuse path in sendEmbed() matches on this, and a row with no creator is never adopted.
Date | null
When it was created, or null when the row carried no usable timestamp.

WebhookMint

Returned by create() and regenerate().
string
The webhook id. Not a secret.
string
The delivery URL. It embeds the token secret, the server stores only its hash, and it is returned exactly once. Treat it like a password: never commit it, never log it, never write it to your keystore.

WebhookFetch

The HTTP seam the lane posts through. Structural on purpose, so globalThis.fetch satisfies it and tests can pass an object literal.
Supply your own with ClientOptions.webhookFetch to route posts through a proxy or a pinned agent.

Client options for this lane

string
Display name for the webhook sendEmbed() mints. Defaults to 'Bot'. Cosmetic only: reuse is matched on creator and channel, never on name, so renaming it does not orphan an existing webhook.
WebhookFetch
Injected fetch for the webhook ingress. Defaults to globalThis.fetch.

CloakWebhookError

A delivery failure, meaning the HTTPS POST itself. A CRUD denial on list, create, edit, regenerate or delete arrives as CloakActionError instead, carrying the missing permission.
string
Always 'CloakWebhookError'.
number
The HTTP status, or 0 for a transport or pre-check failure such as DNS, TLS, timeout or abort.
string
The ingress body text: 'content', 'embeds', 'embeds too large', 'empty', 'unauthorized', 'rate limited' or 'delivery failed'. When status is 0 it is an SDK-side label instead: 'transport' or 'precheck'.
string | undefined
The webhook this post targeted, when known. Never a secret.
The delivery URL contains the token secret, so it never appears in message, in a field, or in toString(). Do not add it back when you log the error.

Constants

Next

Webhook embeds

The guide: what you give up, the low-privilege path, and server prerequisites.

Cards and embeds

The encrypted lane, which most bots should use instead.

Errors

Every error class the SDK raises, and which surface raises it.

The guild() handle

The same webhook surface, pre-bound to one server.