Усі статті
Як локально тестувати вебхуки Zoom через HTTPS
ZoomwebhookslocalhostHMAC verification

Як локально тестувати вебхуки Zoom через HTTPS

Щоб тестувати вебхуки Zoom на localhost, оприлюдніть локальний маршрут POST командою npx portpreview PORT, укажіть цей загальнодоступний HTTPS-маршрут як кінцеву точку сповіщень про події та реалізуйте відповідь на виклик Zoom endpoint.url_validation, перш ніж натискати Validate. Для звичайних подій перевіряйте x-zm-signature за незміненим тілом запиту й часовою позначкою, зберігайте подію ідемпотентно та повертайте 2xx упродовж трьох секунд.

Як підписки на події Zoom надходять на localhost

Вебхуки Zoom — це сповіщення у вигляді HTTP POST із JSON про події, на які підписано застосунок, у таких продуктах, як Meetings, Webinars, Phone, Team Chat, Rooms та інших доступних йому сервісах. Точний перелік подій і полів залежить від типу застосунку, увімкнених продуктів, можливостей облікового запису, дозволів і поточного стану платформи Zoom. Вибирайте лише ті події, які обробник уміє опрацьовувати, і користуйтеся актуальною схемою події, наведеною у процесі створення застосунку.

Кінцева точка має бути загальнодоступною через HTTPS, мати повне доменне ім’я, чинний ланцюжок сертифікатів від центру сертифікації, підтримувати TLS 1.2 або новішу версію та POST-запити із JSON. Адреса зворотного зв’язку на кшталт http://localhost:3000 цим вимогам не відповідає. PortPreview надає загальнодоступний HTTPS-вузол і переспрямовує запити до локального процесу.

Офіційна документація Zoom щодо вебхуків — першоджерело вимог до кінцевої точки, перевірки викликом-відповіддю, підписів подій, поведінки доставки та актуальних кроків налаштування.

Створіть маршрут Express зі збереженням необробленого тіла

Підпис запиту Zoom охоплює точний текст тіла. Отримайте байти до того, як будь-яке проміжне ПЗ для JSON розбере та повторно серіалізує їх. У наведеному нижче прикладі один маршрут обробляє як перевірку URL, так і перевірку звичайних подій:

import crypto from 'node:crypto';
import express from 'express';

const app = express();
app.post('/webhooks/zoom', express.raw({ type: 'application/json', limit: '1mb' }), async (req, res) => {
  const rawBody = req.body.toString('utf8');
  let event;
  try {
    event = JSON.parse(rawBody);
  } catch {
    return res.status(400).json({ error: 'Invalid JSON' });
  }

  const secret = process.env.ZOOM_WEBHOOK_SECRET_TOKEN;
  if (!secret) return res.sendStatus(500);

  if (event.event === 'endpoint.url_validation') {
    const plainToken = event.payload?.plainToken;
    if (typeof plainToken !== 'string') return res.sendStatus(400);
    const encryptedToken = crypto
      .createHmac('sha256', secret)
      .update(plainToken)
      .digest('hex');
    return res.status(200).json({ plainToken, encryptedToken });
  }

  const timestamp = req.get('x-zm-request-timestamp') ?? '';
  const received = req.get('x-zm-signature') ?? '';
  const message = `v0:${timestamp}:${rawBody}`;
  const expected = `v0=${crypto
    .createHmac('sha256', secret)
    .update(message)
    .digest('hex')}`;

  const a = Buffer.from(received);
  const b = Buffer.from(expected);
  const valid = a.length === b.length && crypto.timingSafeEqual(a, b);
  if (!valid) return res.sendStatus(401);

  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > 300) return res.sendStatus(401);

  const requestId = req.get('x-zm-request-id');
  const deliveryKey = requestId || crypto.createHash('sha256').update(rawBody).digest('hex');
  await saveWebhookOnce({ provider: 'zoom', deliveryKey, event });
  return res.sendStatus(200);
});

app.listen(3000);

П’ятихвилинне вікно допустимої давності в цьому прикладі є правилом безпеки застосунку, а не заміною перевірки HMAC. Виберіть допуск відповідно до синхронізації годинника й очікуваних умов доставки. Записуйте в журнал лише категорію причини помилки, але ніколи не секретний токен або повне тіло запиту.

