朝のニュース通知を開くと、記事のリンク1本ごとにプレビューの画像が付いて、肝心の見出しが埋もれる。
2026年5月、Discordに通知を集め始めたころの私のチャンネルは、まさにそんな画面でした。
Discord Webhookなら、PythonからURLにJSONを1回POSTするだけでチャンネルに通知を送れます。
私はいま、SSL証明書の監視やRSSのニュースなど24本のスクリプトから、同じ通知クラスでDiscordに送っています。
基本のコードに加えて、embedの上限、プレビューの消し方、半年の運用で踏んだ落とし穴まで、実物のコードでまとめました。
この記事でわかること
- Discord WebhookとBotの違い、Webhook URLの作り方
- PythonのrequestsでDiscordに通知を送る最小コード
- embedの書き方と上限(10個・合計6,000字)
- URLのプレビューを出さない2つの方法(
<>とflags) - 2000字の上限・429・URL未設定の落とし穴と対策
PARTS LIST /目次
Discord Webhookとは?Botなしで通知だけ送れる仕組み
Discord Webhookとは、発行したURLにHTTPでPOSTするだけで、決まったチャンネルにメッセージを投稿できる仕組みです。
Botのようにアカウントを作ったり、常駐プログラムを動かしたりする必要がありません。
「処理が終わったら知らせてほしい」「毎朝ニュースを届けてほしい」のように、片道の通知だけで足りる用途なら、Webhookがいちばん手軽です。
私の通知はすべて片道なので、24本ともWebhookで送っています。返事をもらう仕組みは1つもありません。
WebhookとBotの違い
違いは「送るだけか、やり取りするか」です。Webhookは送信専用で、Botはメッセージを読んで返事もできます。
Discord WebhookとBotの違い
返事をする仕組みが欲しい場合は、Botの設定が必要です。AIエージェントをDiscordにつないだときの手順はOpenClawのDiscord設定手順に書きました。
Webhook URLの作り方(サーバー設定→連携サービス)
Webhook URLは、Discordのサーバー設定から1分ほどで発行できます。サーバーの「ウェブフックの管理」権限が必要なので、自分で作ったサーバーで試すのが早いです。
step
1サーバー設定の「連携サービス」を開く
サーバー名をクリックして「サーバー設定」を開き、左のメニューから「連携サービス」を選びます。チャンネルの歯車アイコンからも同じ画面に行けます。
step
2「ウェブフック」から新しいウェブフックを作る
「ウェブフックを作成」を押すと、Captain Hookのような仮の名前で1つ作られます。
step
3名前と投稿先のチャンネルを決める
名前は「ニュース通知」「SSL監視」のように、どのスクリプトからの通知か分かるものにします。
step
4「ウェブフックURLをコピー」を押す
https://discord.com/api/webhooks/数字/英数字 の形のURLがコピーされます。
Webhook URLはパスワードと同じ扱いです。URLを知っている人なら誰でもそのチャンネルに投稿できます。GitHubやブログ、スクリーンショットに載せないでください。漏れたら、同じ画面でウェブフックを削除して作り直します。
PythonでDiscord Webhookに通知を送る基本コード
Pythonからの送信は、requestsでWebhook URLにJSONをPOSTするだけです。
ここでは、私が手元で動かしているPython 3.11.14とrequests 2.33.1で確かめたコードを使います。
最初は1行のメッセージを送り、次に送信者の見た目を変えて、最後にURLの渡し方を整えます。この3段階で、自分用の通知ならほぼ足ります。
requestsで1行のメッセージを送る
送る中身は content に入れた文字列だけで足ります。
import os
import requests
# Webhook URLはコードに書かず、環境変数から受け取る
WEBHOOK_URL = os.environ["DISCORD_WEBHOOK_URL"]
res = requests.post(
WEBHOOK_URL,
json={"content": "バックアップが終わりました"},
timeout=10,
)
res.raise_for_status() # 4xx・5xx なら例外にする
print(res.status_code) # 成功すると 204
成功すると、本文のない 204 No Content が返ります。投稿したメッセージの情報が欲しいときは、URLの末尾に ?wait=true を付けると 200 とメッセージのJSONが返ってきます。
requestsは python-requests/2.33.1 のような User-Agent を自動で付けるので、そのまま送れます。
標準ライブラリの urllib で送ると、既定の User-Agent のせいで403になることがあります。その対処はSSL証明書の監視記事のDiscord通知の節にまとめました。
名前・アイコンの変更とメンションの抑止
username と avatar_url を付けると、メッセージごとに送信者の名前とアイコンを上書きできます。
payload = {
"content": "@everyone 証明書の期限が残り14日です",
"username": "SSL監視",
"avatar_url": "https://example.com/icon.png",
# @everyone や @ユーザー名 が本文にあっても通知を飛ばさない
"allowed_mentions": {"parse": []},
}
requests.post(WEBHOOK_URL, json=payload, timeout=10).raise_for_status()
1つのWebhookを複数のスクリプトで共有しても、名前で送り主を見分けられます。
なお、名前に discord や clyde を含めることはできません。
allowed_mentions は地味ですが効きます。ニュースの見出しに @everyone という文字がたまたま入っていると、サーバーの全員に通知が飛びます。"parse": [] にしておけば、本文にどんな文字列が来てもメンションになりません。
Webhook URLは.envで渡す(launchdの環境変数対策)
Webhook URLは .env ファイルに書き、スクリプトの起動時に読み込むのが安全です。
# python/.env(Gitにコミットしない)
DISCORD_NEWS_WEBHOOK_URL=https://discord.com/api/webhooks/xxxx/yyyy
ターミナルで export した環境変数は、cronやlaunchdから起動したスクリプトには引き継がれません。
手で動かすと届くのに、定期実行だと届かない。原因はたいていここです。
私の通知クラスは、.env を自分で読み込んでからURLを探すようにしてあります。定期実行の設定そのものはMac launchdの使い方にまとめています。
Discord Webhookのembedで見やすい通知にする
embedは、色付きの帯・タイトル・項目の表を持った「カード型」のメッセージです。
ただの文字より目に入りやすく、成功は青・失敗は赤のように色で状態を伝えられます。
一方で、項目を詰め込みすぎるとスマホでは縦に長くなり、かえって読みにくくなります。
実は私の通知はembedを使っていません。絵文字と太字のただの文字だけで、成功か失敗かは行頭の✅と❌で見分けています。カードにするほどの情報量がないからです。
embedの基本形と色の指定
embedは embeds に辞書の配列で渡します。色は #2c7be5 のような文字列ではなく、整数で指定する点に注意してください。
from datetime import datetime, timezone
payload = {
"username": "ジョブ通知",
"embeds": [
{
"title": "朝のニュース配信",
"url": "https://example.com/",
"description": "新着3件を配信しました",
"color": 0x2C7BE5, # 16進数で書けば整数として渡せる
"fields": [
{"name": "取得件数", "value": "3件", "inline": True},
{"name": "所要時間", "value": "12秒", "inline": True},
],
"footer": {"text": "news_bot"},
"timestamp": datetime.now(timezone.utc).isoformat(),
}
],
}
requests.post(WEBHOOK_URL, json=payload, timeout=10).raise_for_status()
inline: True の項目は横に並びます。timestamp を付けると、カードの下に送った時刻が読者の地域の時間で表示されます。
embedの上限は10個・合計6000字
embedには、1メッセージあたりの数と文字数に上限があります。超えると 400 Bad Request で送信そのものが失敗します。
embedの上限(Discordの開発者ドキュメントより)
見落としやすいのは、最後の「合計6,000字」です。1つずつは上限内でも、10個のembedに長い説明を入れると合計で超えます。
もう1つ、同じURLを持つembedが複数あると、最初の1つしか表示されません。
URLのプレビューを出さない2つの方法
Discordは、本文にURLがあると自動でリンク先のプレビュー(タイトル・説明・画像)を付けます。
1件なら便利ですが、ニュースを10本並べた通知では画面がサムネイルで埋まります。
止める方法は2つあります。URLごとに選ぶなら <> で囲む方法、メッセージ全体で止めるなら flags を付ける方法です。
URLを <> で囲む
URL1本ごとに止められる。残したいプレビューは残せる。Webhookでも手で書くメッセージでも同じように効く。
flags に 4 を付ける
メッセージ内のプレビューを全部止める。本文の文字列を加工しなくてよい。自分で付けたembedも消える。
URLを<>で囲む(5月にニュース通知へ導入)
URLを半角の < と > で挟むと、そのURLにはプレビューが付きません。リンクとしてはクリックできるままです。
def format_article(title: str, link: str) -> str:
"""記事1件を「タイトル+プレビューなしのURL」の2行にする"""
return f"**{title}**\n<{link}>"
lines = [format_article(a["title"], a["link"]) for a in articles]
requests.post(WEBHOOK_URL, json={"content": "\n\n".join(lines)}, timeout=10)
私が使っているニュース配信とRSS監視のスクリプトも、記事のリンクを必ずこの形で出しています。
5月21日、それまで一般チャンネルに全部流していた通知を3つに分けました。IT/AIニュース・ポイ活情報・はてなブックマークの人気記事を、ニュース・お得・欲しいものの各チャンネルへ振り分けています。そのとき邪魔だったのが画像のプレビューで、URLを<>で囲んで止めています。
翌週には、通知を出しているジョブすべてで「URLは<>で囲む」に統一しました。お得チャンネルのポイ活ニュースにも、同じ週のうちに同じルールを足しています。
flagsにSUPPRESS_EMBEDS(4)を付ける
メッセージ単位で止めたいときは、flags に 4 を渡します。開発者ドキュメントでは SUPPRESS_EMBEDS(1 << 2)と呼ばれるフラグです。
payload = {
"content": "今日の記事\nhttps://example.com/a\nhttps://example.com/b",
"flags": 4, # SUPPRESS_EMBEDS:このメッセージのプレビューを全部止める
}
requests.post(WEBHOOK_URL, json=payload, timeout=10).raise_for_status()
本文の文字列をいじらずに済むので、外部から受け取った文章をそのまま流すときに向いています。
ただし、自分で作ったembedも表示されなくなります。embedのカードを使う通知では <> のほうを選びます。
24本のジョブで使って分かった3つの落とし穴
Webhookは「送れば届く」ので、失敗しても気づきにくい仕組みです。
送る側のログには「送信しました」と残り、受け取る側のチャンネルは静かなまま。通知が来ないのは平和だからなのか、壊れているからなのか、見た目では区別がつきません。
私が通知クラスに入れている対策と、実際に起きた事故を3つまとめます。
2000字を超えると送れない→1900字で分割
content は1メッセージ2,000字までです。超えると 400 Bad Request が返り、メッセージは1文字も届きません。
ニュースの要約やエラーのスタックトレースは、すぐ2,000字を超えます。私の通知クラスは、余裕を見て1,900字ごとに切って順番に送っています。
MAX_LENGTH = 1900 # 上限2000字に対して余裕を持たせる
def send_message(text: str) -> bool:
"""長い本文を1900字ごとに分けて送る"""
ok = True
for i in range(0, len(text), MAX_LENGTH):
if not post({"content": text[i : i + MAX_LENGTH]}):
ok = False
return ok
正直に書くと、この切り方には不満があります。文字数で機械的に切るので、URLやコードブロックの途中で2通に分かれることがあるからです。行の区切りで切るほうが読みやすく、いずれ直したいところです。
429が返ったらretry_afterの秒数だけ待つ
短い時間に送りすぎると、Discordは 429 Too Many Requests を返します。Discordの安全ガイドによると、1つのWebhookから送れるのは1分あたり30件までです。
429の応答には、何秒待てばよいかが retry_after に入っています。その秒数だけ待って送り直すのが正しい対処です。
import time
def post(payload: dict, max_retries: int = 3) -> bool:
"""429なら指定の秒数だけ待って送り直す"""
for _ in range(max_retries):
res = requests.post(WEBHOOK_URL, json=payload, timeout=15)
if res.status_code == 429:
time.sleep(res.json().get("retry_after", 1))
continue
res.raise_for_status()
return True
return False
実は私の通知クラスは、ここを手抜きしています。どのエラーでも2秒待って3回まで送り直すだけで、retry_after を読んでいません。
1回の通知は多くても数通程度で、1分30件に届く使い方をしていないからです。10本単位で連続して送る作りにするなら、上のように retry_after を見る形に直すべきです。
URLが未設定だと黙って何も送られない
一番怖いのは、エラーにすらならないパターンです。私の通知クラスは、Webhook URLが見つからないと通知を「無効」にして、静かに処理を続けます。
設定漏れで本体のジョブまで止まらないのは長所です。ただ、裏を返すと「何も届いていないのに誰も気づかない」状態を作れてしまいます。
2026年5月、通知先をSlackからDiscordへ切り替えました。Pythonのスクリプトは移しましたが、シェルスクリプトの運用ジョブ(Homebrewの更新や依存パッケージの監査)はSlack向けの設定を読んだままでした。その設定ファイルにはWebhookが入っておらず、10月24日に気づくまで、それらのジョブの通知は1件も送られていませんでした。
対策は、URLがないときにログへ必ず一行残すことと、「通知が届いているか」をたまに受け取る側で確かめることです。私はいま、PythonもシェルスクリプトもDISCORD_NEWS_WEBHOOK_URLという同じ環境変数から読む形にそろえています。
ファイル添付と共通クラスでの使い回し
私は、スクリプトごとに requests.post を書かず、送り方を1つのクラスにまとめています。
1か所にまとめておくと、2,000字の分割やリトライを直すときに、24本を1本ずつ触らずに済みます。
最後に、長いレポートをファイルで送る方法と、私の共通クラスの形を紹介します。
レポートをファイルで添付する(multipart)
数千字のレポートは、本文に貼るよりファイルで添付したほうが読みやすくなります。ファイルを送るときは、JSONではなく multipart/form-data で送ります。
import json
with open("report.md", "rb") as fp:
res = requests.post(
WEBHOOK_URL,
# ファイル以外の項目は payload_json にJSON文字列で入れる
data={"payload_json": json.dumps({"content": "週次レポートです"})},
files={"files[0]": ("report.md", fp)},
timeout=15,
)
res.raise_for_status()
本文にローカルのファイルパスを書いても、Discordからは開けません。添付にしておけば、スマホからでもそのままダウンロードできます。
共通クラスにまとめて24本から呼ぶ
私の通知クラスは、ここまでの対策をひとまとめにしたものです。各スクリプトからは、次のように呼ぶだけです。
from discord_notifier import DiscordNotifier
# python/.env を読み、URLが無ければ None を返す
notifier = DiscordNotifier.from_env("DISCORD_NEWS_WEBHOOK_URL")
if notifier:
notifier.send_result("RSS監視", "3件の新着記事") # ✅ RSS監視 — 3件の新着記事
notifier.send_error("取得失敗", "タイムアウト", context="4gamer") # 🚨 付きで送る
私の通知クラス(DiscordNotifier)のメソッド
形をそろえたのは、チャンネルを流し見したときに❌の行だけを拾えるようにするためです。どのジョブの通知も「✅ スクリプト名 — 要約」の1行目で始まり、最後に送った時刻が付きます。
Discord Webhookのよくある質問
Discord WebhookをPythonから使うときに、つまずきやすい点をまとめました。送信できない原因の多くは、URL・文字数・送る頻度のどれかです。細かい書き方は各見出しのコードもあわせて確認してください。
Q1. Discord Webhookは無料で使えますか?
A1. 無料です。Nitroなどの有料プランは要りません。自分が管理者のサーバーなら、チャンネルごとにWebhookを作れます。
Q2. Discord Webhookでメッセージを読んだり返信したりできますか?
A2. できません。Webhookは送信専用です。メッセージを読んで返事をしたい場合は、Botを作る必要があります。
Q3. Webhookで送ると400 Bad Requestになるのはなぜですか?
A3. 本文の2,000字超え、embedの上限超え、content と embeds のどちらも空、のいずれかが多い原因です。応答の本文に、どの項目が問題かが書かれています。
Q4. 429 Too Many Requestsが返ってきたらどうすればいいですか?
A4. 応答の retry_after に書かれた秒数だけ待ってから送り直します。Discordの安全ガイドでは、1つのWebhookから送れるのは1分30件までです。
Q5. URLのプレビュー(埋め込み)を出さないようにできますか?
A5. できます。URLを <> で囲むとそのURLだけ、flags に 4 を付けるとメッセージ全体のプレビューが出なくなります。
Q6. Webhook URLが漏れたらどうすればいいですか?
A6. サーバー設定の「連携サービス」から、そのWebhookを削除して作り直してください。URLを知っている人は誰でも投稿できるため、削除するまで止める方法はありません。
Discord Webhook通知を続けるコツのまとめ
Discord Webhookは、URLにJSONを1回POSTするだけで通知を送れる、いちばん手軽な仕組みです。
ただ、手軽なぶん「届いていない」に気づきにくい性質があります。
半年ほど24本のジョブで使ってきて、効いたのは次の4つでした。
- Webhook URLは
.envに書き、定期実行でも読み込まれるようにする - 本文は1,900字ごとに分け、429では
retry_afterだけ待つ - ニュースのURLは
<>で囲み、プレビューで画面を埋めない - 送り方は共通クラスにまとめ、URLがないときはログに残す
一番効くのは、届かなかったときに気づける形にしておくことです。通知は、来ないことが一番の異常だからです。
届いたら、普段手で確認している処理の終わりに1行足すだけです。cronやlaunchdで回すなら、URLを.envから読む形にしておくと後で困りません。
続きを追う:X(@Tom_Java_Tech) / RSS
途中経過は、また書きます。

