Pricing

How to Connect Calendly to Google Sheets

CalendlySaved SearchDateDocumentAmount07/01INV-40214,280.0007/02INV-40221,860.5007/05INV-40239,140.0007/08INV-40242,375.2507/09INV-40256,902.7507/12INV-40263,118.40BrookedABCDateDocumentAmount123456707/01INV-40214,280.0007/02INV-40221,860.5007/05INV-40239,140.0007/08INV-40242,375.2507/09INV-40256,902.7507/12INV-40263,118.40Google SheetsCalendly report
JW
James Whitfield

Sync Calendly bookings to Google Sheets, including why scheduled events contain no invitee details and the user URI you need before any query works.

Calendly's API is straightforward with one structural surprise: a scheduled event does not include the person who booked it. Invitees, along with everything they typed on your booking form, live on a separate endpoint requiring one extra call per event. Add the fact that every query needs a user URI you have to look up first, and the shape of a working integration is quite different from what most people expect.

The two-minute option: CSV export

In Calendly, open Scheduled Events, set a date range, and use the export option to download a CSV. This includes invitee names and emails already joined onto the events, which is exactly the work the API makes you do yourself. Import it into Sheets via File → Import. For a one-off, this is comfortably the best route.

Calendly to Google Sheets for free (Apps Script + API v2)

Create a personal access token under Integrations & apps → API & webhooks. Note that Calendly's v1 API with its older API keys is retired; v2 with bearer tokens is the current interface.

Every request starts by resolving your user URI. The events endpoint takes a URI, not an email address, so /users/me comes first. Then, for each event, a second call fetches its invitee.

