Calendly Booking Generator
Calendly Booking Link Generator

Sales and customer success teams generating outbound Calendly links were doing it manually: logging into Calendly, creating a link for each recipient, copying it into an email or CRM, and separately noting the activity somewhere. At any meaningful volume — SDR outreach sequences, onboarding invites, renewal touchpoints — that process added friction and left no audit trail.
We built an n8n webhook endpoint that generates a personalized, single-use Calendly scheduling link on demand, pre-fills it with the recipient's name, email, and UTM attribution, logs the link to Google Sheets, fires a Slack notification, and returns the complete URL in the HTTP response — all within a single API call.
The Workflow
Trigger: Webhook (HTTP POST)
Node 1 - Webhook Trigger
Listens for POST requests at the path generate-calendly-link. Response mode is set to responseNode, which holds the HTTP connection open until the Respond to Webhook node fires at the end of the chain — the caller receives the generated link in the same request.
Stage 1: Input Normalization
Node 2 - Set Configuration Extracts four fields from the POST body and assigns fallback values for any absent parameters:
recipient_email— fallback:test@example.comrecipient_name— fallback:Test Userrequested_event_type— the specific Calendly event type URI if provided; fallback: empty string (triggers auto-selection)utm_source— fallback:n8n
Stage 2: Calendly User and Event Type Resolution
Node 3 - Get Current User (Calendly API)
GET request to https://api.calendly.com/users/me using Calendly OAuth2 credentials. Returns the authenticated user's resource object including their URI, which is required to scope the subsequent event types query. Configured with onError: continueRegularOutput.
Node 4 - Extract User
Reads the /users/me response and assigns two fields — user_uri and user_name — while preserving all prior fields for downstream use.
Node 5 - Get Event Types (Calendly API)
GET request to https://api.calendly.com/event_types with two query parameters: user (the resolved user_uri) and active: true. Returns the list of active scheduling event types for the authenticated user. Configured with onError: continueRegularOutput.
Node 6 - Select Event Type
Determines which event type to use for the link. If requested_event_type from the POST body is non-empty, that URI is used. Otherwise defaults to the first active event type returned by Calendly. Extracts selected_event_type_uri, selected_event_type_name, and selected_event_duration (fallback: 30 minutes).
Stage 3: Link Creation and Personalization
Node 7 - Create Single-Use Link (Calendly API)
POST request to https://api.calendly.com/scheduling_links with the following body:
{
"max_event_count": 1,
"owner": "{{ selected_event_type_uri }}",
"owner_type": "EventType"
}max_event_count: 1 enforces single-use behavior — the link expires after one booking. Configured with onError: continueRegularOutput.
Node 8 - Build Personalized Link
Constructs the final URL by appending three URL-encoded query parameters to the base booking URL returned by Calendly: name, email, and utm_source. Pre-filling these parameters means the recipient lands on a form with their details already populated. Also captures link_created_at as an ISO timestamp. Seven fields are assembled here and carried forward to all three downstream nodes.
Stage 4: Logging, Notification, and Response
Node 9 - Log to Google Sheets
Appends a row to the "Generated Links" tab with seven columns: Recipient Name, Recipient Email, Event Type, Duration (min), Booking URL, Created At, and Status (hardcoded as Sent). Configured with onError: continueRegularOutput — a Sheets failure does not block the Slack notification or the HTTP response.
Node 10 - Notify via Slack
Posts a message to the #general channel with recipient name, email, event type name, duration, and the personalized booking URL as a hyperlinked "Click to Book" anchor. Markdown enabled, link unfurling disabled. Configured with onError: continueRegularOutput.
Node 11 - Respond to Webhook Returns HTTP 200 with a JSON body to the calling system:
{
"success": true,
"booking_url": "<personalized_booking_url>",
"base_url": "<base_booking_url>",
"recipient": { "name": "...", "email": "..." },
"event": { "name": "...", "duration_minutes": 30 },
"created_at": "<ISO timestamp>",
"expires": "Single-use or 90 days"
}The caller receives the fully formed link in the same HTTP response that triggered the workflow.
Results
- One API call replaces the full manual link generation process — no Calendly login, no manual copy-paste, no separate logging step
- Every generated link is personalized — recipient name, email, and UTM source are pre-filled as URL-encoded query parameters on the booking URL
- Single-use enforcement —
max_event_count: 1ensures each link can only be used to book once - Full audit trail in Google Sheets — every link generated is logged with recipient details, event type, duration, timestamp, and status
- Slack visibility — the team sees each generated link in real time without polling the sheet
- Resilient by design — five nodes carry
onError: continueRegularOutputso partial API failures in Calendly, Sheets, or Slack do not abort the response back to the caller
Stack
| Layer | Tool |
|---|---|
| Automation | n8n (self-hosted) |
| Trigger | Webhook (HTTP POST, generate-calendly-link) |
| Scheduling API | Calendly REST API v2 (OAuth2) |
| Endpoints Used | GET /users/me · GET /event_types · POST /scheduling_links |
| Logging | Google Sheets (append) |
| Notification | Slack |
| Response | n8n Respond to Webhook node |
My Role
- Mapped all four input fields with their fallback values and designed the input normalization stage to handle partial or absent POST body parameters gracefully
- Wired the
user_urifrom/users/medirectly into theuserquery parameter of the/event_typescall - Implemented the event type selection logic: prefer the caller-specified URI, fall back to
collection[0].uri, with a 30-minute duration fallback - Built the personalized URL by appending name, email, and
utm_sourceas URL-encoded query parameters to the base booking URL - Configured the
/scheduling_linksPOST body withmax_event_count: 1andowner_type: "EventType"to enforce single-use behavior - Defined all seven column mappings for the Google Sheets append, including the hardcoded
Sentstatus value - Specified the Slack message structure with mrkdwn enabled, link unfurling disabled, and the booking URL rendered as a "Click to Book" anchor
- Applied
onError: continueRegularOutputto five nodes and configuredresponseMode: responseNodeon the webhook trigger to hold the HTTP connection until the final response fires