Zoom Webhook: Example Payload, Headers and Signature

Zoom will not save a webhook endpoint until it has answered a validation challenge with a hash made from the app's Secret Token, and it repeats that check from time to time. Here is a meeting.started event, the signature scheme, and the challenge.

No webhook URL yet. Creating one needs no account.

Request details
Requests sent to your webhook URL appear here: method, headers and the body exactly as it arrived.

The button posts the example below to your webhook URL from your browser. It is not sent by Zoom, it carries your browser's User-Agent, and its signature is a placeholder that will not verify. To see a real delivery, follow the steps further down.

Zoom accepts an endpoint only if it answers a validation request with a hash made from your app's Secret Token. Create a webhook URL above, then save the token here so the URL can answer. The token is sent to api-tester.com and held in server memory until the webhook URL is deleted or expires; it is never displayed again. Use a development app's token.

Last updated . How this page is checked.

How the webhook URL works. It is a public address that records whatever is sent to it and answers 200. An inbox keeps the 50 most recent requests, accepts bodies up to 256 KB, and is deleted 24 hours after it was created — or sooner if you press Delete URL or our server restarts, because requests are held in memory and never written to disk.

Anyone who has the URL can read what it received. Use sandbox and test-mode accounts, do not route production traffic or customer data through it, and delete it when you are done. Privacy details.

What is a Zoom webhook?

A Zoom webhook is an HTTP POST sent to the event notification endpoint configured on an app in the Zoom App Marketplace when a subscribed event happens in an account that uses the app: a meeting starts or ends, a participant joins, a recording completes, a user is created. The body names the event in event, gives the time in event_ts, and puts the details in payload.object.

Each app has a Secret Token, shown on the same settings page as the event subscriptions. It does two jobs: it signs every webhook Zoom sends, and it is the key your endpoint must use to answer the endpoint.url_validation challenge. An endpoint that cannot produce the right answer cannot be saved, which is why testing a Zoom webhook takes one more step than most.

What a Zoom webhook request looks like

The request line and headers as they reach your server, followed by the body. Ids, tokens and the signature are placeholders.

POST /your/webhook/path HTTP/1.1
Host: your-server.example
x-zm-signature: v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503
x-zm-request-timestamp: 1767225600
x-zm-trackingid: Client_1f3a9c7e2b4d4c8f9e0b6a5d1f2c3b4e
Content-Type: application/json; charset=utf-8
User-Agent: Zoom Marketplace/1.0a
{
  "event": "meeting.started",
  "event_ts": 1767225600000,
  "payload": {
    "account_id": "AAAAAABBBB",
    "object": {
      "id": "1234567890",
      "uuid": "4444AAAiAAAAAiAiAiiAii==",
      "host_id": "x1yCzABCDEfg23HiJKl4mN",
      "topic": "Weekly sync",
      "type": 2,
      "start_time": "2026-01-01T12:00:00Z",
      "timezone": "Europe/London",
      "duration": 30
    }
  }
}

event_ts is in milliseconds while the timestamp header is in seconds. A meeting has two identifiers: id is the meeting number people type to join and is reused by recurring meetings, whereas uuid identifies one occurrence. A uuid can contain slashes and must be double URL-encoded when used in an API path. type is a number: 2 is a scheduled meeting.

Headers Zoom sends

HeaderExampleWhat it is
x-zm-signaturev0=a2114d57b48eac39b9ad189dd8316235a7b4a8d2…HMAC-SHA256, hex, with a v0= version prefix.
x-zm-request-timestamp1767225600Unix time the request was sent, in seconds. Part of the signed string.
x-zm-trackingidClient_1f3a9c7e2b4d4c8f9e0b6a5d1f2c3b4eZoom's id for the request, useful when contacting their support.
Content-Typeapplication/json; charset=utf-8The body is JSON.
User-AgentZoom Marketplace/1.0aIdentifies the sender.

Header names are case-insensitive. Your framework may show them lower-cased, and HTTP/2 always does.

How Zoom signs a webhook

Zoom builds the string v0:{timestamp}:{raw body} from the x-zm-request-timestamp header and the body, computes an HMAC-SHA256 with the app's Secret Token, and sends v0= plus the hex digest in x-zm-signature. The validation challenge is signed the same way, so a careful endpoint verifies the signature first and only then answers the challenge.

