The Index

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:

FieldNotes
contentPlain message text. Mutually optional with embeds — one of them must be present.
usernameOverrides the webhook’s configured name, per message.
avatar_urlOverrides the avatar, per message.
embedsUp to 10 per message.
ttsText-to-speech. Rarely wanted.
allowed_mentionsThe control that decides whether mentions actually ping.
thread_idQuery 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:

ElementLimit
Embeds per message10
Total characters across all embeds6,000
Title256 characters
Description4,096 characters
Fields per embed25
Field name / value256 / 1,024 characters
Footer text2,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 /github appended 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

ResponseUsually means
404The webhook or its channel was deleted
400A payload ceiling exceeded, or neither content nor embeds
401The token portion of the URL is wrong or truncated
429Rate limited — read retry_after
204 unexpectedlyYou 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

  1. Discord Developer Documentation — Webhook Resource (opens in a new tab)

How we verify