# API and webhook tokens To migrate from Slack to Zoom, create your app, get your developer credentials, and request your chatbot token. ## Get credentials Before you call Zoom chatbot endpoints and use webhooks, you need to [get your Client ID and Client Secret, Bot JID, and webhook secret token](/docs/chat/create/). ## Add app After getting your credentials, you need to [add your chatbot app](/docs/chat/installation-and-authentication/#add-app). ## Request chatbot token Now that we have our developer credentials and have added our app, we can request a Chatbot token. Unlike Slack, which gives you the chatbot token in the developer portal, Zoom requires you to request a chatbot token via the `client_credentials` OAuth grant type. Request URL: ```plaintext POST: https://zoom.us/oauth/token?grant_type=client_credentials ``` Request Headers: ```json { "Authorization": "Basic base64Encoded(ZOOM_CLIENT_ID:ZOOM_CLIENT_SECRET)" } ``` If successful, the response body is a JSON representation of the Chatbot token: ```json { "access_token": "", "token_type": "bearer", "expires_in": 3600, "scope": "imchat:bot", "api_url": "https://api.zoom.us" } ``` Chatbot tokens expire after one hour. After your chatbot token expires, you can simply [request a new one](#request-chatbot-token), there is no separate refresh flow required. ## OAuth token To call APIs endpoints other than the Chatbot ones, you can request an [account managed](/docs/internal-apps/s2s-oauth/) or [admin/user managed](/docs/integrations/oauth/) OAuth `access_token` with your Chatbot's same client ID and client secret. ## Webhook verification [Verifying the webhook request comes from Zoom](/docs/api/webhooks/#verify-webhook-events) is more simple than Slack since Zoom uses `content-type: application/json` for webhook requests where Slack uses both `content-type: application/x-www-form-urlencoded` and `content-type: application/json`. Notice for Slack you have to use the rawBody request body. Below is an example in Node.js. ```javascript import crypto from "crypto"; const message = `v0:${req.headers["x-slack-request-timestamp"]}:${req.rawBody}`; const hashForVerify = crypto .createHmac("sha256", process.env.SLACK_WEBHOOK_SECRET) .update(message) .digest("hex"); const signature = `v0=${hashForVerify}`; // you verifying the request came from Slack if (req.headers["x-slack-signature"] === signature) { // Authorized request, continue to challenge or handling all webhook events } else { // Unauthorized request } ``` ```javascript import crypto from "crypto"; const message = `v0:${req.headers["x-zm-request-timestamp"]}:${JSON.stringify(req.body)}`; const hashForVerify = crypto .createHmac("sha256", process.env.ZOOM_WEBHOOK_SECRET_TOKEN) .update(message) .digest("hex"); const signature = `v0=${hashForVerify}`; // you verifying the request came from Zoom if (req.headers["x-zm-signature"] === signature) { // Authorized request, continue to challenge or handling all webhook events } else { // Unauthorized request } ``` ## Webhook validation [Zoom validating you control the webhook URL](/docs/api/webhooks/#validate-your-webhook-endpoint) is also more simple than Slack since Zoom uses `content-type: application/json` for webhook requests where Slack uses both `content-type: application/x-www-form-urlencoded` and `content-type: application/json`. Notice for Slack you now use the JSON request body. Below is an example in Node.js. Slack validation header: ```json { "host": "hardy-moved-mako.ngrok-free.app", "user-agent": "Slackbot 1.0 (+https://api.slack.com/robots)", "content-length": "129", "accept": "*/*", "accept-encoding": "gzip,deflate", "content-type": "application/json", "x-forwarded-for": "54.209.23.241", "x-forwarded-host": "hardy-moved-mako.ngrok-free.app", "x-forwarded-proto": "https", "x-slack-request-timestamp": "1739921314", "x-slack-signature": "v0=WF9TTEFDS19TSUdOQVRVUkU" } ``` Slack validation body: ```json { "token": "U0xBQ0tfV0VCSE9PS19UT0tFTg", "challenge": "U0xBQ0tfV0VCSE9PS19DSEFMTEVOR0U", "type": "url_verification" } ``` ```javascript if (req.body.type === "url_verification") { // Challenge webhook event type response = { challenge: req.body.challenge, status: 200, }; res.status(response.status); res.json(response.challenge); } else { response = { message: "Authorized request.", status: 200 }; res.status(response.status); res.json(response); // All other webhook event types } ``` Zoom validation header: ```json { "host": "hardy-moved-mako.ngrok-free.app", "user-agent": "Zoom Marketplace/1.0a", "content-length": "110", "authorization": "Wk9PTV9BVVRIT1JJWkFUSU9O", "content-type": "application/json; charset=utf-8", "traceparent": "Wk9PTV9UUkFDRVBBUkVOVA", "x-forwarded-for": "134.224.12.54", "x-forwarded-host": "hardy-moved-mako.ngrok-free.app", "x-forwarded-proto": "https", "x-zm-request-timestamp": "1739923528", "x-zm-signature": "v0=WF9aT09NX1NJR05BVFVSRQ", "accept-encoding": "gzip" } ``` Zoom validation body: ```json { "payload": { "plainToken": "Wk9PTV9QTEFJTl9UT0tFTg" }, "event_ts": 1654503849680, "event": "endpoint.url_validation" } ``` ```javascript if (req.body.event === "endpoint.url_validation") { // Challenge webhook event type const hashForValidate = crypto .createHmac("sha256", process.env.ZOOM_WEBHOOK_SECRET_TOKEN) .update(req.body.payload.plainToken) .digest("hex"); response = { message: { plainToken: req.body.payload.plainToken, encryptedToken: hashForValidate, }, status: 200, }; res.status(response.status); res.json(response.message); } else { response = { message: "Authorized request.", status: 200 }; res.status(response.status); res.json(response); // All other webhook event types } ```