When a webhook is the right tool
A webhook can only post (and edit or delete what it posted) in the one channel it belongs to. It cannot read messages, react, answer commands or see members. That makes it ideal for notifications: a deploy finished, a form was sent, a server went down, a new video is out. If you need to respond to people, you need a bot.
1. Create the webhook
- In Discord, open the channel's settings (the cog next to its name) and go to Integrations, then Webhooks. You need the Manage Webhooks permission.
- Press New Webhook, give it a name and an avatar if you like, and press Copy Webhook URL.
The URL looks like https://discord.com/api/webhooks/123456789012345678/AbC.... The long part at the end is a password: anyone who has the URL can post in that channel as the webhook. Keep it in an environment variable or a .env file, never in code you share or commit. If it leaks, delete the webhook and make a new one.
2. Send a message
A webhook takes a JSON body with at least content (up to 2,000 characters) or embeds. From the command line:
curl -H "Content-Type: application/json" \
-d '{"content": "Deploy finished"}' \
"$WEBHOOK_URL"From Python, with the requests package:
import os
import requests
url = os.environ["WEBHOOK_URL"]
r = requests.post(url, json={"content": "Deploy finished"}, timeout=10)
r.raise_for_status()From Node.js 18 or newer, with the built-in fetch:
const res = await fetch(process.env.WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: "Deploy finished" }),
});
if (!res.ok) throw new Error(`Discord answered ${res.status}: ${await res.text()}`);A successful post answers 204 No Content. Add ?wait=true to the URL to get the created message back as JSON instead, which you need to edit it later.
3. A different name and avatar per message
username and avatar_url override the webhook's own name and picture for that one message, so one webhook can post as "Deploys", "Uptime" and "Shop":
{
"username": "Uptime",
"avatar_url": "https://example.com/uptime.png",
"content": "The website is back up after 4 minutes."
}4. Embeds
An embed is the boxed message with a coloured edge, a title, fields and images. A message can carry up to 10, sent as a list:
{
"embeds": [{
"title": "New order",
"description": "Flurry plan, monthly",
"color": 5814783,
"fields": [
{"name": "Customer", "value": "Sam", "inline": true},
{"name": "Total", "value": "$3.00", "inline": true}
],
"timestamp": "2026-10-23T09:30:00Z"
}]
}color is a number, not a hex string: 0x58B9FF is 5814783. The embed builder builds this JSON for you, shows the result as Discord will, and counts the limits as you type.
5. Stop it pinging everyone
If any text in a webhook message comes from outside, a form, an error message, a username, someone can put @everyone in it. Say which mentions are allowed, and the rest are shown but ping nobody:
{
"content": "New report from @everyone",
"allowed_mentions": {"parse": []}
}"parse": [] turns off every ping. "parse": ["users"] allows user mentions only.
6. Attach a file
Files go as a multipart form instead of JSON: the file in a part called files[0], and the rest of the message as JSON in a part called payload_json:
import json
import os
import requests
with open("report.csv", "rb") as fh:
r = requests.post(
os.environ["WEBHOOK_URL"],
data={"payload_json": json.dumps({"content": "Today's report"})},
files={"files[0]": ("report.csv", fh, "text/csv")},
timeout=30,
)
r.raise_for_status()An embed can show an attached image with "image": {"url": "attachment://chart.png"}, using the file's name.
7. Edit or delete a message
Post with ?wait=true and keep the id from the answer. Then PATCH or DELETE the webhook URL followed by /messages/ and that id:
r = requests.post(url + "?wait=true", json={"content": "Deploying..."}, timeout=10)
message_id = r.json()["id"]
requests.patch(f"{url}/messages/{message_id}", json={"content": "Deployed."}, timeout=10)A status message that edits itself instead of posting again keeps a channel readable. A webhook can only edit its own messages.
8. Rate limits
Discord limits how fast a webhook may post, and answers 429 Too Many Requests past that, with a JSON body that says how many seconds to wait in retry_after. Wait that long and try again; do not retry straight away in a loop, which only extends the block. The X-RateLimit-Remaining and X-RateLimit-Reset-After headers on every answer tell you how close you are. For bursts, such as one message per log line, collect them and send one message every few seconds instead. The rate limits guide has the details.
From a bot library
If the code already uses discord.py or discord.js, both can send through a webhook URL without logging in a bot:
# discord.py
import aiohttp
import discord
async with aiohttp.ClientSession() as session:
webhook = discord.Webhook.from_url(url, session=session)
await webhook.send("Deploy finished", username="Deploys")// discord.js
const { WebhookClient } = require("discord.js");
const webhook = new WebhookClient({ url: process.env.WEBHOOK_URL });
await webhook.send({ content: "Deploy finished", username: "Deploys" });Common errors
401or404("Unknown Webhook"): the URL is wrong or the webhook was deleted. Copy it again from the channel's Integrations.400("Cannot send an empty message"): the body has neithercontent,embedsnor a file, often becauseContent-Type: application/jsonis missing and Discord did not read the body.400namingembeds.0.color: the colour was sent as a string like"#58B9FF". Send the number.413: the file is larger than the server allows for uploads.- The message pings people it should not: add
allowed_mentions(section 5).
Did this page help?
Thanks for the feedback.