Reference
Set up Google OAuth for Gmail, Calendar, Drive, and more
Overview
Section titled “Overview”Connect your users to Google Workspace services including Gmail, Google Calendar, Google Drive, and Google Sheets.
| Property | Value |
|---|---|
| Provider ID | google |
| Category | Productivity |
| PKCE | Supported |
| Token refresh | Automatic (refresh token reused) |
| Access token lifetime | ~1 hour |
| Redirect URI | Shown in Developer Portal |
Step 1: Create a Google Cloud Project
Section titled “Step 1: Create a Google Cloud Project”Go to Google Cloud Console
Navigate to the Google Auth Platform Clients page and sign in with your Google account.
Create or select a project
Click the project dropdown at the top and select New Project. Give it a name (e.g., “My App - Alter Integration”) and click Create.
Enable APIs
Go to APIs & Services > Library and enable the APIs you need:
- Gmail API - for email access
- Google Calendar API - for calendar access
- Google Drive API - for file access
- Google Sheets API - for spreadsheet access
Step 2: Create OAuth Credentials
Section titled “Step 2: Create OAuth Credentials”Open Clients page
Go to the Google Auth Platform > Clients page and click Create Client.
Configure consent screen (first time only)
If prompted, configure the OAuth consent screen:
- User Type: External (unless you only need Google Workspace users)
- App name: The application name
- User support email: The operator’s support email
- Authorized domains: The application’s domain
- Developer contact: The operator’s contact email
Click through the remaining steps and Save.
Create OAuth client ID
- Application type: Web application
- Name: e.g., “Alter Vault Integration”
- Authorized redirect URIs: Copy the Redirect URI from the Developer Portal
- Click Create
Download credentials immediately
Download and securely store the Client ID and Client Secret from the dialog. The secret is only shown once at creation time.
The Client ID looks like: 123456789-abcdefg.apps.googleusercontent.com
Step 3: Add to Alter Vault
Section titled “Step 3: Add to Alter Vault”Open the Developer Portal
Go to portal.alterauth.com and navigate to the application.
Add Google provider
Go to OAuth Providers > Add Provider > Google.
Enter credentials
- Client ID: Paste your Google Client ID
- Client Secret: Paste your Google Client Secret
Select scopes
Choose the scopes the application needs. See the Available Scopes section below.
Save
Click Save. The provider is now active and ready to use.
Step 4: Test It
Section titled “Step 4: Test It”After a user connects via Alter Connect, use the returned grant_id to make API calls:
from alter_sdk import App, HttpMethod
async with App( api_key="alter_key_...", caller="my-agent",) as alter_app: # Use the grant_id returned by Alter Connect response = await alter_app.request( HttpMethod.GET, "https://www.googleapis.com/calendar/v3/calendars/primary/events", grant_id=grant_id, query_params={"maxResults": "5"}, ) events = response.json() print(f"Found {len(events.get('items', []))} events")Available Scopes
Section titled “Available Scopes”Identity (included by default)
Section titled “Identity (included by default)”| Scope | Description |
|---|---|
openid | Authenticate your identity with OpenID Connect |
profile | View your basic profile information |
email | View your email address |
| Scope | Description |
|---|---|
https://www.googleapis.com/auth/gmail.readonly | View your email messages and settings |
https://www.googleapis.com/auth/gmail.send | Send email on your behalf |
https://www.googleapis.com/auth/gmail.compose | Manage drafts and send emails |
https://www.googleapis.com/auth/gmail.modify | Read, compose, send, and permanently delete all your email from Gmail |
Google Calendar
Section titled “Google Calendar”| Scope | Description |
|---|---|
https://www.googleapis.com/auth/calendar.readonly | View your calendars |
https://www.googleapis.com/auth/calendar.events | View and edit events on all your calendars |
Google Drive
Section titled “Google Drive”| Scope | Description |
|---|---|
https://www.googleapis.com/auth/drive.readonly | View your Google Drive files |
https://www.googleapis.com/auth/drive.file | View and manage Google Drive files that you have opened with this app |
https://www.googleapis.com/auth/drive | See, edit, create, and delete all of your Google Drive files |
Common API Endpoints
Section titled “Common API Endpoints”| Use Case | Method | URL |
|---|---|---|
| List calendar events | GET | https://www.googleapis.com/calendar/v3/calendars/primary/events |
| List Gmail messages | GET | https://www.googleapis.com/gmail/v1/users/me/messages |
| Send email | POST | https://www.googleapis.com/gmail/v1/users/me/messages/send |
| List Drive files | GET | https://www.googleapis.com/drive/v3/files |
- Google requires
access_type=offlineandprompt=consentto issue refresh tokens. Alter Vault handles this automatically. - If requesting
profileoremailscopes, theopenidscope is automatically included. - Refresh tokens never expire unless the user revokes access or the token is inactive for 6 months.
- Google enforces a limit of ~50 refresh tokens per user per app. Alter Vault’s connection deduplication prevents hitting this limit.
- See the Google OAuth 2.0 overview and web-server flow guide for details.
Policy parameters
Section titled “Policy parameters”Content rules and conditional approvals can judge these parameters of Google requests:
| Parameter | Operations | Meaning |
|---|---|---|
recipients | Gmail send operations | Every To, Cc, Bcc, and Resent address in the message |
recipients | calendar.events.insert, update, patch, import | Who the request may invite or email: every attendee address the event lists. An update or patch is judged only when sent with sendUpdates=none and no sendNotifications other than false (see below) |
recipients | calendar.events.delete, move, quickAdd | Who the request may email. Empty when a delete or move is sent with sendUpdates=none and no sendNotifications other than false; otherwise it cannot be judged (see below) |
recipients | calendar.acl.insert, update, patch | The person or group the calendar is shared with: the body scope.value on insert, the rule id on update and patch |
calendarId | every Calendar operation whose path names a calendar (calendar.events, calendar.acl, calendar.calendars, calendar.calendarList) | The calendar the request acts on, as its path names it (primary, an address or a calendar id) |
eventId, ruleId, setting | every Calendar operation whose path names one | The event, sharing rule or setting the request acts on, as its path names it |
destination | calendar.events.move | The calendar a move hands the event to |
sender | Gmail send operations | The From address |
subject | Gmail send operations | The Subject header (redactable) |
event_time | calendar.events.insert, update, patch, import | The event’s start-to-end span, judged with outside_windows |
recipients means who the request may email or invite, so one rule on it can limit both who an agent can email through Gmail and who it can invite, notify or share with through Calendar. Every Google operation in the send family carries it. Which calendar a request writes to is a separate question, governed by calendarId and, for a move, destination.
calendarIdrule. Name the operations whosecalendarIdis the calendar written to:calendar.events.insert,importandquickAdd, which create an event there, andupdate,patchanddelete, which change an event that lives there.destinationrule. A move’scalendarIdis the calendar it moves the event out of, so where a move goes needs its owndestinationrule.
The user’s address names their primary calendar just as primary does, and both compare case-insensitively.
These two rules limit which calendars an agent writes events into or moves them to. They do not stop an event from reaching another calendar through its guest list. Google lets a calendar be invited as an attendee: a group calendar (whose id ends in group.calendar.google.com), or a resource calendar that accepts invitations automatically. Such an attendee is a recipient, so it is stopped only by a recipients rule like the third one below. Use all three to keep an agent’s events within the user’s own calendars and the organization’s addresses:
{ "match": {"operations": ["calendar.events.insert", "calendar.events.import", "calendar.events.quickAdd", "calendar.events.update", "calendar.events.patch", "calendar.events.delete"]}, "params": [{"name": "calendarId", "op": "not_subset_of", "value": ["primary", "alice@example.com"]}], "effect": "deny"}{ "match": {"operations": ["calendar.events.move"]}, "params": [{"name": "destination", "op": "not_subset_of", "value": ["primary", "alice@example.com"]}], "effect": "deny"}{ "match": {"operations": ["calendar.events.insert", "calendar.events.import", "calendar.events.quickAdd", "calendar.events.update", "calendar.events.patch"]}, "params": [{"name": "recipients", "op": "not_subset_of", "value": ["*@example.com"]}], "effect": "deny"}The third rule names calendar.events.quickAdd because a quick add can invite anyone its text names, including a calendar. Its recipients can never be judged (see below), so this rule denies every quick add.
A query parameter named like a path parameter (?calendarId=primary on a request whose path names another calendar) does not stand in for it: the two disagree, so a condition on that parameter cannot be judged and denies. A Calendar request whose path is percent-encoded twice (%25) cannot be judged and is denied by any rule with conditions. Google decodes a path once, so judging the twice-decoded calendar or rule id would judge a value Google never reads.
Requests whose recipients or span cannot be judged:
- Updates and patches that email guests. An update or patch emails every guest it removes (a cancellation) and every guest it keeps. The request lists only the guests the event will have afterwards. Unless it is sent with
sendUpdates=noneand nosendNotificationsother thanfalse(Google sends no email then), its recipients cannot be judged. Such a silent patch that does not list attendees emails and invites nobody, so its recipients are empty. - Deletes and moves that email guests. A delete or move emails the event’s current guests, none of whom the request names. With
sendUpdates=noneand nosendNotificationsother thanfalse, its recipients are empty. - Auto-declining out-of-office and focus time. An event whose
outOfOfficePropertiesorfocusTimePropertiessets anautoDeclineModeother thandeclineNone, or adeclineMessage, declines the meetings it overlaps, and each decline goes to that meeting’s organizer, none of whom the request names. - Setting a response. An update or patch where any attendee carries
responseStatusorcommentcan notify the event’s organizer, whom the request need not name. The attendee’sselfandorganizerflags do not change this: Google sets them itself, so a request’s values for them are not trusted. - Quick add.
calendar.events.quickAddlets Google read guests and the time from free text, so its recipients can never be judged and it has noevent_time. - Partial attendee lists. An event with
attendeesOmitted: truelists only some guests. - Sharing with a domain or the public. A
domainordefaultscope (on insert) or rule id (on update or patch) is not a set of people. - Sharing-rule updates and patches whose rule id cannot be read. An update or patch changes the rule its id names (
user:a@example.com,group:…,domain:…, ordefault), so the grantee is taken from the id. Its recipients cannot be judged if the id is in any other form: a patch, or an update that carries ascope. - Patches that change one end. A patch that names only
startor onlyendkeeps the other end, which the request does not show. - Recurring events. An event with
recurrencerules repeats beyond the span the request shows. - Updates and patches that do not state
recurrence. A patch keeps the edited event’s recurrence, and Google does not document what an update withoutrecurrencedoes to a recurring event, soevent_timecannot be judged for either. Including"recurrence": []lets it be judged: Google replaces an array a patch names, and an update replaces the whole event, so the written event does not repeat. A single occurrence of a recurring event has norecurrencefield, and Google does not document whether it accepts one. A change to a single occurrence that does not staterecurrenceis therefore treated like any other undetermined span. - Updates and patches that name neither end. An update or patch without
startorendkeeps the stored time, which the request does not show.
Some sharing-rule requests are refused outright as unclassifiable, whatever the rule: an insert without a scope, an update without a scope (Google requires one), and an update or patch whose body scope names someone other than its rule id.
A deny, redact or step-up rule with a condition that cannot be judged denies the request. A conditional approval sends it to an approver instead. The approval page says which condition was not evaluated and why. It shows the values the request carries as it sent them, with percent-encoding decoded:
- For an event write: the calendar, the listed guests,
attendeesOmitted,startandendwith their time zones,recurrence,sendUpdatesandsendNotifications, and, when the request sets them, the auto-decline settings and each attendee response it sets (the attendee’s address with itsresponseStatusandcomment). - For a sharing rule: the rule id, role and scope.
- For a quick add: the text.
- For a move: the source calendar and destination.
Every Calendar request also lists its query and body as sent, field by field (query.sendUpdates, body.status, body.attendees[0].responseStatus, body.address on a watch, …), whether or not a condition was undetermined: the policy reads only part of the request, and an approval covers everything that executes.
Values outside the request are not shown, and the page says so: the event’s current guests, the organizers an auto-decline or a response notifies, the stored time of a patch that names one end or neither, or the stored scope of a sharing-rule patch. The page never shows the rule’s own address patterns: only whether each listed address matches them.
Each listed address is checked against the rule’s addresses, and the result is shown next to it. An address containing any non-ASCII character is marked as such. A body grantee whose rule id cannot be read is shown as not judged, never as inside the list. Against an allowlist (not_subset_of), a look-alike address, such as one using a Cyrillic letter, is shown as outside the list. Against a blocklist (any_in), it is shown as not matching, so a blocklist does not catch look-alikes; the non-ASCII mark is the only signal.
If the values in the request cannot be shown, the request cannot be approved. A condition on a parameter the operation does not carry at all, such as recipients on calendar.events.list, still denies. Query parameters such as sendUpdates are also available to conditions. See Calendar events only during business hours for business-hours and outside-invitee examples.
Policy-cataloged operations
Section titled “Policy-cataloged operations”Alter policy rules can target these attested operations and families for operation-level and parameter-aware controls.
| Operation ID | Families | Method | Provider path |
|---|---|---|---|
gmail.users.messages.send | send | POST | /gmail/v1/users/{userId}/messages/send |
gmail.users.drafts.send | send | POST | /gmail/v1/users/{userId}/drafts/send |
gmail.users.messages.list | read | GET | /gmail/v1/users/{userId}/messages |
gmail.users.messages.get | read | GET | /gmail/v1/users/{userId}/messages/{id} |
gmail.users.messages.delete | delete | DELETE | /gmail/v1/users/{userId}/messages/{id} |
gmail.users.messages.trash | write | POST | /gmail/v1/users/{userId}/messages/{id}/trash |
gmail.users.drafts.create | write | POST | /gmail/v1/users/{userId}/drafts |
gmail.users.drafts.list | read | GET | /gmail/v1/users/{userId}/drafts |
gmail.users.labels.list | read | GET | /gmail/v1/users/{userId}/labels |
gmail.users.getProfile | read | GET | /gmail/v1/users/{userId}/profile |
calendar.events.insert | send, write | POST | /calendar/v3/calendars/{calendarId}/events |
calendar.events.update | send, write | PUT | /calendar/v3/calendars/{calendarId}/events/{eventId} |
calendar.events.patch | send, write | PATCH | /calendar/v3/calendars/{calendarId}/events/{eventId} |
calendar.events.import | write | POST | /calendar/v3/calendars/{calendarId}/events/import |
calendar.events.quickAdd | send, write | POST | /calendar/v3/calendars/{calendarId}/events/quickAdd |
calendar.events.move | send, write | POST | /calendar/v3/calendars/{calendarId}/events/{eventId}/move |
calendar.events.delete | delete, send | DELETE | /calendar/v3/calendars/{calendarId}/events/{eventId} |
calendar.events.list | read | GET | /calendar/v3/calendars/{calendarId}/events |
calendar.events.get | read | GET | /calendar/v3/calendars/{calendarId}/events/{eventId} |
calendar.events.instances | read | GET | /calendar/v3/calendars/{calendarId}/events/{eventId}/instances |
calendar.events.watch | read, write | POST | /calendar/v3/calendars/{calendarId}/events/watch |
calendar.calendars.insert | write | POST | /calendar/v3/calendars |
calendar.calendars.get | read | GET | /calendar/v3/calendars/{calendarId} |
calendar.calendars.update | write | PUT | /calendar/v3/calendars/{calendarId} |
calendar.calendars.patch | write | PATCH | /calendar/v3/calendars/{calendarId} |
calendar.calendars.delete | delete | DELETE | /calendar/v3/calendars/{calendarId} |
calendar.calendars.clear | delete | POST | /calendar/v3/calendars/{calendarId}/clear |
calendar.calendars.transferOwnership | admin | POST | /calendar/v3/calendars/{calendarId}/transferOwnership |
calendar.acl.insert | admin, send | POST | /calendar/v3/calendars/{calendarId}/acl |
calendar.acl.update | admin, send | PUT | /calendar/v3/calendars/{calendarId}/acl/{ruleId} |
calendar.acl.patch | admin, send | PATCH | /calendar/v3/calendars/{calendarId}/acl/{ruleId} |
calendar.acl.delete | admin, delete | DELETE | /calendar/v3/calendars/{calendarId}/acl/{ruleId} |
calendar.acl.list | read | GET | /calendar/v3/calendars/{calendarId}/acl |
calendar.acl.get | read | GET | /calendar/v3/calendars/{calendarId}/acl/{ruleId} |
calendar.acl.watch | read, write | POST | /calendar/v3/calendars/{calendarId}/acl/watch |
calendar.calendarList.insert | write | POST | /calendar/v3/users/me/calendarList |
calendar.calendarList.update | write | PUT | /calendar/v3/users/me/calendarList/{calendarId} |
calendar.calendarList.patch | write | PATCH | /calendar/v3/users/me/calendarList/{calendarId} |
calendar.calendarList.delete | delete | DELETE | /calendar/v3/users/me/calendarList/{calendarId} |
calendar.calendarList.list | read | GET | /calendar/v3/users/me/calendarList |
calendar.calendarList.get | read | GET | /calendar/v3/users/me/calendarList/{calendarId} |
calendar.calendarList.watch | read, write | POST | /calendar/v3/users/me/calendarList/watch |
calendar.settings.list | read | GET | /calendar/v3/users/me/settings |
calendar.settings.get | read | GET | /calendar/v3/users/me/settings/{setting} |
calendar.settings.watch | read, write | POST | /calendar/v3/users/me/settings/watch |
calendar.colors.get | read | GET | /calendar/v3/colors |
calendar.freebusy.query | read | POST | /calendar/v3/freeBusy |
calendar.channels.stop | write | POST | /calendar/v3/channels/stop |