Automate Google Calendar scheduling using Calendar API v3's events.insert endpoint. Critical gotcha: all dateTime fields require RFC 3339 format with explicit timezone offset, and Google Meet links require conferenceDataVersion=1 as a query parameter — without it, the API silently ignores the conferenceData field. All-day events use 'date' (not 'dateTime') — mixing them causes 400 errors. Quota limits are not publicly documented; check Cloud Console.
API Quick Reference
OAuth 2.0
Not publicly documented — check Cloud Console quotas
JSON
Available
Understanding the Google Calendar API
The Google Calendar API v3 provides full CRUD access to calendar events, calendars, ACLs, and free/busy data. It's a RESTful API where each resource (event, calendar, ACL) has its own set of endpoints. Events are the primary resource — each event has start/end times, attendees, conferenceData (for Meet links), recurrence rules, and reminders.
For scheduling automation, the key insight is that events.insert with conferenceDataVersion=1 creates both the calendar event AND the Google Meet link in a single API call. The Meet link is generated by Google when you include a conferenceData.createRequest object with a unique requestId. If you omit the conferenceDataVersion=1 query parameter, the API processes the request without the conferenceData portion — no error, no Meet link.
Timezone handling is the other major source of bugs. Use IANA timezone names (America/New_York, not EST) for the event's timeZone field, and always include explicit UTC offsets in dateTime strings. Official docs: https://developers.google.com/workspace/calendar/api
https://www.googleapis.com/calendar/v3Setting Up Google Calendar API Authentication
Calendar API uses OAuth 2.0 for user-delegated access or Service Account + DWD for server-to-server Workspace automation. When using Service Account impersonation, add the quotaUser parameter to requests (or x-goog-quota-user header) to ensure quota is counted per-impersonated-user rather than per-service-account.
- 1Go to https://console.cloud.google.com and create or select a project
- 2Enable Google Calendar API: APIs & Services → Library → Google Calendar API → Enable
- 3Configure OAuth consent screen: add required scopes https://www.googleapis.com/auth/calendar.events
- 4Create credentials: Credentials → Create Credentials → OAuth Client ID → Desktop app
- 5Download credentials.json
- 6For Workspace server automation: create a Service Account, download JSON key, enable DWD in Workspace Admin Console
- 7Run auth script once interactively to generate token.json
- 8Install: pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib
1import os2from google.oauth2.credentials import Credentials3from google_auth_oauthlib.flow import InstalledAppFlow4from google.auth.transport.requests import Request5from googleapiclient.discovery import build67SCOPES = ['https://www.googleapis.com/auth/calendar.events']89def get_calendar_service():10 creds = None11 if os.path.exists('token.json'):12 creds = Credentials.from_authorized_user_file('token.json', SCOPES)13 if not creds or not creds.valid:14 if creds and creds.expired and creds.refresh_token:15 creds.refresh(Request())16 else:17 flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)18 creds = flow.run_local_server(port=0)19 with open('token.json', 'w') as f:20 f.write(creds.to_json())21 return build('calendar', 'v3', credentials=creds)Security notes
- •Never commit credentials.json or token.json to version control
- •Use calendar.events scope (not full calendar scope) when you only need to manage events
- •For Service Account + DWD, add quotaUser parameter to avoid all quota being counted against the service account
- •Rotate Service Account keys every 90 days
- •Store Service Account JSON key in Google Secret Manager or equivalent — never hardcode
Key endpoints
/calendar/v3/calendars/{calendarId}/eventsCreates a new calendar event. Pass conferenceDataVersion=1 as query parameter to create Google Meet links. The calendarId 'primary' refers to the user's main calendar.
| Parameter | Type | Required | Description |
|---|---|---|---|
calendarId | string | required | 'primary' for main calendar, or specific calendar ID |
conferenceDataVersion | number | optional | Must be 1 to create Google Meet links — without this, conferenceData is silently ignored |
sendUpdates | string | optional | 'all' sends invitations to all attendees, 'none' creates event silently |
summary | string | optional | Event title |
start.dateTime | string | required | RFC 3339 datetime with timezone offset: 2025-05-12T09:00:00-07:00 |
start.timeZone | string | required | IANA timezone name: America/New_York (not EST/PST) |
Request
1{"summary":"Team Standup","start":{"dateTime":"2025-05-12T09:00:00-07:00","timeZone":"America/Los_Angeles"},"end":{"dateTime":"2025-05-12T09:30:00-07:00","timeZone":"America/Los_Angeles"},"attendees":[{"email":"alice@example.com"}],"conferenceData":{"createRequest":{"requestId":"unique-req-id-123","conferenceSolutionKey":{"type":"hangoutsMeet"}}}}Response
1{"id":"abcde12345","summary":"Team Standup","start":{"dateTime":"2025-05-12T09:00:00-07:00"},"conferenceData":{"entryPoints":[{"entryPointType":"video","uri":"https://meet.google.com/abc-mnop-xyz"}]},"htmlLink":"https://www.google.com/calendar/event?eid=..."}/calendar/v3/calendars/{calendarId}/eventsLists events in a calendar within a time range. Use timeMin and timeMax for date filtering. Set singleEvents=true to expand recurring events into individual instances.
| Parameter | Type | Required | Description |
|---|---|---|---|
timeMin | string | optional | RFC3339 timestamp with timezone offset — lower bound on event start time |
timeMax | string | optional | RFC3339 timestamp — upper bound on event end time |
singleEvents | boolean | optional | true to expand recurring events into individual instances |
orderBy | string | optional | 'startTime' (only with singleEvents=true) or 'updated' |
maxResults | number | optional | Default 250, max 2500 |
Response
1{"kind":"calendar#events","items":[{"id":"evt123","summary":"Meeting","start":{"dateTime":"2025-05-12T09:00:00-07:00"},"attendees":[{"email":"alice@example.com","responseStatus":"accepted"}]}]}/calendar/v3/calendars/{calendarId}/events/{eventId}Full replacement update of an event. Preferred over PATCH because events.patch costs 3 quota units vs 1 for get+update combination per Google's own guidance.
| Parameter | Type | Required | Description |
|---|---|---|---|
eventId | string | required | The event ID from events.insert or events.list |
sendUpdates | string | optional | 'all' to notify attendees of changes |
Request
1{"summary":"Updated Meeting Title","start":{"dateTime":"2025-05-12T10:00:00-07:00","timeZone":"America/Los_Angeles"},"end":{"dateTime":"2025-05-12T11:00:00-07:00","timeZone":"America/Los_Angeles"}}Response
1{"id":"evt123","summary":"Updated Meeting Title","updated":"2025-05-07T12:00:00Z"}Step-by-step automation
Create a Calendar Event with Google Meet Link
Why: The single most common Calendar API use case — create an event AND generate a Meet link in one call, but only when conferenceDataVersion=1 is set.
Pass conferenceDataVersion=1 as a query parameter (not in the body) and include conferenceData.createRequest with a unique requestId. Google generates the Meet link server-side and returns it in the response's conferenceData.entryPoints array. The requestId must be unique per request to prevent accidental duplicate events — use UUID4 or a timestamp-based ID.
1curl -X POST \2 -H "Authorization: Bearer $ACCESS_TOKEN" \3 -H "Content-Type: application/json" \4 -d '{5 "summary": "Sales Call with Acme Corp",6 "start": {"dateTime": "2025-05-15T14:00:00-05:00", "timeZone": "America/Chicago"},7 "end": {"dateTime": "2025-05-15T14:30:00-05:00", "timeZone": "America/Chicago"},8 "attendees": [{"email": "prospect@acmecorp.com"}],9 "conferenceData": {"createRequest": {"requestId": "unique-id-001", "conferenceSolutionKey": {"type": "hangoutsMeet"}}},10 "reminders": {"useDefault": false, "overrides": [{"method": "email", "minutes": 1440}, {"method": "popup", "minutes": 10}]}11 }' \12 'https://www.googleapis.com/calendar/v3/calendars/primary/events?conferenceDataVersion=1&sendUpdates=all'Pro tip: Use UUID4 for requestId — reusing a requestId in a short window returns the previously created event rather than creating a new one. This is actually useful for idempotency: if a request times out, retry with the same requestId to avoid duplicates.
Expected result: Returns event ID, Google Meet link (https://meet.google.com/xxx-xxxx-xxx), and htmlLink to the event. Attendees receive email invitations automatically (when sendUpdates='all').
Check Calendar Availability Before Scheduling
Why: Scheduling a meeting when a participant is already busy creates conflicts — check events.list before inserting.
Query the calendar with events.list for the proposed time window. Set singleEvents=true to expand recurring events into individual instances (important for correctly detecting recurring meeting conflicts). Check if any existing events overlap with your proposed start/end time. For free/busy checking across multiple calendars, use the freebusy.query endpoint.
1# Check for events in a 1-hour window2curl -H "Authorization: Bearer $ACCESS_TOKEN" \3 'https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin=2025-05-15T14:00:00-05:00&timeMax=2025-05-15T15:00:00-05:00&singleEvents=true&orderBy=startTime'Pro tip: For checking availability across multiple attendees, use the Calendar API's freebusy.query endpoint — it's more efficient than querying each calendar separately and works across domains.
Expected result: Returns true if the time slot is free, false with conflict details if busy.
Create Recurring Events with RRULE
Why: Weekly standups, monthly reviews, and other recurring meetings should be created as a single recurring event, not as hundreds of individual events.
Add a recurrence array to the event body with RFC 5545 RRULE strings. Common patterns: FREQ=WEEKLY;BYDAY=MO,WE,FR for weekly M/W/F meetings, FREQ=MONTHLY;BYDAY=-1FR for the last Friday of each month. Important: set start.timeZone and end.timeZone on recurring events — without these, Daylight Saving Time transitions create incorrect times. Do NOT include DTSTART or DTEND inside the RRULE string — they go in the event body's start/end fields.
1curl -X POST \2 -H "Authorization: Bearer $ACCESS_TOKEN" \3 -H "Content-Type: application/json" \4 -d '{5 "summary": "Weekly Team Standup",6 "start": {"dateTime": "2025-05-12T09:00:00-07:00", "timeZone": "America/Los_Angeles"},7 "end": {"dateTime": "2025-05-12T09:30:00-07:00", "timeZone": "America/Los_Angeles"},8 "recurrence": ["RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;UNTIL=20251231T235959Z"],9 "attendees": [{"email": "team@example.com"}]10 }' \11 'https://www.googleapis.com/calendar/v3/calendars/primary/events?sendUpdates=all'Pro tip: Never modify individual instances of a recurring event if you intend to change all occurrences — always update the master event. Modifying instances one-by-one creates 'exceptions' that are stored separately and clutter the calendar.
Expected result: A single recurring event is created. Calendar.list will show individual instances when expanded with singleEvents=true. The Meet link is shared across all occurrences.
Watch for Calendar Changes with Push Notifications
Why: For reactive scheduling systems (like booking apps), you need to know when events are added, modified, or deleted in real-time.
Use events.watch to register a HTTPS webhook channel. The channel must have a valid CA certificate — self-signed certs are rejected. Your webhook domain must be verified in Google Search Console for your GCP project. When an event changes, Google sends a POST with X-Goog-Resource-State header (sync, exists, not_exists) but an empty body — you must call events.list with a syncToken to get the actual changes.
1# Register a push notification channel2curl -X POST \3 -H "Authorization: Bearer $ACCESS_TOKEN" \4 -H "Content-Type: application/json" \5 -d '{6 "id": "unique-channel-id-001",7 "type": "web_hook",8 "address": "https://your-server.com/calendar-webhook",9 "token": "your-secret-token",10 "expiration": 174960000000011 }' \12 'https://www.googleapis.com/calendar/v3/calendars/primary/events/watch'Pro tip: Calendar push notifications are not 100% reliable per Google's documentation — combine push notifications with periodic polling using syncToken for production scheduling systems.
Expected result: Returns a channel object with id, resourceId, and expiration. Google sends a sync notification immediately, then change notifications as events are modified.
Complete working code
Complete scheduling automation: accepts a booking request (date, time, attendees), checks availability, creates the event with a Google Meet link, and sends invitations. Suitable for embedding in a Calendly-like custom booking system.
1#!/usr/bin/env python32"""Google Calendar Scheduling Automation — availability check + event creation."""3import os4import uuid5import logging6from datetime import datetime, timedelta7from googleapiclient.discovery import build8from google.oauth2.credentials import Credentials9from google_auth_oauthlib.flow import InstalledAppFlow10from google.auth.transport.requests import Request11import pytz1213logging.basicConfig(level=logging.INFO)14log = logging.getLogger(__name__)1516SCOPES = ['https://www.googleapis.com/auth/calendar.events']1718def get_service():19 creds = None20 if os.path.exists('token.json'):21 creds = Credentials.from_authorized_user_file('token.json', SCOPES)22 if not creds or not creds.valid:23 if creds and creds.expired and creds.refresh_token:24 creds.refresh(Request())25 else:26 flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)27 creds = flow.run_local_server(port=0)28 with open('token.json', 'w') as f: f.write(creds.to_json())29 return build('calendar', 'v3', credentials=creds)3031def check_availability(service, start_dt, end_dt, calendar_id='primary'):32 result = service.events().list(33 calendarId=calendar_id,34 timeMin=start_dt.isoformat(),35 timeMax=end_dt.isoformat(),36 singleEvents=True37 ).execute()38 return len([e for e in result.get('items', []) if e.get('status') != 'cancelled']) == 03940def create_booking(service, title, attendee_emails, start_dt, duration_min=30,41 timezone='America/New_York', calendar_id='primary'):42 tz = pytz.timezone(timezone)43 if start_dt.tzinfo is None:44 start_dt = tz.localize(start_dt)45 end_dt = start_dt + timedelta(minutes=duration_min)4647 if not check_availability(service, start_dt, end_dt, calendar_id):48 raise ValueError(f'Time slot unavailable: {start_dt}')4950 event = {51 'summary': title,52 'start': {'dateTime': start_dt.isoformat(), 'timeZone': timezone},53 'end': {'dateTime': end_dt.isoformat(), 'timeZone': timezone},54 'attendees': [{'email': e} for e in attendee_emails],55 'conferenceData': {'createRequest': {56 'requestId': str(uuid.uuid4()),57 'conferenceSolutionKey': {'type': 'hangoutsMeet'}58 }},59 'reminders': {'useDefault': False, 'overrides': [60 {'method': 'email', 'minutes': 1440},61 {'method': 'popup', 'minutes': 10}62 ]}63 }64 result = service.events().insert(65 calendarId=calendar_id, body=event,66 conferenceDataVersion=1, sendUpdates='all'67 ).execute()68 meet_link = next(69 (e['uri'] for e in result.get('conferenceData', {}).get('entryPoints', [])70 if e['entryPointType'] == 'video'), None71 )72 log.info(f'Booked: {title} | Meet: {meet_link}')73 return {'id': result['id'], 'meet_link': meet_link, 'html_link': result['htmlLink']}7475if __name__ == '__main__':76 service = get_service()77 booking = create_booking(78 service,79 title='Discovery Call - RapidDev Demo',80 attendee_emails=['prospect@example.com'],81 start_dt=datetime(2025, 5, 20, 14, 0),82 duration_min=30,83 timezone='America/Chicago'84 )85 print(f'Meeting scheduled: {booking["html_link"]}')86 print(f'Join: {booking["meet_link"]}')Error handling
Invalid value for: start.dateTimedateTime format is wrong — missing timezone offset, using 'Z' for a non-UTC time, or mixing date (all-day) with dateTime formats.
Use RFC 3339 format with explicit offset: '2025-05-12T09:00:00-07:00'. For all-day events, use 'date' field with 'YYYY-MM-DD' format — never mix date and dateTime in the same event.
No retry — fix the date format.
The caller does not have permissionInsufficient OAuth scope, or trying to create events on a calendar the user doesn't own. Also returned for recurring event instances when the user is not the organizer.
Use calendar.events scope (not calendar.readonly). For shared calendars, ensure the user has 'writer' role via ACL.
No retry — fix authorization.
The requested identifier already existsYou pre-specified an event ID that already exists in the calendar.
Let Google generate the event ID (don't set the 'id' field), or use a unique UUID-based ID. For resumable creation with idempotency, use the same conferenceData requestId.
If retrying after a network timeout, use the same requestId — Google returns the previously created event safely.
Calendar usage limits exceededHit the per-minute quota limit. Exact limits are not published — check Cloud Console.
Implement exponential backoff. Reduce request frequency. For bulk operations, add delays between requests.
Exponential backoff: 2^n seconds (1s, 2s, 4s, 8s, 16s, 32s, max 64s) with jitter.
timeRangeEmptyEnd time is before or equal to start time.
Ensure end > start. For all-day events, end date must be one day after start (end is exclusive in all-day format).
No retry — fix the time range.
Rate Limits for Google Calendar API
| Scope | Limit | Window |
|---|---|---|
| Per project | Not publicly documented | per minute — check Cloud Console |
| Per user per project | Not publicly documented | per minute — check Cloud Console |
| Daily limits | None (removed May 2021) | no daily cap |
1import time2import random3from googleapiclient.errors import HttpError45def insert_with_backoff(service, calendar_id, event_body, conference_version=0, max_retries=6):6 for attempt in range(max_retries):7 try:8 return service.events().insert(9 calendarId=calendar_id,10 body=event_body,11 conferenceDataVersion=conference_version,12 sendUpdates='all'13 ).execute()14 except HttpError as e:15 if e.resp.status in [403, 429, 500, 503]:16 wait = min((2 ** attempt) + random.uniform(0, 1), 64)17 time.sleep(wait)18 else:19 raise20 raise Exception('Max retries exceeded')- Use events.get + events.update instead of events.patch — patch costs 3 quota units while get+update costs 2
- Batch related read operations with the Google API batch endpoint when checking availability across multiple calendars
- Set sendUpdates='none' during testing and bulk imports to avoid spamming calendar invitations
- Cache calendar IDs — listing calendars on every request wastes quota
- For high-frequency scheduling (100+ events/hour), monitor quota usage in Cloud Console and request increases proactively
Security checklist
- Use calendar.events scope instead of full calendar scope when you only need event management
- Never expose OAuth tokens in client-side code — handle Calendar API calls server-side
- For Service Account + DWD, add quotaUser parameter to track quota per impersonated user
- Validate attendee email addresses before adding to events — invalid addresses cause hard bounces
- Set sendUpdates='none' for test/development to avoid sending real invitations
- For webhook channels, validate the X-Goog-Channel-Token header matches your expected token
- Rotate Service Account keys every 90 days and revoke old keys immediately after rotation
- Log event creation/deletion for audit trail — Google Calendar events are binding commitments for attendees
Automation use cases
Calendly-Style Booking System
intermediateBuild a custom booking page that checks real-time availability via events.list and creates events with Meet links on confirmation.
Bulk Recurring Meeting Setup
advancedCreate standardized recurring meetings (weekly 1:1s, quarterly reviews) across an entire team at once using Service Account + DWD.
CRM-Triggered Meeting Creation
intermediateAutomatically schedule a follow-up call when a CRM deal moves to a specific stage, creating the calendar event with the prospect's email as attendee.
Interview Scheduling Pipeline
advancedAutomate interview scheduling: read candidate availability from a form, check interviewer calendars, create events at optimal times for all parties.
No-code alternatives
Don't want to write code? These platforms can automate the same workflows visually.
Zapier
Free tier (100 tasks/month); Starter from $19.99/monthZapier's Google Calendar integration creates events triggered by form submissions, CRM updates, or other app events without code.
- + No code required
- + 500+ trigger sources
- + Easy for simple event creation
- - No availability checking
- - Limited recurrence control
- - 100 tasks/month free tier
Make (Integromat)
Free (1,000 ops/month); Core from $9/monthMake provides Calendar event creation with conditional routing for availability checks and multi-attendee scheduling.
- + More logic control than Zapier
- + Can check availability before creating
- + Better pricing at scale
- - Complex setup for availability logic
- - 1,000 ops/month free tier
- - Still no native booking UI
n8n
Free self-hosted; Cloud from €20/monthSelf-hosted n8n includes Google Calendar nodes for full event management with JavaScript function nodes for complex scheduling logic.
- + Free self-hosted
- + Full API access
- + Code nodes for complex availability logic
- - Server setup required
- - No booking UI included
- - OAuth setup needed
Best practices
- Always pass conferenceDataVersion=1 as a query parameter when creating events with Meet links — forgetting it silently creates events without Meet links
- Use IANA timezone names (America/New_York) not abbreviations (EST) — abbreviations are ambiguous and cause Daylight Saving Time errors
- Never mix date and dateTime fields in the same event — use date+date for all-day or dateTime+dateTime for timed events exclusively
- Generate unique requestIds for each conferenceData.createRequest — reusing IDs returns the previously created event (useful for idempotency, dangerous if unintended)
- Set start.timeZone and end.timeZone on all recurring events — without these, Daylight Saving Time transitions shift event times
- Use sendUpdates='none' during development and testing — every insert with 'all' sends real email invitations to attendees
- Implement the events.watch + syncToken pattern for scheduling apps that need to react to calendar changes rather than polling on a timer
Ask AI to help
Copy one of these prompts to get a personalized, working implementation.
I'm using the Google Calendar API v3 in Python to create events with Google Meet links. The event is created successfully but conferenceData is null in the response — no Meet link is generated. My request body includes the conferenceData.createRequest object with requestId and conferenceSolutionKey.type='hangoutsMeet'. What am I missing?
Build a custom meeting booking page using the Google Calendar API. Features: 1) Calendar view showing available time slots (green = free, red = busy) fetched via events.list, 2) Time slot selection form with attendee email and meeting title, 3) Auto-create Google Calendar event with Meet link on submission, 4) Confirmation page with event link and Meet URL, 5) Time zone detection from browser. Use Supabase Edge Functions to proxy Calendar API calls (keep OAuth tokens server-side).
Frequently asked questions
Why is my Google Meet link not being created even though I include conferenceData in the request?
You must pass conferenceDataVersion=1 as a URL query parameter, not just in the request body. The endpoint is events.insert?conferenceDataVersion=1. Without this parameter, the API silently ignores the conferenceData field and creates the event without a Meet link. No error is returned — this is one of the most common Calendar API gotchas.
What date format does the Calendar API require?
RFC 3339 format with explicit timezone offset: 2025-05-12T09:00:00-07:00. Do not use 'Z' suffix (UTC) for events in non-UTC timezones — it creates the correct UTC time but displays incorrectly in local timezones. For all-day events, use the 'date' field with YYYY-MM-DD format instead of 'dateTime'. Never mix 'date' and 'dateTime' in the same event's start and end fields.
Is the Google Calendar API free?
Yes, the Google Calendar API is completely free with no per-call charges. The API has per-minute rate limits (exact values not published — check Cloud Console), but no daily limits (these were removed in May 2021). There are no overage charges.
What happens when I hit the Calendar API rate limit?
You receive HTTP 403 with reason 'rateLimitExceeded' or 'userRateLimitExceeded', or HTTP 429. Implement exponential backoff: wait 2^n seconds (1s, 2s, 4s, 8s, max 64s) then retry. The actual quota values are only visible in your Cloud Console under APIs & Services → Google Calendar API → Quotas.
Can I create events on other users' calendars?
For Google Workspace domains: yes, using Service Account + Domain-Wide Delegation. Your service account impersonates the target user and creates events on their behalf. For personal Gmail accounts: you can only create events on calendars that have been shared with you (with 'make changes' access) via the regular calendar ID.
How do I handle Daylight Saving Time in recurring events?
Always set start.timeZone and end.timeZone on recurring events using IANA timezone names (America/New_York, not EST). Google's Calendar API handles DST transitions automatically when you specify a named timezone. If you don't set these fields, recurring events in affected regions will shift by 1 hour during DST transitions.
Can RapidDev help build a custom Google Calendar scheduling system?
Yes. RapidDev has built 600+ apps including custom booking systems, interview schedulers, and calendar automation pipelines. We handle OAuth setup, availability logic, timezone handling, and the full booking UI. Contact us at rapidevelopers.com for a free consultation.
Need this automated?
Our team has built 600+ apps with API automations. We can build this for you.
Book a free consultation