Wszystkie artykuły
Lokalne testowanie webhooków Zoom przez HTTPS
ZoomwebhookslocalhostHMAC verification

Lokalne testowanie webhooków Zoom przez HTTPS

Aby testować webhooki Zoom na localhost, udostępnij lokalną trasę POST poleceniem npx portpreview PORT, wpisz publiczny adres HTTPS jako endpoint powiadomień i przed użyciem Validate obsłuż wyzwanie endpoint.url_validation. Zwykłe zdarzenia weryfikuj przez x-zm-signature na niezmienionym body i znaczniku czasu, zapisuj idempotentnie i odpowiadaj 2xx w ciągu trzech sekund.

Jak subskrypcje zdarzeń Zoom docierają do localhost

Webhooki Zoom to żądania HTTP POST z JSON-em dla zdarzeń Meetings, Webinars, Phone, Team Chat, Rooms i innych produktów. Dostępne typy oraz pola zależą od rodzaju aplikacji, aktywnych produktów, uprawnień konta, zakresów i bieżącej wersji platformy. Wybieraj wyłącznie zdarzenia obsługiwane przez kod i korzystaj z aktualnego schematu pokazanego w kreatorze aplikacji.

Endpoint musi być publicznym adresem HTTPS z pełną nazwą domenową, prawidłowym certyfikatem CA, TLS 1.2 lub nowszym i obsługą POST JSON. http://localhost:3000 tych warunków nie spełnia; PortPreview kończy publiczne połączenie HTTPS i przekazuje ruch do procesu lokalnego. Źródłem aktualnych wymagań pozostaje oficjalna dokumentacja webhooków Zoom.

Utwórz trasę Express zachowującą surowe body

Podpis obejmuje dokładny tekst body, dlatego przechwyć bajty przed parserem JSON:

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);

Pięciominutowe okno świeżości jest przykładową polityką aplikacji, a nie zamiennikiem HMAC. Dopasuj je do synchronizacji zegarów i oczekiwanych opóźnień; nie loguj sekretu ani pełnego body.

Uruchom lokalny tunel

  1. Uruchom aplikację i sprawdź lokalny POST na porcie 3000.
  2. W drugim terminalu wykonaj npx portpreview 3000, podając faktyczny port.
  3. Do wygenerowanej domeny dodaj trasę, np. https://YOUR-TUNNEL.portpreview.dev/webhooks/zoom.
  4. Podczas walidacji i testów nie wyłączaj aplikacji ani tunelu.

Zmiana hosta oznacza dla Zoom nowy endpoint, więc adres trzeba zaktualizować i zwalidować. URL powinien prowadzić bezpośrednio do handlera POST; przekierowania są zawodne, a odpowiedzi 3xx nie są ponawiane.

Dodaj subskrypcję zdarzeń w Zoom

W Zoom App Marketplace otwórz utworzoną aplikację i bieżącą sekcję Features lub Access. Włącz Event Subscriptions, dodaj subskrypcję, wybierz zdarzenia i odbiorcę, po czym wklej pełny URL HTTPS. Opcje zależą od typu aplikacji i konfiguracji konta, a zmiana opublikowanej aplikacji może wymagać kolejnej recenzji.

Webhook secret token zapisz w ignorowanej zmiennej lokalnej, np. ZOOM_WEBHOOK_SECRET_TOKEN, i zrestartuj serwer. Nie jest to OAuth client secret, access token ani wycofany verification token.

Poprawnie zaimplementuj walidację URL endpointu

Validate wysyła POST z event równym endpoint.url_validation i polem plainToken. Oblicz HMAC SHA-256, używając webhook secret token jako klucza, a wyłącznie plain token jako wiadomości; wynik zakoduj małymi znakami szesnastkowymi i zwróć niezmieniony plainToken oraz encryptedToken.

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

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

Odpowiedź HTTP 200 z JSON-em musi nadejść w trzy sekundy. Nie haszuj całego żądania, nie używaj sekretu OAuth, Base64 ani prefiksu v0=. Zoom automatycznie ponawia walidację co 72 godziny; po kolejnych błędach informuje właściciela, a po sześciu wyłącza subskrypcję. Usuń tymczasowe subskrypcje, natomiast produkcyjny handler wyzwania utrzymuj stale.

Weryfikuj zwykłe żądania webhooków Zoom

Odczytaj x-zm-request-timestamp i zbuduj dokładnie:

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

Policz HMAC SHA-256 z webhook secret token, zakoduj hex, dodaj v0= i porównaj w stałym czasie z x-zm-signature. Musisz użyć oryginalnego body; parsowanie i JSON.stringify może zmienić format. Brakujące, błędne, nieprawidłowe lub zbyt stare podpisy odrzucaj przed logiką biznesową i synchronizuj zegar.

Stary webhook verification token miał zostać wyłączony w czerwcu 2025, więc nie kopiuj dawnych kontroli nagłówka Authorization. Stosuj udokumentowany HMAC i zobacz przewodnik po weryfikacji podpisów webhooków.

Rozdzielaj typy zdarzeń właściwe dla dostawcy

