Як тестувати Slack Events API та interactivity
Дивимося, що Slack надсилає на Request URL, проходимо перевірку url_verification, валідуємо X-Slack-Signature і розкодовуємо form-encoded payload від interactivity.
Оновлено:
Slack-застосунок отримує два типи HTTP-запитів: колбеки Events API (повідомлення, згадки, реакції) у JSON і запити interactivity (кліки по кнопках, модалки, шорткати, slash-команди) у form-encoded вигляді. Обидва підписані, і на обидва треба відповісти протягом трьох секунд. Покажемо, як їх роздивитися, перевірити підпис і що робити з єдиним кроком, з яким адреса-перехоплювач не впорається: перевіркою challenge в Events API.
Куди Slack надсилає запити
У налаштуваннях застосунку на api.slack.com:
- Event Subscriptions: увімкніть події, вкажіть Request URL і підпишіться на події бота чи користувача, наприклад
app_mentionабоmessage.channels. - Interactivity & Shortcuts: увімкніть interactivity і вкажіть Request URL для кнопок, меню, відправки модалок і шорткатів.
Перевірка url_verification
Щойно ви вводите Request URL для Events API, Slack надсилає запит на перевірку:
{
"token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",
"challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P",
"type": "url_verification"
}
Ендпоінт має відповісти HTTP 200 і повернути значення challenge: простим текстом, як form data challenge=... або як JSON {"challenge":"..."}.
Важливо: Webhook Inspector цю перевірку не пройде. Власна відповідь у ньому статична, тож випадковий challenge вона не поверне, а пересилання одностороннє: Slack завжди отримує відповідь інспектора, а не вашого застосунку. Тому є два сценарії:
| Можливість Slack | Є challenge? | Як використовувати інспектор |
|---|---|---|
| Interactivity & Shortcuts, Slash Commands | Ні | Вставте адресу перехоплювача як Request URL напряму, за потреби пересилайте запити в локальний застосунок |
| Events API | Так | Підтвердьте Request URL своїм застосунком, а застосунок нехай надсилає в інспектор копію кожного запиту |
Крок 1: створіть адресу для перехоплення
Відкрийте Webhook Inspector і створіть адресу, наприклад:
https://webhook.voronyuk.com/abcd23efgh/slack/events
Для різних запитів візьміть окремі підшляхи, скажімо .../slack/interactive і .../slack/events. Усе ловиться тією самою адресою, а фільтрувати й шукати можна окремо.
Тіло відповіді на slash-команду Slack показує користувачу, тож у налаштуваннях Відповідь цієї адреси увімкніть власне тіло й залиште його порожнім (статус 200). Тоді Slack отримає просте підтвердження.
Крок 2: перевірка X-Slack-Signature
Кожен запит має два заголовки:
X-Slack-Request-Timestamp: Unix-час запиту.X-Slack-Signature:v0=і далі HMAC-SHA256 відv0:{timestamp}:{сире тіло}у hex, ключ — Signing Secret застосунку (розділ Basic Information).
Щоб захиститися від повторного відтворення, відкидайте запити, чий timestamp відрізняється від вашого годинника більш ніж на п’ять хвилин. Ось мінімальний застосунок на Node.js, який перевіряє підпис, відповідає на challenge і підтверджує події та взаємодії:
const crypto = require('crypto');
const express = require('express');
const SIGNING_SECRET = process.env.SLACK_SIGNING_SECRET;
const app = express();
function verifySlack(req) {
const ts = req.get('X-Slack-Request-Timestamp');
const sig = req.get('X-Slack-Signature');
if (!ts || !sig) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected =
'v0=' +
crypto
.createHmac('sha256', SIGNING_SECRET)
.update(`v0:${ts}:`)
.update(req.body) // сирий Buffer
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(sig);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.use(express.raw({ type: '*/*' }));
app.post('/events', (req, res) => {
if (!verifySlack(req)) return res.sendStatus(401);
const body = JSON.parse(req.body.toString('utf8'));
if (body.type === 'url_verification') {
return res.type('text/plain').send(body.challenge);
}
res.sendStatus(200); // підтверджуємо протягом 3 секунд
console.log(body.event && body.event.type, body.event_id);
});
app.post('/interactive', (req, res) => {
if (!verifySlack(req)) return res.sendStatus(401);
const params = new URLSearchParams(req.body.toString('utf8'));
const payload = JSON.parse(params.get('payload'));
res.sendStatus(200);
console.log(payload.type, payload.user && payload.user.id);
});
app.listen(3000);
Та сама перевірка підпису на Python:
import hashlib
import hmac
import os
import time
SIGNING_SECRET = os.environ["SLACK_SIGNING_SECRET"].encode()
def verify_slack(raw_body: bytes, timestamp: str, signature: str) -> bool:
if not timestamp or abs(time.time() - int(timestamp)) > 300:
return False
base = b"v0:" + timestamp.encode() + b":" + raw_body
expected = "v0=" + hmac.new(SIGNING_SECRET, base, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")
Крок 3: interactivity — перехоплення й пересилання
Вкажіть https://webhook.voronyuk.com/abcd23efgh/slack/interactive як Request URL у розділі Interactivity (і як адресу slash-команд). Challenge тут немає, тож Slack приймає адресу одразу.
Щоб заодно виконувався код вашого обробника, запустіть застосунок вище на порту 3000 і пересилайте запити в нього. Підшлях дописується, тож .../abcd23efgh/slack/interactive прийде на http://localhost:3000/interactive. Запустіть CLI-агент:
WI_TOKEN='<токен>' inspector listen https://webhook.voronyuk.com/abcd23efgh/slack --to localhost:3000
Точну команду з токеном скопіюйте з панелі Пересилання; relay через браузер теж працює, якщо сервер дозволяє CORS. Пам'ятайте: Slack бачить лише відповідь інспектора, тож відповідати користувачу застосунок має через response_url із payload, а не через HTTP-відповідь. Як працює кожен режим пересилання, описано в гайді як тестувати вебхуки локально.
Крок 4: Events API — дзеркалюйте запити в інспектор
Відкрийте застосунок назовні через тунель або задеплойте його і вкажіть як Request URL в Events API його власну адресу (наприклад, https://your-tunnel.example.com/events), щоб на challenge відповідав застосунок. Потім додайте кілька рядків, які надсилають копію кожного запиту на вашу адресу в інспекторі:
const INSPECTOR_URL = 'https://webhook.voronyuk.com/abcd23efgh/slack/events';
app.use((req, res, next) => {
// Копія сирого запиту «вистрілив і забув»; відповідь Slack не затримує.
fetch(INSPECTOR_URL, {
method: 'POST',
headers: {
'Content-Type': req.get('Content-Type') || 'application/json',
'X-Slack-Signature': req.get('X-Slack-Signature') || '',
'X-Slack-Request-Timestamp': req.get('X-Slack-Request-Timestamp') || '',
},
body: req.body,
}).catch(() => {});
next();
});
Поставте цей middleware після express.raw(), щоб req.body був сирим Buffer (у Node.js 18+ fetch вбудований). Тепер кожна подія з'являється в інспекторі саме такою, якою її надіслав Slack, разом із заголовками підпису.
Крок 5: роздивіться події та взаємодії
Згадайте бота в каналі. Перехоплений запит матиме тіло типу event_callback:
{
"type": "event_callback",
"team_id": "T0001",
"api_app_id": "A0001",
"event_id": "Ev0001",
"event_time": 1760950000,
"event": {
"type": "app_mention",
"user": "U0001",
"text": "<@U0BOT> як там деплой?",
"channel": "C0001",
"ts": "1760950000.000100"
}
}
Натисніть кнопку в повідомленні, і запит interactivity прийде як application/x-www-form-urlencoded з єдиним полем payload, у якому лежить JSON. Інспектор розкодовує form data, тож payload можна читати одразу, без ручного URL-декодування.
Ліміт у 3 секунди й повтори
Slack чекає 2xx протягом трьох секунд. Якщо не встигли або повернули помилку, Slack повторить подію до трьох разів: майже одразу, через хвилину і через п’ять хвилин. У повторах є два заголовки:
X-Slack-Retry-Num: номер спроби (1,2або3).X-Slack-Retry-Reason: причина, наприкладhttp_timeout,http_error,connection_failedчиssl_error.
Спершу підтверджуйте отримання, обробляйте в черзі, а дублікати відсіюйте за event_id. Пошуком в інспекторі запитів Slack легко знайти X-Slack-Retry-Num і побачити, які обробники відповідають надто повільно.
Схожі гайди: вебхуки GitHub і вебхук Telegram-бота.
Часті питання
Чому Slack пише, що Request URL не повернув challenge?
Ендпоінт має повернути challenge із запиту url_verification зі статусом 200. Адреса-перехоплювач цього не вміє — її відповідь статична, а пересилання не повертає відповідь застосунку, — тож підтвердьте URL для Events API своїм застосунком і дзеркальте запити в інспектор, як показано вище.
Як перевірити підпис запиту від Slack?
Порахуйте HMAC-SHA256 від v0:{X-Slack-Request-Timestamp}:{сире тіло} із Signing Secret, додайте до hex-дайджесту префікс v0= і порівняйте з X-Slack-Signature за сталий час. Timestamp старший за п’ять хвилин відкидайте.
Чому Slack надсилає ту саму подію кілька разів?
Застосунок не повернув 2xx за три секунди, і Slack повторив доставку. Подивіться на X-Slack-Retry-Num і X-Slack-Retry-Reason, пришвидшіть відповідь і відсіюйте дублікати за event_id.
У якому форматі приходять payload від interactivity?
У form-encoded. У тілі одне поле payload з JSON, яке ви парсите вже після перевірки підпису по сирому тілу.
Спробуйте зараз
Отримайте безкоштовний URL для вебхуків і дивіться, як приходять запити, — без реєстрації.
Отримати URL для вебхуків