---
name: gmail-oauth-app
description: The Gmail API layer on top of the shared `google-cloud-console-oauth` skill — why every mailbox-reading Gmail scope is restricted and there is no `drive.file`-style escape hatch, the one genuinely narrow scope (`gmail.send`) and what it cannot do, the permitted application types that gate Gmail restricted scopes, refresh tokens dying on a user's password change, Pub/Sub push registration, Gmail quota units and sending limits, and the Workspace admin controls that block Gmail API access specifically. Use when asked to get Gmail OAuth credentials, register or scope a Gmail app, choose between `gmail.readonly`, `gmail.modify`, `gmail.metadata` and `gmail.send`, judge whether a Gmail integration needs a CASA assessment, or explain why Gmail connections keep dying or returning 403. Read `google-cloud-console-oauth` first for the console mechanics; use that skill alone when the Google product is not Gmail, and the relevant product skill for Drive, Google Ads, Google Business Profile or another vendor's portal.
---

# Gmail OAuth2 App Registration

Registering the client is the easy part. **Every Gmail scope that can read a mailbox is a restricted scope**, which
means Google verification *plus* an externally-performed CASA security assessment *plus* an annual revalidation — and
unlike Drive, **Gmail has no `drive.file`-style per-item escape hatch**. There is no scope that reads "only the
messages the user picked." If the product reads mail at all, the assessment is on the roadmap, and that fact sets the
project timeline before a single console field is filled in.

One real lever exists, and it is not a hatch so much as a different product: **`gmail.send` is a *sensitive* scope,
not a restricted one**, and an app that only ever sends mail can stay out of the restricted tier entirely. It also
cannot read, list or search anything. That trade, the narrowest-scope ladder behind it, and the Gmail-specific token
and quota behaviour are what this file is for. Everything else is shared.

## Built on: `google-cloud-console-oauth`

**Read `google-cloud-console-oauth` first, then this file.** It owns all the console mechanics, and this file does not
repeat them:

- Choosing/creating the Cloud project and organization, enabling APIs, and who may administer it.
- Configuring the app on the Google Auth Platform (Branding, Audience, Data Access, Clients) and creating a **Web
  application** client — the only type that yields a usable client secret.
- Redirect-URI rules (exact matching, HTTPS-only, propagation delay) and the platform's per-data-center callback list.
- The generic scope model: declaring every scope on Data Access, the non-sensitive / sensitive / restricted tiers, the
  rule that the app's tier is its most sensitive scope, and `access_type=offline` + `prompt=consent` for refresh tokens.
- The client secret shown and downloadable **only once at creation**, add-then-disable rotation, `Testing` vs
  `In production` (including the seven-day refresh-token expiry), the 100-user caps, verification, and the annual CASA
  assessment at assurance level **AL1 or AL2**.

If you are reading only this file, you are missing all of the above.

## Inputs to collect before you start

The base skill lists the console inputs. These are the Gmail-specific ones, and they decide the cost of the project:

| Input | Notes |
| --- | --- |
| **Does the product read mail, or only send it?** | The single question that decides restricted vs sensitive tier (§1, §2) |
| **If it reads: bodies, or headers and labels only?** | Does not lower the tier — `gmail.metadata` is fully restricted (§2) |
| **Does it need to permanently delete, or is trash enough?** | Only `https://mail.google.com/` deletes past trash; almost nothing needs it (§2) |
| **Which of Google's four permitted Gmail application types is this?** | Restricted Gmail scopes are unobtainable outside them (§3) |
| **Is there budget and a named owner for a CASA assessment and its annual renewal?** | Blocking for any mailbox read |
| **Consumer accounts, one Workspace customer, or a multi-tenant connector?** | Decides whether `Internal` removes the review at all (§3) |
| **Real-time updates needed?** | Pub/Sub push adds a topic, an IAM grant and a 7-day renewal loop (§6) |
| **Expected send volume per user per day** | Gmail sending limits are per mailbox and unrelated to API quota (§5) |

## Quick Start

1. Work the base skill's console steps, and **enable the Gmail API** on the project (`gmail.googleapis.com`). A perfect
   client against a project with the API off fails on the first call, not at authorization.