The second function below is that answer: an HMAC-SHA256 of the plainToken from the request, keyed with the same Secret Token, returned as hex next to the token itself. Compute it over the raw request body exactly as received — before any JSON parsing — and compare with a constant-time function.

const crypto = require('crypto');

// rawBody: string of the body exactly as received
function verifyZoom(rawBody, signature, timestamp, secretToken, toleranceSec = 300) {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSec) return false;
  const expected = 'v0=' + crypto.createHmac('sha256', secretToken)
    .update('v0:' + timestamp + ':' + rawBody).digest('hex');
  const a = Buffer.from(signature || '');
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// The JSON to send back when body.event === 'endpoint.url_validation'
function zoomValidationReply(plainToken, secretToken) {
  return {
    plainToken: plainToken,
    encryptedToken: crypto.createHmac('sha256', secretToken)
      .update(plainToken).digest('hex'),
  };
}

URL validation, the three-second rule and retries

When you press Validate in the app's settings, Zoom posts an event named endpoint.url_validation whose payload contains a random plainToken. The endpoint has three seconds to reply 200 with JSON holding that token and its HMAC as encryptedToken. Zoom repeats the check periodically afterwards, and an endpoint that stops answering correctly stops receiving events.

A webhook URL created on this site can answer the challenge once you have saved the app's Secret Token in the field under the inbox, because the reply cannot be computed without it. The inbox answers only a validation request whose x-zm-signature verifies with the saved token. For ordinary events Zoom expects a 200 or 204 within three seconds and, per its documentation at the time of writing, makes a few further attempts over the following hours when it does not get one.

Catch a real Zoom webhook

  1. Press Create webhook URL at the top of this page and copy the address.
  2. In the Zoom App Marketplace, open Develop → Build App and create or open a development app. Find the Secret Token on the page that holds its event subscriptions.
  3. Paste that Secret Token into the field under the inbox on this page and press Save token.
  4. In Zoom, add an event subscription, paste the webhook address as the event notification endpoint URL and press Validate. The challenge appears in the inbox and Zoom reports the endpoint as validated.
  5. Add event types such as Start Meeting, save, then start a meeting and read the event in the inbox above.

The saved token is held in our server's memory for as long as the inbox exists — at most 24 hours — and is never displayed or returned, not even to you. It is still a credential leaving your hands: use a development app, and regenerate the Secret Token in Zoom when you have finished.

To run a caught request against your own code, select it and press Open in API Tester: the method, headers and body are loaded into the request console, where you enter your server's public URL and send it. A body you edit will no longer match its signature — that is the signature doing its job.

Things that catch people out

  • Validation is not a one-off. Zoom re-validates endpoints on a schedule, so the challenge handler has to stay in place in production.
  • Answering the challenge without checking its signature leaks signatures. The reply is an HMAC of caller-supplied text with your token, which is the same operation that signs a webhook.
  • The old verification token is not the Secret Token. Earlier integrations compared a static value in the authorization header; signed requests replaced it.

api-tester.com is independent and is not affiliated with Zoom. Providers change header names, limits and retry schedules; treat the official Zoom documentation as the source of truth.

Related examples and tools

Other webhooks with a timestamp in the signature

References

These links leave api-tester.com, which is independent of the companies and projects they point to.

Zoom webhook FAQ

What does a Zoom webhook payload look like?

A JSON object with event (for example meeting.started), event_ts in milliseconds, and payload containing account_id and an object with the details, such as the meeting id, uuid, host_id, topic and start_time.

How do I verify the x-zm-signature header?

Build the string "v0:" plus the x-zm-request-timestamp value, a colon and the raw body, compute an HMAC-SHA256 with the app Secret Token, hex-encode it, prefix it with v0= and compare it with x-zm-signature.

How do I pass Zoom endpoint URL validation?

When the event is endpoint.url_validation, respond within three seconds with status 200 and JSON containing plainToken from the request and encryptedToken, the hex HMAC-SHA256 of plainToken keyed with your Secret Token.

Why does Zoom say my webhook URL is not validated?

The endpoint did not return the expected plainToken and encryptedToken in time. Common causes are using the wrong secret, hashing the whole body instead of plainToken, returning base64 instead of hex, or taking longer than three seconds.

How long do I have to respond to a Zoom webhook?

Three seconds. Reply with 200 or 204 straight away and process the event afterwards.