Discord webhook отправляет сообщение в канал одним HTTP POST и не требует отдельного бота. Скрипт возьмёт секретный URL из переменной окружения, отправит JSON и дождётся подтверждения. В коде не будет токена, который легко случайно загрузить на GitHub.
Webhook подходит для уведомлений о сборке, резервной копии, новом заказе или результате учебного скрипта. У него узкая задача: принимать данные по выданному URL и публиковать их в выбранный канал. Команды, чтение переписки и сложная логика уже требуют приложения или бота. Именно поэтому webhook проще для первого знакомства с HTTP, но обращаться с его URL нужно как с паролем.
В настройках сервера выбираем канал и копируем Webhook URL.
Python кодирует content в JSON и делает POST с Content-Type.
wait=true возвращает созданное сообщение, а ошибки не теряются.
Что такое webhook и где взять URL
Откройте настройки своего Discord-сервера, затем Integrations и Webhooks. Создайте webhook, выберите текстовый канал и скопируйте URL. Для этого нужны права управления webhook. Ссылка уже содержит идентификатор и токен, поэтому дополнительный логин в Python не нужен. Добавлять её прямо в файл нельзя: код будем читать значение из DISCORD_WEBHOOK_URL.
| Часть | Где находится | Зачем нужна |
|---|---|---|
| Webhook URL | Переменная окружения | Адрес и секрет доступа |
| content | JSON-тело | Текст сообщения до 2000 символов |
| allowed_mentions | JSON-тело | Запрещает неожиданные упоминания |
| wait=true | Строка запроса | Возвращает созданное сообщение |
Webhook URL заменяет авторизацию для этого узкого действия. Тот, кто получил ссылку, сможет писать в канал от имени webhook, поэтому URL нельзя показывать в статье, скриншоте, логе или репозитории.

От Python до выбранного канала. wait=true возвращает подтверждение
Отправляем сообщение стандартной библиотекой Python
Пример использует только стандартную библиотеку. Он добавляет wait=true, ограничивает длину content и передаёт allowed_mentions с пустым parse, чтобы строка из внешнего источника неожиданно не позвала @everyone. Перед запуском задайте переменную окружения в своём терминале, не вставляя реальный URL в файл.
import json
import os
from urllib.error import HTTPError, URLError
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
from urllib.request import Request, urlopen
def add_wait_parameter(url: str) -> str:
parts = urlsplit(url)
query = dict(parse_qsl(parts.query, keep_blank_values=True))
query["wait"] = "true"
return urlunsplit((
parts.scheme,
parts.netloc,
parts.path,
urlencode(query),
parts.fragment,
))
def send_message(webhook_url: str, text: str) -> dict:
clean_text = text.strip()
if not clean_text:
raise ValueError("Сообщение не может быть пустым")
if len(clean_text) > 2000:
raise ValueError("Discord принимает до 2000 символов в content")
payload = {
"content": clean_text,
"allowed_mentions": {"parse": []},
}
data = json.dumps(payload).encode("utf-8")
request = Request(
add_wait_parameter(webhook_url),
data=data,
headers={
"Content-Type": "application/json",
"User-Agent": "codik-webhook-example/1.0",
},
method="POST",
)
with urlopen(request, timeout=10) as response:
return json.load(response)
def main() -> None:
webhook_url = os.environ.get("DISCORD_WEBHOOK_URL")
if not webhook_url:
raise SystemExit("Сначала задайте DISCORD_WEBHOOK_URL")
try:
message = send_message(
webhook_url,
"Сборка завершена. Проверка webhook из Python прошла.",
)
except HTTPError as error:
raise SystemExit(f"Discord вернул HTTP {error.code}") from error
except URLError as error:
raise SystemExit(f"Нет соединения: {error.reason}") from error
print(f"Сообщение отправлено, id: {message['id']}")
if __name__ == "__main__":
main()В исходнике нет реального Webhook URL. POST содержит JSON и правильный Content-Type. wait=true заставляет Discord вернуть объект сообщения. HTTP-ошибка и отсутствие сети дают разные понятные сообщения.

