---
title: "Test Calendly Webhooks Locally | WebhookRelay"
meta:
  "og:description": "Test Calendly webhooks locally on localhost without deploying. Create the API webhook subscription, inspect the real invitee.created payload, and verify the signature."
  "og:title": "Test Calendly Webhooks Locally"
  description: "Test Calendly webhooks locally on localhost without deploying. Create the API webhook subscription, inspect the real invitee.created payload, and verify the signature."
---

![Stripes](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/images/stripes.svg)

# **Test Calendly Webhooks Locally**

Test Calendly webhooks locally on localhost without deploying. Create the API webhook subscription, inspect the real invitee.created payload, and verify the signature.

![Test Calendly Webhooks Locally (Calendly Webhook on localhost)](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/images/blog/heroes/localhost.jpg)

You are building a Calendly integration — syncing bookings into your CRM, kicking off an onboarding flow, posting to Slack when someone schedules — and you need to see your handler react to a real `invitee.created` event. The problem is immediate: Calendly will only POST to a **public URL**, and your handler is running on `localhost:8080`. Calendly has no way to reach it.

The usual workarounds are painful. Deploying to a staging server for every code change is slow. Pasting a sample payload from the docs into curl gives you a _guess_ at the real request, not the real headers and body Calendly actually sends. What you want is to **test Calendly webhooks locally** — real booking events, hitting your local handler, with a URL that does not change every time you restart.

This guide shows how to do exactly that.

## [Why testing Calendly webhooks locally is tricky](#why-testing-calendly-webhooks-locally-is-tricky)

A webhook is just an HTTP request that Calendly sends to a URL when someone schedules or cancels a meeting. Calendly sits on the public internet; your dev machine usually does not. It is behind a router, a corporate firewall, or both, with no public IP and no inbound ports open.

So you need something in the middle: a public endpoint Calendly can hit, that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you create the subscription once and never touch it again.

One Calendly detail to know up front: **webhook subscriptions require a paid Calendly plan**, and for most plans there is no dashboard form. You create the subscription through the **Calendly API**, not a UI toggle.

## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin)

Before you write any handler code, find out what Calendly actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL.

1. Copy the Webhook Bin URL.
2. Create a webhook subscription pointed at it. Calendly's subscriptions are API-created: POST to `https://api.calendly.com/webhook_subscriptions` with your **personal access token** in the `Authorization` header.

```
curl -X POST https://api.calendly.com/webhook_subscriptions \
  -H "Authorization: Bearer $CALENDLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://YOUR-WEBHOOK-BIN-URL",
    "events": ["invitee.created", "invitee.canceled"],
    "organization": "https://api.calendly.com/organizations/AAAA",
    "scope": "organization"
  }'
```

Now book a test meeting on one of your event types (and cancel it) and inspect the captured request in Webhook Bin: the full JSON body, the query string, and every header, including `Calendly-Webhook-Signature`. The body wraps the booking under an `event` field (`invitee.created` / `invitee.canceled`) with a `payload` object containing the invitee's name, email, scheduled time, and answers to your booking questions.

Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/blog/what-is-webhook/).

## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent)

Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `calendly`. The bucket gives you a stable public input endpoint.

Start forwarding to your local server:

```
relay forward --bucket calendly http://localhost:8080/webhook
```

The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/docs/webhooks/internal/localhost/).

Now create (or update) the Calendly subscription with your **Webhook Relay endpoint** as the `url`, using the same API call as above. Schedule a test meeting and watch it arrive on `localhost`.

## [Calendly-specific configuration and quirks](#calendly-specific-configuration-and-quirks)

A few Calendly details worth knowing:

- **API-created, not a UI form:** for most plans there is no "Add webhook" button. You manage subscriptions via the `/webhook_subscriptions` endpoint with a personal access token (or an OAuth token).
- **Events:** subscribe to `invitee.created` (scheduled) and `invitee.canceled` (canceled). A **reschedule fires both** — an `invitee.canceled` for the old slot and an `invitee.created` for the new one. Branch on the `event` field in the body.
- **Scope:** set `scope` to `user` for only your own bookings, or `organization` to cover every member in your org.
- **Paid plan required:** webhook subscriptions are a paid Calendly feature.
- **Signing key:** the API returns a per-subscription signing key when you create the subscription — store it, you will need it to verify signatures.

## [Step 3: Verify the Calendly webhook signature](#step-3-verify-the-calendly-webhook-signature)

Calendly signs every request. It computes an HMAC-SHA256 over the **timestamp plus the raw request body** using your subscription's signing key, and sends the result in the **`Calendly-Webhook-Signature`** header. The value looks like `t=1700000000,v1=abc123...` — a timestamp and a signature, the same shape Stripe uses.

To verify: split the header into `t` and `v1`, build the signed string as `t + "." + rawBody`, compute HMAC-SHA256 with your signing key, and compare your digest against `v1` in constant time. The timestamp lets you reject stale requests and defend against replay attacks.

To sanity-check your implementation, paste a captured body, your signing key, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body after a JSON parser has consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/blog/verify-webhook-signature/) and the [HMAC auth docs](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/docs/webhooks/auth/hmac/).

## [Replay and iterate](#replay-and-iterate)

This is where local development gets fast:

- **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured `invitee.created` without scheduling a new meeting in Calendly every time.
- **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a code path.

Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Calendly subscription never needs to change.

## [Get started](#get-started)

1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/webhook-bin/) — no signup needed.
2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket calendly http://localhost:8080/webhook`.
3. Create a Calendly webhook subscription pointed at the stable endpoint, schedule a test meeting, and watch it hit `localhost`.

You will be testing real Calendly bookings against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once.