How to Send Discord Webhook Messages with Python
A practical guide to sending Discord webhook messages using Python. Learn to send plain text, rich embeds, and files with the requests library.
What is a Discord Webhook?
A Discord webhook is a simple way to post messages to a Discord channel from external applications. Unlike a bot, a webhook doesn’t require a persistent connection or authentication with a token — you just send an HTTP POST request to a URL, and the message appears in the channel.
Webhooks are perfect for:
- Notifications — CI/CD pipelines, server monitoring, error alerts
- Integrations — Connect third-party services to Discord
- Automation — Scheduled messages, status updates, data feeds
Prerequisites
You’ll need Python 3.7+ and the requests library:
pip install requests
Step 1: Create a Webhook in Discord
- Open your Discord server and go to Server Settings
- Navigate to Integrations → Webhooks
- Click New Webhook
- Choose a name and the channel where messages will be posted
- Click Copy Webhook URL
Your webhook URL will look like this:
https://discord.com/api/webhooks/1234567890/abcdefghijklmnop
Important: Keep your webhook URL secret. Anyone with the URL can post messages to your channel.
Step 2: Send a Simple Text Message
The most basic webhook request sends a plain text message:
import requests
WEBHOOK_URL = "https://discord.com/api/webhooks/YOUR_WEBHOOK_URL"
data = {
"content": "Hello from Python! 🐍"
}
response = requests.post(WEBHOOK_URL, json=data)
if response.status_code == 204:
print("Message sent successfully!")
else:
print(f"Failed to send: {response.status_code}")
A successful webhook request returns a 204 No Content status code.
Step 3: Send a Rich Embed Message
Embeds let you create beautifully formatted messages with colors, fields, images, and more:
import requests
from datetime import datetime
WEBHOOK_URL = "https://discord.com/api/webhooks/YOUR_WEBHOOK_URL"
embed = {
"title": "Server Status Report",
"description": "All systems are operational.",
"color": 5763719, # Green color in decimal
"fields": [
{
"name": "CPU Usage",
"value": "23%",
"inline": True
},
{
"name": "Memory",
"value": "4.2 GB / 16 GB",
"inline": True
},
{
"name": "Uptime",
"value": "14 days, 6 hours",
"inline": False
}
],
"footer": {
"text": "Monitoring Bot"
},
"timestamp": datetime.utcnow().isoformat()
}
data = {
"embeds": [embed]
}
response = requests.post(WEBHOOK_URL, json=data)
print(f"Status: {response.status_code}")
Embed Color Reference
Discord embed colors are specified as decimal integers. Here are some common ones:
- Red:
15548997(#ED4245) - Green:
5763719(#57F287) - Blue:
5793266(#5865F2 — Discord blurple) - Yellow:
16776960(#FFFF00) - White:
16777215(#FFFFFF)
Step 4: Send with Custom Username and Avatar
You can override the webhook’s default name and avatar per-message:
data = {
"content": "Deployment complete! ✅",
"username": "Deploy Bot",
"avatar_url": "https://example.com/deploy-avatar.png"
}
response = requests.post(WEBHOOK_URL, json=data)
Step 5: Send Files and Attachments
To send files, use multipart/form-data instead of JSON:
import requests
import json
WEBHOOK_URL = "https://discord.com/api/webhooks/YOUR_WEBHOOK_URL"
# Message payload
payload = {
"content": "Here's the latest log file:"
}
# File to upload
with open("server.log", "rb") as f:
files = {
"payload_json": (None, json.dumps(payload), "application/json"),
"file": ("server.log", f, "text/plain")
}
response = requests.post(WEBHOOK_URL, files=files)
print(f"Status: {response.status_code}")
Rate Limits
Discord enforces rate limits on webhook requests. The standard limits are:
- 5 requests per 2 seconds per webhook
- 30 requests per 60 seconds per channel
If you exceed these limits, you’ll receive a 429 Too Many Requests response with a retry_after field:
import requests
import time
def send_webhook(url, data, max_retries=3):
for attempt in range(max_retries):
response = requests.post(url, json=data)
if response.status_code == 204:
return True
if response.status_code == 429:
retry_after = response.json().get("retry_after", 1)
print(f"Rate limited. Retrying in {retry_after}s...")
time.sleep(retry_after)
continue
print(f"Error: {response.status_code}")
return False
return False
Complete Example: Monitoring Script
Here’s a real-world example — a script that monitors a website and sends Discord alerts:
import requests
import time
from datetime import datetime
WEBHOOK_URL = "https://discord.com/api/webhooks/YOUR_WEBHOOK_URL"
TARGET_URL = "https://example.com"
CHECK_INTERVAL = 60 # seconds
def check_website():
try:
response = requests.get(TARGET_URL, timeout=10)
return response.status_code, response.elapsed.total_seconds()
except requests.RequestException as e:
return None, str(e)
def send_alert(status, details):
is_up = status and status == 200
color = 5763719 if is_up else 15548997
embed = {
"title": f"{'✅' if is_up else '🔴'} {TARGET_URL}",
"description": f"Status: **{status or 'DOWN'}**",
"color": color,
"fields": [
{"name": "Details", "value": str(details), "inline": False}
],
"timestamp": datetime.utcnow().isoformat()
}
requests.post(WEBHOOK_URL, json={"embeds": [embed]})
if __name__ == "__main__":
print(f"Monitoring {TARGET_URL}...")
last_status = None
while True:
status, details = check_website()
if status != last_status:
send_alert(status, details)
last_status = status
time.sleep(CHECK_INTERVAL)
Next Steps
Now that you know how to send webhook messages with Python, you can:
- Build automated notification systems
- Create custom integrations for your Discord server
- Use our visual webhook builder to design complex embed layouts, then export the JSON and send it from your Python scripts
The Discord Webhook Builder makes it easy to visually design your messages before coding the automation.
discord-webhook.com also offers scheduled messages, thread and forum support, polls, and interactive buttons with actions — all configurable through the visual builder without writing code.
Error Handling and Resilience
Production webhook scripts need to handle network failures gracefully. The basic requests.post() call can fail for many reasons — DNS resolution errors, connection timeouts, Discord API outages, or malformed payloads. Wrapping every webhook call in proper error handling prevents your script from crashing unexpectedly.
Here is a production-ready function that handles common failure modes and retries with exponential backoff:
import requests
import time
def send_webhook_reliable(url, data, max_retries=5, base_delay=1.0):
"""Send a webhook with retry logic and exponential backoff."""
for attempt in range(max_retries):
try:
response = requests.post(url, json=data, timeout=10)
if response.status_code == 204:
return True
if response.status_code == 429:
retry_after = response.json().get("retry_after", base_delay)
time.sleep(retry_after)
continue
if response.status_code >= 400:
print(f"HTTP {response.status_code}: {response.text}")
return False
except requests.exceptions.ConnectionError:
print(f"Connection failed (attempt {attempt + 1}/{max_retries})")
except requests.exceptions.Timeout:
print(f"Request timed out (attempt {attempt + 1}/{max_retries})")
except requests.exceptions.HTTPError as e:
print(f"HTTP error: {e}")
return False
# Exponential backoff: 1s, 2s, 4s, 8s, 16s
delay = base_delay * (2 ** attempt)
time.sleep(delay)
print("All retry attempts exhausted.")
return False
The exponential backoff pattern is critical for two reasons. First, it avoids hammering Discord’s API when the service is temporarily unavailable. Second, it gives transient network issues time to resolve on their own. The timeout=10 parameter on the request itself ensures you don’t wait indefinitely for a response from a server that has stopped responding.
For long-running scripts that send many messages, consider logging failures to a file so you can inspect and resend them later. You can also integrate this with your embed formatting workflow — validate the embed structure before sending to reduce errors caused by malformed payloads.
Async Webhooks with aiohttp
When your application needs to send messages to multiple channels or webhooks simultaneously, synchronous requests calls become a bottleneck. Each call blocks until it completes, so sending to 10 webhooks takes 10 times as long. The aiohttp library lets you send all requests concurrently using Python’s async/await syntax.
Install it first:
pip install aiohttp
Here is a practical example that broadcasts a message to multiple webhook URLs at once:
import aiohttp
import asyncio
WEBHOOK_URLS = [
"https://discord.com/api/webhooks/CHANNEL_1_WEBHOOK",
"https://discord.com/api/webhooks/CHANNEL_2_WEBHOOK",
"https://discord.com/api/webhooks/CHANNEL_3_WEBHOOK",
]
async def send_one(session, url, data):
"""Send a single webhook request."""
try:
async with session.post(url, json=data, timeout=aiohttp.ClientTimeout(total=10)) as resp:
if resp.status == 204:
print(f"Sent to {url[:60]}...")
elif resp.status == 429:
body = await resp.json()
retry_after = body.get("retry_after", 1)
await asyncio.sleep(retry_after)
async with session.post(url, json=data) as retry_resp:
print(f"Retry result: {retry_resp.status}")
else:
print(f"Failed ({resp.status}): {url[:60]}...")
except asyncio.TimeoutError:
print(f"Timeout: {url[:60]}...")
async def broadcast(message_data):
"""Send a message to all webhooks concurrently."""
async with aiohttp.ClientSession() as session:
tasks = [send_one(session, url, message_data) for url in WEBHOOK_URLS]
await asyncio.gather(*tasks)
# Example usage
data = {
"embeds": [{
"title": "Deployment Complete",
"description": "Version 2.4.1 is now live on all servers.",
"color": 5763719,
}]
}
asyncio.run(broadcast(data))
The key advantage is asyncio.gather(), which fires all requests in parallel and waits for all of them to finish. A broadcast to 10 webhooks that would take 5-10 seconds synchronously completes in roughly the time of the slowest single request. This pattern pairs well with automation patterns where you need to notify multiple teams or channels about the same event — deployment alerts, incident responses, or scheduled status reports.
Note that aiohttp.ClientSession should be reused across requests within the same function rather than created per-request. Creating a new session for each call defeats the purpose of connection pooling.
Security Best Practices
A webhook URL is essentially an API key. Anyone who has it can post messages to your Discord channel — including spam, phishing links, or misleading announcements. Treat webhook URLs with the same care you would give a database password.
Store URLs in environment variables, not in your source code:
import os
WEBHOOK_URL = os.environ.get("DISCORD_WEBHOOK_URL")
if not WEBHOOK_URL:
raise ValueError("DISCORD_WEBHOOK_URL environment variable is not set")
For local development, use a .env file with the python-dotenv package:
from dotenv import load_dotenv
import os
load_dotenv()
WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL")
Add .env to your .gitignore immediately — this is not optional. A single accidental commit that includes a webhook URL means you should consider that URL compromised and regenerate it in Discord’s settings.
Additional measures to keep your webhooks secure:
- Rotate URLs periodically — delete and recreate webhooks every few months, especially for high-traffic integrations
- Use separate webhooks per service — if one URL is leaked, you only need to replace that single integration, not everything
- Monitor for abuse — if unexpected messages appear in your channel, regenerate the webhook URL immediately
- Restrict webhook permissions — create webhooks in channels with limited visibility rather than public announcement channels
When building embeds with sensitive data like server IPs or internal URLs, review your color values and field content carefully to ensure you are not inadvertently exposing private infrastructure details in embed fields.
Testing and Debugging
Testing webhook integrations during development can quickly flood your Discord channel with test messages. A better approach is to validate your payloads before sending them to Discord.
Use httpbin.org as a stand-in endpoint during development:
import requests
import json
TEST_URL = "https://httpbin.org/post"
data = {
"content": "Test message",
"embeds": [{
"title": "Debug Embed",
"description": "Checking payload structure",
"color": 5793266,
}]
}
response = requests.post(TEST_URL, json=data)
# httpbin echoes back what you sent — inspect the response
print(json.dumps(response.json()["json"], indent=2))
This lets you verify the exact JSON structure Discord will receive without generating any actual messages. Once the payload looks correct, switch the URL back to your real webhook.
For validating embed structures specifically, you can also use the Discord Webhook Builder to visually construct your embed, export the JSON, and compare it against what your Python code generates. Mismatched field names (e.g., footer_text instead of footer.text) are a common source of silent failures where Discord returns 204 but the embed renders incorrectly or not at all.
Related Articles
- Send Discord Webhooks from JavaScript (Node.js & Browser) — Learn how to send webhook messages using JavaScript with Node.js and browser fetch API
- Complete Guide to Discord Embed Formatting — Master Discord embed builder techniques including fields, colors, images, and character limits
- Discord Webhook Notifications and Automation - Complete Guide — Automate Discord notifications for server monitoring, CI/CD pipelines, and e-commerce alerts
- Discord Webhook Polls Guide — Create interactive polls in Discord channels using webhooks
- Discord Webhook Scheduled Messages — Automate recurring messages and timed notifications with scheduled webhooks
- Thread and Forum Support — Post webhook messages to Discord threads and forum channels
Try it in our tool
Open Discord Webhook Builder