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.
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:
| Scope | Gives you |
|---|---|
https://www.googleapis.com/auth/calendar.readonly | every read action, including free/busy |
https://www.googleapis.com/auth/calendar.events | the 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.
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
| Action | What it does | On by default |
|---|---|---|
calendars.list | Lists the calendars on the account | Yes |
calendars.get | Reads one calendar's metadata and time zone | Yes |
events.list | Lists or searches events in a time window | Yes |
events.get | Reads a single event | Yes |
events.instances | Lists the occurrences of a recurring event | Yes |
freebusy.query | Looks up busy blocks across calendars | Yes |
events.create | Creates an event, optionally with a Meet link | No — and asks a human every time |
events.update | Replaces an event | No — and asks a human every time |
events.delete | Cancels and removes an event | No — 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.createwithconferenceDataVersion=1and aconferenceData.createRequestin the body; Google generates a fresh conference and returns itshangoutLink. 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.updateandevents.deletetake asendUpdatesparameter, andallmails every attendee, including people outside your organisation. That is why all three are approval-gated. SetsendUpdates=nonewhile you are testing. events.updateis a full replace. It has no patch semantics: read the event withevents.get, change what you need, and send the whole resource back, or you will silently drop fields.calendarId. Useprimaryfor the signed-in user's own calendar; anything else is the calendar's id fromcalendars.list.- Recurring events.
events.listreturns the series as one entry unless you passsingleEvents=true;orderBy=startTimeonly works when you do.events.instancesis 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
403or429.events.patchcosts three quota units against one for a get plus update, which is part of why this connector exposesevents.updaterather 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.