Webhooks
The single Zoom endpoint — the setup challenge, signature checks, event dispatch, and answering Zoom quickly.
Zoom delivers everything a bot hears to one HTTP endpoint. Hand the request straight over:
// app/api/webhooks/zoom/route.ts
import { after } from "next/server";
import { bot } from "@/lib/bot";
export async function POST(request: Request) {
return bot.webhooks.zoom(request, { waitUntil: after });
}
At the endpoint
Zoom sends
POST /api/webhooks/zoom
{
"event": "endpoint.url_validation",
"payload": { "plainToken": "qgg8vlvZRS6UYooatFL8Aw" }
}The adapter answers
200 OK
{
"plainToken": "qgg8vlvZRS6UYooatFL8Aw",
"encryptedToken": "23a89b634c017..."
}Zoom sends this when you save the endpoint URL. It carries no signature, so it is answered before any signature check — otherwise the Marketplace could never verify the endpoint.
Answer Zoom first, work after
waitUntil — Next.js after, or a Cloudflare ExecutionContext.waitUntil —
hands your event processing to the runtime so the 200 goes back immediately.
Without it the response waits for your handlers to finish, and a slow handler
becomes a slow webhook.
What comes back
| Situation | Response |
|---|---|
| The body is not valid JSON | 400 Invalid JSON |
The setup challenge (endpoint.url_validation) |
200 with { plainToken, encryptedToken } |
| Missing, stale, or mismatched signature | 401 Invalid signature |
| Handled or unrecognised event | 200 ok |
Return the response as it is. An unrecognised event is answered 200 ok on
purpose: Zoom retries anything else.
Events
| Event | What it is |
|---|---|
endpoint.url_validation |
The challenge Zoom sends when you save the endpoint URL |
bot_notification |
A DM, a slash command, or a message to the bot in a channel |
team_chat.app_mention |
An @-mention of the bot in a channel |
Both message events reach your onNewMention handlers: Zoom has no separate
mention event for bot_notification, so the adapter marks those messages as
mentions too and your handlers fire consistently with other platforms.
Signature checks
Each event carries x-zm-signature and x-zm-request-timestamp. The adapter
recomputes v0:{timestamp}:{raw body} as HMAC-SHA256 with your webhook secret
token and compares it in constant time, rejecting anything signed more than five
minutes ago.
If verification fails, the adapter writes the first 200 bytes of the body as hex
to the logger at debug level. That is also where to look for the Unicode
signature bug described in Limitations.
Before the bot is ready
Events that arrive before Chat has initialized the adapter are logged and
dropped rather than queued. In practice this only shows up if your endpoint is
live before the bot is constructed.