Podpisany payload nadal wymaga walidacji schematu i uprawnień. Meeting ID i UUID mają różne znaczenie, a dla spotkań cyklicznych korelacja po UUID jest szczególnie ważna.

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);
  }
}

Kolejność dostarczenia nie jest dziennikiem transakcji: sieć, ponowienia i równoległość mogą ją zmienić. Zachowuj czas dostawcy, stosuj monotoniczne reguły stanu, a nieznane typy obserwuj i potwierdzaj po bezpiecznym zapisie.

Dotrzymaj trzysekundowego terminu dostawy

Zoom oczekuje HTTP 200 lub 204 w trzy sekundy. Zweryfikuj żądanie, sprawdź minimalną kopertę, trwale zapisz do inboxa lub kolejki i odpowiedz. Obróbka wideo, CRM, kalendarz, e-mail oraz analityka powinny działać w workerach.

Aktualna dokumentacja przewiduje trzy ponowienia kwalifikujących się błędów serwera i połączenia: około 5 minut po pierwszej próbie, następnie po 20 i 60 minutach. 2xx oznacza sukces; 3xx i błędy klienta 4xx nie są ponawiane. Przed budową alertów sprawdź bieżące zasady.

Zapewnij idempotencję każdego zdarzenia

Timeout może być niejednoznaczny mimo zatwierdzenia pierwszej operacji. Deduplikuj przed efektami ubocznymi. Użyj x-zm-request-id, gdy jest dostępny; w przeciwnym razie wyprowadź stabilny klucz ze zweryfikowanych danych niemutowalnych albo skrótu surowego body. Unikalność egzekwuj w bazie, nie wyłącznie w pamięci.

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 });
});

Zajęcie wpisu i utworzenie zadania muszą być atomowe. Błąd workera powinien ponowić zadanie, nie dostawę Zoom. Więcej opisuje przewodnik po ponowieniach i idempotencji.

Diagnozuj walidację i dostarczanie

Validate zgłasza błąd

Sprawdź publiczny HTTPS, dokładną trasę, brak redirectu, aktywny port, HTTP 200 JSON i mały hex HMAC wyłącznie tokenu oraz czas poniżej trzech sekund.

Każde zwykłe zdarzenie ma zły podpis

Użyj właściwego webhook secret token, surowych bajtów sprzed parsera, dokładnego timestampu, obu dwukropków w v0:timestamp:body i v0= tylko przy końcowym podpisie.

Walidacja działa, lecz zdarzeń brak

Sprawdź zapisanie i aktywność subskrypcji, typy zdarzeń, odbiorcę, uprawnienia konta, status rewalidacji i niezmieniony URL tunelu.

Handler działa, ale Zoom ponawia

Mierz publiczną latencję i status. Szybko zapisz, zwróć 2xx i przetwarzaj asynchronicznie; inbox odporny na duplikaty ochroni efekty uboczne.

Przechwycone zdarzenie nie działa po odtworzeniu

Stary timestamp powinien zostać odrzucony, a zmiana JSON-u psuje HMAC. Test end-to-end wymaga świeżego zdarzenia; test logiki może wywołać dispatcher z oczyszczoną fixture. Zobacz przewodnik po odtwarzaniu webhooków.

Lista bezpieczeństwa testów webhooków Zoom

  • Trzymaj sekrety w ignorowanych plikach i obracaj ujawnione dane.
  • Sprawdzaj HMAC surowego body przed zaufaniem polom.
  • Wymuszaj tolerancję czasu i synchronizuj zegar.
  • Waliduj typ, konto, identyfikatory, content type i rozmiar.
  • Usuwaj z logów nazwiska, e-maile, tematy, czat i nagrania.
  • Rozdzielaj endpointy lub sekrety dev i produkcji.
  • Po sesji usuwaj tymczasowe URL-e i subskrypcje.

W produkcji zachowaj ten sam wzorzec: stabilny HTTPS, stałą obsługę wyzwania, HMAC surowego body, trwałą idempotencję, odpowiedź poniżej trzech sekund i osobne workery. Śledzenie żądań omawia przewodnik po lokalnym debugowaniu webhooków.

Najczęściej zadawane pytania

Jak zwalidować URL webhooka Zoom na localhost?
Udostępnij lokalną trasę przez HTTPS i odpowiedz na endpoint.url_validation, zwracając oryginalny plainToken oraz szesnastkowy encryptedToken HMAC SHA-256 wyliczony z webhook secret token.
Jak sprawdzić podpis zwykłego webhooka Zoom?
Zbuduj v0:{x-zm-request-timestamp}:{raw body}, oblicz HMAC SHA-256 z webhook secret token, poprzedź wynik hex przez v0= i porównaj go z x-zm-signature.
Dlaczego walidacja URL działa, a podpis zdarzenia nie?
Te przepływy podpisują inne wiadomości: walidacja tylko plainToken, a zdarzenie wersję, timestamp i dokładne surowe body. Ponowna serializacja JSON-u może unieważnić podpis.
Jak szybko webhook Zoom musi odpowiedzieć?
Aktualna dokumentacja wymaga HTTP 200 lub 204 w ciągu trzech sekund. Trwale zapisz lub zakolejkuj zweryfikowane zdarzenie, odpowiedz i wolną logikę wykonaj asynchronicznie.