Skip to main content

Google Calendar

This connector pins https://www.googleapis.com/calendar/v3 and exposes the parts of Google Calendar an assistant actually needs: reading your calendars, listing and searching events in a time window, expanding a recurring series, checking who is free, and — once you switch them on — creating, amending and cancelling events. Nothing else on Google's API surface is reachable through the connection.

What you need

Google Calendar has no API keys. Access is OAuth only: you register your own Google OAuth app, and your gateway runs the authorisation code flow against it and keeps the connection refreshed from then on.

Needs gateway 1.1.0

From 1.1.0 the gateway performs the OAuth exchange itself and refreshes the access token two minutes before it expires, indefinitely. Older gateways have no OAuth endpoint at all. curl -s http://127.0.0.1:7376/health reports the version you are running.

Registering the app is the same for every Google connector, and it is written out once on the Google Drive page: Cloud console project, consent screen, OAuth client id and secret, redirect URI. Two things differ here.

Enable the Google Calendar API rather than the Drive one, under APIs & Services → Library.

Ask for the least privilege that covers what you intend to enable:

ScopeGives you
https://www.googleapis.com/auth/calendar.readonlyevery read action, including free/busy
https://www.googleapis.com/auth/calendar.eventsthe create, update and delete actions as well

A read-only scope is the sensible default. Grant calendar.readonly alone unless you have decided to switch the write actions on; the gateway will then refuse the writes and so will Google, which is two locks rather than one.

Do not leave the app in Testing

While your OAuth app's publishing status is Testing, Google expires each refresh token seven days after it is issued, and refreshing does not extend that — so the connection breaks weekly however well the gateway behaves. Set the app's user type to Internal (Google Workspace organisations only) or publish it to In production before you rely on this. The Drive page has the detail.

Add the connection

Both halves of this are gateway-side today: the console's Add connection dialog cannot finish an OAuth connection yet. Create the connection with POST /v1/connections/from-preset and "preset_id": "google-calendar", exactly as the Drive page shows. Creating it makes the actions possible, not permitted — an agent still needs a grant.

Then authorise the connection_id that came back, with an operator token, as the Drive page describes but with Calendar's scopes:

BASE=http://127.0.0.1:7376
curl -s -X POST $BASE/v1/connections/<connection_id>/oauth/start \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{
"client_id": "<your-client-id>",
"client_secret": "<your-client-secret>",
"redirect_uri": "http://127.0.0.1:7376/oauth/callback",
"scopes": ["https://www.googleapis.com/auth/calendar.readonly"]
}'

Open the authorize_url it returns and approve. Google redirects back to your gateway, which exchanges the code itself and seals the access token, refresh token and client secret as the connection's credential: encrypted on your gateway, injected server-side on every approved call, never readable again. permaura.com is not part of the exchange and never sees any of it. The redirect_uri has to match what you registered with Google character for character, the authorize link is single-use and expires after 10 minutes, and with a device enrolled for approvals the flow has to be started from the gateway's own host rather than over a tunnel.

What the agent can do

ActionWhat it doesOn by default
calendars.listLists the calendars on the accountYes
calendars.getReads one calendar's metadata and time zoneYes
events.listLists or searches events in a time windowYes
events.getReads a single eventYes
events.instancesLists the occurrences of a recurring eventYes
freebusy.queryLooks up busy blocks across calendarsYes
events.createCreates an event, optionally with a Meet linkNo — and asks a human every time
events.updateReplaces an eventNo — and asks a human every time
events.deleteCancels and removes an eventNo — and asks a human every time

freebusy.query is a POST that only reads, so it is classified as a read and stays on. All three writes land switched off, and all three ask for approval when you enable them: each one can email people through sendUpdates, and an invitation that has landed in someone's inbox cannot be recalled. Enable the ones you want on the connection's capability list.

Good to know

  • Getting a Meet link. A Meet link comes from Calendar, not from the Google Meet connector. Call events.create with conferenceDataVersion=1 and a conferenceData.createRequest in the body; Google generates a fresh conference and returns its hangoutLink. Reusing another event's conference data exposes the meeting to people who should not have it, so always let Google mint a new one.
  • Invitations are real email. events.create, events.update and events.delete take a sendUpdates parameter, and all mails every attendee, including people outside your organisation. That is why all three are approval-gated. Set sendUpdates=none while you are testing.
  • events.update is a full replace. It has no patch semantics: read the event with events.get, change what you need, and send the whole resource back, or you will silently drop fields.
  • calendarId. Use primary for the signed-in user's own calendar; anything else is the calendar's id from calendars.list.
  • Recurring events. events.list returns the series as one entry unless you pass singleEvents=true; orderBy=startTime only works when you do. events.instances is the other way to expand one series.
  • Quotas. Calendar allows 10,000 requests a minute per project and 600 a minute per user, on a sliding window; a chatty agent hits the per-user ceiling first and gets a 403 or 429. events.patch costs three quota units against one for a get plus update, which is part of why this connector exposes events.update rather than patch.
  • Attendee email addresses are redacted from responses by default. Turn that off on the connection only if the agent genuinely needs to see them.