Выбирайте по задаче, а не по моде. Сравнение показывает, какой вариант легче поддерживать и проверять.
Почему URL webhook считается секретом
У входящего webhook есть URL с токеном. При выполнении endpoint Discord ждёт хотя бы content, embeds, components, файл или poll. Без параметра wait успешная отправка обычно отвечает 204 No Content. С wait=true сервер ждёт подтверждения и возвращает созданное сообщение, поэтому скрипт может прочитать его id. Это удобнее для первого теста: успех подтверждается ответом API, а не только появлением текста в приложении.
| Ситуация | Ожидаемый ответ | Что проверить |
|---|---|---|
| Успех с wait=true | 200 и JSON сообщения | Текст и id |
| Неверный или удалённый URL | 401 или 404 | Пересоздать webhook |
| Слишком длинный content | Проверка до запроса | Не больше 2000 символов |
| Слишком много запросов | 429 | Не повторять запрос в тесном цикле |
Никогда не публикуйте настоящий URL webhook. Переменная окружения защищает только от случайного попадания в код, но не делает секрет неуязвимым: не печатайте её, не добавляйте в traceback и не показывайте терминал на стриме. Если ссылка утекла, удалите webhook или смените токен в настройках Discord. Для текста из формы оставляйте allowed_mentions: {"parse": []}, иначе строка пользователя может вызвать массовое упоминание.
Практика: проверяем ответ и типичные ошибки
Создайте отдельный тестовый канал и webhook. Сначала отправьте короткую нейтральную строку. Затем проверьте пустое сообщение, отключённую сеть и временно испорченный конец URL. Не устраивайте цикл из десятков запросов: API применяет ограничения частоты, а уведомление должно отправляться по событию, а не непрерывно. После теста удалите сообщение вручную, если оно больше не нужно.
- Создайте тестовый webhook в Server Settings → Integrations → Webhooks.
- Задайте DISCORD_WEBHOOK_URL в текущем терминале, не добавляя ссылку в файл.
- Запустите
python send_discord.pyи найдите сообщение в выбранном канале. - Сверьте напечатанный id с данными ответа, не выводя сам URL.
- Испытайте пустую строку через вызов send_message и получите ValueError до сети.
- Удалите или поверните webhook после теста, если URL мог попасть в запись экрана.
Сообщение появляется только в выбранном канале. Секрет отсутствует в исходнике и истории Git. Скрипт различает HTTP-ошибку и сетевую ошибку. Внешний текст не создаёт упоминания автоматически.

До первого реального уведомления. Пройдите все четыре сценария перед публикацией проекта.
Доводим уведомление до надёжного скрипта
Следующий шаг: вынести текст в аргумент командной строки или вызывать send_message после реального события, например завершения резервной копии. Не превращайте webhook в универсального бота. Если нужно читать сообщения, отвечать на команды, работать с ролями или хранить состояние пользователей, используйте Discord-приложение и библиотеку для бота. У webhook другой плюс: маленькая поверхность и один понятный POST.
Секрет легко уйдёт в GitHub или скриншот. Читайте его из окружения.
Discord ожидает корректно закодированный JSON.
Добавьте wait=true и читайте ответ API.
Передавайте allowed_mentions и не включайте массовые упоминания без необходимости.
Что получится в итоге

Готовый скрипт отправляет одно безопасное уведомление, получает id сообщения и не хранит секретный URL в исходнике.
Нужен ли бот-токен?
Нет. Входящий webhook использует собственный URL с токеном.
Зачем wait=true?
Чтобы Discord дождался сохранения и вернул JSON созданного сообщения.
Что делать, если URL попал в GitHub?
Сразу удалить или повернуть webhook в настройках Discord, затем очистить секрет из истории репозитория.
Как понять, что проект готов?
Повторите основной сценарий несколько раз, затем проверьте пустой ввод, граничные значения и повторный запуск. Ошибка должна объяснять, что исправить, а не закрывать программу.
Можно ли скопировать код целиком?
Можно использовать его как стартовую точку. После первого запуска измените хотя бы один параметр и объясните себе, где хранятся данные, где выполняется действие и где появляется результат.
Функции, JSON, исключения и HTTP можно закрепить в курсе Python. Для полноценного приложения откройте Discord-бота на Python.
Если хотите разобрать запросы со стороны браузера, изучите Fetch API. Серверные запросы подробнее разобраны в статье про API и requests. В Кодике удобно сначала протестировать функцию с подменённым HTTP-ответом.