> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.meetstream.ai/guides/webhooks/webhook-signature-verification/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.meetstream.ai/_mcp/server. # Verifying Webhook Signatures > Verify that webhook deliveries really came from MeetStream by checking the X-MeetStream-Signature header against your endpoint's signing secret. Every delivery to a [workspace webhook endpoint](/guides/webhooks/workspace-webhooks) is signed, so your server can prove the request came from MeetStream and not an impostor. Verification takes about ten lines of code. ## Where the secret comes from When you create an endpoint on the [Webhooks page](https://app.meetstream.ai/webhooks), MeetStream generates a **webhook secret** and shows it **once**: ![The endpoint-created dialog: the signing secret is displayed a single time](/_fern-img/d68c605ea14d7b003d33aa26c792d41c70ce34510926c4f6f6943f246fd98663.webp) Store it in your secrets manager immediately — it cannot be retrieved again, only regenerated (which invalidates the old one). ## What a signed delivery looks like A real delivery, captured live: ```http POST /webhook HTTP/1.1 Content-Type: application/json X-Meetstream-Signature: sha256=632cd9654a5c8dadf104d769f7694849c0049fef1a06fe7f83d775402fff7dbf X-Meetstream-Timestamp: 2026-08-09T07:16:44.675Z {"bot_id":"a29d00c3-...","message":"Bot is waiting to be admitted","event":"bot.in_waiting_room","bot_event":"bot.in_waiting_room","bot_status":"InWaitingRoom","status_code":200,"custom_attributes":{"purpose":"docs-signing-verify"},"timestamp":"2026-08-09T07:16:44.675Z"} ``` * `X-MeetStream-Signature` — `sha256=` followed by the hex HMAC-SHA256 digest of the **raw request body**, keyed with your endpoint's secret. * `X-MeetStream-Timestamp` — ISO 8601 time of the delivery, for replay-window checks. > **Warning** > > Signatures apply to **workspace webhook endpoints** (created in the dashboard). Deliveries to a per-bot `callback_url` passed on `create_bot` are **not signed** — if you need authenticated deliveries, receive events through a workspace endpoint. You can still route per-bot using the `custom_attributes` echoed in every payload. ## Verify in Python ```python import hashlib, hmac from datetime import datetime, timezone def verify_meetstream_webhook(secret: str, raw_body: bytes, headers: dict, tolerance_seconds: int = 300) -> bool: signature = headers.get("X-Meetstream-Signature", "") if not signature.startswith("sha256="): return False expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, signature.removeprefix("sha256=")): return False # optional replay protection ts = headers.get("X-Meetstream-Timestamp") if ts: sent = datetime.fromisoformat(ts.replace("Z", "+00:00")) age = abs((datetime.now(timezone.utc) - sent).total_seconds()) if age > tolerance_seconds: return False return True ``` > **Note** > > Compute the HMAC over the **raw body bytes exactly as received** — do not parse and re-serialize the JSON first, or the digest will not match. In frameworks that eagerly parse JSON (Express, FastAPI), read the raw body before any body parser runs. ## Verify in Node ```javascript const crypto = require("crypto"); function verifyMeetstreamWebhook(secret, rawBody, headers, toleranceSeconds = 300) { const signature = headers["x-meetstream-signature"] || ""; if (!signature.startsWith("sha256=")) return false; const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex"); const given = Buffer.from(signature.slice(7), "hex"); if (given.length !== 32 || !crypto.timingSafeEqual(Buffer.from(expected, "hex"), given)) return false; const ts = headers["x-meetstream-timestamp"]; if (ts) { const age = Math.abs(Date.now() - Date.parse(ts)) / 1000; if (age > toleranceSeconds) return false; } return true; } // Express: capture the raw body before json parsing // app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } })) ``` ## Best practices * **Reject on failure with a 4xx** and log the event — a failed signature on a real endpoint is worth alerting on. * **Respond 2xx quickly** and process async: delivery is best-effort and non-2xx responses are [not retried](/guides/webhooks/webhooks-and-events). * **Rotate by regenerating** the secret in the dashboard when a secret may have leaked; update your server before regenerating to minimize the gap. * During local development, `meetstream listen` plus a tunnel gets you receiving events in seconds — see [Set Up Local Server for Webhook](/guides/webhooks/local-webhook-server). > Meeting bot API documentation for Zoom, Google Meet and Microsoft Teams: create bots, stream real-time audio, transcribe, and run in-meeting voice agents.