2. Settle the scope decision before touching Data Access: §1 (tiers), §2 (the ladder), §3 (permitted app types).
3. If any mailbox-reading scope is in the set, confirm the app is a permitted application type **and** that CASA is
   funded and owned, then start verification now — not after launch.
4. If the product only sends, request `gmail.send` alone and say plainly that this keeps the app out of the restricted
   tier (§2).
5. Add Pub/Sub topic and IAM grant only if push is in scope (§6).
6. Finish the base skill's capture, round-trip and handoff steps, adding the Gmail checks in §8.

## Gmail platform state (verified 2026-09-20 — re-verify before trusting)

- **Restricted Gmail scopes, per Google's own restricted-scope list and the Gmail scopes page:**
  `https://mail.google.com/`, `gmail.readonly`, `gmail.metadata`, `gmail.modify`, `gmail.insert`, `gmail.compose`,
  `gmail.settings.basic`, `gmail.settings.sharing`. That is every scope that can see a mailbox.
- **`gmail.send` is *sensitive*, not restricted** — it appears on the Gmail scopes page under Sensitive and is absent
  from the restricted list. Sensitive means verification with a justification and a demo video; it does **not** pull in
  the CASA assessment. This is the one meaningful tier lever Gmail offers, and it is easy to miss because so much
  third-party writing (and some older runbooks) flattens "all Gmail scopes" into "restricted."
- **`gmail.labels` and `gmail.addons.current.action.compose` are non-sensitive**; the other add-on scopes
  (`gmail.addons.current.message.metadata`, `...message.readonly`) are sensitive. Add-on scopes only apply inside a
  Workspace add-on's runtime and are not a way for a server-side connector to read mail.
- **There is no per-message equivalent of `drive.file`.** Nothing grants access to "just the messages the user picked."
- **Restricted Gmail scopes require a permitted application type** — Google publishes four for Gmail (§3) — and, above
  Google's thresholds, an annual third-party security assessment.
- **Refresh tokens carrying Gmail scopes are invalidated when the user changes their account password.** This is a
  documented Gmail-only rule, not a general OAuth one (§4).

If the scope tables do not look like this, stop and report what you actually see.

## 1. The Gmail scope tiers

| Tier | Gmail scopes | What it costs |
| --- | --- | --- |
| **Non-sensitive** | `gmail.labels`, `gmail.addons.current.action.compose` | Basic app verification only. No assessment. |
| **Sensitive** | `gmail.send`, `gmail.addons.current.message.metadata`, `gmail.addons.current.message.readonly` | Additional app verification: per-scope justification and a demo video. No security assessment. |
| **Restricted** | `https://mail.google.com/`, `gmail.readonly`, `gmail.metadata`, `gmail.modify`, `gmail.insert`, `gmail.compose`, `gmail.settings.basic`, `gmail.settings.sharing` | Full verification **plus** an annual CASA security assessment. |

Three traps in that table:

- **`gmail.compose` is restricted, `gmail.send` is not**, even though both end in a sent message. `gmail.compose`
  manages drafts (it can create, list and read drafts, which means reading mail the app did not write), and that is
  what puts it in the restricted tier. An app that composes its own body and posts it does not need it.
- **`gmail.metadata` is fully restricted despite never returning a body.** "It's only headers and labels" is not an
  argument Google's tiering accepts. Choosing it buys the entire restricted-tier cost for a fraction of the capability
  — and it also disables mail search (§2). If metadata is genuinely all the product needs, that is an argument about
  the product, not a cheaper configuration.
- **`https://mail.google.com/` is the full-access scope and covers IMAP, SMTP and POP3 as well as the REST API.** Its
  only unique capability over `gmail.modify` is permanent deletion that bypasses trash. Requesting it "to be safe"
  hands a reviewer the widest possible scope to interrogate, for a capability almost no integration uses.

**The tier of the app is the tier of its most sensitive scope** (base skill §5). One `gmail.readonly` in the set puts
the whole project into restricted verification, however narrow everything else is.

## 2. The narrowest-scope ladder

Google's own guidance is to "choose the most narrowly focused scope possible." For Gmail that is an argument you can
actually win with a reviewer, because the scopes differ sharply in what they permit. Work down this ladder and stop at
the first rung that does the job:

| Scope | Can do | Cannot do |
| --- | --- | --- |
| `gmail.labels` (non-sensitive) | See and edit label definitions | Touch any message |
| `gmail.send` (**sensitive**) | Send a message the app composed | **Read anything at all** — no list, no get, no search, no drafts, no threads |
| `gmail.metadata` (restricted) | List messages, read headers and labels | **Read message bodies**; and it **cannot use the `q` search parameter** — filtering is limited to `labelIds` and `includeSpamTrash` |
| `gmail.readonly` (restricted) | Read messages, threads, attachments, settings; full search | Any write |
| `gmail.compose` (restricted) | Create/read/update drafts and send | Modify labels on arbitrary messages |
| `gmail.modify` (restricted) | Read, compose, send, label, trash | Permanently delete bypassing trash |
| `https://mail.google.com/` (restricted) | Everything, including IMAP/SMTP/POP | — |

Two of those rows carry most of the argument:

- **`gmail.send` cannot read.** That is the whole point, and it is why it escapes the restricted tier. Any feature that
  says "show the user their sent history", "thread the reply", "detect a bounce", "sync the conversation into the CRM"
  is not implementable on `gmail.send`. If the product only injects outbound mail, `gmail.send` removes an assessment
  and its annual renewal outright — present that to the owner rather than deciding it.
- **`gmail.metadata` cannot read bodies and cannot search.** The `q` parameter is rejected under that scope, which
  rules out every "find messages matching…" feature and forces label-based enumeration. Teams routinely pick
  `gmail.metadata` believing it is the cheap tier, get the full restricted cost anyway, and then discover search is
  gone. If you are paying restricted prices regardless, `gmail.readonly` is usually the honest request.

Requesting a narrower scope later is easy; widening one after launch **re-consents every customer** and can force an
out-of-cycle re-assessment. Decide up front.

## 3. Permitted application types — the gate before the paperwork

Restricted Gmail scopes are not available to every app willing to pay for an assessment. Google's Workspace user-data
and developer policy limits Gmail restricted scopes to four approved use cases:

- **Email clients** — built-in and web clients that let users compose, send, read and process mail through a UI.
- **Email backup applications** — apps that automatically back up mail.
- **Productivity enhancement** — apps that improve the email experience for productivity purposes (CRM sync, delayed
  send, AI summarization).
- **Reporting / monitoring** — apps that use information from email to provide reporting or monitoring services
  (itinerary extraction, package tracking).

**If the app is not one of these, restricted Gmail scopes are not obtainable at any price.** Check it first; it is the
one Gmail constraint no budget or scheduling fixes, and Google's restricted-scope review explicitly asks the app to
demonstrate that it is a permitted application type.

The same policy attaches **Limited Use** obligations that survive approval and that you must not answer on the user's
behalf: no transfer or sale of Gmail data except under the narrow listed exceptions (explicit user consent, security,
legal compliance, merger with prior notice); **no use of Gmail data for advertising of any kind, including retargeting
and interest-based ads**; and **no human reading of user mail** unless there is documented explicit consent, the data
is aggregated and anonymized for internal operations, it is needed for a security investigation, or law requires it.
The last one has teeth for anything involving manual QA of customer mailboxes or training on mail content — raise it
with the owner rather than attesting to it.

The `Internal` audience exemption in the base skill applies to Gmail too: an app whose every user is in the owner's own
Workspace organization needs no Google review. That is available to a customer registering their own app, and by
definition not to a vendor registering one client for many companies.

## 4. Gmail-specific token behaviour

On top of the base skill's refresh-token rules (offline access, `prompt=consent`, 100 tokens per account per client,
six-month inactivity expiry), Gmail adds one of its own, and it is the source of most "our Gmail connections randomly
break" reports:

> **A refresh token that contains Gmail scopes is invalidated when the user changes their account password.**

Google documents this as a Gmail-scope-specific condition. It is not a bug, not rotation, and not recoverable in code:
the token is gone and the user must re-authorize. Consequences worth designing for:

- **`invalid_grant` on refresh is the expected outcome of a routine password reset.** Treat it as "reconnect required"
  and surface that to the end user, rather than retrying or paging someone.
- **Enterprise password-rotation policies produce correlated waves of disconnects** across one customer's users. A
  cluster of `invalid_grant` failures inside one domain, on one day, is almost always this — not a client problem.
- A mixed-scope token is still a Gmail token. Bundling `gmail.readonly` into an otherwise identity-only grant brings
  this invalidation rule along with it.
