Вебхук Discord - это URL, который публикует сообщения в один канал. Вы создаёте его в настройках канала, отправляете ему HTTP-запрос POST с небольшим JSON-телом, и в канале появляется сообщение под тем именем и аватаром, которые вы выбрали. Бот-аккаунт не нужен, токен регистрировать не нужно, библиотеку ставить не нужно, и держать запущенный процесс тоже не нужно. Поэтому вебхук подходит для односторонних сообщений от программ - деплой завершён, форма отправлена, backup не удался, вышло новое видео - и не подходит для всего, что должно слушать и отвечать.
Всё описанное ниже работает из любого языка и с любой машины, которая умеет делать HTTPS-запросы. Ошибаются обычно в другом: URL - это единственный секрет, и он никогда не истекает; несколько параметров запроса (wait, thread_id) полностью меняют поведение; а вебхук, к которому применили ограничение частоты запросов, сам сообщает, сколько ждать, если прочитать ответ. Если вам нужно живое сообщение о состоянии игрового сервера, это целиком разобрано в статье Discord webhooks for game server alerts; этот материал - общий справочник, на который она опирается.
Что такое вебхук и почему весь он - это URL#
Вебхук принадлежит каналу. У него есть числовой id и длинный случайный токен, а URL - это просто они оба, соединённые вместе:
https://discord.com/api/webhooks/1290000000000000000/Xy7k...the-token...Qp2Любой, кто может отправить на этот URL запрос с правильным телом, может писать в канал. Второго фактора нет, входа в систему нет, область действия - только этот канал, срока действия нет. URL - это и есть пароль, и он действует, пока кто-нибудь не удалит вебхук. Именно из этого факта вытекает большая часть советов по безопасности в конце статьи.
Сообщение, отправленное через вебхук, показывает имя и аватар вебхука (или те, что вы переопределили для конкретного сообщения), а рядом с именем - небольшую метку, показывающую, что сообщение автоматическое. Вебхук не является участником сервера, не появляется в списке участников и не имеет ролей. Его нельзя упомянуть, ему нельзя отправить личное сообщение, и он ничего не видит в канале - в том числе собственных сообщений, кроме как через специальные эндпоинты, которые получают, изменяют или удаляют их.
Вебхуки встречаются и в двух местах, где вы можете их не ожидать:
- Подписка на канал объявлений. Когда сервер подписывается на канал объявлений другого сервера, Discord создаёт в канале назначения вебхук, который доставляет кросс-посты. Он отображается в списке интеграций, как и любой другой.
- Интеграции, которые пишут от вашего имени. Многие сервисы (системы CI, инструменты мониторинга, сам GitHub) просят у вас URL вебхука Discord, а не установку бота, именно потому, что вебхуку не нужны права за пределами одного канала.
Создание вебхука в настройках канала#
Вам нужно право Manage Webhooks в этом канале; у владельцев и администраторов сервера оно есть по умолчанию.
- Наведите курсор на канал и нажмите на значок шестерёнки (Edit Channel) либо щёлкните канал правой кнопкой и выберите Edit Channel.
- Откройте Integrations, затем Webhooks.
- Нажмите New Webhook. Discord сразу создаст вебхук со сгенерированным именем.
- Нажмите на него, чтобы развернуть, задайте имя, при желании загрузите аватар и проверьте, что в выпадающем списке выбран нужный канал.
- Нажмите Copy Webhook URL, а затем Save Changes, если что-то меняли.
Server Settings, Integrations, Webhooks показывает то же самое для всего сервера: каждый вебхук в каждом канале и того, кто его создал. Эта страница нужна для аудита и наведения порядка, а журнал аудита сервера фиксирует создание, изменение и удаление вебхуков.
На момент написания действуют несколько практических ограничений: в канале может быть до 15 вебхуков, имя вебхука должно содержать от 1 до 80 символов, а имена со словами discord или clyde отклоняются. Discord время от времени меняет эти значения, так что если при создании возникает ошибка о лимитах, смотрите документацию для разработчиков.
Называйте вебхук по источнику - CI, Backups, Shop orders - а не оставляйте «Captain Hook» или другое сгенерированное имя. Имя становится автором по умолчанию для каждого сообщения и меткой, которую вы будете искать в списке интеграций, когда что-то пойдёт не так.
Первое сообщение через curl#
Минимальный корректный запрос - это POST с JSON-телом, в котором есть content:
$ curl -X POST "$WEBHOOK_URL" \ -H "Content-Type: application/json" \ -d '{"content": "Deploy finished on main."}'При успехе Discord отвечает 204 No Content с пустым телом. Это не ошибка, и пустой ответ не случаен: по умолчанию API не дожидается сохранения сообщения, прежде чем ответить. Добавьте ?wait=true, и вы получите 200 OK с полным объектом сообщения, включая его id, который понадобится, чтобы позже изменить или удалить сообщение.
Поля, которые пригодятся почти в каждом сообщении:
| Поле | Что делает |
|---|---|
content | Текст сообщения, до 2000 символов, разметка Discord |
username | Переопределяет имя вебхука только для этого сообщения |
avatar_url | Переопределяет аватар, прямая ссылка на изображение |
embeds | До 10 объектов embed (карточки с заголовком и полями) |
allowed_mentions | Определяет, какие упоминания действительно уведомляют |
flags | 4096 отправляет сообщение без уведомлений |
tts | Зачитывает сообщение вслух тем, у кого включён TTS |
Должно присутствовать хотя бы одно из полей content, embeds, файл или опрос, иначе запрос завершится ошибкой 400. Полный пример с переопределённым именем и отключёнными упоминаниями:
$ curl -X POST "$WEBHOOK_URL?wait=true" \ -H "Content-Type: application/json" \ -d '{ "username": "Shop", "avatar_url": "https://example.com/shop-icon.png", "content": "New order #1042 - 2 items, paid by card", "allowed_mentions": {"parse": []} }'avatar_url должен вести прямо на файл изображения, который Discord может скачать: страница, содержащая картинку, не подойдёт, а ссылка, требующая входа, молча заменится аватаром по умолчанию. username и avatar_url действуют только для одного сообщения; сохранённые имя и аватар вебхука не меняются.
В содержимом работает обычная разметка Discord: **bold**, встроенный код, блоки кода, ссылки с подписью вроде [changelog](https://example.com), упоминания пользователей как <@USER_ID>, упоминания ролей как <@&ROLE_ID> и метки времени вроде <t:1760000000:R>, которые отображаются как «3 часа назад» в часовом поясе каждого читателя. Будет ли упоминание действительно уведомлять, решает allowed_mentions. Пустой массив parse, как выше, означает, что не уведомляется никто, и это безопасный вариант для всего, что пересылает чужой текст. Чтобы упомянуть одну конкретную роль и никого больше, перечислите её явно:
{ "content": "<@&123456789012345678> the nightly build failed", "allowed_mentions": { "parse": [], "roles": ["123456789012345678"] }}Если что-то не так, Discord отвечает 400 и JSON-телом, где названо отклонённое поле. Прочитайте его, прежде чем менять что-либо ещё: оно почти всегда конкретнее, чем вы ожидаете.
Отправка из Python и JavaScript#
Для вебхука не нужна никакая библиотека Discord. Подойдёт любой HTTP-клиент, а кода так мало, что зависимость не обязательна.
Python с requests:
import osimport requestsurl = os.environ["WEBHOOK_URL"]payload = { "username": "Backups", "content": "Nightly backup finished: 412 MB in 38 s", "allowed_mentions": {"parse": []},}r = requests.post(url, json=payload, params={"wait": "true"}, timeout=10)r.raise_for_status()print("message id:", r.json()["id"])Если проект уже использует discord.py 2.x, в нём есть синхронный клиент вебхуков, которому не нужны ни бот, ни цикл событий:
from discord import SyncWebhookhook = SyncWebhook.from_url(os.environ["WEBHOOK_URL"])msg = hook.send("Deploy finished", username="CI", wait=True)print(msg.id)В JavaScript на Node 18 и новее fetch встроен, так что простой вариант обходится без пакетов:
const res = await fetch(`${process.env.WEBHOOK_URL}?wait=true`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: "CI", content: "Build 512 passed", allowed_mentions: { parse: [] }, }),});if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);const message = await res.json();console.log(message.id);В discord.js есть эквивалент - WebhookClient, который создаётся через new WebhookClient({ url }) и используется с send(). Он сам обрабатывает ограничения частоты запросов, что полезно, если скрипт отправляет много сообщений.
На любом языке храните URL в переменной окружения, а не в исходном коде. Остальной код - это всегда три шага: собрать словарь, отправить его как JSON, проверить код статуса. О том, где значение должно храниться на сервере, рассказывает статья Environment variables and secrets.
Embed, файлы и лимиты, которые приводят к 400#
Обычный content подходит для однострочных уведомлений. Для всего, у чего есть структура - заказ с несколькими полями, сборка с коммитом и автором, отчёт с цветом, означающим хорошо или плохо, - используйте embed:
{ "username": "CI", "embeds": [{ "title": "Build 512 passed", "url": "https://example.com/builds/512", "description": "main - fix login redirect", "color": 3066993, "fields": [ { "name": "Duration", "value": "2m 14s", "inline": true }, { "name": "Tests", "value": "418 passed", "inline": true } ], "footer": { "text": "runner-02" }, "timestamp": "2026-10-15T09:30:00.000Z" }]}На двух деталях спотыкаются почти все. color - это десятичное целое, поэтому цвет #2ECC71 отправляется как 3066993, а строка в этом поле даст 400. А timestamp должен быть в формате ISO 8601, и Discord затем показывает его в местном времени каждого читателя.
Лимиты применяются строго, а не обрезаются: 2000 символов content, 10 embed на сообщение, 256 символов в заголовке embed, 4096 в описании, 25 полей с именами до 256 символов и значениями до 1024 символов, и 6000 символов суммарно во всех embed одного сообщения. В статье про статус сервера есть полная таблица лимитов и описание того, что происходит, когда stack trace выходит за её пределы. Обрезайте текст на своей стороне до отправки.
Файлы отправляются как multipart/form-data, а не JSON. JSON-тело переносится в поле формы payload_json, а каждый файл - это поле с именем files[0], files[1] и так далее:
$ curl -X POST "$WEBHOOK_URL" \ -F 'payload_json={"content": "Error log from the failed run"}' \ -F 'files[0]=@build.log'Ограничение на размер загрузки зависит от уровня буста сервера, и Discord менял его не раз; на сервере без буста сейчас это 10 МБ на сообщение. Логи большего размера лучше хранить в объектном хранилище или системе логов, а в сообщении оставить ссылку.
Две конечные точки совместимости избавляют от необходимости писать адаптер. Если добавить /slack к URL вебхука, он принимает данные в формате Slack, а /github принимает формат вебхуков GitHub, так что можно вставить https://discord.com/api/webhooks/ID/TOKEN/github в настройки вебхука репозитория (тип содержимого application/json) и получать уведомления о коммитах и pull request без единой строки кода.
wait=true, треды и форум-каналы#
Строка запроса в URL отправки меняет поведение, и основную работу выполняют два параметра.
| Параметр | Эффект |
|---|---|
wait=true | Ответить 200 с сохранённым сообщением, а не 204 |
thread_id=ID | Отправить в существующий тред канала вебхука |
Используйте wait=true всякий раз, когда собираетесь снова обращаться к сообщению или хотите убедиться, что оно действительно сохранено, прежде чем скрипт пойдёт дальше. Без него 204 говорит лишь о том, что запрос принят, и ни о чём больше.
thread_id позволяет одному вебхуку писать во много тредов. Тред должен принадлежать каналу вебхука: вебхук в #builds может писать в любой тред под #builds, но не в тред в другом месте. Распространённый приём - один тред на релиз или на инцидент, с публикацией всех связанных сообщений в него, чтобы родительский канал оставался читаемым.
В форум-каналах и медиаканалах нет обычной области сообщений, есть только посты, поэтому вебхук должен указать, какой пост имеется в виду. Либо передайте thread_id, чтобы ответить в существующем посте, либо отправьте в теле thread_name, чтобы создать новый пост с вашим сообщением в качестве первого:
$ curl -X POST "$WEBHOOK_URL?wait=true" \ -H "Content-Type: application/json" \ -d '{"thread_name": "Incident 2026-10-15", "content": "API returning 502 since 09:12"}'Ответ на этот запрос содержит сообщение, а его channel_id - это id нового треда; сохраните его, если будете отправлять продолжения. Запрос к вебхуку форума без thread_id и без thread_name завершается ошибкой 400. Если форум требует теги, applied_tags принимает список id тегов.
Изменение, удаление и управление сообщениями#
Вебхук может изменять только те сообщения, которые отправил сам, и обращается к ним по id под своим URL:
# Read a message the webhook sent$ curl "$WEBHOOK_URL/messages/$MESSAGE_ID"# Edit it - only the fields you send are changed$ curl -X PATCH "$WEBHOOK_URL/messages/$MESSAGE_ID" \ -H "Content-Type: application/json" \ -d '{"content": "Deploy finished on main (rolled back 09:40)"}'# Delete it$ curl -X DELETE "$WEBHOOK_URL/messages/$MESSAGE_ID"Если сообщение находится в треде, добавьте ?thread_id=THREAD_ID и к каждому из этих URL, иначе Discord будет искать его в родительском канале и вернёт 404. При редактировании имя автора и аватар остаются прежними; username и avatar_url при редактировании не принимаются. Чтобы убрать embed, отправьте "embeds": []; чтобы убрать текст, отправьте "content": "" (в сообщении всё равно должно что-то остаться).
Редактирование никого не уведомляет. Поэтому изменение на месте - правильный вариант для всего, что меняется многократно: строки прогресса, длины очереди, живой карточки статуса, а новое сообщение - правильный вариант для событий, которые люди должны заметить. Статья про статус сервера строит небольшой конечный автомат именно на этом.
Сам вебхук управляется через тот же URL:
GET $WEBHOOK_URLвозвращает объект вебхука: его имя, id канала, id сервера и хеш аватара. Если запрос выполнен только с токеном, пользователь, создавший вебхук, в ответ не попадает. Это дешёвый способ проверить, что сохранённый URL ещё работает.PATCH $WEBHOOK_URLсnameи (или)avatarменяет сохранённые имя и аватар.avatar- это data URI (data:image/png;base64,...), а не ссылка. Для переноса вебхука в другой канал нужен токен бота или пользователя, а не токен вебхука.DELETE $WEBHOOK_URLудаляет вебхук. После этого любой запрос к URL возвращает404, навсегда.
Последний пункт важен: любой, у кого есть URL, может удалить вебхук. Это полезно, когда вы нашли утёкший URL в чужом paste, и досадно, когда небрежный скрипт случайно его выполнит.
Ограничения частоты и ответ 429#
Discord ограничивает частоту запросов на каждом маршруте, включая вебхуки. Практический потолок для одного вебхука - порядка пяти запросов за две секунды, а канал принимает примерно тридцать сообщений в минуту независимо от того, сколько вебхуков в него пишут. Эти цифры не опубликованы как фиксированные и меняются, поэтому не стоит зашивать в код паузу и считать вопрос решённым. Читайте ответ.
При превышении ответом будет 429 Too Many Requests с заголовками и телом:
{ "message": "You are being rate limited.", "retry_after": 0.53, "global": false }retry_after указан в секундах и может быть дробным. Подождите столько, затем повторите тот же запрос один раз. Заголовки X-RateLimit-Remaining и X-RateLimit-Reset-After позволяют аккуратному клиенту замедлиться ещё до того, как он упрётся в предел. Минимальная повторная отправка на Python:
import time, requestsdef send(url, payload, attempts=5): for _ in range(attempts): r = requests.post(url, json=payload, timeout=10) if r.status_code != 429: r.raise_for_status() return r time.sleep(float(r.json().get("retry_after", 1))) raise RuntimeError("still rate limited")Проблемы у вебхуков создаёт не редкий 429, а реакция на него. Скрипт, который мгновенно повторяет запрос в цикле, превращает полсекундное ожидание в лавину, а повторяющиеся некорректные запросы (ответы 401, 403 и 429) могут привести к временной блокировке IP-адреса отправителя во всём API Discord. Объединяйте запросы, где можно: десять строк лога, склеенных через переводы строк, - это один запрос; и делайте паузу при ошибках 5xx, которые Discord возвращает под нагрузкой.
Утёк URL вебхука: что делать#
URL вебхуков утекают самыми обыденными способами: попали в закоммиченный конфиг, вставлены в чат поддержки, видны на скриншоте, напечатаны в логе CI, встроены в клиентский JavaScript сайта. Последнее встречается чаще всего и проще всего предотвращается: любой URL, попавший в браузер, публичен, а существуют сканеры, которые ищут именно такой шаблон и используют находку, чтобы публиковать спам или фишинговые ссылки под баннером вашего сервера.
Когда URL утёк, порядок действий короткий, и выполнять его стоит именно в таком порядке:
- Удалите вебхук. Channel settings, Integrations, Webhooks, выберите вебхук, Delete Webhook. Или
curl -X DELETE "$WEBHOOK_URL"откуда угодно. Старый URL перестанет работать немедленно. Варианта «сбросить токен» нет; удаление и повторное создание и есть ротация. - Создайте новый в том же канале и поместите новый URL в переменную окружения или хранилище секретов, которое читает ваш отправитель. Перезапустите отправителя, чтобы он его подхватил.
- Очистите канал. Удалите всё, что опубликовал злоумышленник. Уже отправленные сообщения остаются после исчезновения вебхука, а раз вебхука больше нет, удалять их приходится вручную, как модератору.
- Уберите источник. Перепишите файл, отзовите paste, сделайте ротацию лога CI. Если URL был в Git-репозитории, удаления его новым коммитом недостаточно - он остаётся в истории, а утёкший URL и так уже не работает, так что сосредоточьтесь на том, чтобы новый никогда не попал туда же.
- Проверьте остальное. Server Settings, Integrations, Webhooks перечисляет каждый вебхук на сервере. Удалите те, происхождение которых никто не может объяснить.
Относитесь к URL как к паролю от базы данных: переменная окружения на отправляющем сервере, скрытый на скриншотах и в логах, по одному на отправителя, чтобы одна утечка означала одно удаление.
Вебхук или бот: как выбрать#
Выбор определяется направлением. Вебхук пишет в канал. Бот подключается к gateway Discord, получает события и может на них реагировать.
| Вам нужно | Используйте |
|---|---|
| Отправлять уведомления из скрипта, CI или сайта | Вебхук |
| Позже изменять или удалять эти уведомления | Вебхук |
| Писать под несколькими именами и аватарами | Вебхук |
| Отвечать на слэш-команды или кнопки | Бот |
| Читать чат, реакции или вступления участников | Бот |
| Назначать роли, модерировать, отправлять личные сообщения | Бот |
| Писать на многих серверах, которыми вы не владеете | Бот, приглашённый на каждый |
Вебхуку не нужно ничего запущенного между сообщениями, поэтому держать его ничего не стоит. Бот - это долгоживущий процесс с токеном, intents и соединением с gateway, которое должно оставаться на связи; если оно пропало, бот отображается как офлайн, а его команды не работают. Это отдельная задача хостинга, о ней рассказывают статьи hosting a Discord bot 24/7 для discord.js и hosting a discord.py bot для Python.
Стоит знать о двух гибридах. Бот может сам создавать вебхуки и публиковать через них сообщения под другими именами - так чат-мосты показывают внутриигровых игроков с их собственными именами и аватарами. А вебхук, созданный приложением (а не человеком в настройках канала), может отправлять интерактивные компоненты, такие как кнопки, потому что взаимодействия возвращаются этому приложению. Вебхук, созданный в настройках канала, ничего принимать не может, так что кнопкам на нём некуда было бы сообщать о нажатии.
Есть и отдельная функция, которую Discord называет webhook events: приложение может зарегистрировать HTTPS-эндпоинт в портале разработчика и получать определённые события, например авторизацию приложения, как входящие запросы. Это обратное направление - Discord обращается к вам - и предназначено оно для разработчиков приложений, а не как способ читать канал.
Последний вопрос - где работает отправитель. Вебхуку нужно лишь то, что умеет сделать запрос, когда происходит событие: задача CI, задание cron, уже существующий бэкенд. Если его ещё нет, хватит небольшого постоянно работающего сервера приложений. Тарифы RE:NODE для приложений на Node.js и Python начинаются от $4 в месяц, URL вебхука хранится как переменная окружения на вкладке Startup, а деплой идёт прямо из GitHub.
FAQ#
Нужно ли уметь программировать, чтобы использовать вебхук Discord?
Нет. Многие сервисы принимают URL вебхука и сами занимаются отправкой - вставьте его, и они начнут публиковать сообщения. Чтобы отправлять собственные сообщения, достаточно одной команды curl, а ещё есть веб-инструменты, которые собирают JSON за вас. Код нужен только тогда, когда ваша собственная программа решает, что и когда публиковать.
Почему мой вебхук возвращает 204 и не возвращает id сообщения?
Потому что 204 No Content - это стандартный ответ при успехе. Добавьте ?wait=true к URL, и Discord вернёт 200 с полным сообщением, включая id, который нужен, чтобы изменить или удалить его.
Может ли вебхук писать в несколько каналов?
Нет. Каждый вебхук принадлежит одному каналу, и через thread_id он может дотянуться только до тредов внутри этого канала. Для нескольких каналов создайте по вебхуку в каждом, что в любом случае лучше и с точки зрения утечек, и с точки зрения шума.
Может ли вебхук упоминать роль?
Да. Поместите <@&ROLE_ID> в content и укажите id в allowed_mentions.roles, оставив parse пустым, чтобы больше никто не получал уведомление. Id роли можно получить, щёлкнув роль правой кнопкой при включённом Developer Mode в расширенных настройках Discord; так же копируются id пользователей, каналов и сообщений.
Истекают ли URL вебхуков?
Нет. Вебхук работает, пока его не удалят, не удалят его канал или сервер не уберёт его. URL, который работал в прошлом году, а сегодня возвращает 404, был кем-то удалён - проверьте Server Settings, Integrations, Webhooks.
Почему вебхук отклоняет моё имя пользователя?
Имя должно содержать от 1 до 80 символов и не может включать discord или clyde. В теле ответа 400 указано, какое правило нарушено; сократите имя или уберите это слово.


