Start here: a webhook is a URL that posts into one channel
A Discord webhook is a channel-specific URL that accepts an HTTP POST with a JSON body and posts it as a message - no bot, no token, no Gateway connection. Create one in Server Settings > Integrations > Webhooks, copy the URL, and send {"content": "Hello"} to it with Content-Type: application/json. Discord answers 204 No Content on success, or 200 with the created message if you add ?wait=true.
Last verified: November 2026 against Discord's Webhook Resource, Message Resource and Rate Limits documentation.
curl -H "Content-Type: application/json" \
-d '{"content": "Deploy finished: v2.4.1 is live"}' \
"https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN"
That is the whole integration for most alerting, CI and form-notification use cases. The rest of this guide covers what you reach for next: usernames and avatars, embeds, mentions, forum threads, files, editing a message you already sent, rate limits, and what to do when the URL leaks.
| Use a webhook when... | Use a bot when... |
|---|---|
| You only send messages into a known channel | You need to read messages or react to users |
| The sender is a server, script, CI job or form | You need slash commands, buttons that do something, or DMs |
| You want zero hosting and no token management | You need to act across many channels or servers |
Creating a webhook and reading the URL
- Open the server, then Server Settings > Integrations. You need the Manage Webhooks permission.
- Open Webhooks and create a new webhook (Discord's help article calls the button Create Webhook).
- Pick the channel it posts to, set a name and avatar, and save.
- Click Copy Webhook URL.
Programmatically, a bot with Manage Webhooks can call POST /channels/{channel.id}/webhooks with a name of 1 to 80 characters. Discord rejects names that contain clyde or discord (case-insensitive). A channel can hold up to 15 webhooks (error 30007) and a server up to 1,000 (error 30058).
URL anatomy
https://discord.com/api/webhooks/123456789012345678/AbCdEf...xyz
\________________/ \__________/
webhook.id webhook.token
The ID is a public snowflake. The token is the secret: anyone holding the full URL can post as the webhook and can even delete it, because the token-authenticated routes need no other authorization. Treat the URL like a password. Requests without a version go to the API's current default version; pinning one (/api/v10/webhooks/...) is fine and avoids surprises.
Sending messages: curl, Python and Node
Every execute request is a POST to /webhooks/{webhook.id}/{webhook.token}. You must include at least one of content, embeds, components, a file, or poll; an empty body returns 50006 Cannot send an empty message.
Python (requests)
import os, requests
url = os.environ["DISCORD_WEBHOOK_URL"]
r = requests.post(url, params={"wait": "true"}, json={
"content": "Nightly backup completed",
"username": "Backup Bot",
"avatar_url": "https://example.com/backup.png",
}, timeout=10)
r.raise_for_status()
print(r.status_code, r.json()["id"]) # 200 and the message id because wait=true
Node 18+ (built-in fetch)
const res = await fetch(process.env.DISCORD_WEBHOOK_URL + "?wait=true", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: "New signup: acme.io" }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const message = await res.json();
Always log the response body on failure. Discord's 400 responses carry a JSON error code and, for 50035 Invalid Form Body, a nested errors object pointing at the exact field that failed (for example embeds.0.fields.3.value).
The execute fields you will actually use
| Field | Where | What it does |
|---|---|---|
content | JSON body | Message text, up to 2,000 characters |
username, avatar_url | JSON body | Override the webhook's default name and avatar for this message only |
embeds | JSON body | Up to 10 rich embeds |
allowed_mentions | JSON body | Controls which mentions actually notify |
tts | JSON body | Text-to-speech message |
flags | JSON body | Only SUPPRESS_EMBEDS, SUPPRESS_NOTIFICATIONS and IS_COMPONENTS_V2 are allowed |
thread_name, applied_tags | JSON body | Create a post in a forum or media channel |
wait | Query string | Wait for the message to be saved; returns it with 200 |
thread_id | Query string | Post into an existing thread (unarchives it automatically) |
with_components | Query string | Lets non-application-owned webhooks send non-interactive components |
204 vs 200, and why ?wait=true matters
Without wait, Discord returns 204 No Content as soon as it accepts the request. Discord's Webhook Resource docs add an important caveat: when wait is false, "a message that is not saved does not return an error". A 204 means accepted, not necessarily posted.
With ?wait=true you get 200 and the full message object, including its id. Use it whenever you need to:
- Confirm delivery in CI or monitoring, where a silent drop is worse than a slow request.
- Edit the message later - for example a status message that goes from "Deploying..." to "Deployed" - with
PATCH /webhooks/{id}/{token}/messages/{message.id}. - Delete it later with
DELETEon the same path (returns 204).
# edit a message the webhook posted earlier
curl -X PATCH -H "Content-Type: application/json" \
-d '{"content": "Deploy v2.4.1: done in 3m12s"}' \
"https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN/messages/MESSAGE_ID"
A webhook can only edit or delete messages it sent itself, from the same token.
Embeds, colors, timestamps and allowed_mentions
Embeds are where webhook messages start looking like a product. A typical alert:
{
"username": "Status",
"embeds": [{
"title": "API latency above threshold",
"description": "p95 is 840 ms on eu-west-1",
"url": "https://status.example.com/incidents/42",
"color": 15158332,
"fields": [
{ "name": "Service", "value": "checkout-api", "inline": true },
{ "name": "Since", "value": "<t:1793620800:R>", "inline": true }
],
"footer": { "text": "pager rotation: platform" },
"timestamp": "2026-11-02T09:30:00.000Z"
}]
}
Three details people miss: color is a decimal integer, not a hex string (0xE74C3C is 15158332); timestamp is ISO 8601; and the <t:UNIX:R> syntax renders a relative time in each reader's own timezone. Build and preview these visually with the Discord embed builder, generate timestamp tags with the Discord timestamp generator, and use the colored text generator for ANSI code blocks inside content.
Embed limits
| Field | Limit |
|---|---|
| title | 256 characters |
| description | 4,096 characters |
| fields | Up to 25 |
| field name / value | 256 / 1,024 characters |
| footer text | 2,048 characters |
| author name | 256 characters |
| All embeds combined | 6,000 characters across title, description, field names and values, footer and author |
Exceeding any of them returns a 400 with 50035. Webhook embeds cannot set type, provider, video, or image height/width/proxy_url.
Mentions
Per Discord's Message Resource docs, webhooks parse only user mentions by default - equivalent to {"parse": ["users"]}. So @everyone and role mentions in a webhook message render but do not notify unless you opt in:
{ "content": "<@&123456789012345678> deploy failed", "allowed_mentions": { "roles": ["123456789012345678"] } }
The opposite case matters more: if you pass user-generated text into content, set "allowed_mentions": {"parse": []} so a form submission cannot ping half your server.
Forum channels, threads and file uploads
Forum and media channels
A webhook attached to a forum or media channel cannot post "into the channel", because those channels only contain posts. You must choose one of two options:
thread_namein the body creates a new post with that title.?thread_id=...in the query string posts into an existing post.
| Error | Meaning |
|---|---|
| 220001 | Forum webhook sent neither thread_name nor thread_id |
| 220002 | Sent both - pick one |
| 220003 | thread_name used on a channel that is not a forum |
In regular text channels, thread_id still works for posting into an existing thread.
Files
Attachments use multipart/form-data: each file goes in a part named files[0], files[1] and so on, and the JSON fields move into a payload_json part. Discord's Uploading Files reference requires each file part to carry a filename.
curl -F 'payload_json={"content": "Weekly report attached"}' \
-F "files[0][email protected]" \
"https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN"
Reference an uploaded image inside an embed with "image": {"url": "attachment://chart.png"}, using the same filename. Oversized uploads fail with 40005 Request entity too large.
What changed recently: on September 3, 2026, Discord's change log raised the default upload limit from 10 MiB to 20 MiB per file for users, bots, webhooks and interaction responses.
Slack- and GitHub-compatible endpoints, and webhooks going the other way
Discord ships two adapters on every webhook URL, which save you from writing a translation layer:
| Append to the URL | Accepts | Notes from Discord's docs |
|---|---|---|
/slack | Slack incoming-webhook payloads | Slack's channel, icon_emoji, mrkdwn and mrkdwn_in are not supported. wait defaults to true here |
/github | GitHub repository webhook events | Paste as the Payload URL in the repo's webhook settings and pick individual events (push, pull_request, issues, release, check_run and others). wait defaults to true |
Two practical consequences. First, a tool that only knows how to talk to Slack (many monitoring and CI products do) can usually post to Discord by pointing it at the /slack URL. Second, these adapters do not work in forum channels: Discord returns 220004 Webhook services cannot be used in forum channels, so point them at a regular text channel.
Incoming webhooks vs webhook events
Everything in this guide is an incoming webhook: you call Discord. Discord also has webhook events, where Discord calls a URL you host when something happens to your app (for example, an app being authorized). Those are configured in the Developer Portal, are signed, and have nothing to do with channel webhook URLs. If you are debugging an endpoint that Discord is supposed to call - interactions or webhook events - and the requests never arrive, that is covered in our webhook not firing guide rather than here.
A safe pattern for forms and user input
The most common production use of a webhook is "post every contact form submission to #leads". Do it server-side, cap the text you forward below 2,000 characters, put user-supplied text inside an embed field rather than raw content, set allowed_mentions to an empty parse list, and log non-2xx responses with their body. Discord also warns that it may strip invalid unicode or formatting-breaking characters from content, so sanitize what you forward.
Rate limits: read the headers, do not hard-code numbers
Discord does not publish a fixed per-webhook rate limit, and its rate limit documentation says limits "should not be hard coded into your app". Instead, every response carries headers you should honor:
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset-After- the current bucket.X-RateLimit-Bucket- identifies shared limits.- On a
429, a JSON body withretry_after(seconds) andglobal.
Webhook limits are computed per webhook (the docs list webhook_id, or webhook_id + webhook_token, as a top-level resource), so two webhooks do not share a bucket. Practical rules:
- Queue messages per webhook and send them one at a time; when
Remaininghits 0, sleep forReset-After. - On 429, wait
retry_afterand retry once. Do not retry in a tight loop. - Batch: one message with several embeds (up to 10) beats ten messages.
- Stop on 404. Discord explicitly says that if a webhook returns 404 you should not use it again; repeated attempts cause a temporary restriction.
Those repeated failures also count toward Discord's invalid-request limit: 10,000 responses with status 401, 403 or 429 per 10 minutes, per IP, after which the IP is temporarily blocked. One noisy cron job hammering a deleted webhook can block every other Discord integration on the same host. See HTTP 429 on Discord and our broader guide to API rate limiting.
Leaked webhook URLs and common errors
Webhook URLs leak constantly: committed config files, client-side JavaScript, screenshots of a settings panel. Anyone with the URL can spam the channel with your webhook's name and any avatar. There is no "regenerate token" for a webhook, so rotation means replacing it:
- Delete the leaked webhook - in Server Settings > Integrations > Webhooks, or with
DELETE /webhooks/{id}/{token}, which needs no other auth and returns 204. - Create a new webhook on the same channel and store its URL in your secrets manager, not in code.
- Never call a webhook from browser code. Proxy it through your backend, which also lets you validate input and set
allowed_mentions.
| Code | Meaning | Usual cause |
|---|---|---|
| 10015 Unknown webhook | The webhook ID does not exist | Deleted webhook or a typo in the ID; stop calling it |
| 50027 Invalid webhook token | ID exists, token does not match | Truncated URL or a token from a deleted and recreated webhook |
| 50035 Invalid form body | Validation failed | Content over 2,000 chars, embed limits, bad color type, invalid JSON |
| 50006 Cannot send an empty message | No content, embeds, files, components or poll | Template rendered to an empty string |
| 40005 Request entity too large | Upload too big | File over the 20 MiB default |
| 220001 / 220002 | Forum thread parameters missing or doubled | Add exactly one of thread_name or thread_id |
Every Discord JSON code is explained in the Discord error code directory. Webhooks are one-way by design; once you need the channel to answer back, you need a bot. If that bot is a support or FAQ assistant rather than a project you want to host, Conferbot's Discord integration is one way to get there without running the Gateway yourself.
Was this article helpful?
Build and deploy in 10 minutes. No coding needed.
Discord Webhooks FAQ
Everything you need to know about chatbots for discord webhooks.
About the Author
The Conferbot team writes about building, deploying, and improving AI chatbots.
View all articlesRelated Articles
From the reference shelf
Fact-checked reference pages and free tools for the platform this article covers.