- Workspace session-control policies produce a similar-looking `invalid_grant`; the distinguishing signal is that
  session-control expiry hits the whole domain on a schedule, and the fix is the admin's.

## 5. Enable the API, then live inside Gmail's two separate limits

Enable **Gmail API** (`gmail.googleapis.com`) on the project. If push is in scope, enable the **Cloud Pub/Sub API**
too (§6).

Then keep two unrelated ceilings straight — people conflate them constantly, and the fixes are opposite:

**API quota, measured in quota units.** Per project: 1,200,000 units per minute. **Per user per project: 6,000 units
per minute.** Method costs are uneven: `labels.list` 1, `history.list` 2, `messages.list` 5, `messages.get` 20,
`threads.get` 40, and `messages.send`, `drafts.send` and `users.watch` 100 each. So a per-user budget of 6,000 units a
minute is roughly 300 full message fetches — a naive "list then get every message" backfill burns it in seconds, and
it burns it **per mailbox**, so the project-level headroom never saves you. Google's billing threshold sits at
80,000,000 units per 24 hours per project.

- Over the per-project ceiling you get `403 dailyLimitExceeded` / `rateLimitExceeded` — a project quota increase is at
  least possible.
- Over the **per-user** ceiling you get `403 userRateLimitExceeded` or `429`, and Google states plainly that **per-user
  limits cannot be raised**. The only remedies are fewer requests, exponential backoff starting at one second or more,
  and spreading work across accounts. Note that Gmail signals rate limiting as a **403**, not only a 429 — code that
  treats 403 as an authorization failure will disconnect healthy connections under load.
- Keep batches to **50 requests or fewer**; larger batches trigger rate limiting on their own.

**Mailbox sending limits, measured in messages and recipients.** Entirely separate from quota units, enforced per
mailbox by Gmail, not by the API:

- **Workspace:** 2,000 messages per day (500 on a trial, and trial limits are not raised); 2,000 recipients per
  message with at most 500 external; **500 recipients per message when sending via the Gmail API**; 10,000 recipients
  per day; 3,000 external recipients per day; 3,000 unique recipients per day of which 2,000 external.
- **Consumer Gmail:** 500 messages per day and 500 recipients per message.
- Hitting either locks sending for **up to 24 hours** ("You have reached a limit for sending email"), while incoming
  mail and everything else keeps working. No quota increase touches this; it is a mailbox property.

## 6. Push notifications via Pub/Sub

Gmail push is not a webhook URL you paste into a form — it is a Cloud Pub/Sub topic in **your** project, and it changes
what the registration has to include:

1. Enable the Cloud Pub/Sub API and create a topic (`projects/<project>/topics/<name>`) plus a push or pull
   subscription.
2. Grant **Publisher** rights on that topic to Google's own service account, `gmail-api-push@system.gserviceaccount.com`.
   Missing this grant is the single most common cause of "watch succeeds, nothing ever arrives." An organization with
   a domain-restricted-sharing policy needs an explicit exception before that grant can be made.
3. Call `users.watch` with the topic name. It accepts `https://mail.google.com/`, `gmail.modify`, `gmail.readonly` or
   `gmail.metadata` — so **push needs a restricted scope**; there is no send-only or non-sensitive path to it.
4. **The watch expires and must be re-registered at least every seven days** — Google recommends calling `watch` daily.
   A push integration that works for a week and then silently stops is this, not a token problem.
5. Each watched user is capped at **one notification per second**; excess notifications are dropped, not queued. Push
   tells you *that* something changed; you still reconcile with `history.list`.

`users.watch` costs 100 quota units per call, so a daily re-watch across a large tenant is a real line in the per-user
budget from §5.

## 7. Workspace admin controls specific to Gmail

The base skill covers generic API controls. Gmail sits under a sharper version of them, and a Workspace admin can stop
a perfectly verified app cold:

- **Gmail is one of the services for which an admin can mark high-risk OAuth scopes restricted**, independently of any
  general app allowlisting. With Gmail marked restricted, an app requesting a Gmail scope is blocked at consent unless
  that specific app is **Trusted** — the user simply sees that the app is blocked.
- The three app states matter here: **Trusted** overrides the service restriction; **Limited** can reach only
  unrestricted services, which means a Limited app cannot touch Gmail once Gmail is restricted; **Blocked** reaches
  nothing.
