appscript.dev
Automation Intermediate Calendar

Sync calendar bookings with Calendly

Bridge Google Calendar and Calendly — Northwind bookings on either side appear on both.

Published Jan 7, 2026

Northwind takes client calls through Calendly, but the team runs its day off Google Calendar. When a prospect books a slot, it lands in Calendly — and then nowhere else until someone notices and copies it across. Double-bookings and missed calls follow, because the calendar everyone actually looks at does not know about the booking.

This script bridges the two. It reads upcoming events from the Calendly API and creates a matching event on the Google Calendar the team lives in. It checks for an existing copy first, so running it repeatedly never produces duplicates. Put it on a frequent trigger and every Calendly booking shows up where it will be seen, within minutes.

What you’ll need

  • A Calendly account with the API enabled, and a personal access token.
  • Your Calendly user URI — the https://api.calendly.com/users/... URL that identifies your account. The fastest way to find it is a one-off call to the /users/me endpoint.
  • That token saved as CALENDLY_TOKEN and the URI as CALENDLY_USER_URI in Script Properties — see Store API keys and secrets securely.
  • Edit access to the Google Calendar you want the bookings to land on. The script uses your default calendar.

The script

// Calendly credentials, kept out of the code.
const CALENDLY_TOKEN = PropertiesService.getScriptProperties()
  .getProperty('CALENDLY_TOKEN');
const CALENDLY_USER_URI = PropertiesService.getScriptProperties()
  .getProperty('CALENDLY_USER_URI');

// Only sync events that are still active — skip ones the invitee cancelled.
const EVENT_STATUS = 'active';

/**
 * Reads upcoming Calendly events and creates a matching Google Calendar
 * event for each one that is not already there.
 */
function pullCalendlyBookings() {
  // 1. Ask Calendly for this user's active scheduled events.
  const url = 'https://api.calendly.com/scheduled_events' +
    '?user=' + encodeURIComponent(CALENDLY_USER_URI) +
    '&status=' + EVENT_STATUS;
  const res = JSON.parse(UrlFetchApp.fetch(url, {
    headers: { Authorization: 'Bearer ' + CALENDLY_TOKEN },
    muteHttpExceptions: true,
  }).getContentText());

  // 2. Bail out early if there are no bookings to sync.
  if (!res.collection || !res.collection.length) {
    Logger.log('No Calendly events returned — nothing to do.');
    return;
  }

  const cal = CalendarApp.getDefaultCalendar();
  let created = 0;

  // 3. Walk each Calendly event and mirror it onto Google Calendar.
  for (const e of res.collection) {
    const start = new Date(e.start_time);
    const end = new Date(e.end_time);

    // 4. Look for an event with the same name in that exact window.
    //    If one exists, this booking is already synced — skip it.
    const existing = cal.getEvents(start, end, { search: e.name });
    if (existing.length) continue;

    // 5. No copy found, so create the event on Google Calendar.
    cal.createEvent(e.name, start, end, {
      description: 'Synced from Calendly\n' + e.uri,
    });
    created++;
  }

  Logger.log('Created ' + created + ' new calendar event(s) from Calendly.');
}

How it works

  1. pullCalendlyBookings calls the Calendly scheduled_events endpoint, scoped to your user URI and filtered to active events so cancelled slots are ignored.
  2. If the response collection is empty, it logs a message and stops — no calendar work to do.
  3. It grabs your default Google Calendar and loops over each Calendly event, converting the ISO start and end strings into Date objects.
  4. Before creating anything, it searches the calendar for an event with the same name in that exact time window. A match means the booking was synced on an earlier run, so it is skipped. This check is what makes the script safe to run on a tight schedule.
  5. When no copy exists, it creates the event and tags the description with the Calendly URI, so anyone looking at the calendar knows where it came from.

Example run

Say Calendly returns two upcoming bookings:

namestart_timeend_time
Discovery call — Acme2026-01-12T14:00:00Z2026-01-12T14:30:00Z
Strategy review — Globex2026-01-13T09:00:00Z2026-01-13T10:00:00Z

On the first run, both are new, so the log reads:

Created 2 new calendar event(s) from Calendly.

Two events now sit on Google Calendar, each described Synced from Calendly. On the next run nothing has changed, the name-and-window search finds both, and the log reads Created 0 new calendar event(s) from Calendly. — no duplicates.

Trigger it

Bookings should appear quickly, so run this often:

  1. In the Apps Script editor, open Triggers (the clock icon).
  2. Click Add Trigger.
  3. Choose pullCalendlyBookings, a Time-driven source, a Minutes timer, and Every 15 minutes.

Fifteen minutes is a good balance — fresh enough that nobody misses a call, infrequent enough to stay well inside Apps Script’s daily quotas.

Watch out for

  • The duplicate check matches on name plus time window. If two distinct Calendly events share a name and overlap, the second is treated as already synced and skipped. For high volume, store synced Calendly URIs in Script Properties and check against those instead.
  • This is one-way: Calendly to Google Calendar. An event you create directly in Google Calendar is not pushed back to Calendly, and editing the Google copy does not change the Calendly booking.
  • The script reads one page of events. Calendly paginates with a pagination block; a busy account needs a loop that follows pagination.next_page until it is null.
  • Cancellations are not propagated. Filtering to status=active stops cancelled slots being created, but a booking cancelled after it was synced still sits on Google Calendar. Add a cleanup pass if that matters.
  • Times come from Calendly as UTC ISO strings. new Date() parses them correctly, but the event displays in your calendar’s timezone — check it shows the hour you expect before trusting it.

Related