Discord webhooks
Create a webhook in a Discord channel and store it as:
DISCORD_WEBHOOK_URL
Example
import os
import requests
def discord_send(content: str):
url = os.environ["DISCORD_WEBHOOK_URL"]
r = requests.post(url, json={"content": content}, timeout=10)
r.raise_for_status()
# discord_send("Automation finished")How a webhook works
A webhook is a URL that accepts a message. In Discord you open a channel's settings, choose Integrations, and create a webhook. Discord gives you a URL that contains a secret token. Anyone who has that URL can post to the channel, so it is a password. That is why the example reads it from the DISCORD_WEBHOOK_URL environment variable rather than writing it in the code.
Your script does an HTTP POST with a JSON body. {"content": "text"} is the simplest form. Discord replies with status 204 No Content on success. There is no login and no bot to run, which makes webhooks the easiest way to send a notification from a script.
A more useful sender
The basic function fails if Discord is unreachable, and a long message is rejected because content is limited to 2000 characters. This version handles both and adds a username and a formatted error message.
import os
import time
import requests
LIMIT = 2000
def discord_send(content: str, username: str = "Automation") -> bool:
url = os.environ["DISCORD_WEBHOOK_URL"]
if len(content) > LIMIT:
content = content[: LIMIT - 3] + "..."
for attempt in range(3):
r = requests.post(url, json={"content": content, "username": username}, timeout=10)
if r.status_code == 429: # rate limited
wait = r.json().get("retry_after", 1)
time.sleep(float(wait))
continue
r.raise_for_status()
return True
return False
def notify_failure(job: str, error: Exception) -> None:
discord_send(f"**{job} failed**\n```\n{type(error).__name__}: {error}\n```")429 means you are sending too fast, and the response includes retry_after in seconds. Discord formats **bold** and code fences in messages, so the failure message is easy to read in the channel. Use notify_failure inside the except block of a scheduled script so you find out when it breaks.
Common mistakes
- Committing the webhook URL. If it leaks, delete the webhook in Discord and create a new one.
- Sending too often. Loops that post on every iteration hit rate limits and annoy people. Send one summary.
- Sending secrets or personal data to a shared channel.
- Ignoring errors.
raise_for_status()makes a bad URL visible. - Using a user account token to post as yourself. That breaks Discord's terms of service. Use webhooks or a proper bot.
Adapting it
Use the same function at the end of your backup, report, or scraper scripts to post "finished" with a count. Add an embeds list to the JSON for richer cards with a title and colour.
Check your understanding
Why should the Discord webhook URL be stored in an environment variable?
A webhook URL is effectively a password for the channel. Keep it out of source control, and replace it if it leaks.
Show answer
A — The URL contains a secret token, so anyone with it can post to the channel — A webhook URL is effectively a password for the channel. Keep it out of source control, and replace it if it leaks.
Discord responds with HTTP 429. What does it mean and what should the script do?
429 is Too Many Requests. The response says how long to wait, and retrying after that pause is the correct behaviour.
Show answer
B — You are being rate limited, so wait for retry_after seconds and try again — 429 is Too Many Requests. The response says how long to wait, and retrying after that pause is the correct behaviour.
A message is 2500 characters long. What happens if you send it as content?
The API enforces a 2000 character limit on content. Truncate the text or send it as several messages.
Show answer
B — It is rejected because content is limited to 2000 characters, so truncate or split it first — The API enforces a 2000 character limit on content. Truncate the text or send it as several messages.