- **Flipping Gmail to restricted revokes tokens immediately** for already-installed apps that are not trusted. Existing
  connections stop working at once — the symptom is a customer-wide outage with no change on your side.
- An admin can also **block all third-party API access** domain-wide, which kills even sign-in scopes.
- **Pre-authorization is by OAuth client ID** (or a Marketplace App ID): the admin adds the app under API controls and
  marks it Trusted. Give the customer the exact client ID — nothing else identifies the app to them. A Workspace
  customer who wants the connection approved before users touch it does exactly this.
- **Domain-wide delegation** is the other pre-authorization route: the admin authorizes a service account's client ID
  against a comma-delimited list of Gmail scopes under API controls. It works only for mailboxes in a Workspace domain
  — consumer `@gmail.com` accounts cannot be impersonated — and the scope list registered there must match what the
  app actually requests, character for character.

When exactly one customer cannot connect and everyone else can, this section is the first place to look, and the fix
is theirs, not yours.

## Product fact — what the Gmail connector asks for

> **As of 2026-09-20, Unified.to's Gmail connector requests:**
>
> - **Login/identity only:** `openid`, `profile`, `email`, `https://www.googleapis.com/auth/userinfo.email`,
>   `https://www.googleapis.com/auth/userinfo.profile`
> - **Read (messages and channels):** the identity set **plus** `https://www.googleapis.com/auth/gmail.readonly`
> - **Write (messages):** the identity set **plus** `https://www.googleapis.com/auth/gmail.compose` and
>   `https://www.googleapis.com/auth/gmail.modify`
> - **Webhooks:** the identity set **plus** `https://www.googleapis.com/auth/gmail.readonly`
>
> Tier consequence: `gmail.readonly`, `gmail.compose` and `gmail.modify` are **all restricted**, so the read, write and
> webhook configurations each put the registering app squarely in the restricted tier — verification *and* an annual
> CASA assessment, and the app must be one of the four permitted Gmail application types (§3). Only the
> login/identity-only configuration stays non-sensitive. There is no metadata-only or send-only variant on offer, so
> the §2 ladder cannot be walked down without a change on the connector side.
>
> Auth quirks worth knowing before registering: the connector drives the **legacy authorize endpoint**
> (`accounts.google.com/o/oauth2/auth`) against the current token endpoint, and it does send `access_type` and
> `prompt`, so offline refresh tokens are obtainable. It also offers a **Google service-account credential path** as an
> alternative to the user OAuth flow, and that path mints its token against the **full-access Gmail scope**
> (`https://mail.google.com/`) — meaning a Workspace admin using it must authorize the widest Gmail scope under
> domain-wide delegation (§7), not a narrow one. Finally, the connector **suppresses background connection tests** for
> Gmail specifically, because the per-user quota in §5 is tight enough that routine health checks would compete with
> customer traffic, and it **remaps Gmail's 403 rate-limit responses to 429** so sync backs off instead of treating
> throttling as an auth failure.
>
> Scope sets change. This note is dated, not live; confirm with the connector's owner before submitting anything.

## 8. Gmail-specific end-to-end checks

On top of the base skill's authorize → refresh round trip:

1. Make one real Gmail read (list messages, then get one) and confirm the Gmail API is enabled and returns data.
2. Inspect the granted `scope` in the token response against what was requested — a granular-consent screen lets the
   user drop scopes, and a partially granted Gmail set fails later on a specific call, not at consent.
3. If the app requested `gmail.metadata`, confirm a `q` search is rejected and a body is absent **before** shipping a
   feature that assumes either works.
4. If the app requested `gmail.send`, confirm it can send **and** that any read call is refused — that asymmetry is the
   whole scope, and it is easy to test with a leftover broader token and conclude wrongly.
5. If push is in scope, publish a test change, confirm a Pub/Sub message arrives, and confirm the 7-day re-watch job
   exists and runs.
6. Change the test account's password, then refresh. The refresh **must** fail with `invalid_grant` (§4). Confirm the
   product surfaces "reconnect" rather than retrying.

