You wired up a Calendly webhook subscription, pointed it at your app, and now every invitee.created event vanishes into a public endpoint you can't step through with a debugger. To iterate, you push to staging, wait for a deploy, book a fake meeting, and stare at logs. That loop is slow, and it gets slower every time you tweak the handler.
The fix is to bring Calendly's events straight to your machine. Relayers gives you a stable public ingest URL, forwards the original request — body and headers, untouched — down a tunnel to localhost, and lets you filter and reshape payloads before they ever hit your code. Because the raw bytes and the Calendly-Webhook-Signature header arrive intact, your signature verification runs locally exactly as it will in production.
What you'll build
Calendly delivers to one durable Relayers URL. Relayers applies your routing rules, optionally transforms the payload, and streams each event through the CLI tunnel to whatever port your app runs on.
Prerequisites
- A Calendly account with API access and a personal access token (or OAuth token).
- Your
organizationoruserURI from the Calendly API (GET https://api.calendly.com/users/me). - A local app listening on a port — we'll use
localhost:3000. - The Relayers app installed and a free account. Grab it at relayers.app.
1Create an endpoint in Relayers
Open the Relayers desktop app and go to Endpoints → Create. Name it something like calendly-dev. You'll get a public ingest URL shaped like:
https://api.relayers.app/v1/webhooks/wep_9f3c2ab1d4e5
Copy it. Before touching Calendly, hit Send test to push a sample request through the pipeline so you can confirm the plumbing works end to end. The payload inspector and Events view will show it arrive.
2Create a webhook subscription in Calendly
Calendly has no dashboard toggle for webhooks — subscriptions are created through the API. POST to the Webhook Subscriptions endpoint with your Relayers URL, the events you care about, your scope, and a signing_key so deliveries are signed.
curl -X POST https://api.calendly.com/webhook_subscriptions \
-H "Authorization: Bearer $CALENDLY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.relayers.app/v1/webhooks/wep_9f3c2ab1d4e5",
"events": ["invitee.created", "invitee.canceled"],
"organization": "https://api.calendly.com/organizations/AAAAAAAAAAAAAAAA",
"scope": "organization",
"signing_key": "your-random-signing-secret"
}'
From now on, every time someone books or cancels, Calendly signs the request with an HMAC-SHA256 digest and sends it in the Calendly-Webhook-Signature header — a Stripe-style t=<timestamp>,v1=<signature> value you verify against your signing_key.
The payload looks like this:
{
"event": "invitee.created",
"payload": {
"email": "sam@example.com",
"name": "Sam Rivera",
"scheduled_event": {
"start_time": "2026-07-20T15:00:00.000000Z"
}
}
}
3Forward events to localhost
Log in and start the tunnel from the CLI:
relayers login
relayers listen --forward localhost:3000
relayers listen opens a WebSocket tunnel and forwards each event to your local port. Book a test meeting on your Calendly link and watch the request land in your terminal and in the app's Events view. If your handler is broken, set a breakpoint and re-run — no redeploys, no waiting.
4Route and filter with JQ
You probably don't want cancellations hitting your "send welcome email" handler. In the Rules tab, add a JQ filter so only invitee creations flow through:
{ "event": "invitee.created" }
A rule matches when the JQ expression's output matches the event. Set the rule's destination to your tunnel (localhost:3000) or a public URL. Rules are evaluated top to bottom and first match wins, so you can add a second rule for invitee.canceled pointing at a different port or service and reorder them to control precedence.
5Transform the payload
Calendly's envelope is nested. If your handler just wants the essentials, add a Transformation (jq, jsonata, javascript, or go_template) to flatten it before delivery:
{
event: .event,
name: .payload.name,
email: .payload.email,
start: .payload.scheduled_event.start_time
}
Given the sample above, your app now receives:
{
"event": "invitee.created",
"name": "Sam Rivera",
"email": "sam@example.com",
"start": "2026-07-20T15:00:00.000000Z"
}
Use the test playground to run your transform against a real captured payload until the output is exactly what your handler expects.
Signature verification still works
A common gotcha with webhook proxies is that they re-encode the body or drop headers, which breaks HMAC checks. Relayers forwards the original body and headers untouched, so the Calendly-Webhook-Signature header reaches localhost intact and your verification code — recomputing the HMAC-SHA256 over the raw payload with your signing_key — behaves locally exactly as it does in production. Note that if you apply a transformation, verify the signature against the raw event before the transform, or run verification on the untransformed copy.
Going to production
When you're ready, point a rule's destination at your deployed public URL instead of the tunnel and stop the CLI. Nothing else changes on Calendly's side — the subscription URL stays the same. Even better, use Export to save your endpoint, rules, and transformations as a JSON bundle, then Import it into your production or staging setup so the config is reproducible instead of hand-rebuilt.
Troubleshooting
- No events arriving. Confirm the subscription
scopematches how you created it —organizationscope requires theorganizationURI,userscope requires theuserURI. A mismatch silently produces no deliveries. 403or401when creating the subscription. Your token lacks webhook permissions or belongs to a different organization. Re-check the token and theorganization/userURIs fromGET /users/me.- Signature verification failing. Make sure you're using the exact
signing_keyyou sent when creating the subscription, computing HMAC-SHA256 over the raw request body, and comparing against thev1value inCalendly-Webhook-Signature. Use the payload inspector to confirm the header is present. - Filter never matches. Test your JQ in the Rules editor against a captured event — Calendly nests the type under
.event, not.type.
Wrap-up
With one stable Relayers endpoint, a JQ filter, and an optional transform, Calendly's invitee.created and invitee.canceled events land on your localhost in seconds — signed, filtered, and shaped the way your handler wants them. Build the whole integration with a debugger attached, then flip the destination to production without touching Calendly again.
Ready to try it? Head to relayers.app and download the app to start testing your calendly webhook localhost flow on the free tier.