Guide

Google Calendar API Free/Busy Query

Query the Google Calendar API freeBusy endpoint for busy intervals. Compare freebusy.query with events.list, then check availability across providers from the command line.

Written by Pouya Sanooei Software Engineer

VerifiedCLI 3.1.22 · Google · last tested June 19, 2026

What is the Google Calendar freeBusy endpoint?

The freeBusy endpoint is a single POST to https://www.googleapis.com/calendar/v3/freeBusy that reports which time blocks are busy across one or more calendars. It returns only start and end timestamps for busy intervals, so it answers an availability question without exposing event titles, guests, or locations. One request can report on up to 50 calendars at once.

This is the lightweight availability primitive in Google Calendar. The request body needs three fields: timeMin and timeMax as RFC 3339 timestamps, and an items array where each entry is { "id": "<calendarId or email>" }. According to the official freebusy.query reference, a single call accepts up to 50 calendars, controlled by calendarExpansionMax (maximum 50). The method is read-only and does not move events, so it is safe to call on calendars a scheduler does not own.

The request below asks whether one person is busy over a one-day window. It names the calendar by email address, which Google resolves to that user's primary calendar when permissions allow. Both timestamps carry an explicit UTC offset, which the API requires.

POST https://www.googleapis.com/calendar/v3/freeBusy
Content-Type: application/json
Authorization: Bearer <access_token>

{
  "timeMin": "2026-06-20T00:00:00Z",
  "timeMax": "2026-06-21T00:00:00Z",
  "items": [
    { "id": "alice@example.com" }
  ]
}

What does the freeBusy query response look like?

A freeBusy response contains a calendars object keyed by the calendar IDs you requested. Each entry holds a busy array of { start, end } intervals and an optional errors array. There are no event objects anywhere in the payload, which is the privacy guarantee that makes the endpoint cheap to share across 50 calendars at once.

Because the response carries only intervals, a scheduler computes free slots by inverting the busy list against the query window. A calendar with no meetings in the window returns an empty busy: [] array, which means fully free. If a calendar cannot be read, Google places a reason in errors rather than failing the whole request, so a 50-calendar query still returns usable data when one calendar is inaccessible. The documented error reason here is notFound, returned when a calendar ID does not resolve or the calendar cannot be read.

The payload below answers the earlier one-day query. The single busy block runs from 14:00 to 15:00 UTC; every other minute in the window is free. A scheduler reads this and offers any non-overlapping slot. Note that the busy intervals Google returns can be clipped to the query window, so a meeting that started yesterday and runs into timeMin is reported starting at timeMin, not at its real start. Treat the intervals as opaque busy blocks rather than as one-to-one event boundaries.

{
  "kind": "calendar#freeBusy",
  "timeMin": "2026-06-20T00:00:00Z",
  "timeMax": "2026-06-21T00:00:00Z",
  "calendars": {
    "alice@example.com": {
      "busy": [
        { "start": "2026-06-20T14:00:00Z", "end": "2026-06-20T15:00:00Z" }
      ]
    }
  }
}

How is freeBusy.query different from events.list?

The freeBusy.query method returns busy intervals only and never paginates, while events.list returns full event objects and pages through results with nextPageToken. Use freeBusy when the question is "is this slot open?" and events.list when you need titles, attendees, descriptions, or conference links.

The cost difference is real. The events.list reference caps a page at 2,500 events and defaults to 250, so reading a busy quarter can mean several round trips plus client-side recurrence expansion. A freeBusy call collapses the same window into a flat interval list in one request, with no page token to manage. For a scheduling agent checking five colleagues across the next 7 days, that is one freeBusy call instead of five paginated event scans. The privacy gap matters too: events.list exposes meeting titles, guest lists, and descriptions, so handing an agent that method to answer "is Tuesday free?" leaks far more than the question needs. freeBusy returns intervals only, which keeps the data surface narrow when the task is purely availability.

AspectfreeBusy.queryevents.list
ReturnsBusy intervals onlyFull event objects
PrivacyNo titles or guestsExposes event details
PaginationNonenextPageToken
Calendars per callUp to 50One calendar
Best forAvailability checksReading or editing events

How do you check many calendars and limit the span?

Add one items entry per calendar to check several people in a single request, and tune calendarExpansionMax and groupExpansionMax when querying group addresses. The API expands a group into its members, then reports busy intervals per resolved calendar, all inside the same response.

These fields cap the expansion: calendarExpansionMax has a maximum of 50 and groupExpansionMax a maximum of 100, so one request resolves at most 50 calendars and 100 group members. Exceeding either limit returns an error (tooManyCalendarsRequested or groupTooBig), not a silently truncated result. Keep the timeMin/timeMax span tight: a one-week window returns compact data, while a multi-month span across 50 calendars produces a large interval list and slower responses. An optional timeZone field (default UTC) controls how Google interprets day boundaries for the busy calculation.

The request below checks three people over a working week and pins the time zone so day boundaries align with the team. Each name maps to a separate key in the response calendars object. One subtlety: if you list the same calendar ID twice in items, Google deduplicates it to a single key, so a 50-entry array with duplicates resolves fewer than 50 distinct calendars. Build the items array from a deduplicated set before sending.