| Symptom | Cause |
| --- | --- |
| Every Gmail call fails although scopes look right | Gmail API not enabled on the project |
| `invalid_grant` on refresh for scattered users | Those users changed their Google password — Gmail-scoped refresh tokens are invalidated (§4) |
| A whole customer domain fails `invalid_grant` on one day | Enterprise password rotation, or a Workspace session-control policy (§4) |
| One customer's users are blocked at the consent screen | Their admin marked Gmail's high-risk scopes restricted and this app is not Trusted (§7) |
| A customer's connections all die at once with no change on your side | Admin flipped Gmail to restricted; untrusted apps' tokens are revoked immediately (§7) |
| `403 userRateLimitExceeded` / `429` under load | Per-user 6,000 units/minute — cannot be raised; back off and batch ≤50 (§5) |
| `403 rateLimitExceeded` misread as an auth failure | Gmail signals throttling as 403; treat it as a retry, not a disconnect (§5) |
| Sending stops for a day, reads still work | Mailbox sending limit, not quota and not auth (§5) |
| `users.watch` succeeds, no notifications ever arrive | `gmail-api-push@system.gserviceaccount.com` lacks Publisher on the topic (§6) |
| Push works for a week, then stops | `watch` not re-registered within 7 days (§6) |
| `q` search parameter rejected | App is on `gmail.metadata`, which forbids it (§2) |
| Restricted-scope request rejected despite complete paperwork | App is not one of the four permitted Gmail application types (§3) |

## Stop and ask

Beyond the base skill's list, hand back to a human when:

- The product reads mail and nobody has confirmed **budget and a named owner for the CASA assessment and its annual
  renewal** — register nothing until that is answered.
- The app may not qualify as one of Google's **four permitted Gmail application types** (§3). That is a product
  classification, not a console choice.
- Someone wants to narrow live connections to `gmail.send`, or widen them from it — either direction re-consents every
  customer and changes what the product can do (§2).
- Anyone asks you to attest to **Limited Use** terms: data transfer, advertising use, or whether humans read customer
  mail (§3). Those are commitments the owner makes, not answers you compose.
- A customer's Workspace admin must mark the app **Trusted** or authorize **domain-wide delegation** — you supply the
  client ID and the exact scope strings; they make the change (§7).
- Send volumes are near the mailbox sending limits and someone expects a quota increase to fix it (§5).
- The Gmail scope tiers do not match the **Gmail platform state** section above — in particular if `gmail.send` has
  moved into the restricted list.

## References

Gmail-specific only; the base skill carries the generic Google OAuth references. Verified to resolve on 2026-09-20.

- Choose Gmail API scopes (tiers and per-scope descriptions) — https://developers.google.com/gmail/api/auth/scopes
- Restricted scopes list (the Gmail entries, including IMAP/SMTP/POP under `mail.google.com`) — https://support.google.com/cloud/answer/13464325?hl=en
- Google Workspace user data and developer policy (Gmail appropriate access + Limited Use) — https://developers.google.com/workspace/workspace-api-user-data-developer-policy
- Gmail API usage limits (quota units per method, per-project and per-user ceilings) — https://developers.google.com/gmail/api/reference/quota
- Handle Gmail API errors (403 vs 429, backoff, batch size) — https://developers.google.com/gmail/api/guides/handle-errors
- Push notifications with Cloud Pub/Sub (topic, publisher grant, 7-day renewal, 1/sec cap) — https://developers.google.com/gmail/api/guides/push
- `users.watch` reference (accepted scopes, expiration) — https://developers.google.com/gmail/api/reference/rest/v1/users/watch
- `users.messages.list` reference (the `gmail.metadata` restriction on `q`) — https://developers.google.com/gmail/api/reference/rest/v1/users.messages/list
- `users.messages.send` reference (which scopes may send) — https://developers.google.com/gmail/api/reference/rest/v1/users.messages/send
- Synchronizing a Gmail client (`history.list` partial sync) — https://developers.google.com/gmail/api/guides/sync
- Gmail sending limits in Google Workspace — https://knowledge.workspace.google.com/admin/gmail/gmail-sending-limits-in-google-workspace
- Consumer Gmail sending limits — https://support.google.com/mail/answer/22839?hl=en
- Control which third-party and internal apps access Workspace data (trusted/limited/blocked, high-risk Gmail scopes, token revocation) — https://knowledge.workspace.google.com/admin/apps/control-which-third-party-and-internal-apps-access-google-workspace-data
