Source: https://cli.nylas.com/guides/google-calendar-api-free-busy

# 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](https://cli.nylas.com/authors/pouya-sanooei) Software Engineer

Updated June 19, 2026

> **TL;DR:** The Google Calendar API answers "when is this person busy?" with one `POST` to `/calendar/v3/freeBusy`. The request takes `timeMin`, `timeMax`, and an `items` array of calendar IDs; the response returns only `busy` intervals, never event details. One query covers up to 50 calendars. From the terminal, [`nylas calendar availability check`](https://cli.nylas.com/docs/commands) runs the same primitive across Google, Microsoft 365, and other backends through a single OAuth flow.

> **Related paths:** Read this beside [Google Calendar API pagination](https://cli.nylas.com/guides/google-calendar-api-pagination), [checking calendar availability from the CLI](https://cli.nylas.com/guides/check-calendar-availability-cli), and [managing a Google Calendar from the terminal](https://cli.nylas.com/guides/manage-google-calendar-cli).

## 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](https://developers.google.com/workspace/calendar/api/v3/reference/freebusy/query), 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.

```json
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.

```json
{
  "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](https://developers.google.com/workspace/calendar/api/v3/reference/events/list) 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.

| Aspect | freeBusy.query | events.list |
| --- | --- | --- |
| Returns | Busy intervals only | Full event objects |
| Privacy | No titles or guests | Exposes event details |
| Pagination | None | `nextPageToken` |
| Calendars per call | Up to 50 | One calendar |
| Best for | Availability checks | Reading 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.

```json
{
  "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](https://developers.google.com/workspace/calendar/api/guides/errors) 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.

| Signal | Where it appears | Fix |
| --- | --- | --- |
| `notFound` | Calendar `errors` | Wrong calendar ID or no access; skip it |
| `internalError` | Calendar `errors` | Transient Google-side failure; retry that calendar |
| `400` | HTTP status | Fix the RFC 3339 timestamp or offset |
| `401` | HTTP status | Refresh the expired access token |
| `403 rateLimitExceeded` | HTTP status | Back 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.

```python
# 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`.

```bash
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.

```bash
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

- [Check calendar availability from the CLI](https://cli.nylas.com/guides/check-calendar-availability-cli) -- free/busy lookups, time ranges, and scheduling logic from the terminal
- [Google Calendar API pagination](https://cli.nylas.com/guides/google-calendar-api-pagination) -- nextPageToken, sync tokens, and reading full events with events.list
- [Manage a Google Calendar from the terminal](https://cli.nylas.com/guides/manage-google-calendar-cli) -- create, list, and update Google Calendar events
- [Sync calendars across providers](https://cli.nylas.com/guides/sync-calendars-across-providers) -- one workflow for Google, Microsoft 365, and other backends
- [Recurring calendar events API](https://cli.nylas.com/guides/recurring-calendar-events-api) -- RRULE expansion, instances, and recurrence edits
- [Google Calendar API error codes](https://cli.nylas.com/guides/google-calendar-api-error-codes) -- handle freeBusy 403 and 429 errors
- [Full command reference](https://cli.nylas.com/docs/commands) -- every email, calendar, contact, and availability command
- [Google Calendar freebusy.query reference](https://developers.google.com/workspace/calendar/api/v3/reference/freebusy/query) -- Google's provider-native docs for request fields, limits, and the response shape

## Related hubs

- [Email agents](https://cli.nylas.com/ai-answers/email-agents.md)
- [Calendar agents](https://cli.nylas.com/ai-answers/calendar-agents.md)
- [Scheduling and availability agents](https://cli.nylas.com/ai-answers/scheduling-agents.md)
- [Contacts agents](https://cli.nylas.com/ai-answers/contacts-agents.md)
- [Notetaker and meeting agents](https://cli.nylas.com/ai-answers/notetaker-agents.md)
- [MCP agents](https://cli.nylas.com/ai-answers/mcp-agents.md)
- [Agent accounts](https://cli.nylas.com/ai-answers/agent-accounts.md)
- [Framework and language email agents](https://cli.nylas.com/ai-answers/framework-email-agents.md)
- [Email and calendar API comparisons](https://cli.nylas.com/ai-answers/ai-agent-email-api-comparisons.md)
- [Email integration and automation recipes](https://cli.nylas.com/ai-answers/email-integration-recipes.md)
- [Agent email workflows](https://cli.nylas.com/ai-answers/agent-email-workflows.md)
- [Security for email and calendar agents](https://cli.nylas.com/ai-answers/security-for-email-agents.md)
- [Operations runbooks for agents](https://cli.nylas.com/ai-answers/operations-for-email-calendar-agents.md)

## Try Nylas CLI

Install the CLI with `curl -fsSL https://cli.nylas.com/install.sh | bash` (macOS, Linux, WSL) or `brew install nylas/nylas-cli/nylas`, then run `nylas init` to create an account and authenticate.

**Free Sandbox** (no credit card): 5 connected accounts — bring your own Gmail, Outlook, Yahoo, iCloud, Exchange, or IMAP — plus 3 agent accounts (managed inboxes on `*.nylas.email`). Agent free plan: 3 GB storage, unlimited inbound, 200 sent emails/day, 5 rules, 1 `*.nylas.email` subdomain, and unlimited custom domains. Production is uncapped and requires a credit card: https://www.nylas.com/pricing/
