Markdown в Discord Webhook: полный справочник по форматированию
Справочник по markdown в Discord webhook: стили текста, заголовки, код, спойлеры, скрытые ссылки, таймстампы <t:unix:R>, упоминания и allowed_mentions, эмодзи.
На этой странице
- Стили текста
- Заголовки, списки и цитаты
- Код
- Спойлеры, ссылки и превью
- Таймстампы: <t:unix:STYLE>
- Упоминания и allowed_mentions
- Кастомные эмодзи
- Content и embed: в чём разница
- Экранирование
- Частые ошибки
- FAQ
- Как сделать таймстамп Discord, который показывает время в часовом поясе читателя?
- Работают ли скрытые ссылки в сообщениях вебхука?
- Почему упоминание отрисовывается, но никого не уведомляет?
- Можно ли использовать markdown-таблицы в Discord?
- Что дальше
Всё, что отправляет вебхук, проходит через тот же рендерер markdown, что и обычное сообщение, набранное в чате. Правила почти те же, но есть пара бонусов, доступных только ботам и вебхукам (например, скрытые ссылки прямо в тексте), и несколько вещей, которые внутри embed ведут себя иначе. Это полный справочник по markdown в Discord webhook: стили текста, заголовки, списки, код, цитаты, спойлеры, ссылки, таймстампы, упоминания, кастомные эмодзи и экранирование всего этого, когда текст приходит от пользователей или из логов.
Все примеры — JSON-тела для POST на https://discord.com/api/webhooks/{id}/{token} с заголовком Content-Type: application/json. Одно правило на всю статью: перенос строки внутри JSON-строки — это \n, а не настоящий Enter, а кавычка — \".
Стили текста
| Markdown | Результат |
|---|---|
**жирный** | жирный |
*курсив* или _курсив_ | курсив |
__подчёркнутый__ | подчёркнутый текст |
~~зачёркнутый~~ | |
***жирный курсив*** | жирный курсив |
__**подчёркнутый жирный**__ | подчёркнутый жирный |
`код в строке` | код в строке |
||спойлер|| | скрыт до клика |
Стили вкладываются друг в друга, если закрывать их в обратном порядке. Маркеры должны прилегать к тексту вплотную: * курсив * с пробелами внутри отрисуется как обычные звёздочки.
{
"content": "**Деплой завершён** — *v2.4.1* выкачена на __production__.\n~~Планировался откат~~ Не понадобился. Сборка: `a1f9c3e`"
}
Заголовки, списки и цитаты
Заголовки — это #, ## и ### в начале строки, после решётки обязателен пробел. Четвёртого уровня нет: #### останется обычным текстом. Списки начинаются с - или * , нумерованные — с 1. ; вложенность даёт отступ в два пробела.
{
"content": "## Релиз 2.4.1\n### Изменения\n- Быстрее доставка вебхуков\n- Новая команда `/status`\n - Показывает длину очереди\n### Исправления\n1. Падение на пустом embed\n2. Неверный часовой пояс в футере"
}
Цитаты бывают двух видов. > в начале строки цитирует одну строку. >>> цитирует всё от этого места до конца сообщения, поэтому ставьте его последним:
{
"content": "Сообщил QA:\n> Кнопка входа не реагирует в Safari\n\nПолный лог:\n>>> 12:01 клик\n12:02 запрос не ушёл\n12:03 ошибка в консоли"
}
Markdown-таблиц в Discord нет. Таблица вида | a | b | придёт как текст с вертикальными чертами. Для колонок используйте поля embed с inline: true, для выравнивания моноширинным шрифтом — блок кода.
Код
Код в строке — одинарные обратные кавычки; блок — три обратные кавычки и, по желанию, имя языка для подсветки:
{
"content": "Тело запроса:\n```json\n{ \"user\": 42, \"plan\": \"pro\" }\n```\nА `diff` подсвечивает строки красным и зелёным:\n```diff\n- retries: 3\n+ retries: 5\n```"
}
Внутри блока кода ничего больше не парсится: ни жирный, ни упоминания, ни эмодзи. Поэтому это самый безопасный контейнер для вывода логов. Единственное, что в блок кода не положить, — три обратные кавычки подряд; если они есть в самом логе, отправьте его файлом.
Спойлеры, ссылки и превью
||текст|| скрывает текст до клика. Работает вокруг любого строчного содержимого, включая кастомные эмодзи и скрытые ссылки. Для вложений добавьте к имени файла префикс SPOILER_ при загрузке.
Скрытые ссылки [текст](https://example.com) обычные пользователи в чате написать не могут, а вебхук может — прямо в content, а также в описании и полях embed:
{
"content": "Сборка ||упала|| — смотрите [лог пайплайна](<https://ci.example.com/job/1842>) и [ранбук](<https://wiki.example.com/deploy>).\nБез превью: <https://ci.example.com/job/1842>"
}
Голый URL в content заставляет Discord подтянуть превью под сообщением, и URL внутри скрытой ссылки — тоже. Оберните URL в угловые скобки — <https://…> или [текст](<https://…>), — чтобы превью не было, или передайте "flags": 4 (SUPPRESS_EMBEDS), чтобы отключить все превью в этом сообщении сразу.
Таймстампы: <t:unix:STYLE>
Тег <t:UNIX:STYLE> — единственный способ показать время так, чтобы каждый читатель видел его в своём часовом поясе и локали. UNIX — секунды с 1970-01-01 UTC (не миллисекунды), STYLE — одна буква:
| Стиль | Тег | Как выглядит (русская локаль) |
|---|---|---|
t | <t:1789646400:t> | 12:00 |
T | <t:1789646400:T> | 12:00:00 |
d | <t:1789646400:d> | 17.09.2026 |
D | <t:1789646400:D> | 17 сентября 2026 г. |
f | <t:1789646400:f> | 17 сентября 2026 г., 12:00 |
F | <t:1789646400:F> | четверг, 17 сентября 2026 г., 12:00 |
R | <t:1789646400:R> | через 3 часа / 2 дня назад |
Без стиля получите f. R обновляется вживую — то, что нужно для строк «начнётся через», «истекает через» и «последний раз в сети». Число берётся из шелла командой date +%s или в коде:
const startsAt = Math.floor(Date.now() / 1000) + 3600; // через час
await fetch(process.env.WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: `Техработы начнутся <t:${startsAt}:R> (<t:${startsAt}:F>)`,
allowed_mentions: { parse: [] },
}),
});
import os
import time
import requests
starts_at = int(time.time()) + 3600
requests.post(os.environ["WEBHOOK_URL"], json={
"content": f"Техработы начнутся <t:{starts_at}:R> (<t:{starts_at}:F>)"
})
Не путайте с полем timestamp на уровне embed: там строка ISO 8601 ("2026-09-17T12:00:00Z"), она отрисовывается в футере и синтаксис <t:…> не принимает. Сам тег внутри описания и полей embed работает.
Упоминания и allowed_mentions
Упоминание — это ID в угловых скобках: <@userId> — пользователь, <@&roleId> — роль, <#channelId> — канал, плюс буквальные @everyone и @here. Чтобы получить ID, включите режим разработчика в настройках Discord и выберите «Копировать ID» в контекстном меню.
Отрисовка и пинг — разные вещи. Синяя плашка упоминания появляется от одной только разметки, а кого уведомить, решает allowed_mentions. Всегда передавайте его явно:
{
"content": "<@&123456789012345678> деплой завершён, <@987654321098765432> проверь, пожалуйста. @everyone остаётся без пинга.",
"allowed_mentions": {
"parse": [],
"roles": ["123456789012345678"],
"users": ["987654321098765432"]
}
}
"parse": [] отключает автоматический разбор @everyone, @here, всех пользователей и всех ролей; массивы roles и users затем задают точный список тех, кого пинговать. Для текста извне (сообщения коммитов, поля форм) одного {"parse": []} достаточно как безопасного значения по умолчанию. Упоминания внутри embed отрисовываются, но никого не пингуют. Полный набор правил — в руководстве по упоминаниям.
Кастомные эмодзи
Стандартные Unicode-эмодзи вставляются в строку как есть ("content": "Задеплоено 🚀"). Кастомным эмодзи сервера нужна форма с ID: <:name:id> для статичных, <a:name:id> для анимированных. Чтобы её узнать, напишите эмодзи в Discord с обратным слэшем перед ним (\:deploy:) и отправьте — в сообщении окажется сырой тег.
{
"content": "<:deploy:123456789012345678> Продакшен обновлён <a:party:234567890123456789>"
}
Используйте эмодзи того сервера, которому принадлежит вебхук. Про эмодзи с других серверов сверьтесь с документацией Discord для разработчиков, прежде чем на них полагаться.
Content и embed: в чём разница
Одна и та же разметка парсится по-разному в зависимости от того, где она стоит:
| Возможность | content | Описание и поля embed | Заголовок, автор, футер embed |
|---|---|---|---|
| Жирный, курсив, код, цитаты, спойлеры | Да | Да | Нет (обычный текст) |
| Заголовки и списки | Да | Да | Нет |
| Скрытые ссылки | Да | Да | Нет |
Таймстампы <t:…> | Да | Да | Нет |
| Упоминания | Отрисовка и пинг | Отрисовка без пинга | Нет |
| Кастомные эмодзи | Да | Да | Нет |
| Превью для голых URL | Да | Нет | Нет |
| Лимит длины | 2000 | 4096 / 1024 | 256 / 256 / 2048 |
Два практических вывода. Нужна строка-заголовок в поле embed — используйте name поля, оно и так жирное. А если смысл сообщения — пинг, ставьте упоминание в content, а детали — в embed, не наоборот. Цвета, картинки и остальная вёрстка embed разобраны в руководстве по конструктору embed и статье про цвета embed.
Экранирование
Обратный слэш перед символом разметки делает его буквальным: \*не жирный\*, \|\|не спойлер\|\|, \# не заголовок (в начале строки), \> не цитата.
В JSON сам обратный слэш тоже надо экранировать, так что строка превращается в "\\*не жирный\\*". Именно это чаще всего упускают: одиночный \* внутри JSON-строки — невалидный JSON, и Discord ответит 400 Bad Request.
Если текст приходит оттуда, где вы его не контролируете (сообщения коммитов, названия тикетов, ввод пользователей), экранируйте его перед подстановкой:
function escapeMarkdown(text) {
return text.replace(/([\\*_~`|])/g, '\\$1').replace(/^([>#-])/gm, '\\$1');
}
const commit = 'fix: *очень* важная __штука__ #123';
const content = `Новый коммит: ${escapeMarkdown(commit)}`;
// Новый коммит: fix: \*очень\* важная \_\_штука\_\_ #123
import re
def escape_markdown(text: str) -> str:
text = re.sub(r"([\\*_~`|])", r"\\\1", text)
return re.sub(r"^([>#-])", r"\\\1", text, flags=re.M)
Две вещи, которые экранирование не решает. Упоминания слэшем не нейтрализуются — для этого есть allowed_mentions: {"parse": []}. А URL внутри текста всё равно развернутся в превью, если не обернуть их в <…>.
Частые ошибки
- Переносы строк не появляются. Вы отправили настоящий перенос внутри JSON-строки или забыли
\n. Каждый перенос вcontentи тексте embed должен быть\n. - Разметка видна как звёздочки. Пробелы между маркером и текстом (
* текст *) либо текст стоит в заголовке, футере или авторе embed, где markdown не рендерится. <@123456789012345678>показывается буквально. ID неверный или вместо ID подставлено имя пользователя. Скопируйте ID через режим разработчика.- Упоминание отрисовалось, но никого не пингует. В
allowed_mentionsнет этого пользователя или роли, либо упоминание стоит внутри embed. - Таймстамп показывает бессмысленную дату. Вы передали миллисекунды. Разделите
Date.now()на 1000 и округлите вниз. >>>съел остаток сообщения. Так и задумано. Если после цитаты нужен текст, используйте>на каждой строке.- 400 Bad Request с JSON-ошибкой. Обычно
contentдлиннее 2000 символов, описание embed длиннее 4096 или неэкранированный слэш либо кавычка в JSON. В теле ответа названо проблемное поле; полный список — в статье про ошибки вебхуков. | таблица |пришла как палочки. В Discord нет синтаксиса таблиц; используйте inline-поля или блок кода.
FAQ
Как сделать таймстамп Discord, который показывает время в часовом поясе читателя?
Используйте <t:UNIX:STYLE>, где UNIX — секунды с начала эпохи, а STYLE — одна из букв t, T, d, D, f, F или R. <t:1789646400:R> отрисуется как «через 3 часа» и обновляется вживую; F даёт полный день недели, дату и время.
Работают ли скрытые ссылки в сообщениях вебхука?
Да. Вебхук может ставить [текст](https://url) прямо в content, и тот же синтаксис работает в описании и полях embed. Превью создают и голые URL, и скрытые ссылки; оберните URL в <>, чтобы его не было.
Почему упоминание отрисовывается, но никого не уведомляет?
Потому что пинг управляется allowed_mentions, а не разметкой. Добавьте ID пользователя или роли в allowed_mentions.users или allowed_mentions.roles и держите упоминание в content: упоминания внутри embed никогда не пингуют.
Можно ли использовать markdown-таблицы в Discord?
Нет. В рендерере Discord нет синтаксиса таблиц. Для сетки используйте поля embed с inline: true, для моноширинных колонок — блок кода.
Что дальше
Самый быстрый способ проверить, как будет выглядеть разметка, — вставить её в конструктор Discord Webhook: предпросмотр рендерит content и embed так же, как клиент, а готовый payload можно выгрузить как JSON или код для discord.js и discord.py. Дальше — руководство по упоминаниям со всеми случаями allowed_mentions, руководство по конструктору embed про вёрстку embed и статья про цвета embed с десятичными значениями для боковой полоски.