Discord називає «вебхуками» дві речі, які поводяться абсолютно по-різному. Одна — це URL за принципом «надіслав і забув» для публікації повідомлень. Інша — endpoint interactions, який отримує підписані POST щоразу, коли користувач виконує slash-команду. Перша працює на localhost. Друга — ніколи. Цей посібник про другу, бо саме їй потрібен тунель.
Два різновиди вебхуків Discord
Вихідні вебхуки — це URL, на які ви робите POST. Ви надсилаєте JSON-повідомлення, воно зʼявляється в каналі. Тунель не потрібен, бо трафік іде з вашої машини назовні.
Endpoint-и interactions — у зворотному напрямку. Ви реєструєте URL у Discord; Discord робить POST на нього, коли користувач виконує /вашу-команду. Endpoint має бути HTTPS, відповідати менш ніж за три секунди і перевіряти підпис Ed25519 на кожному запиті. Localhost сам цього не вміє.
Чому endpoint-и interactions суворіші за більшість
Discord хоче не лише HTTPS — він вимагає довести, що ви контролюєте endpoint, перш ніж прийняти URL. Перевірка відбувається при реєстрації: Discord шле interaction PING, ваш сервер має відповісти підписаним PONG. Якщо перевірка Ed25519 провалиться хоч раз, Discord відмовиться зберігати URL.
Частину з Ed25519 з першого разу ви не налаштуєте. Майже ні в кого не виходить.
Перевірка Ed25519 — частина, що кусає
Discord використовує Ed25519 (не HMAC SHA-256) і надає два заголовки на кожній interaction:
X-Signature-Ed25519— підпис у hexX-Signature-Timestamp— мітка часу в секундах
Ви підписуєте timestamp + rawBody публічним ключем, який Discord показує в порталі розробника. У більшості мов для цього є бібліотека. У Node:
import nacl from 'tweetnacl';
const valid = nacl.sign.detached.verify(
Buffer.from(timestamp + rawBody),
Buffer.from(signature, 'hex'),
Buffer.from(PUBLIC_KEY, 'hex'),
);
Причина номер один провалу перевірки — знову — middleware, який парсить тіло до того, як виконається цей код. Вам потрібен сирий рядок, який надіслав Discord, а не перезібрана версія розпарсеного обʼєкта.
Крок за кроком: endpoint interactions на localhost
- Запустіть HTTP-сервер вашого бота локально (будь-який порт).
- Виконайте
npx portpreview 3000, щоб отримати публічний HTTPS-адрес. - У порталі розробника Discord відкрийте застосунок і вставте URL тунелю разом зі шляхом interactions у Interactions Endpoint URL.
- Натисніть Save. Discord одразу надішле
PING. Якщо перевірка Ed25519 працює, URL збережеться. Якщо ні, поле покаже помилку. - Після збереження виконайте slash-команду на будь-якому сервері, де є ваш бот. Запит надійде у ваш обробник.
Якщо збереження раз за разом не вдається, дослідіть захоплений запит — заголовки, тіло і відповідь вашого сервера. У девʼяти випадках із десяти тіло було тихо змінене JSON-middleware до перевірки.
Дедлайн у три секунди
Discord вимагає відповідь менш ніж за три секунди, інакше зараховує interaction як провалену. Для всього, що довше, надішліть першу відповідь типу 5 (відкладену), а потім PATCH-ом оновіть вихідну interaction справжньою відповіддю пізніше. Локальне тестування одразу показує, які обробники надто повільні — захоплення запитів показує точну затримку.
Вихідні вебхуки досі корисні для тестів
Якщо потрібно лише надсилати повідомлення в канал із локального коду, URL вихідного вебхука працює без тунелю. POST JSON — повідомлення зʼявилося. Ми використовуємо це для алертів із логів локальних прогонів.
Коли ця схема перестає вистачати
Для одного бота з кількома командами локального тестування з тунелем цілком досить. Якщо ви ведете публічного бота на мільйони користувачів, з часом захочеться staging-розгортання зі стабільним URL, щоб Discord не перевіряв заново щоразу при ротації сесії тунелю.
Тайм-аут Discord у три секунди — та сама родина проблем, що й десятисекундне вікно GitHub. Прочитайте наш погляд на повтори вебхуків та ідемпотентність для докладної версії. Приєднайтеся до списку очікування PortPreview заради тунелю + захоплення в одному CLI.