{
  "timeMin": "2026-06-22T00:00:00-07:00",
  "timeMax": "2026-06-27T00:00:00-07:00",
  "timeZone": "America/Los_Angeles",
  "calendarExpansionMax": 50,
  "items": [
    { "id": "alice@example.com" },
    { "id": "bob@example.com" },
    { "id": "carol@example.com" }
  ]
}

What errors does freeBusy.query return?

Calendar-level problems appear inside the errors array for that calendar, while request-level problems return a standard HTTP error code. This split means a partial failure does not lose the calendars that did resolve, so reliable scheduling code reads per-calendar errors as well as the HTTP status.

The Calendar API errors guide documents that a 403 with reason rateLimitExceeded should be retried with exponential backoff, and the guide states the rule directly: "Retry the request using exponential backoff." A 400 usually means a malformed timeMin/timeMax (missing the RFC 3339 offset is the common cause), and a 401 means the access token expired after its 3,600-second lifetime. The table maps each signal to the right fix.

SignalWhere it appearsFix
notFoundCalendar errorsWrong calendar ID or no access; skip it
internalErrorCalendar errorsTransient Google-side failure; retry that calendar
400HTTP statusFix the RFC 3339 timestamp or offset
401HTTP statusRefresh the expired access token
403 rateLimitExceededHTTP statusBack off and retry

How do you turn busy intervals into open slots?

A free/busy response lists busy blocks; you derive open slots by subtracting those blocks from the query window. The algorithm sorts the busy intervals by start time, merges any that overlap, then walks the gaps between them inside timeMin and timeMax to produce the free ranges.

Three details decide correctness. First, normalize every timestamp to one time zone before comparing, because a busy block returned in UTC and a working-hours window expressed in local time will misalign by the offset. Second, merge overlapping busy blocks first: two meetings from 14:00 to 15:00 and 14:30 to 16:00 collapse into one 14:00 to 16:00 block, otherwise the gap math double-counts. Third, apply a minimum slot length, since a 5-minute gap between two meetings is rarely a bookable slot. A 30-minute floor is a common default for meeting scheduling.

The pseudocode below shows the subtraction over a single day. It assumes the busy list is already merged and sorted, and it emits any gap that meets the 30-minute minimum. Run this per calendar key in the response when scheduling one person, or intersect the free ranges across keys when every attendee must be free at once.

# inputs: window_start, window_end, merged_busy (sorted)
cursor = window_start
free = []
for block in merged_busy:
    if block.start - cursor >= timedelta(minutes=30):
        free.append((cursor, block.start))
    cursor = max(cursor, block.end)
if window_end - cursor >= timedelta(minutes=30):
    free.append((cursor, window_end))

How do you check free/busy from the CLI?

The nylas calendar availability check command runs the same busy-interval lookup that freeBusy.query performs, but across Google, Microsoft 365, and other backends through one OAuth flow. You pass emails, a start, and a duration; the tool returns busy slots without you constructing JSON or refreshing tokens by hand.

This removes the four pieces of plumbing a direct integration owns: OAuth setup, RFC 3339 formatting, per-provider endpoints, and token refresh every 3,600 seconds. The command below checks two people for a 7-day window starting on a fixed date, which keeps reruns repeatable. The -e/--emails flag takes a comma-separated list, -s/--start sets the window start, and -d/--duration accepts values like 8h, 1d, or 7d.

nylas calendar availability check \
  --emails alice@co.com,bob@co.com \
  --start "2026-06-20" \
  --duration 7d \
  --format json

How do you parse availability output with jq?

The --format json flag emits machine-readable output that pipes cleanly into jq, so a script or scheduling agent can extract busy intervals without screen scraping. The three accepted formats are text, json, and yaml; text is the default.

Piping to jq turns a one-line command into a building block for a cron job or agent tool. The example below requests JSON, then filters to the busy blocks so the next step in a pipeline sees only the data it needs. This mirrors the API's own design, where the busy array is the single payload a scheduler consumes. A 7-day check for two people typically returns a handful of intervals, small enough to inline into an LLM prompt under a few hundred tokens.

nylas calendar availability check \
  --emails alice@co.com,bob@co.com \
  --start "2026-06-20" \
  --duration 7d \
  --format json \
  | jq '.[].busy'

When should you call the API directly instead?

Call the Google Calendar API directly when you need a Google-only control plane: domain-wide delegation, resource calendars for rooms, group address expansion via groupExpansionMax, or a UI that already embeds Google's OAuth flow. Reach for the command-line path for terminal workflows, agent tools, cron jobs, and any script that should also work against Microsoft 365 without a second integration.

The trade-off is ownership, not capability. A direct freeBusy integration owns OAuth client registration, token refresh every 3,600 seconds, RFC 3339 formatting, retry logic for 403 rateLimitExceeded, and Google-specific endpoints. The CLI path owns command selection and output handling while the provider integration sits behind it. For a one-off availability check or a multi-provider scheduler, starting from the terminal keeps a short script from becoming a long-lived OAuth client maintained for one endpoint.

Next steps