API
Discord webhooks reference
What a webhook can and cannot do, the fields it accepts, the rate limits it is subject to, and the security properties of the URL.
- Key
- API-004
- Section
- API
- Read
- 8 min
- Checked
- 30 August 2026
A webhook is a channel-scoped URL that accepts a POST and turns it into a message. It requires no bot, no gateway connection and no OAuth flow, which makes it the shortest path from an external system into a Discord channel — and the easiest thing to leak.
What a webhook can do
A webhook posts messages into exactly one channel. It can set a per-message username and avatar, send embeds, attach files, and mention users and roles.
It cannot read messages, react, manage anything, or post anywhere other than the channel it was created in. It has no permissions in the normal sense — it is not a member and holds no role.
That narrow surface is the point. A webhook is a write-only pipe into one room.
Creating one
From the client: Channel Settings → Integrations → Webhooks → New Webhook. You need Manage Webhooks on that channel.
From the API: POST /channels/{channel.id}/webhooks with a name, using a bot token that holds
Manage Webhooks. The response contains the id and token that compose the URL.
There is a cap on how many webhooks one channel may hold, and creating past it returns an error rather than silently replacing one — if creation fails on a busy channel with correct permissions, check the existing list before debugging anything else.
The URL is the credential
There is no separate token or signature. Anyone holding the URL can post to that channel as that webhook, from anywhere, with no further authentication.
Treat a webhook URL exactly as you would a password. A URL committed to a public repository will be found and used, usually within hours.
If a URL leaks, the fix is to delete the webhook and create a new one. Rotating is not possible — the URL is the identity, so there is nothing to rotate.
The safe rotation sequence is therefore create-then-repoint-then-delete: make a second webhook,
point the sender at it, confirm a message arrives, and only then delete the first. Deleting first
guarantees a gap in which every sender receives 404.
Give each sender its own webhook. It costs nothing, makes the channel’s integration list self-documenting, and means revoking one sender does not break the others.
Message fields
The payload is a subset of the message object. The fields that matter in practice:
| Field | Notes |
|---|---|
content | Plain message text. Mutually optional with embeds — one of them must be present. |
username | Overrides the webhook’s configured name, per message. |
avatar_url | Overrides the avatar, per message. |
embeds | Up to 10 per message. |
tts | Text-to-speech. Rarely wanted. |
allowed_mentions | The control that decides whether mentions actually ping. |
thread_id | Query parameter, not body. Posts into an existing thread. |
Embed ceilings
These apply to every message, webhook or bot, and exceeding one returns a validation error rather than a truncated message:
| Element | Limit |
|---|---|
| Embeds per message | 10 |
| Total characters across all embeds | 6,000 |
| Title | 256 characters |
| Description | 4,096 characters |
| Fields per embed | 25 |
| Field name / value | 256 / 1,024 characters |
| Footer text | 2,048 characters |
The 6,000-character total is shared across every embed in the message, not allowed per embed. A payload assembled by concatenating records passes in testing with short data and fails in production with real data — truncate at the source rather than relying on the total.
Always set allowed_mentions
By default, a mention in webhook content pings. If any part of the content originates outside your
control — a commit message, an issue title, a form submission — then @everyone in that text will
ping the server.
Send allowed_mentions explicitly on every request, even when you believe the content is safe:
{
"content": "Build failed: @everyone check the logs",
"allowed_mentions": { "parse": [] }
}
With "parse": [], the text still displays but nothing is notified. This single field prevents the
most common webhook incident.
To permit some mentions and not others, parse accepts "users", "roles" and "everyone"
selectively, and users / roles accept explicit ID allow-lists. The safe default for machine
content is an empty parse array plus an explicit list of the few IDs you actually intend to ping.
Editing and deleting what you sent
A webhook can edit and delete its own messages — but only if it kept the message ID, and by default it does not.
?wait=true is required. Without it, a send returns 204 No Content with no body: the message
posts and you have no ID. With it, the response is the full message object.
POST /webhooks/{id}/{token}?wait=true
PATCH /webhooks/{id}/{token}/messages/{message.id}
DELETE /webhooks/{id}/{token}/messages/{message.id}
This is what makes a webhook usable for a status message that updates in place — a deploy progress line, a live scoreboard, an incident banner — rather than a channel of near-identical posts. Store the ID at send time; there is no way to look it back up later.
Files and attachments
Sending a file uses multipart/form-data with the JSON payload in a payload_json field and the
file as a separate part. Embeds can reference an uploaded file with the attachment://filename
scheme in an image or thumbnail URL.
That scheme is the only way an embed displays a file uploaded in the same request — a plain file name or a local path does not resolve, and neither does referencing the attachment’s CDN URL before the message exists.
The Slack and GitHub compatible endpoints
Appending a suffix to the webhook URL makes it accept another service’s payload format directly:
/github— GitHub’s own webhook payloads, translated into Discord messages. Configure the Discord URL with/githubappended as the payload URL in the repository’s webhook settings, and nothing else is needed./slack— Slack-format payloads, useful for any tool that only offers a Slack integration.
Both remove the need for an automation platform in the middle, and both are routinely replaced with a paid subscription by people who do not know they exist.
Rate limits
Webhooks are rate limited per webhook, not per channel or per application. The limit is not published as a fixed number and has changed over time, so treat the response headers as the source of truth rather than a value memorised from a blog post.
Read X-RateLimit-Remaining and X-RateLimit-Reset-After on every response and back off when
remaining reaches zero. A 429 returns retry_after in seconds — wait that long, do not retry
immediately.
For bursty sources, queue on your side and drain at a steady rate. Discord will not thank you for discovering the limit empirically in production.
Because the budget is per webhook, splitting high-volume senders across separate webhooks in the same channel raises the effective throughput — one of the few places where creating more of something is the correct fix.
Threads
To post into a thread, append ?thread_id= to the webhook URL. The thread must already exist and
must be in the webhook’s channel. A webhook cannot create a thread.
If the thread is archived, posting to it unarchives it.
Failure modes
| Response | Usually means |
|---|---|
404 | The webhook or its channel was deleted |
400 | A payload ceiling exceeded, or neither content nor embeds |
401 | The token portion of the URL is wrong or truncated |
429 | Rate limited — read retry_after |
204 unexpectedly | You forgot ?wait=true and there is no body to parse |
Deleting a channel deletes its webhooks. Every sender holding that URL starts receiving 404
and nothing else announces it — which is why the single most valuable habit here is logging the
response status at the sending end. An integration broken for three weeks is common precisely
because the only symptom is an absence nobody was watching for.
How a platform bot uses webhooks, both directions
Webhooks show up on both sides of a bot like Vetox, and the two uses are worth telling apart.
As a delivery destination. Logging can point any of its 148 event types at a webhook URL instead of a channel ID, and so can each of the eight notification platforms. A webhook destination keeps working when the channel is renamed or moved, and does not require the bot to hold send permission in that channel — the webhook carries its own authorisation, exactly the property described above. It also means a leaked log-channel webhook URL is a real incident: anyone holding it can post into that channel as the bot, so it deserves the same handling as any other credential leak.
As a surface it publishes to developers. The Vito public API sends its own outbound webhooks — up to five callback URLs per project, HTTPS only — for five event types, each signed HMAC-SHA256 with a timestamp and a 300-second replay tolerance, retried five times on exponential backoff. During a signing-secret rotation it emits two valid signatures for a 24-hour window, so a verifier checking only the new one does not drop deliveries on the day of the change. That is the inbound-webhook lesson applied to the outbound case: the secret is the identity, and rotating it without a grace window breaks every verifier that has not redeployed yet. Outbound webhooks are still uncommon among Discord bot developer APIs — only a handful of the ones profiled in the API comparison on this site offer them at all, Vetox included.
When a webhook is the wrong tool
Reach for a bot instead when you need to read messages, react, respond to interactions, post to more than one channel, or act with permissions. A webhook that is being wrapped in increasingly elaborate workarounds is a bot that has not been written yet.
Frequently asked questions
Can a webhook read messages? No. It posts into one channel and cannot read, react or act elsewhere.
Can a webhook URL be rotated? No — the URL is the identity. Create a new one, repoint the sender, then delete the old.
Why did my send return no message ID? Add ?wait=true. Without it the response is 204 with no
body.
Can a webhook create a thread? No. It can post into an existing thread with ?thread_id=, and
posting to an archived thread unarchives it.
Do I need an automation platform for GitHub? No — append /github to the webhook URL and use it
as the repository’s payload URL directly.
Sources
Checked