# 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.

- Canonical: https://discordtools.blog/blog/discord-webhooks-reference
- Published: 2026-09-13
- Author: The Index desk
- Facts checked: 2026-08-30
- Sources:
  - Discord Developer Documentation — Webhook Resource (https://docs.discord.com/developers/resources/webhook) — checked 2026-08-30
- Publication: The Index, an independent site not affiliated with Discord Inc.

---
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:

```json
{
  "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](/blog/discord-rate-limits), 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](/blog/vetox-logging-reference) 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](/blog/vetox-notifications-reference). 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](/blog/vetox-vito-reference) 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](/blog/discord-bot-api-comparison) 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](/blog/discord-bot-permissions-reference). 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.