Troubleshooting
The failure modes that actually come up, and what each one means.
Something read the request body before the adapter did. A Web Request body can
only be read once and the signature covers the exact bytes Zoom sent, so a
logger or middleware calling request.json() breaks verification for every
event. Pass the untouched Request.
If the body is clean, check that ZOOM_WEBHOOK_SECRET_TOKEN matches the Secret
Token under Features → Access → Token, and that your server’s clock is
right — signatures older than five minutes are refused.
That is ZOOM-506645, a normalisation bug on Zoom’s side. Turn on debug
logging: the adapter writes the first 200 bytes of the body as hex when the
comparison fails, which is what Zoom Support needs. See
Limitations.
Check all three in the Marketplace app:
- Surface → Team Chat Subscription — the Bot Endpoint URL matches your deployment.
- Features → Access → Event Subscription — the webhook URL matches too, and
bot_notificationandteam_chat.app_mentionare both subscribed. - Local Test → Add app now — the app is actually installed in the account.
With ngrok, a restart means a new URL and both fields need it again.
The events are probably arriving and failing verification — check the signature first. Otherwise confirm the app is Admin-managed, and that the process handling the webhook is the one holding your credentials.
ZoomAdapter: postMessage failed with HTTP 401 almost always means the wrong
OAuth grant. The /v2/im/chat/messages endpoints need a client_credentials
token, which is what the adapter requests; an account_credentials token is
rejected. If you are supplying credentials yourself, check they belong to the
Server-to-Server OAuth app you registered the bot under.
Confirmed Zoom behaviour, not a configuration problem: no event fires for replies inside a one-to-one thread. See Limitations.
The package is ESM only. Use import, and make sure the importing package is
ESM too ("type": "module", or a .mjs file).
Check that Markdown mode is on: markdownSupport defaults to true, so only an
explicit false (or ZOOM_MARKDOWN_SUPPORT=false) turns it off. If you develop
against a local copy of the package, rebuild it — a file: dependency is a
snapshot, so pnpm build alone does not update it. A link: dependency or a
reinstall picks up the new dist/.
Also note that Zoom’s chatbot parser uses *bold*, not **bold**, for bold. The
adapter already sends the right token; if you are constructing raw payloads
yourself, **text** renders as plain text. Formatting has the
verified token table.
Expected. Zoom’s Markdown parser has no underline token and no table token. The words and cells are still sent as plain text. See Formatting.