Code.gs
function importCalendlyBookings() {  const TOKEN = "YOUR_PERSONAL_ACCESS_TOKEN";  const headers = { Authorization: "Bearer " + TOKEN };   // 1. The events endpoint needs a user URI, not an email address.  const me = JSON.parse(    UrlFetchApp.fetch("https://api.calendly.com/users/me", { headers })      .getContentText()  );  const userUri = me.resource.uri;   const rows = [];  let pageToken = null;   do {    let url = "https://api.calendly.com/scheduled_events"      + "?user=" + encodeURIComponent(userUri)      + "&count=100"                                  // 100 is the maximum      + "&min_start_time=2026-01-01T00:00:00.000000Z";    if (pageToken) url += "&page_token=" + pageToken;     const page = JSON.parse(      UrlFetchApp.fetch(url, { headers, muteHttpExceptions: true })        .getContentText()    );    if (!page.collection) throw new Error(JSON.stringify(page));     page.collection.forEach((ev) => {      // 2. The event has no invitee. Names, emails and form answers all      //    require a second call against the event's own UUID.      const uuid = ev.uri.split("/").pop();      const inv = JSON.parse(UrlFetchApp.fetch(        "https://api.calendly.com/scheduled_events/" + uuid + "/invitees?count=100",        { headers, muteHttpExceptions: true }      ).getContentText());       const invitee = (inv.collection || [])[0] || {};      const answers = (invitee.questions_and_answers || [])        .map((qa) => qa.question + ": " + qa.answer)        .join("; ");       rows.push([        ev.name,        ev.start_time,        ev.end_time,        ev.status,                                   // active or canceled        invitee.name || "",        invitee.email || "",        (invitee.tracking || {}).utm_source || "",        answers,        ev.cancellation ? ev.cancellation.reason : "",      ]);       Utilities.sleep(150);    });     pageToken = page.pagination ? page.pagination.next_page_token : null;  } while (pageToken);   const header = ["Event","Start","End","Status","Invitee","Email",                  "UTM source","Form answers","Cancellation reason"];  const ss = SpreadsheetApp.getActiveSpreadsheet();  const sheet = ss.getSheetByName("Calendly") || ss.insertSheet("Calendly");  sheet.clearContents();  sheet.getRange(1, 1, 1, header.length).setValues([header]);  if (rows.length) {    sheet.getRange(2, 1, rows.length, header.length).setValues(rows);  }}

The per-event invitee call is what makes this expensive: 300 bookings means 300 extra requests plus the pages of events themselves. Always bound the query with min_start_time and max_start_time rather than pulling all history, and note that group events can have several invitees, so taking only the first one, as the example does, is a simplification worth revisiting if you run group sessions.

Limits: Apps Script's six-minute cap will be reached quickly given the request volume, so restrict each run to a narrow date window. Calendly's API is read-only for bookings, so there is no write-back path here.

The Brooked method

Brooked resolves the user URI, pages the events, and joins invitees onto them for you, so bookings arrive with names, emails, UTM tracking and booking form answers already on the same row. Cancellations come through with their reasons rather than being silently mixed into held meetings.

Step 1: Install Brooked

Install Brooked from the Google Workspace Marketplace. Free tier includes 100 imports per month, no credit card required.

Step 2: Connect Calendly

Choose Calendly in Brooked and paste a personal access token from Integrations & apps → API & webhooks. Pick a date range and whether to include cancelled bookings, then import.

Step 3: Schedule the refresh

Set the import to re-run daily. Because it re-reads rather than appending on webhook, bookings cancelled or rescheduled after they were first created show their current state instead of the state they had at booking time.

BrookedBrooked AI, Calendly
Live
Ask anything about your Calendly data…

Method comparison

MethodCostSetupRefreshTwo-wayBest for
Calendly CSV exportFree2 minManualNoA one-off list of bookings
Apps Script + API v2Free25 to 40 minTime-driven triggerNoBooking form answers, custom reporting
Native Google Sheets integrationPaid plans5 minOn new bookingNoAppending new bookings going forward
BrookedFree tier · Pro $29/user/mo~3 minScheduled (hourly, daily, weekly)NoScheduled booking reporting with form answers

Common use cases

  • Demo pipeline: track bookings, cancellations and no-shows per rep
  • Lead source attribution: report bookings by UTM source captured on the booking link
  • Qualification: surface booking form answers alongside the meeting for pre-call prep
  • Capacity planning: bookings per day and hour to see where availability is tight
  • CRM enrichment: join bookings against deal data already in the spreadsheet

Troubleshooting common issues

Frequently asked questions

Why do Calendly scheduled events not include who booked them?

The scheduled_events endpoint returns the meeting itself, its time, event type, location and status, but not the person who booked. Invitees live on a separate endpoint, /scheduled_events/{uuid}/invitees, so building a useful booking report means one call per event to fetch the invitee. This is the main reason a naive export looks like it is missing every name and email.

Why do my Calendly API requests fail without a user URI?

The events endpoint requires a user or organization URI, and it will not accept an email address. Call /users/me first and read resource.uri, which looks like https://api.calendly.com/users/AAAAAAAAAAAA, then pass that as the user parameter. Every query starts with this lookup.

Can I connect Calendly to Google Sheets for free?

Yes. Calendly exports scheduled events to CSV from the Scheduled Events page at no cost, which handles a one-off. The API method here uses a free personal access token and a free time-driven trigger. Calendly's own native Google Sheets integration is limited to paid plans and only appends bookings going forward.

How do I get the answers people typed on the booking form?

They come back on the invitee object, not the event, as a questions_and_answers array with question text and answer pairs. Since invitees require their own call per event, form answers are only available once you have made that second request. Custom UTM and tracking parameters live in a separate tracking object on the same invitee.

Does the API return cancelled bookings?

Yes, and by default it returns both. Each event has a status of active or canceled, and cancelled ones carry a cancellation object with the reason and who cancelled. Filter on status if you only want held meetings, otherwise your booking counts include meetings that never happened.

How does pagination work?

The count parameter caps at 100, and the response contains a pagination object with a next_page_token. Pass it as page_token on the following request until no token comes back. Because each event needs a follow-up invitee call, a busy account generates a lot of requests, so filter by date range rather than pulling everything.

Connect Calendly to Google Sheets today.

Free tier: 100 imports per month, no credit card required. Or see the full Calendly integration details.

Install Brooked free →

Get your spreadsheet hours back

Brooked sets up in seconds. Your team is querying live data before lunch.

Get started free