Integrate Calendly Webhooks with Node.js

I’m running into issues with getting Calendly webhooks set up in my Node.js application. Even though I tried following the documentation carefully, something isn’t quite right.

Here’s how I approached the integration:

Step 1: I retrieved my user information by making a GET request to https://api.calendly.com/users/me:

{
    "resource": {
        "avatar_url": null,
        "created_at": "2024-05-22T09:45:15.000000Z",
        "current_organization": "https://api.calendly.com/organizations/xyz456-1234-5678-abcd-xyz456789012",
        "email": "[email protected]",
        "name": "User Example",
        "resource_type": "User",
        "scheduling_url": "https://calendly.com/user-example",
        "slug": "user-example",
        "timezone": "Europe/London",
        "updated_at": "2024-09-22T12:40:00.000000Z",
        "uri": "https://api.calendly.com/users/abc123-4567-8901-cdef-abc123456789"
    }
}

Step 2: I created a webhook subscription by sending a POST request to https://api.calendly.com/webhook_subscriptions:

{
 "url": "https://prod.api.user-app.com/calendly-webhook",
 "events": [
   "invitee.created",
   "invitee.canceled",
   "invitee_no_show.created",
   "invitee_no_show.deleted"
 ],
 "organization": "https://api.calendly.com/organizations/xyz456-1234-5678-abcd-xyz456789012",
 "scope": "organization",
 "signing_key": "3fQ8s9D-K58VuwXyAZsHykp7zBGD45JhZaLqk5h6j1M"
}

Step 3: I implemented the handler in Node.js:

const checkWebhookSignature = (request) => {
  const receivedSignature = request.headers['calendly-webhook-signature'];
  const secret = process.env.CALENDLY_SECRET;

  const requestBody = JSON.stringify(request.body);
  const hmacHash = crypto.createHmac('sha256', secret);
  const generatedHash = hmacHash.update(requestBody).digest('hex');

  return receivedSignature === generatedHash;
};

// WEBHOOK HANDLER FOR CALENDLY
ApiRouter.post('/calendly-webhook', async (req, res) => {
  try {
    console.log('Webhook payload received:');
    if (!checkWebhookSignature(req)) {
      return res.status(401).send('Signature mismatch');
    }

    const eventPayload = req.body;

    switch (eventPayload.event) {
      case 'invitee.created':
        await handleInviteeCreation(eventPayload.payload);
        break;
      case 'invitee.canceled':
        await handleAppointmentCancellation(eventPayload.payload);
        break;
      case 'invitee_no_show.created':
        await handleNoShow(eventPayload.payload);
        break;
      case 'invitee_no_show.deleted':
        await handleNoShowRemoval(eventPayload.payload);
        break;
      default:
        console.log(`Unknown event received: ${eventPayload.event}`);
    }

    res.status(200).send('Webhook handled successfully');
  } catch (error) {
    console.error('Error handling Calendly webhook:', error);
    res.status(500).send('Failed to handle the webhook');
  }
});

When I try to test using Postman with this payload, I keep encountering “Signature mismatch”:

Header:
Calendly-Webhook-Signature: t=1694609256,v1=3fQ8s9D-K58VuwXyAZsHykp7zBGD45JhZaLqk5h6j1M

Body:
{
  "event": "invitee.created",
    "payload": {
      "event": {
      "start_time": "2024-10-05T15:00:00Z",
      "end_time": "2024-10-05T15:30:00Z"
      },
    "invitee": {
      "email": "[email protected]",
      "name": "Test User"
    }
  }
}

I can’t seem to understand what could be causing the problem with the signature validation. Any insights or help would be appreciated!

That signature format shows Calendly uses timestamped signatures, not straight HMAC comparison. The t=1694609256,v1=3fQ8s9D... format means you need to extract the timestamp and signature separately, then verify against timestamp.raw_body before hashing. I ran into this same thing with Stripe webhooks. Here’s what works: javascript const verifySignature = (signature, body, secret) => { const elements = signature.split(','); const timestamp = elements[0].split('=')[1]; const v1 = elements[1].split('=')[1]; const signedPayload = timestamp + '.' + body; const expectedSignature = crypto.createHmac('sha256', secret) .update(signedPayload, 'utf8') .digest('hex'); return expectedSignature === v1; }; Also make sure you’re using the signing key Calendly returns in the webhook creation response, not the one you sent in your POST request. Calendly generates the signing key and sends it back to you.

Been dealing with webhook integrations for years and manual signature validation is honestly a pain. You’re hitting the classic raw body vs parsed body issue SilentSailing34 mentioned, but there’s a bigger problem.

Managing webhook endpoints, signature validation, retries, and edge cases manually eats up dev time. Learned this the hard way after building these integrations over and over.

Now I just use Latenode for webhook integrations. It handles Calendly webhooks without the signature validation headaches - connect your Calendly account, pick your events, done.

Real win is piping webhook data straight to your database, sending notifications, triggering APIs, whatever you need. No more debugging HMAC signatures or wrestling with raw request bodies.

Set up Calendly to database integration last month in 10 minutes. Would’ve taken hours to code and debug manually.

Check it out: https://latenode.com

Your signature validation issue is probably happening because you’re processing the request body before validating it. Express.js body parsing middleware transforms the raw body into a JavaScript object, but Calendly calculates their signature against the original raw request body string.

I hit this exact problem last year. Fixed it by capturing the raw body before any parsing happens. You need to modify your middleware setup to store the raw buffer. Add this before your JSON body parser:

app.use('/calendly-webhook', express.raw({type: 'application/json'}));

Then use the raw buffer in your signature verification function instead of JSON.stringify. Also, make sure you’re using the actual signing key from your webhook subscription response - not the one you sent in the POST request. Grab the signing key from the webhook subscription creation response and store it securely as your CALENDLY_SECRET environment variable.