Запустіть локальний тунель

  1. Запустіть застосунок і переконайтеся, що маршрут приймає локальний POST-запит на порту 3000.
  2. Відкрийте інший термінал і виконайте npx portpreview 3000. Замініть 3000 на порт, який насправді використовує застосунок.
  3. Додайте маршрут до згенерованої базової адреси, наприклад https://YOUR-TUNNEL.portpreview.dev/webhooks/zoom.
  4. Не зупиняйте застосунок і тунель під час перевірки URL та тестування подій.

Якщо новий тунель має інше ім’я хоста, Zoom сприйматиме його як іншу кінцеву точку. Оновіть і перевірте нову URL-адресу, перш ніж очікувати подій. URL має вести безпосередньо до обробника POST; переспрямування непридатні для надійної доставки вебхуків, а Zoom не повторює запити після відповідей 3xx.

Додайте підписку на події в Zoom

У Zoom App Marketplace відкрийте створений застосунок і перейдіть до розділу Features або Access відповідно до поточного інтерфейсу створення. Увімкніть Event Subscriptions, додайте підписку, виберіть типи подій та одержувача, а потім вставте повну HTTPS-адресу кінцевої точки. Доступні одержувачі й події залежать від типу застосунку та налаштувань облікового запису. Після зміни підписок опублікований застосунок може потребувати повторної перевірки.

Скопіюйте секретний токен вебхука, пов’язаний із застосунком, у локальну змінну середовища, яку виключено з контролю версій, наприклад ZOOM_WEBHOOK_SECRET_TOKEN. Це не секрет клієнта OAuth, не токен доступу й не застарілий токен перевірки. Після зміни середовища перезапустіть локальний сервер.

Правильно реалізуйте перевірку URL кінцевої точки

Коли ви натискаєте Validate, Zoom надсилає POST-запит, у якому значення event дорівнює endpoint.url_validation. Корисне навантаження містить plainToken. Обчисліть HMAC SHA-256, використовуючи секретний токен вебхука як ключ, а цей незашифрований токен — як повідомлення; подайте хеш малими шістнадцятковими символами й поверніть JSON, що містить незмінений plainToken та отриманий encryptedToken.

const encryptedToken = createHmac('sha256', webhookSecret)
  .update(event.payload.plainToken)
  .digest('hex');

return {
  plainToken: event.payload.plainToken,
  encryptedToken
};

Поверніть HTTP 200 із тілом JSON упродовж трьох секунд. Не хешуйте весь запит перевірки, не використовуйте секрет клієнта OAuth, не кодуйте хеш у Base64 і не додавайте до хешу перевірки префікс v0=. Це частини інших процесів. Кінцеву точку не можна зберегти, доки первинна перевірка не завершиться успішно.

Поточна документація Zoom також описує автоматичну повторну перевірку кожні 72 години. Після кількох невдалих перевірок власник застосунку отримує сповіщення; після шести послідовних невдач Zoom вимикає підписку на події та припиняє їх надсилання. Тому закритий тунель для розробки згодом не пройде перевірку. Видаляйте тимчасові підписки після тестування, а у виробничому середовищі забезпечте постійну доступність обробника виклику.

Перевіряйте звичайні запити вебхуків Zoom

Перевірка URL доводить, що в момент виклику кінцева точка знає секрет. Окрема перевірка звичайної події доводить, що отримане тіло відповідає HMAC, надісланому Zoom. Прочитайте x-zm-request-timestamp і складіть точно таке повідомлення:

v0:{x-zm-request-timestamp}:{raw request body}

Обчисліть HMAC SHA-256 для цього повідомлення із секретним токеном вебхука як ключем, подайте хеш у шістнадцятковому форматі, додайте v0= на початку та порівняйте результат із x-zm-signature за сталий час. Потрібне саме початкове тіло запиту. Розбір JSON із подальшим викликом JSON.stringify може змінити пробіли або формат властивостей і зробити чинний підпис недійсним.

