Using ngrok to receive Zoom Events
Tags: zoom, webhooks, ngrok
Published: 2023-01-31 00:37:39

Zoom utilizes **webhooks** as a medium to notify third-party applications about events that occur in a Zoom account.

So, instead of making repeated calls to pull data from the Zoom API, you can use webhooks to get information on events that happen in a Zoom account. 

This guide covers how to implement Zoom webhooks using [**ngrok**](https://ngrok.com/docs/) as our secure URL to receive events from our Zoom account.

We will use our [Webhook-sample-node.js](https://github.com/zoom/webhook-sample-node.js) app available on Gitbhub and we will be setting up a sample app in the [Zoom Marketplace](https://marketplace.zoom.us/).






###**Step 1: Setting up our local environment**

- Install the sample app by running the following commands in your terminal:

```javascript
git clone https://github.com/zoom/webhook-sample-node.js.git
cd webhook-sample-node.js
npm install
```

- Create a .env file by running the following command in your terminal:

```javascript
touch .env
```

And add the following placeholder

```javascript
ZOOM_WEBHOOK_SECRET_TOKEN= 
```

**Note:** Do not run your app quite yet, we need to head to the [Zoom Marketplace](https://marketplace.zoom.us/) and create an app first!

###**Step 2: Create an app in the Marketplace.**

For this tutorial will use a _Webhook only app_.

1. Log in to your Zoom account and head to the Zoom Marketplace
2. Click the dropdown that says Develop and then click Build App
3. Select the Webhook Only app and give your app a name
4. Make sure to fill up the required Basic information (Company name, your name, email address)
5. On the Feature tab, copy the **Secret Token** and paste it in your **.env** file, under the **ZOOM_WEBHOOK_SECRET_TOKEN** placeholder; this value will be used later to validate our endpoint URL and to start receiving event notifications.
6. Click the Event subscription slider and click on Add Event Subscritions, give your subscription a name; and Add events to your app to subscribe, for example: 
     - Meeting has been created
     - Meeting has been updated 
     - Meeting has been deleted.

**Note:** for now, we will be skipping the Event Notification Endpoint URL Validation, but we will come back to this later in the guide.


###**Step 3: Running our sample app and ngrok**

Make sure you’ve saved your project after copying the value of the **Secret Token** from our newly created Webhook Only sample app, into the .env file.

Now you can run your app with the following command in your terminal:

```javascript
npm run start
```


Now that your app is running locally on **port 4000**, we need to expose our local server to the internet to accept post requests, we will be using ngrok for this. (If you are not an ngrok user yet, just sign up for [ngrok for free](https://ngrok.com/) and follow the steps to install ngork and connect your account) 

Start your ngrok by running the following command in your terminal:

```javascript
ngrok http 4000
```

Copy the ngrok https url displayed in the terminal and paste it in your Event notification url input, followed by the **/webhook** path, (it should look like this https://exampleurl.ngrok.io/webhook)


###**Step 4: Validate your endpoint URL**

Once you have added your ngrok https url followed by /webhook path, we will have to validate our endpoint. 

1. Click on the **Validate** button and expect to get a “Validated” message.
2. Click on Save and then Continue to make sure that your app is activated on the account and that it’s ready to receive events from Zoom.


![Screenshot from Marketplace validation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uhuzyi2g24paq8cuo7oi.jpg)


_**What happened here is:**_
Zoom requires to [manually trigger the webhook validation](https://marketplace.zoom.us/docs/api-reference/webhook-reference/#validate-your-webhook-endpoint) when you add a new event or make changes to an existing one. 

Zoom uses a _challenge-response check_ (CRC) for webhook validation and when you click the **Validate** button in your app, a **CRC** occurs and Zoom will make a POST request to your https endpoint with a challenge-request body and your app will need to response with the challenge response and a 200 or 204 HTTP response code, within 3 seconds

This is how the CRC event sent by Zoom looks like:

```javascript
{
  "payload": {
    "plainToken": "qgg8vlvZRS6UYooatFL8Aw"
  },
  "event_ts": 1654503849680,
  "event": "endpoint.url_validation"
}

```

Our sample app, will be listening to the event ‘ednpoint.url_validation’ and once it receives it, it we will create a HMAC SHA-256 hash using the Zoom webhook secret token and the plainToken received in the CRC request body; to lastly respond to Zoom with a Challenge Response and 200 status

```javascript
if(request.body.event === 'endpoint.url_validation') {
  const hashForValidate = crypto.createHmac('sha256', ZOOM_WEBHOOK_SECRET_TOKEN).update(request.body.payload.plainToken).digest('hex')

  response.status(200)
  response.json({
    "plainToken": request.body.payload.plainToken,
    "encryptedToken": hashForValidate
  })
}
```

Challenge response body example:

```javascript
{
  "plainToken": "qgg8vlvZRS6UYooatFL8Aw",
  "encryptedToken": "23a89b634c017e5364a15599e2bb04bb1558d9c3a121faa5"
}
```

###**Step 5: Receiving events in our sample app**

Head to your Zoom account and trigger the respective Webhook, in this case as we chose the Meeting has been created event, you will have to schedule a Zoom meeting to be able to receive the event notification.

Once you create a meeting, you will see the Webhook headers and payload logged in terminal, something like this:

Request headers
```javascript
{
   "host":"db05-2603-7000-8f03-fe4c-b56e-6f3b-bded-7b6d.ngrok.io",
   "user-agent":"Zoom Marketplace/1.0a",
   "content-length":"564",
   "authorization":"{LEGACY_WEBHOOK_VERIFICATION_TOKEN}",
   "clientid":"{clientID}",
   "content-type":"application/json; charset=utf-8",
   "x-forwarded-for":"{X_FORWARDED_FOR}",
   "x-forwarded-proto":"https",
   "x-zm-request-timestamp":"{X_ZM_REQUEST_TIMESTAMP}",
   "x-zm-signature":"{X_ZM_SIGNATURE}",
   "x-zm-trackingid":"{X_ZM_TRACKINGID}",
   "Accept-encoding":"gzip"
}
```
Response body
```javascript
{
   "event":"meeting.created",
   "payload":{
      "account_id":"{ACCOUNT_ID}",
      "operator":"{EMAIL}",
      "operator_id":"{OPERATOR_ID}",
      "object":{
         "uuid":"{MEETING_UUID}",
         "id":"{MEETING_ID}",
         "host_id":"{HOST_ID",
         "topic":"My Meeting",
         "type":2,
         "start_time":"2023-01-26T21:00:00Z",
         "duration":60,
         "timezone":"America/New_York",
         "join_url":"{JOIN_URL}",
         "password":"{PASSWORD}",
         "settings":[
            "Object"
         ],
         "is_simulive":false
      }
   },
   "event_ts":1674764224723
}
```

**What happened here is:**

After receiving the webhook event, the sample app constructed  a message string with _“v0”_, the webhook request header _“x-zm-request-timestamp”_ value and the webhook request body

```javascript
const message = `v0:${request.headers['x-zm-request-timestamp']}:${JSON.stringify(request.body)}`
```


Once the app constructed the message, it created a hash message, by setting the webhook’s secret token as the secret/salt and the message as the string to hash and outputed in hex format

```javascript
const hashForVerify = crypto.createHmac('sha256', ZOOM_WEBHOOK_SECRET_TOKEN).update(message).digest('hex')
```

Created a signature using the hashed Message by prepending "v0=" to it:

```javascript
const signature = `v0=${hashForVerify}`
```

Compared the signature that we created with the value from our request header _“x-zm-signature”_

If the value of **signature** matches with the value of the **request header “x-zm-signature”**, we will start receiving events.


You should now have a running app enabled to receive events in a secure URL.

It is important to remember that every time that you make changes your App in the Marketplace (adding new events or restarting our ngrok secure tunnel) you will have to validate the endpoint URL again.

Thank you for follow along and happy coding! 🙂