Alle Artikel
Zoom Webhooks lokal mit HTTPS testen
ZoomwebhookslocalhostHMAC verification

Zoom Webhooks lokal mit HTTPS testen

Um Zoom Webhooks auf localhost zu testen, gib deine lokale POST-Route mit npx portpreview PORT frei, trage diese öffentliche HTTPS-Route als Endpunkt für Ereignisbenachrichtigungen ein und implementiere Zooms Challenge endpoint.url_validation, bevor du auf Validate klickst. Prüfe bei normalen Events x-zm-signature anhand des unveränderten Request-Bodys und des Zeitstempels, speichere das Event idempotent und antworte innerhalb von drei Sekunden mit 2xx.

Wie Zoom-Event-Abonnements localhost erreichen

Zoom Webhooks sind HTTP-POST-Benachrichtigungen im JSON-Format für abonnierte Events aus Produkten wie Meetings, Webinars, Phone, Team Chat, Rooms und weiteren Diensten, die deiner App zur Verfügung stehen. Der genaue Event-Katalog und die Felder hängen vom App-Typ, den aktivierten Produkten, den Berechtigungen des Kontos, den Scopes und der aktuellen Zoom-Plattform ab. Wähle nur Events aus, die dein Handler verarbeitet, und orientiere dich am aktuellen Event-Schema im App-Erstellungsprozess.

Dein Endpunkt muss öffentlich über HTTPS erreichbar sein, einen vollständig qualifizierten Domainnamen und eine gültige, von einer Zertifizierungsstelle ausgestellte Zertifikatskette besitzen, TLS 1.2 oder neuer verwenden und JSON-POST-Requests unterstützen. Eine Loopback-URL wie http://localhost:3000 erfüllt diese Anforderungen nicht. PortPreview stellt den öffentlichen HTTPS-Zugang bereit und leitet die Requests an deinen lokalen Prozess weiter.

Die offizielle Zoom-Webhook-Dokumentation ist die maßgebliche Quelle für Anforderungen an Endpunkte, Challenge-Response-Validierung, Event-Signaturen, Zustellverhalten und die aktuellen Konfigurationsschritte.

Eine Express-Route erstellen, die den Raw Body erhält

Zooms Request-Signatur umfasst den exakten Body-Text. Erfasse die Bytes, bevor eine JSON-Middleware sie parst und erneut serialisiert. Das folgende Beispiel verarbeitet URL-Validierung und normale Event-Prüfung in einer Route:

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

Das fünfminütige Zeitfenster für die Aktualität ist in diesem Beispiel eine Sicherheitsrichtlinie der Anwendung und kein Ersatz für die HMAC-Prüfung. Wähle eine Toleranz, die zur Zeitsynchronisierung und zum erwarteten Zustellverhalten passt. Protokolliere bei Fehlern nur die Ursachenkategorie, niemals das Secret Token oder den vollständigen Request-Body.

Den lokalen Tunnel starten

  1. Starte die Anwendung und prüfe, ob die Route einen lokalen POST-Request auf Port 3000 annimmt.
  2. Öffne ein weiteres Terminal und führe npx portpreview 3000 aus. Ersetze 3000 durch den tatsächlich verwendeten Port deiner App.
  3. Hänge die Route an den erzeugten Origin an, zum Beispiel https://YOUR-TUNNEL.portpreview.dev/webhooks/zoom.
  4. Lass Anwendung und Tunnel während der Validierung und der Event-Tests laufen.

Wenn ein neuer Tunnel einen anderen Hostnamen hat, behandelt Zoom ihn als anderen Endpunkt. Aktualisiere und validiere die neue URL, bevor du Events erwartest. Die URL sollte direkt auf den POST-Handler zeigen. Weiterleitungen sind für eine zuverlässige Webhook-Zustellung ungeeignet, und Zoom wiederholt keine Requests nach 3xx-Antworten.

Das Event-Abonnement in Zoom hinzufügen

