Automate Calendly webhook bookings into Google Sheets and send Slack alerts. Set up a secure Apps Script endpoint with idempotent rows.
Introduction
When a new booking lands in Calendly, the useful information does not go anywhere by default unless you build an integration. In day-to-day operations, that turns into copy-paste work: you grab the invitee name, email, and start time, paste it into Google Sheets, then message the team in Slack. It is also easy to miss updates like cancellations or reschedules.
This is a practical Cross-Platform Automation (XPA) setup to automatically send Calendly webhook bookings into Google Sheets and notify a Slack channel. By the end, you will have a webhook receiver that writes each booking to a dedicated Google Sheets tab and posts a concise Slack alert for your team.
What You'll Need
- A Calendly account with access to webhook subscriptions
- A Google account with permission to create a Google Sheet and deploy an Apps Script web app
- A Google Sheet to store bookings (create it up front)
- A Slack workspace and a Slack Incoming Webhook (the simplest option)
- Automation platform in this post: Google Apps Script (the webhook receiver)
Notes on requirements:
- You will need a Slack Incoming Webhook URL.
- Your Apps Script web app must be deployed so Calendly can reach it.
How It Works (The Logic)
- Calendly sends an HTTPS webhook payload when an invitee creates (or cancels, depending on your subscription) a booking.
- Your Google Apps Script web app receives that JSON payload on its
doPostendpoint. - Apps Script maps payload fields into a row schema and appends it to Google Sheets.
- After a successful write, Apps Script posts a message to Slack using the Incoming Webhook.
Reliability detail that matters: webhooks can retry. This setup uses an idempotency key to prevent duplicate rows.
Step-by-Step Setup
1) Create the Google Sheet and headers
In your Google Sheet, create a tab called calendly_events and add headers in row 1:
idempotency_keyingested_atevent_typeevent_nameevent_startevent_endtime_zoneinvitee_nameinvitee_emailevent_urlpayload_event_uuidpayload_invitee_uuid
If you collect custom questions in Calendly, add columns for the answers you want to store.
2) Create a Slack Incoming Webhook
In Slack:
- Configure Incoming Webhooks
- Create a new webhook for the channel you want alerts in
- Copy the Incoming Webhook URL
3) Open Apps Script and create the webhook receiver
In your Google Sheet:
- Extensions → Apps Script
- Create a new script file like
Code.gs
You are building a tiny HTTP endpoint, exposed by Apps Script, that Calendly will call.
4) Store the Slack webhook URL in Script Properties
In Apps Script:
- Project Settings → Script Properties
- Add a property:
SLACK_WEBHOOK_URL= your Slack Incoming Webhook URL
5) Implement doPost with idempotent writes
Add logic with this structure:
- Parse JSON from
e.postData.contents - Compute an
idempotency_key - Check whether that key already exists in
calendly_events - If not present, append a new row
- Post a Slack message
Practical idempotency approach for Calendly payloads:
- Prefer
payload_event_uuid+payload_invitee_uuid - If one of those is missing, fall back to a key you can reproduce, such as
event_name + event_start + invitee_email
Also add defensive field access so missing nested fields do not crash the receiver.
6) Deploy the Apps Script web app
In Apps Script:
- Deploy → New deployment
- Choose Web app
- Set who can execute it, based on your org’s settings
- Copy the Web app URL
This URL becomes the webhook endpoint Calendly will call.
7) Create the Calendly webhook subscription
In Calendly:
- Go to Settings → Webhooks
- Create a new webhook subscription
- Set the webhook URL to your Apps Script web app URL
- Subscribe to the event(s) you care about, typically invitee scheduled/created
For deeper payload behavior and event types, start from Calendly’s webhook docs here: Receive data from scheduled events in real time with webhook subscriptions.
8) Test end-to-end
Do one real test booking:
- Verify a row appears in
calendly_events - Verify a Slack message posts
If this fails, the common causes are:
- Wrong deployment URL (copying an older web app URL)
- Apps Script deployment access not allowing Calendly to call it
- Missing or invalid
SLACK_WEBHOOK_URL - Payload mapping assumptions that do not match your event configuration
9) Add a failure log tab (highly recommended during rollout)
Create a second tab like calendly_errors with headers:
ingested_aterror_messageraw_payload
When the payload cannot be parsed or mapped, write a row to calendly_errors so you can fix mapping without guessing.
Real-World Business Scenario
A coaching firm uses Calendly to schedule discovery calls. Before automation, the operations lead manually copied the booking details into Google Sheets and then messaged the team in Slack with what to prep.
After implementing this Calendly webhook → Google Sheets (Apps Script) → Slack alert XPA, new bookings appeared in the sheet quickly and consistently. When they added cancellation subscriptions, the team stopped prepping for calls that no longer existed.
Common Variations
-
Route by event type Add a filter in Apps Script, for example only notify Slack for specific Calendly event types.
-
Post richer Slack messages Keep it readable, include start time, invitee email, and a short link back to the event or sheet context.
-
Write custom question answers to dedicated columns Map Calendly custom answers into specific sheet columns so reporting and downstream automation are straightforward.
What you built, and what to do next
You built a clean XPA pipeline: Calendly webhooks into a Google Apps Script receiver, reliable row appends in Google Sheets, and immediate team visibility via Slack alerts.
If you want this adapted to your exact Calendly event types and your preferred sheet schema, Olmec Dynamics builds these kinds of Cross-Platform Automation workflows for teams that want fewer spreadsheets and fewer missed booking updates. You can see what we do here and revisit the Cross-Platform Automation (XPA) overview.
Internal note: no related posts were available from the automated internal-blog lookup during this run.