A WordPress plugin that syncs Calendly event types as WordPress service posts and provides an embeddable booking widget via shortcode. Visitors can browse available time slots and book appointments directly on your site — no redirect to Calendly.
| Requirement | Minimum |
|---|---|
| WordPress | 6.0 |
| PHP | 8.0 |
| Calendly plan | Any (booking via POST /invitees requires a paid plan) |
| Calendly API access | Personal Access Token (PAT) |
- Upload the
calendly-service-schedulerfolder to/wp-content/plugins/. - Activate the plugin from Plugins > Installed Plugins.
- Go to Calendly Scheduler > Settings and enter your Calendly Personal Access Token.
- Go to Calendly Scheduler > Sync and click Sync Now to import your event types as service posts.
- Embed the booking widget on any page or post using the shortcode (see below).
Navigate to Calendly Scheduler > Settings.
| Option | Description |
|---|---|
| Personal Access Token | Your Calendly PAT. Generate one at https://calendly.com/integrations/api_webhooks. |
| Sync Frequency | How often the background sync runs (daily, twicedaily, hourly). |
| Booking Mode | inline_iframe — renders the widget inline on the page. |
Go to Calendly Scheduler > Sync and click Sync Now. The plugin will:
- Create a
servicescustom post type entry for each active Calendly event type. - Update existing service posts when event type details change.
- Deactivate (not delete) service posts for event types that are no longer active on Calendly.
A background cron job also runs the sync automatically at the configured frequency.
[calendly_booking service="your-service-slug"]
Or by post ID:
[calendly_booking id="42"]
The shortcode for each service is shown pre-filled in the Calendly Event Type Details metabox on the service's edit screen. Click the field to select it, then copy and paste it into any page or post.
- Displays the service name and duration.
- Presents a date picker — the user must select a date before any API call is made.
- Fetches available time slots from Calendly for the chosen date.
- Allows the user to select a slot and enter their name and email.
- Books the appointment directly via the Calendly API and displays an on-page confirmation with cancel and reschedule links.
The plugin registers a custom REST namespace: calendly-scheduler/v1.
Returns available time slots for a service on a given date.
Query parameters
| Parameter | Required | Format | Description |
|---|---|---|---|
date |
Yes | YYYY-MM-DD |
The date to fetch slots for. |
Response
{
"service_id": 42,
"date": "2026-03-20",
"slots": [
{ "start_time": "2026-03-20T09:00:00.000000Z", "status": "available", "invitees_remaining": 1 }
]
}Slot-fetch rules
- Past dates return an empty
slotsarray immediately without calling the Calendly API. - For today's date,
start_timesent to Calendly is the current UTC time plus a 90-second buffer, preventing rejections due to clock skew or network latency. - If the buffered time for today is already past
23:59:59Z, an empty array is returned. - Results for today are cached per UTC hour (refreshed every hour as slots fill up). Results for future dates are cached for 5 minutes using a plain date-based key.
Creates a booking (invitee) in Calendly. Requires a valid WordPress REST nonce in the X-WP-Nonce header.
Request body (JSON)
| Field | Required | Description |
|---|---|---|
start_time |
Yes | ISO 8601 UTC start time from the slots response. |
booker_name |
Yes | Invitee full name. |
booker_email |
Yes | Invitee email address. |
timezone |
Yes | IANA timezone string (e.g. Europe/London). Auto-detected by the widget. |
Response
{
"status": "confirmed",
"name": "Jane Smith",
"email": "jane@example.com",
"start_time": "2026-03-20T09:00:00.000000Z",
"cancel_url": "https://calendly.com/cancellations/...",
"reschedule_url": "https://calendly.com/reschedulings/..."
}Triggers an on-demand sync. Requires manage_options capability.
Created on plugin activation via dbDelta.
Records every completed booking.
| Column | Type | Description |
|---|---|---|
id |
BIGINT UNSIGNED |
Primary key. |
service_post_id |
BIGINT UNSIGNED |
WordPress post ID of the service. |
event_type_uri |
VARCHAR(500) |
Calendly event type URI. |
scheduling_url |
VARCHAR(1000) |
Calendly invitee URI returned after booking. |
booker_email |
VARCHAR(200) |
Invitee email. |
booker_name |
VARCHAR(200) |
Invitee name. |
status |
ENUM |
initiated, completed, cancelled, error. |
created_at |
DATETIME |
Row creation timestamp. |
updated_at |
DATETIME |
Row last-updated timestamp. |
Records the result of each sync run.
| Column | Type | Description |
|---|---|---|
id |
BIGINT UNSIGNED |
Primary key. |
started_at |
DATETIME |
Sync start time. |
completed_at |
DATETIME |
Sync end time (NULL if still running). |
status |
ENUM |
running, success, error. |
event_types_found |
INT |
Number of event types returned by Calendly. |
services_created |
INT |
New service posts created. |
services_updated |
INT |
Existing service posts updated. |
services_deactivated |
INT |
Service posts deactivated (event type no longer active). |
error_message |
TEXT |
Error detail, if any. |
calendly-service-scheduler/
├── calendly-service-scheduler.php # Plugin entry point, constants, activation hooks
├── uninstall.php # Cleanup on uninstall
├── includes/
│ ├── api/
│ │ ├── class-calendly-auth.php # PAT auth helper
│ │ ├── class-calendly-client.php # Low-level HTTP wrapper (wp_remote_*)
│ │ └── class-calendly-endpoints.php # High-level Calendly API methods
│ ├── booking/
│ │ ├── class-booking-manager.php # Slot retrieval, caching, booking creation
│ │ └── class-scheduling-link.php # Single-use scheduling link helper
│ ├── cpt/
│ │ ├── class-service-cpt.php # Registers the 'services' custom post type
│ │ └── class-service-meta.php # Metabox for event type meta fields
│ ├── rest/
│ │ └── class-rest-controller.php # REST route registration and handlers
│ ├── shortcode/
│ │ └── class-booking-shortcode.php # [calendly_booking] shortcode handler
│ ├── sync/
│ │ └── class-event-type-sync.php # Syncs Calendly event types to WP posts
│ ├── admin/
│ │ ├── class-admin-menu.php # Admin menu registration
│ │ ├── class-settings-page.php # Settings page and AJAX save handler
│ │ └── class-sync-page.php # Manual sync page and AJAX trigger
│ ├── class-plugin.php # Core bootstrap / service locator (singleton)
│ ├── class-activator.php # Activation: tables, defaults, cron
│ └── class-deactivator.php # Deactivation: cron cleanup
├── admin/
│ ├── css/admin.css
│ ├── js/admin.js
│ └── views/
│ ├── settings-page.php
│ ├── sync-page.php
│ └── metabox-service-details.php
├── public/
│ ├── css/public.css
│ ├── js/booking.js # Vanilla JS: slot loading, booking flow
│ └── views/
│ └── booking-widget.php # Booking widget HTML template
└── templates/
└── single-services.php # Single service post template
- All REST input is validated and sanitized using WordPress core callbacks (
sanitize_text_field,sanitize_email,absint). - The booking endpoint is protected by a WordPress REST nonce (
X-WP-Nonce), preventing CSRF from unauthenticated third parties. - The admin sync endpoint requires
manage_optionscapability. - The
ABSPATHguard (defined( 'ABSPATH' ) || exit) is present in every PHP file.
Does this require a paid Calendly plan?
Fetching available times works on any plan. Creating bookings directly via POST /invitees requires a paid Calendly plan. On free plans you can still link users to the Calendly scheduling page using the scheduling URL stored on the service post.
Why does the date picker have no default value? By design — the widget waits for the user to explicitly choose a date before calling the API. This avoids unnecessary API requests on page load and prevents edge-case errors when today's slots are already exhausted.
Slots show as unavailable for today — why?
Calendly rejects any start_time in the past. The plugin automatically uses the current UTC time plus a 90-second buffer as start_time when today is selected. If all remaining time today falls within that buffer window, an empty slot list is returned.
How do I clear the slot cache?
Today's slots are cached per UTC hour and expire automatically. Future date slots are cached for 5 minutes. You can also delete all transients prefixed css_slots_ from the database if you need to force a refresh.
Improvements
- Shortcode pre-filled in metabox. The Calendly Event Type Details metabox on each service edit screen now shows a read-only shortcode field (
[calendly_booking service="slug"]) populated from the post slug. Click the field to select it all for easy copy/paste — no need to construct the shortcode manually. - Post content preserved on sync updates. Subsequent syncs no longer overwrite the
post_content(description) of existing service posts. The Calendly description is written only when a service post is first created; after that, any edits made directly in WordPress are retained across syncs.post_titleandpost_statuscontinue to reflect the Calendly event type on every sync.
Bug fixes
- Date picker no longer defaults to today. The
<input type="date">is rendered without avalueattribute. On load the slot area shows "Choose a date to see available times." and no API call is made until the user picks a date. - Removed auto-load on page init in JS. The JavaScript
initWidget()function no longer callsloadSlots()on startup; slot fetching is triggered exclusively by thechangeevent on the date input. - Fixed slot fetch failure for today's date.
CSS_Booking_Manager::get_slots()now uses the current UTC time plus a 90-second buffer asstart_timewhen the requested date is today, preventing Calendly from rejecting the request due to a paststart_time. - Switched
minattribute togmdate(). The date input'sminattribute now usesgmdate( 'Y-m-d' )(UTC) instead ofdate()(server local time) for consistent behaviour across timezones.
New guards in CSS_Booking_Manager::get_slots()
- Returns an empty array immediately for any past date, without calling the Calendly API.
- Returns an empty array if today's buffered
start_timeis already at or past23:59:59Z(no meaningful booking window left). - Uses an hourly transient cache key for today (
css_slots_{uuid}_{date}_h{HH}) so cached results refresh each hour as slots fill up. Future dates continue to use the plain date-based key with a 5-minute TTL.
- Registers a
servicescustom post type for Calendly event types. - Syncs event types from Calendly via the admin sync page and a scheduled cron job.
- Provides the
[calendly_booking]shortcode to embed a booking widget. - Booking widget fetches available slots via the plugin's REST API and books directly via Calendly's
POST /inviteesendpoint. - On-page booking confirmation with cancel and reschedule links.
- Automatic timezone detection in the browser via
Intl.DateTimeFormat. - Booking log (
css_booking_log) and sync log (css_sync_log) database tables. - Settings page for PAT configuration, sync frequency, and booking mode.
- GPL v2 license.