Öffne deine erstellte App im Zoom App Marketplace und navigiere entsprechend dem aktuellen Erstellungsprozess zu Features oder Access. Aktiviere Event Subscriptions, füge ein Abonnement hinzu, wähle Event-Typen und Empfänger und füge anschließend die vollständige HTTPS-Endpunkt-URL ein. Die verfügbaren Empfänger und Events unterscheiden sich je nach App-Typ und Kontokonfiguration. Bei veröffentlichten Apps kann nach Änderungen an Abonnements eine erneute Prüfung erforderlich sein.

Speichere das zur App gehörende Webhook Secret Token in einer lokalen, von der Versionsverwaltung ausgeschlossenen Umgebungsvariable wie ZOOM_WEBHOOK_SECRET_TOKEN. Dabei handelt es sich weder um ein OAuth Client Secret noch um ein Access Token oder das veraltete Verification Token. Starte den lokalen Server nach Änderungen an seiner Umgebung neu.

Die Validierung der Endpunkt-URL korrekt implementieren

Wenn du auf Validate klickst, sendet Zoom einen POST-Request, dessen event den Wert endpoint.url_validation hat. Der Payload enthält plainToken. Berechne HMAC SHA-256 mit dem Webhook Secret Token als Schlüssel und diesem Plain Token als Nachricht, kodiere den Digest als kleingeschriebene Hexadezimalzahl und antworte mit JSON, das sowohl den unveränderten plainToken als auch den daraus berechneten encryptedToken enthält.

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

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

Antworte innerhalb von drei Sekunden mit HTTP 200 und dem JSON-Body. Hashe nicht den gesamten Validierungs-Request, verwende nicht das OAuth Client Secret, kodiere den Digest nicht mit Base64 und stelle dem Validierungs-Digest kein v0= voran. Diese Verfahren gehören zu anderen Abläufen. Der Endpunkt lässt sich erst speichern, wenn die erste Validierung erfolgreich war.

Laut der aktuellen Zoom-Dokumentation erfolgt außerdem alle 72 Stunden eine automatische erneute Validierung. Nach wiederholten Fehlschlägen werden Benachrichtigungen an den App-Eigentümer gesendet; nach sechs aufeinanderfolgenden Fehlern deaktiviert Zoom das Event-Abonnement und stellt keine Events mehr zu. Ein geschlossener Entwicklungstunnel wird deshalb bei einer späteren Validierung scheitern. Entferne temporäre Abonnements nach dem Test und halte die Challenge-Verarbeitung in der Produktion dauerhaft verfügbar.

Normale Zoom-Webhook-Requests verifizieren

Die URL-Validierung belegt, dass der Endpunkt zum Zeitpunkt der Challenge das Secret kennt. Die separate Prüfung normaler Events belegt, dass ein empfangener Body mit dem von Zoom gesendeten HMAC übereinstimmt. Lies x-zm-request-timestamp aus und bilde exakt folgende Nachricht:

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

Berechne für diese Nachricht HMAC SHA-256 mit dem Webhook Secret Token als Schlüssel, kodiere den Digest hexadezimal, stelle v0= voran und vergleiche das Ergebnis in konstanter Zeit mit x-zm-signature. Als Body muss der ursprüngliche Request-Body verwendet werden. Wenn du JSON parst und anschließend JSON.stringify aufrufst, können sich Leerzeichen oder die Formatierung von Properties ändern und eine gültige Signatur ungültig werden.

Weise fehlende, fehlerhaft formatierte, ungültige oder unangemessen alte Signaturen zurück, bevor du Geschäftslogik ausführst. Halte die Systemzeit synchron. Das frühere Webhook Verification Token wurde als veraltet eingestuft und sollte im Juni 2025 abgeschaltet werden. Neuer Code sollte den von Zoom dokumentierten HMAC-Ablauf mit Secret Token verwenden und nicht einen aus älteren Tutorials kopierten Vergleich des Authorization-Headers. Weitere Details zu Raw Body und Timing-safe-Vergleichen findest du im Leitfaden zur Prüfung von Webhook-Signaturen.

Anbieterspezifische Event-Typen verteilen

Auch ein verifizierter Payload benötigt Schema-Validierung und Berechtigungsprüfungen. Bei Meeting-Events dienen Meeting-ID und UUID unterschiedlichen Zwecken; bei wiederholten oder wiederkehrenden Meetings ist die Zuordnung auf UUID-Ebene besonders wichtig. Behandle Payload-Felder eventspezifisch und halte dich an die aktuelle Referenz des jeweiligen Abonnements.

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