Відхиляйте відсутні, пошкоджені, недійсні або надто давні підписи до виконання бізнес-логіки. Синхронізуйте системний час. Старий токен перевірки вебхуків було визнано застарілим, а завершення його підтримки планувалося на червень 2025 року; новий код має використовувати описану Zoom схему HMAC із секретним токеном, а не перевірку рівності Authorization зі старого посібника. Подробиці щодо необробленого тіла та порівняння за сталий час наведено в посібнику з перевірки підписів вебхуків.

Розподіляйте типи подій, специфічні для постачальника

Навіть перевірене корисне навантаження потребує валідації схеми та перевірок авторизації. Для подій зустрічей ідентифікатор зустрічі та UUID мають різне призначення; для повторюваних і регулярних зустрічей особливо важливо зіставляти дані на рівні UUID. Вважайте поля корисного навантаження залежними від конкретної події та дотримуйтеся поточної довідки для кожної підписки.

async function processZoomEvent(event) {
  switch (event.event) {
    case 'meeting.started':
      await markMeetingStarted({
        uuid: event.payload.object.uuid,
        startedAt: event.payload.object.start_time
      });
      break;
    case 'meeting.ended':
      await markMeetingEnded({
        uuid: event.payload.object.uuid,
        endedAt: event.payload.object.end_time
      });
      break;
    default:
      await recordUnhandledZoomEvent(event.event);
  }
}

Не вважайте порядок подій журналом транзакцій. Затримки мережі, повторні спроби та паралельне опрацювання можуть змінити порядок надходження. Зберігайте часову позначку події від постачальника й за потреби застосовуйте монотонні правила зміни стану. Невідомі типи подій мають бути видимими для спостереження та підтверджуватися після надійного збереження, а не спричиняти повторні помилки кінцевої точки.

Дотримуйтеся трисекундного строку доставки

Для успішної доставки Zoom очікує HTTP 200 або 204 упродовж трьох секунд. Перевірте запит, провалідуйте мінімальну оболонку, запишіть подію до надійної вхідної скриньки або черги й поверніть відповідь. Оброблення відео, оновлення CRM, виклики календаря, електронна пошта й аналітика мають виконуватися у фонових обробниках.

Згідно з поточною документацією Zoom про доставку сповіщень, у разі відповідних помилок сервера або з’єднання запит повторюється тричі: приблизно через п’ять хвилин після першої спроби, потім через 20 хвилин після цього повтору й через 60 хвилин після другого повтору. Zoom вважає 2xx успіхом; запити не повторюються після переспрямувань 3xx або клієнтських помилок 4xx. Оскільки правила можуть змінитися, перевірте офіційну сторінку ще раз, перш ніж будувати робочі сповіщення навколо точних інтервалів.

Забезпечте ідемпотентність кожної події

Повторна спроба може відбутися після неоднозначного тайм-ауту, хоча перша операція вже була зафіксована. Усуньте дублікати до побічних ефектів. Заголовок x-zm-request-id наведено в документованій Zoom структурі запиту, але код має витримувати його відсутність у деяких продуктах або версіях. Використовуйте цей заголовок, коли він є; інакше створіть стабільний ключ із перевірених незмінних даних події або криптографічного хешу перевіреного необробленого тіла. Забезпечуйте унікальність у сховищі, а не лише в кеші пам’яті.

await db.transaction(async (tx) => {
  const claimed = await tx.webhookInbox.insertOnce({
    provider: 'zoom',
    deliveryKey,
    eventType: event.event,
    payload: event
  });
  if (!claimed) return;
  await tx.jobs.enqueue({ type: 'process-zoom-event', deliveryKey });
});

Резервування запису у вхідній скриньці та створення завдання мають бути атомарними. Якщо фоновий обробник завершується помилкою, повторіть завдання, не просячи Zoom доставити подію знову. Посібник із повторних спроб та ідемпотентності докладно пояснює таблиці вхідних подій, унікальні ключі й межі побічних ефектів.

Усувайте проблеми з перевіркою та доставкою

Validate повідомляє про помилку

Переконайтеся, що URL загальнодоступний через HTTPS, містить точний маршрут, не має переспрямування й веде до активного локального порту. Відповіддю має бути JSON із кодом HTTP 200, початковим незашифрованим токеном і поданим малими шістнадцятковими символами HMAC лише цього токена. Виміряйте повний час відповіді: перевірка має завершитися впродовж трьох секунд.

