Skip to main content
Share
Guides

Discord Webhooks: How to Create, Send, Embed and Secure Them

A Discord webhook is a URL that turns an HTTP POST into a channel message. This guide covers creating one, sending with curl, Python and Node, usernames, embeds and allowed_mentions, forum threads, file uploads, 204 vs 200 with wait=true, rate limit headers, leaked URLs and the error codes you will meet.

Content & Engineering
Oct 17, 2026
13 min read
Updated Nov 2026Last verified November 2026
discord webhookhow to use discord webhooksdiscord webhook curlsend message to discord webhookdiscord webhook embed
TL;DR

A Discord webhook is a URL that turns an HTTP POST into a channel message. This guide covers creating one, sending with curl, Python and Node, usernames, embeds and allowed_mentions, forum threads, file uploads, 204 vs 200 with wait=true, rate limit headers, leaked URLs and the error codes you will meet.

Key Takeaways
  • 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.

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 channelYou need to read messages or react to users
The sender is a server, script, CI job or formYou need slash commands, buttons that do something, or DMs
You want zero hosting and no token managementYou need to act across many channels or servers

Creating a webhook and reading the URL

  1. Open the server, then Server Settings > Integrations. You need the Manage Webhooks permission.
  2. Open Webhooks and create a new webhook (Discord's help article calls the button Create Webhook).
  3. Pick the channel it posts to, set a name and avatar, and save.
  4. 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

FieldWhereWhat it does
contentJSON bodyMessage text, up to 2,000 characters
username, avatar_urlJSON bodyOverride the webhook's default name and avatar for this message only
embedsJSON bodyUp to 10 rich embeds
allowed_mentionsJSON bodyControls which mentions actually notify
ttsJSON bodyText-to-speech message
flagsJSON bodyOnly SUPPRESS_EMBEDS, SUPPRESS_NOTIFICATIONS and IS_COMPONENTS_V2 are allowed
thread_name, applied_tagsJSON bodyCreate a post in a forum or media channel
waitQuery stringWait for the message to be saved; returns it with 200
thread_idQuery stringPost into an existing thread (unarchives it automatically)
with_componentsQuery stringLets non-application-owned webhooks send non-interactive components
Try it yourself
Build your first chatbot free
Free plan, no credit card required. Live on your site in about 10 minutes.
Start building free

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 DELETE on 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

FieldLimit
title256 characters
description4,096 characters
fieldsUp to 25
field name / value256 / 1,024 characters
footer text2,048 characters
author name256 characters
All embeds combined6,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.

Try the free chatbot builder
600 conversations a month, every channel, no credit card.
Start free

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_name in the body creates a new post with that title.
  • ?thread_id=... in the query string posts into an existing post.
ErrorMeaning
220001Forum webhook sent neither thread_name nor thread_id
220002Sent both - pick one
220003thread_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 URLAcceptsNotes from Discord's docs
/slackSlack incoming-webhook payloadsSlack's channel, icon_emoji, mrkdwn and mrkdwn_in are not supported. wait defaults to true here
/githubGitHub repository webhook eventsPaste 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 with retry_after (seconds) and global.

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:

  1. Queue messages per webhook and send them one at a time; when Remaining hits 0, sleep for Reset-After.
  2. On 429, wait retry_after and retry once. Do not retry in a tight loop.
  3. Batch: one message with several embeds (up to 10) beats ten messages.
  4. 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:

  1. Delete the leaked webhook - in Server Settings > Integrations > Webhooks, or with DELETE /webhooks/{id}/{token}, which needs no other auth and returns 204.
  2. Create a new webhook on the same channel and store its URL in your secrets manager, not in code.
  3. Never call a webhook from browser code. Proxy it through your backend, which also lets you validate input and set allowed_mentions.
CodeMeaningUsual cause
10015 Unknown webhookThe webhook ID does not existDeleted webhook or a typo in the ID; stop calling it
50027 Invalid webhook tokenID exists, token does not matchTruncated URL or a token from a deleted and recreated webhook
50035 Invalid form bodyValidation failedContent over 2,000 chars, embed limits, bad color type, invalid JSON
50006 Cannot send an empty messageNo content, embeds, files, components or pollTemplate rendered to an empty string
40005 Request entity too largeUpload too bigFile over the 20 MiB default
220001 / 220002Forum thread parameters missing or doubledAdd 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.

Share this article:

Was this article helpful?

Ready to build your chatbot?

Join the businesses. Deploy on website, WhatsApp, and 11 more channels in minutes. Free forever plan available.

No credit cardNo coding13+ channels
Start Building Free

Get chatbot insights delivered weekly

Join 5,000+ professionals getting actionable AI chatbot strategies, industry benchmarks, and product updates.

🎯Automate this with a free chatbot

Build and deploy in 10 minutes. No coding needed.

❓FAQ

Discord Webhooks FAQ

Everything you need to know about chatbots for discord webhooks.

🔍
Popular:

POST a JSON body to the webhook URL with the Content-Type header set to application/json, for example curl -H "Content-Type: application/json" -d '{"content":"Hello"}' followed by the URL. Discord returns 204 No Content on success. Add ?wait=true to the URL if you want a 200 response containing the created message, including its ID.

That is the normal success response when the wait query parameter is false, which is the default. Discord accepts the request and returns nothing. Discord's docs note that in this mode a message that is not saved does not return an error, so add ?wait=true when you need confirmation or the message ID for later edits.

Only if you allow it. Webhooks parse just user mentions by default, so @everyone and role mentions render without notifying anyone. Pass an allowed_mentions object, such as parse with everyone, or a roles array of specific role IDs. Mentions are still validated against permissions, and roles must be mentionable unless the webhook can mention everyone.

Include either thread_name in the JSON body to create a new forum post with that title, or thread_id in the query string to reply inside an existing post. Sending neither returns error 220001, and sending both returns 220002. Use applied_tags in the body to attach forum tags when creating a post.

Discord does not publish a fixed number for webhooks and says rate limits should not be hard-coded. Read the X-RateLimit-Remaining and X-RateLimit-Reset-After headers on each response, and on a 429 wait the retry_after value from the body. Limits are tracked per webhook, so separate webhooks do not share a bucket.

Use multipart/form-data. Put each file in a part named files[0], files[1] and so on, with a filename, and put the JSON fields such as content and embeds in a part named payload_json. The default per-file limit is 20 MiB since September 2026; larger uploads fail with error 40005.

Webhook tokens cannot be regenerated. Delete the webhook in Server Settings, Integrations, Webhooks, or send a DELETE request to the webhook URL, then create a new webhook and store the new URL as a secret. Never embed webhook URLs in frontend code or public repositories, since anyone holding one can post as it or delete it.

Yes. Send the original request with ?wait=true to get the message ID, then send a PATCH to the webhook URL followed by /messages/ and that ID, with the fields you want to change. A webhook can only edit or delete messages it posted itself with the same token. This is the standard pattern for live status messages.

Discord rejected a field. Read the errors object in the response body, which names the exact path, such as embeds.0.description. Common causes are content longer than 2,000 characters, embeds that exceed per-field limits or 6,000 combined characters, a color sent as a hex string instead of an integer, or malformed JSON.

No. Creating a webhook in the server settings requires the Manage Webhooks permission, but executing it needs only the URL; there is no bot user, token or Gateway connection. Webhooks are one-way, though. To read messages, respond to slash commands or send DMs you need a bot application.

About the Author

Content & Engineering

The Conferbot team writes about building, deploying, and improving AI chatbots.

View all articles
Skip the blank canvas
Start from one of 250+ free chatbot templates for lead generation, support, e-commerce, and 20+ industries - customize and launch in minutes.
Browse free templates

Related Articles

From the reference shelf

Fact-checked reference pages and free tools for the platform this article covers.

Omnichannel Platform

One Chatbot,
Every Channel

Your chatbot works seamlessly across WhatsApp, Messenger, Slack, and 6 more platforms. Build once, deploy everywhere.

View All Channels
Conferbot
online
Hi! How can I help you today?
I need pricing info
Conferbot
Active now
Welcome! What are you looking for?
Book a demo
Sure! Pick a time slot:
#support
Conferbot
New ticket from Sarah: "Can't access dashboard"
Auto-resolved. Password reset link sent.