Gehe nicht davon aus, dass die Reihenfolge der Events einem Transaktionsprotokoll entspricht. Netzwerkverzögerungen, Wiederholungen und parallele Verarbeitung können zu einer unerwarteten Eingangsreihenfolge führen. Speichere den Event-Zeitstempel des Anbieters und wende gegebenenfalls monotone Zustandsregeln an. Unbekannte Event-Typen sollten beobachtbar sein und nach sicherer Persistierung bestätigt werden, statt den Endpunkt wiederholt fehlschlagen zu lassen.

Die Zustellfrist von drei Sekunden einhalten

Zoom erwartet für eine erfolgreiche Zustellung innerhalb von drei Sekunden HTTP 200 oder 204. Verifiziere den Request, validiere den minimalen Envelope, schreibe ihn in eine dauerhafte Inbox oder Queue und antworte. Videoverarbeitung, CRM-Aktualisierungen, Kalenderaufrufe, E-Mails und Analytics gehören in Worker.

Laut der aktuellen Zoom-Dokumentation zur Zustellung von Benachrichtigungen werden geeignete Server- und Verbindungsfehler dreimal wiederholt: ungefähr fünf Minuten nach dem ersten Versuch, dann 20 Minuten nach dieser Wiederholung und schließlich 60 Minuten nach der zweiten Wiederholung. Zoom wertet 2xx als Erfolg; 3xx-Weiterleitungen und 4xx-Clientfehler werden nicht erneut versucht. Da sich Richtlinien ändern können, solltest du die offizielle Seite erneut prüfen, bevor du operative Alerts auf exakte Intervalle ausrichtest.

Jedes Event idempotent verarbeiten

Nach einem uneindeutigen Timeout kann Zoom einen Request wiederholen, obwohl dein erster Versuch bereits committed wurde. Dedupliziere vor Seiteneffekten. Der Header x-zm-request-id ist Teil der dokumentierten Request-Struktur von Zoom; dein Code sollte jedoch mit seinem Fehlen umgehen können, da sich Produkte oder Versionen unterscheiden. Verwende ihn, wenn er vorhanden ist. Leite andernfalls einen stabilen Schlüssel aus verifizierten unveränderlichen Event-Daten oder einem kryptografischen Digest des verifizierten Raw Bodys ab. Erzwinge Eindeutigkeit im Storage und nicht nur mit einem In-Memory-Cache.

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

Das Beanspruchen des Inbox-Eintrags und das Erstellen des Jobs müssen atomar erfolgen. Wenn ein Worker fehlschlägt, wiederhole den Job, ohne Zoom um eine erneute Zustellung zu bitten. Der Leitfaden zu Webhook-Wiederholungen und Idempotenz behandelt Inbox-Tabellen, eindeutige Schlüssel und Grenzen von Seiteneffekten.

Fehler bei Validierung und Zustellung beheben

Validate meldet einen Fehler

Prüfe, ob die URL öffentlich über HTTPS erreichbar ist, die exakte Route enthält, nicht weiterleitet und den aktiven lokalen Port erreicht. Stelle sicher, dass die Antwort HTTP-200-JSON mit dem ursprünglichen Plain Token und dessen kleingeschriebenem hexadezimalem HMAC enthält, das ausschließlich über diesen Token berechnet wurde. Miss die gesamte Antwortzeit; die Validierung muss innerhalb von drei Sekunden abgeschlossen sein.

Bei jedem normalen Event schlägt die Signaturprüfung fehl

Prüfe, ob du das Webhook Secret Token kopiert hast und nicht ein OAuth Client Secret oder das alte Verification Token. Erfasse den Body als Raw Bytes vor der JSON-Middleware, verwende exakt den Timestamp-Header, schließe beide Doppelpunkte in die Nachricht v0:timestamp:body ein und stelle nur der endgültigen Event-Signatur v0= voran.

Die Validierung funktioniert, aber Events kommen nicht an