Жодна звичайна подія не проходить перевірку підпису

Переконайтеся, що скопіювали секретний токен вебхука, а не секрет клієнта OAuth чи старий токен перевірки. Отримайте тіло як необроблені байти до проміжного ПЗ для JSON, використайте точну часову позначку із заголовка, збережіть обидві двокрапки в повідомленні v0:timestamp:body і додавайте v0= лише до остаточного підпису події.

Перевірка URL успішна, але події не надходять

Переконайтеся, що підписку ввімкнено й збережено, вибрано потрібні типи подій та одержувача, а обліковий запис або користувачі створюють такі події. Перевірте стан повторної перевірки й вимоги до публікації застосунку. Упевніться, що URL тунелю не змінився після перевірки.

Обробник працює успішно, але Zoom повторює запити

Перевіряйте затримку та код загальнодоступної відповіді, а не лише локальні журнали. Повільна операція може тривати понад три секунди, навіть якщо зрештою завершується успішно. Швидко збережіть подію, поверніть 2xx і продовжте опрацювання асинхронно. Вхідна скринька, стійка до дублікатів, не дасть повторній спробі знову виконати побічний ефект.

Збережена подія не проходить перевірку під час пізнішого відтворення

Перевірка давності має відхиляти стару часову позначку, а зміна JSON робить HMAC недійсним. Для наскрізних тестів отримайте нову подію від постачальника. Для тестів бізнес-логіки збережіть очищений розібраний тестовий зразок і викликайте диспетчер, оминаючи перевірку вхідного запиту лише в тестовому середовищі. Це розмежування пояснено в посібнику з налагодження відтворення вебхуків.

Контрольний список безпеки для тестування вебхуків Zoom

  • Зберігайте секретні токени вебхуків у виключених із контролю версій файлах середовища й замінюйте розкриті облікові дані.
  • Перевіряйте HMAC за необробленим тілом, перш ніж довіряти будь-якому полю корисного навантаження.
  • Установіть допуск для часової позначки та синхронізуйте годинник сервера, щоб зменшити ризик повторного відтворення.
  • Перевіряйте тип події, контекст облікового запису, ідентифікатори об’єкта, тип вмісту й розмір тіла.
  • Приховуйте в журналах імена учасників, адреси електронної пошти, теми зустрічей, вміст чатів і дані записів.
  • Використовуйте окремі кінцеві точки або секрети для розробки та виробничого середовища, якщо це дозволяють налаштування застосунку.
  • Видаляйте тимчасові загальнодоступні URL-адреси й підписки після завершення локального сеансу.

Готова до виробничого використання схема та сама, яку перевірено локально: стабільна вхідна точка HTTPS, постійно доступний обробник виклику, перевірка HMAC за необробленим тілом, надійна ідемпотентність, підтвердження менш ніж за три секунди й ізольовані фонові обробники. Для загального трасування запитів і перевірки маршрутів скористайтеся посібником із локального налагодження вебхуків.

Поширені запитання

Як перевірити URL вебхука Zoom на localhost?
Оприлюдніть локальний маршрут через HTTPS, а у відповідь на endpoint.url_validation поверніть початковий plainToken і його шістнадцятковий encryptedToken HMAC SHA-256, обчислений із секретним токеном вебхука Zoom як ключем.
Як перевірити підпис звичайного вебхука Zoom?
Складіть v0:{x-zm-request-timestamp}:{raw body}, обчисліть HMAC SHA-256 із секретним токеном вебхука, додайте v0= перед шістнадцятковим хешем і порівняйте результат із x-zm-signature.
Чому перевірка URL Zoom успішна, а перевірка підпису події — ні?
Ці процеси підписують різні повідомлення. Під час перевірки URL хешується лише plainToken, а для звичайної події підпис охоплює версію, часову позначку запиту й точне необроблене тіло. Розбір і повторна серіалізація тіла також можуть зіпсувати підпис події.
Як швидко має відповісти вебхук Zoom?
Поточна документація Zoom вимагає повернути HTTP 200 або 204 упродовж трьох секунд. Надійно збережіть перевірену подію або поставте її в чергу, поверніть відповідь, а повільну бізнес-логіку виконайте асинхронно.