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
- Uruchom aplikację i sprawdź lokalny POST na porcie 3000.
- W drugim terminalu wykonaj
npx portpreview 3000, podając faktyczny port. - Do wygenerowanej domeny dodaj trasę, np.
https://YOUR-TUNNEL.portpreview.dev/webhooks/zoom. - 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.