Stelle sicher, dass das Abonnement aktiviert und gespeichert ist, die vorgesehenen Event-Typen und der richtige Empfänger ausgewählt sind und dein Konto beziehungsweise deine Benutzer diese Events auslösen. Prüfe den Status der erneuten Validierung und die Anforderungen an die Veröffentlichung der App. Vergewissere dich, dass sich die Tunnel-URL seit der Validierung nicht geändert hat.

Der Handler ist erfolgreich, aber Zoom wiederholt die Zustellung

Untersuche Latenz und Status der öffentlichen Antwort, nicht nur die lokalen Logs. Langsame Verarbeitung kann drei Sekunden überschreiten, auch wenn sie später erfolgreich endet. Persistiere schnell, antworte mit 2xx und verarbeite asynchron. Eine duplikatsichere Inbox verhindert, dass eine Wiederholung denselben Seiteneffekt erneut ausführt.

Ein aufgezeichnetes Event scheitert bei einer späteren Wiedergabe

Eine Aktualitätsprüfung sollte einen alten Zeitstempel zurückweisen, und jede Änderung am JSON macht den HMAC ungültig. Erzeuge für End-to-End-Tests ein frisches Provider-Event. Speichere für Tests der Geschäftslogik eine bereinigte, geparste Fixture und rufe den Dispatcher nur im Test-Harness unter Umgehung der Eingangsverifizierung auf. Der Leitfaden zum Debuggen per Webhook-Replay erklärt diese Trennung.

Sicherheitscheckliste für Tests von Zoom Webhooks

  • Bewahre Webhook Secret Tokens in ignorierten Umgebungsdateien auf und rotiere offengelegte Zugangsdaten.
  • Prüfe den HMAC gegen den Raw Body, bevor du einem Feld im Payload vertraust.
  • Erzwinge eine Zeitstempeltoleranz und synchronisiere die Serveruhr, um das Replay-Risiko zu reduzieren.
  • Validiere Event-Typ, Kontokontext, Objektbezeichner, Content-Type und Body-Größe.
  • Entferne Teilnehmernamen, E-Mail-Adressen, Meeting-Themen, Chat-Inhalte und Aufzeichnungsdaten aus Logs.
  • Verwende getrennte Entwicklungs- und Produktionsendpunkte oder Secrets, sofern deine App-Konfiguration dies zulässt.
  • Entferne temporäre öffentliche URLs und Abonnements, wenn die lokale Sitzung endet.

Das produktionsreife Muster ist dasselbe, das du lokal erprobt hast: stabiler HTTPS-Eingang, dauerhaft verfügbare Challenge-Verarbeitung, HMAC-Prüfung des Raw Bodys, persistente Idempotenz, Bestätigung in weniger als drei Sekunden und isolierte Worker. Für allgemeines Request-Tracing und die Prüfung von Routen folgst du dem Leitfaden zum lokalen Debuggen von Webhooks.

Häufig gestellte Fragen

Wie validiere ich eine Zoom-Webhook-URL auf localhost?
Gib die lokale Route über HTTPS frei und beantworte endpoint.url_validation mit dem ursprünglichen plainToken sowie dem hexadezimalen HMAC-SHA-256-encryptedToken, das mit dem Zoom Webhook Secret Token berechnet wurde.
Wie prüfe ich die Signatur eines normalen Zoom Webhooks?
Bilde v0:{x-zm-request-timestamp}:{raw body}, berechne dafür HMAC SHA-256 mit dem Webhook Secret Token, stelle dem hexadezimalen Digest v0= voran und vergleiche das Ergebnis mit x-zm-signature.
Warum funktioniert die Zoom-URL-Validierung, aber die Event-Signaturprüfung nicht?
Die Abläufe signieren unterschiedliche Nachrichten. Bei der Validierung wird nur plainToken gehasht; normale Events signieren Version, Request-Zeitstempel und den exakten Raw Body. Das Parsen und erneute Serialisieren des Bodys kann die Event-Signatur ungültig machen.
Wie schnell muss ein Zoom Webhook antworten?
Die aktuelle Zoom-Dokumentation verlangt HTTP 200 oder 204 innerhalb von drei Sekunden. Persistiere das verifizierte Event dauerhaft oder stelle es in eine Queue, antworte sofort und führe langsame Geschäftslogik asynchron aus.