# Verify Zoom Webhooks using a CloudFlare Worker

**URL:** <https://devforum.zoom.us/t/verify-zoom-webhooks-using-a-cloudflare-worker/77172>\
**Category:** API and Webhooks\
**Created:** [October 10, 2022, 8:51am UTC](https://devforum.zoom.us/t/verify-zoom-webhooks-using-a-cloudflare-worker/77172 "2022-10-10T08:51:32Z")\
**Posts on this page:** 12\
**Page:** 1

<div class="post-metadata">

**Author:** ![MattB](https://avatars.discourse-cdn.com/v4/letter/m/8797f3/32.png) [@MattB](https://devforum.zoom.us/u/MattB)\
**Post date:** [October 10, 2022, 8:51am UTC](https://devforum.zoom.us/t/verify-zoom-webhooks-using-a-cloudflare-worker/77172/1 "2022-10-10T08:51:32Z")

</div>

I am trying to implement the webhook validation here: [Using webhooks](https://marketplace.zoom.us/docs/api-reference/webhook-reference/#verify-webhook-events) in a cloudflare worker.

I have replicated the code in the example using the subtle crypto library built into CF Workers.

When I generate the hashed signature, the string generated does not match the signature in the header.

My CF Worker code is below:

```auto
async function handleRequest(request) {
    const req = await request.json()
    const ZOOM_WEBHOOK_SECRET_TOKEN = 'XXXXXXXXXXXXXXXXXXXXXX'

	// https://marketplace.zoom.us/docs/api-reference/webhook-reference/#verify-webhook-events
	let requestHeaders = Object.fromEntries(request.headers)
	let response

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

	const hashForVerify = await digestMessage(message)

	const signature = `v0=${hashForVerify}`

	console.log(req)
	console.log(requestHeaders['x-zm-signature'])
	console.log(signature)

	if (requestHeaders['x-zm-signature'] === signature) {

		// Webhook request came from Zoom

		// business logic here, example make API request to Zoom or 3rd party

		response = {
			message: 'Authorized request',
			status: 200
		}
	} else {
		// Webhook request did not come from Zoom
		response = {
			message: 'Request not authorized',
			status: 401
		}
	}

	// return the reponse (authorized or not)
	console.log(response)
	return new Response(response.message, {
		status: response.status
	})
}

async function digestMessage(message) {
  const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
  const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); // hash the message
  const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
  const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string
  return hashHex;
}

```

Anyone else successfully done this in a worker? Is there anywhere I’ve not correctly implemented the verification algorithm?

Also posted here: [https://community.cloudflare.com/t/verify-zoom-webhook-in-worker/425280/2](https://community.cloudflare.com/t/verify-zoom-webhook-in-worker/425280/2)

---

<div class="post-metadata">

**Author:** ![elisa.zoom](https://sea2.discourse-cdn.com/flex016/user_avatar/devforum.zoom.us/elisa.zoom/32/47836_2.png) [@elisa.zoom](https://devforum.zoom.us/u/elisa.zoom)\
**Post date:** [October 10, 2022, 1:03pm UTC](https://devforum.zoom.us/t/verify-zoom-webhooks-using-a-cloudflare-worker/77172/2 "2022-10-10T13:03:18Z")

</div>

Hi @MattB  
Thanks for reaching out to the Zoom Developer Forum, I am happy to help here!  
I have looked into the code snippet you have shared with us and it looks like when you are constructing the message, you are passing the entire request 🙂

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

```

Could you please try passing the req.body like so:

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

```

Let me know if this helps,  
Best,  
Elisa

---

<div class="post-metadata">

**Author:** ![MattB](https://avatars.discourse-cdn.com/v4/letter/m/8797f3/32.png) [@MattB](https://devforum.zoom.us/u/MattB)\
**Post date:** [October 14, 2022, 5:15am UTC](https://devforum.zoom.us/t/verify-zoom-webhooks-using-a-cloudflare-worker/77172/3 "2022-10-14T05:15:57Z")

</div>

Hi @elisa.zoom

Thank you for getting back to me. I’m not passing the whole request, as

```auto
const req = await request.json()

```

returns the request body as JSON. I’ve also tried

```auto
const req = await request.text()

```

and then

```auto
const message = `v0:${request.headers['x-zm-request-timestamp']}:${req}`

```

I can confirm both pass a text string of the request body that parse as valid JSON. Neither work.

I’ve noted two things.

1. [Using webhooks](https://marketplace.zoom.us/docs/api-reference/webhook-reference/#2-construct-the-message) example shows the {WEBHOOK\_REQUEST\_BODY} is not inclused in curly braces as expected of a JSON string and is messing the ‘event’ element from the sample data provided in the previous section. I coded this in (both just removing the outer braces, removing the outer braces + the event element and just removing the event element) and it still doesn’t produce a matching signature.

2. The sample webhook app has been updated to bypass this verfication, 2 months ago. (See [updated code to pass crc validation · zoom/webhook-sample-node.js@7c1689b · GitHub](https://github.com/zoom/webhook-sample-node.js/commit/7c1689b2f4c9d3b8e304a3a99f896e23bd5c5841) - interestingly the commit message calls this validation, I understand this to be webhook verification though)  
This has been left commented out. Why is this being bypassed in the sample app? Is there an issue with the signature generated by Zoom in the webhook?

---

<div class="post-metadata">

**Author:** ![elisa.zoom](https://sea2.discourse-cdn.com/flex016/user_avatar/devforum.zoom.us/elisa.zoom/32/47836_2.png) [@elisa.zoom](https://devforum.zoom.us/u/elisa.zoom)\
**Post date:** [October 14, 2022, 3:57pm UTC](https://devforum.zoom.us/t/verify-zoom-webhooks-using-a-cloudflare-worker/77172/4 "2022-10-14T15:57:36Z")

</div>

Hi @MattB

> 1. The sample webhook app has been updated to bypass this verfication, 2 months ago. (See [updated code to pass crc validation · zoom/webhook-sample-node.js@7c1689b · GitHub](https://github.com/zoom/webhook-sample-node.js/commit/7c1689b2f4c9d3b8e304a3a99f896e23bd5c5841) - interestingly the commit message calls this validation, I understand this to be webhook verification though)  
> This has been left commented out. Why is this being bypassed in the sample app? Is there an issue with the signature generated by Zoom in the webhook?

Thanks for bringing this to our attention, I will make sure to reach out to the team so we can fix this. I do not know why it has been commented out but you need to validate using the signature.

I have been trying to debug this issue on my end and I always see the request body in curly braces { }, I am always getting something like this:

```auto
{
  event: 'meeting.deleted',
  payload: {
    account_id: 'lKw',
    operator: 'elisa@gmail.com',
    operator_id: '6Gtdh2JlFW6vLFw',
    operation: 'single',
    object: {
      uuid: 'EwNQRGO+B4h68w0vg==',
      id: 837778,
      host_id: '6GtdheZQSq-l2JlFw',
      topic: 'Testing via API',
      type: 8,
      start_time: '2022-10-17T11:37:00Z',
      duration: 60,
      timezone: 'America/Mexico_City',
      occurrences: [Array]
    }
  },
  event_ts: 1665762910901
}

```

And I just ran the sample app that you shared

> **[GitHub - zoom/webhook-sample: Receive Zoom webhooks.](https://github.com/zoom/webhook-sample)**
>
> Receive Zoom webhooks. Contribute to zoom/webhook-sample development by creating an account on GitHub.

And what I did is that I commented out lines 24, 26, 29, 32 and 33 and it works just fine and I made sure to get ride of the validation of the URL:

```auto
  var response

  console.log(req.body)
  console.log(req.headers)

  // construct the message string
   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')

  // hash the message string with your Webhook Secret Token and prepend the version semantic
  const signature = `v0=${hashForVerify}`

  // you validating the request came from Zoom https://marketplace.zoom.us/docs/api-reference/webhook-reference#notification-structure
   if (req.headers['x-zm-signature'] === signature) {
    console.log(signature)

    // Zoom validating you control the webhook endpoint https://marketplace.zoom.us/docs/api-reference/webhook-reference#validate-webhook-endpoint
    // if(req.body.event === 'endpoint.url_validation') {
    // 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
    // }

    // console.log(response.message)

    // res.status(response.status)
    // res.json(response.message)

```

Please note how in line 24 we are passing req.body and you are passing the entire requests.  
Hope this helps,  
Elisa

---

<div class="post-metadata">

**Author:** ![MattB](https://avatars.discourse-cdn.com/v4/letter/m/8797f3/32.png) [@MattB](https://devforum.zoom.us/u/MattB)\
**Post date:** [October 15, 2022, 8:39am UTC](https://devforum.zoom.us/t/verify-zoom-webhooks-using-a-cloudflare-worker/77172/5 "2022-10-15T08:39:52Z")

</div>

Elisa - as previously stated, I am NOT passing the entire request. I am passing the request body exactly as you are. `const req = await request,json()` gets the body in JSON format into the const req. I can change the const name from ‘req’ to ‘body’ if you prefer.

I think the issue then is that on a cloudflare worker I cannot use nodeJS crypto. I am using the subtle crypto equivalent but must be missing something. As far as I can see I am doing the same, constructing a digest and converting it to hex for comparison (see [SubtleCrypto.digest() - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string))

The JSON Sample you provided is not validating properly. It should have double quotes instead of single quotes and identifiers should be quoted. Like this:

```auto
{
   "event":"meeting.deleted",
   "payload":{
      "account_id":"lKw",
      "operator":"elisa@gmail.com",
      "operator_id":"6Gtdh2JlFW6vLFw",
      "operation":"single",
      "object":{
         "uuid":"EwNQRGO+B4h68w0vg==",
         "id":837778,
         "host_id":"6GtdheZQSq-l2JlFw",
         "topic":"Testing via API",
         "type":8,
         "start_time":"2022-10-17T11:37:00Z",
         "duration":60,
         "timezone":"America/Mexico_City",
         "occurrences":[
            "Array"
         ]
      }
   },
   "event_ts":1665762910901
}

```

When I put the above JSON body through my worker I get a hashForVerify of `v0=8d49d6cc06393da72b9fcf8e927fa6d6f1bb51f6d21d35551976d0eae348c75c`

This assumes the x-zm-request-timestamp header is 1665762910901

As the worker would throw a 500 error if the JSON is not validate I’m assuming the Zoom Webhook does send valid JSON. Is all whitespace stripped out as this would change the hash?

Could you provide the exact request body you have run through your code, the value of the x-zm-request-timestamp header, and the signature for verification? I’ll run this through the worker and see if I’m getting the same signature or not.

---

<div class="post-metadata">

**Author:** ![elisa.zoom](https://sea2.discourse-cdn.com/flex016/user_avatar/devforum.zoom.us/elisa.zoom/32/47836_2.png) [@elisa.zoom](https://devforum.zoom.us/u/elisa.zoom)\
**Post date:** [November 1, 2022, 3:21pm UTC](https://devforum.zoom.us/t/verify-zoom-webhooks-using-a-cloudflare-worker/77172/6 "2022-11-01T15:21:12Z")

</div>

Hi @MattB  
I am so sorry for the late reply.  
I was out of office for the last 2 weeks and I am only catching up into my messages.  
Were you able to resolve this issue?

---

<div class="post-metadata">

**Author:** ![ross.hopkins](https://sea2.discourse-cdn.com/flex016/user_avatar/devforum.zoom.us/ross.hopkins/32/43699_2.png) [@ross.hopkins](https://devforum.zoom.us/u/ross.hopkins)\
**Post date:** [December 12, 2022, 12:55am UTC](https://devforum.zoom.us/t/verify-zoom-webhooks-using-a-cloudflare-worker/77172/7 "2022-12-12T00:55:40Z")

</div>

Hi @MattB

Did you ever resolve your issue?

I’m trying to solve this problem too, albeit by implementing the JavaScript within a Code “App” within Zapier. Within Zapier JavaScript you cannot import the libraries the sample Zoom web app includes (I found your thread on the Cloudflare forum via a Google search).

I’m no developer but can often reverse engineer code, but this is beyond me. However, Cloudflare or Zapier aside, I think any JS you get working here I could adapt.

([https://community.zapier.com/code-webhooks-52/zoom-validate-your-webhook-endpoint-19708?postid=81312#post81312](https://community.zapier.com/code-webhooks-52/zoom-validate-your-webhook-endpoint-19708?postid=81312#post81312))

Cheers,  
Ross

---

<div class="post-metadata">

**Author:** ![MattB](https://avatars.discourse-cdn.com/v4/letter/m/8797f3/32.png) [@MattB](https://devforum.zoom.us/u/MattB)\
**Post date:** [December 20, 2022, 8:36pm UTC](https://devforum.zoom.us/t/verify-zoom-webhooks-using-a-cloudflare-worker/77172/8 "2022-12-20T20:36:32Z")

</div>

Hi @elisa.zoom - no this is not resolved. I’m still unable to verify Zoom Webhooks

---

<div class="post-metadata">

**Author:** ![elisa.zoom](https://sea2.discourse-cdn.com/flex016/user_avatar/devforum.zoom.us/elisa.zoom/32/47836_2.png) [@elisa.zoom](https://devforum.zoom.us/u/elisa.zoom)\
**Post date:** [December 21, 2022, 4:29pm UTC](https://devforum.zoom.us/t/verify-zoom-webhooks-using-a-cloudflare-worker/77172/9 "2022-12-21T16:29:07Z")

</div>

Hi @MattB  
Have you been able to run the sample app that I shared with you?

> **[GitHub - zoom/webhook-sample-node.js: Receive Zoom Webhooks](https://github.com/zoom/webhook-sample-node.js)**
>
> Receive Zoom Webhooks. Contribute to zoom/webhook-sample-node.js development by creating an account on GitHub.

---

<div class="post-metadata">

**Author:** ![MattB](https://avatars.discourse-cdn.com/v4/letter/m/8797f3/32.png) [@MattB](https://devforum.zoom.us/u/MattB)\
**Post date:** [February 17, 2023, 5:48am UTC](https://devforum.zoom.us/t/verify-zoom-webhooks-using-a-cloudflare-worker/77172/10 "2023-02-17T05:48:27Z")

</div>

So I’m still not verifying webhooks. @elisa.zoom that app is for node.js. I can’t use it as a cloudflare worker. Please see my comments from 15th Oct 22 - I never had a response to my requests from you.

---

<div class="post-metadata">

**Author:** ![elisa.zoom](https://sea2.discourse-cdn.com/flex016/user_avatar/devforum.zoom.us/elisa.zoom/32/47836_2.png) [@elisa.zoom](https://devforum.zoom.us/u/elisa.zoom)\
**Post date:** [February 17, 2023, 3:20pm UTC](https://devforum.zoom.us/t/verify-zoom-webhooks-using-a-cloudflare-worker/77172/11 "2023-02-17T15:20:57Z")

</div>

Hi @MattB  
I will go ahead and send you a private message to share more information with you

---

<div class="post-metadata">

**Author:** ![system](https://us1.discourse-cdn.com/flex016/uploads/zoomdeveloper/original/3X/6/1/614bdd549b610bbaa46ff934617683a02bdaa03c.png) [@system](https://devforum.zoom.us/u/system)\
**Post date:** [February 21, 2024, 6:57am UTC](https://devforum.zoom.us/t/verify-zoom-webhooks-using-a-cloudflare-worker/77172/12 "2024-02-21T06:57:07Z")

</div>

Closed on February 21, 2024, 6:57am UTC

This topic was automatically closed 368 days after the last reply. New replies are no longer allowed.
