---
name: google-docs-oauth-app
description: >-
  The Google Docs API layer on top of the shared `google-cloud-console-oauth`
  skill — the two `documents` scopes and why they are only sensitive, the
  Drive dependency that decides the project's tier (the Docs API cannot list
  or search, so discovery needs `drive.readonly` and a CASA assessment,
  `drive.file` and a Picker, or document IDs from elsewhere), the
  structural-element and index model behind `batchUpdate`, export through
  Drive, and the Docs quotas. Use when asked to get Google Docs OAuth
  credentials, scope a Docs integration, decide whether reading or writing
  documents needs a restricted Drive scope, or explain why a Docs connection
  cannot find a user's documents. Read `google-cloud-console-oauth` first for
  the console mechanics, and `google-drive-oauth-app` whenever a Drive scope
  is in play; use the relevant product skill for other Google products.
---

# Google Docs OAuth2 App Registration

The Docs scopes themselves are the cheap part of this job: `documents` and `documents.readonly` are **sensitive**, not
restricted, so on their own they cost a verification review and no security assessment.

What makes a Docs integration expensive is the scope it almost always drags in beside them. **The Docs API addresses a
document by ID and has no way to list or search for one.** Everything that finds a document — enumerating a user's
documents, watching for new ones, exporting one to PDF or DOCX, reading its comments — is a Drive call. Pick the
obvious Drive scope and the project lands in the restricted tier with an annual CASA assessment; pick `drive.file` and
the product changes shape; pick neither and the product only works on documents whose IDs arrive from somewhere else.

That fork is §2, and it is the whole point of this file. Settle it before anyone touches Data Access.

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

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

- The Cloud project and organization, enabling APIs, and who may administer the project.
- The Google Auth Platform pages (Branding, Audience, Data Access, Clients) and creating a **Web application** client.
- Redirect-URI rules — exact matching, HTTPS only, the propagation delay — and the 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**, and 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.

**Read `google-drive-oauth-app` too the moment a Drive scope enters the picture.** It owns the Drive scope tiers, the
permitted-application-type gate that no paperwork can satisfy, the `drive.file` + Picker trade-off in full, shared
drives, the verification exemptions, and the Search Console domain-ownership problem. This file says *whether* you need
a Drive scope; that file says what one costs.

## Inputs to collect before you start

The base skill lists the console inputs. These are the Docs-specific ones, and the first two decide the project's tier:

| Input | Notes |
| --- | --- |
| **Where do document IDs come from?** | User picks / pastes / an upstream system supplies them, or the app must discover them. This is the Drive-dependency question (§2) |
| **Read-only, or create and edit documents?** | `documents.readonly` vs `documents`; and on the Drive side `drive.readonly` vs `drive` |
| **Structured content, or a rendered file?** | Structured parsing is Docs; PDF/DOCX/TXT export is a Drive call with its own scopes (§4) |
| **Do tabs, headers/footers, footnotes, suggestions or comments matter?** | Changes what a read returns, and comments change the scope set (§5) |
| **Expected read and write volume per customer** | Docs quotas are per-minute and per-user-per-project (§6) |
| **Are any target documents "published to the web"?** | Published documents are unreachable by `documents.get` (§3) |

## Quick Start

1. Work the base skill's console steps, and **enable the Google Docs API** on the project — and the Google Drive API
   too if §2 lands on any Drive scope. A perfect client against a project with the API disabled fails at the first
   call, not at authorization.
2. Settle §2 — the Drive dependency — **before** declaring anything on Data Access. It decides sensitive vs restricted
   for the whole project.
3. If any Drive scope is restricted, switch to `google-drive-oauth-app` for the application-type gate, CASA ownership
   and the exemptions, and start verification now rather than after launch.
4. Declare the narrowest Docs scope the product needs, and check §2's redundancy trap before adding it at all.
5. Finish the base skill's capture, round-trip and handoff steps, adding the Docs checks in §7.

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

- **Docs scope tiers, from the Docs API's own scope page:** `https://www.googleapis.com/auth/documents` and
  `https://www.googleapis.com/auth/documents.readonly` are both **Sensitive**. The same page lists `drive.file` as
  **Recommended / Non-sensitive** and `drive` and `drive.readonly` as **Restricted**. There is no restricted Docs
  scope — the restricted tier only ever arrives via Drive.
- **The `documents` resource exposes exactly three methods**: `documents.create`, `documents.get` and
  `documents.batchUpdate`. `get` and `batchUpdate` both require a `documentId`. There is **no list, no search and no
  change-notification method.**
- **`documents.get` accepts any one of** `documents`, `documents.readonly`, `drive`, `drive.readonly` or `drive.file`.
  **`documents.create` and `documents.batchUpdate` accept any one of** `documents`, `drive` or `drive.file`.
- **`files.export` — the only way to get a Docs file as PDF/DOCX/TXT — accepts only Drive scopes** (`drive`,
  `drive.file`, `drive.readonly`, `drive.meet.readonly`). It does **not** accept `documents` or `documents.readonly`.
  Exported content is limited to **10 MB**.
- **Docs quotas:** 3,000 read requests/minute/project and 300/minute/user/project; 600 write requests/minute/project
  and 60/minute/user/project.

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

## 1. The Docs scopes

| Scope | Grants | Tier |
| --- | --- | --- |
| `https://www.googleapis.com/auth/documents.readonly` | See all the user's Google Docs documents | **Sensitive** |
| `https://www.googleapis.com/auth/documents` | See, edit, create and delete all the user's Google Docs documents | **Sensitive** |

Two things follow, and both are easy to get backwards:

- **Neither is restricted.** A Docs integration that never touches Drive needs verification with a scope
  justification and a demo video, and **no security assessment and no annual renewal**. That is a materially cheaper
  project than a Drive one, and it is worth knowing before anyone budgets for CASA out of habit.
- **Both are all-documents scopes.** `documents.readonly` reads *every* document the user can open, not a subset. The
  word "readonly" reduces the write risk, not the breadth. Google's own guidance on this page is to prefer a
  non-sensitive per-file scope where the product can live with one — which for Docs means `drive.file` (§2).

## 2. The Drive dependency — the decision that sets the project's tier

**This is the centrepiece. Raise it as a product decision, do not make it on the owner's behalf.**

The Docs API can `get` a document, `create` a blank one and `batchUpdate` one. All three are addressed by
`documentId`. It cannot answer "which documents does this user have?", "which ones changed since Tuesday?", "what is
this document called before I fetch it?" or "give me this as a PDF". Google's own Docs documentation points at Drive
for all of it: `files.list` with `q: mimeType = 'application/vnd.google-apps.document'` to find documents,
`files.copy` to duplicate one, `files.export` to render one, `files.get` for metadata.

So the real question is not *which Docs scope* but **where document IDs come from**. There are three honest answers,
and they cost wildly different amounts:

| Path | Scopes | Tier and cost | What the product can do |
| --- | --- | --- | --- |
| **A. Discover through Drive** | `drive.readonly` (read) or `drive` (write), plus a Docs scope if you still want one | **Restricted** — verification **plus** annual CASA, **plus** the permitted-application-type gate | Enumerate, search, sync, watch, export, read comments. The full product. |
| **B. Per-file through the Picker** | `drive.file` (covers Docs `get`, `create` and `batchUpdate` on picked files, and `files.export`) | **Non-sensitive** — basic verification, no assessment, no annual renewal | Only files the user explicitly picks, files shared with the app, and files the app created. No enumeration, ever. |
| **C. No Drive scope at all** | `documents.readonly` or `documents` alone | **Sensitive** — verification, no assessment | Read or edit a document **whose ID the app already has** — pasted URL, upstream record, a document the app created. No listing, no export, no comments, no metadata beyond what the document resource itself carries. |

Path **A** is what "sync the customer's documents" means, and the Drive skill's §2 application-type gate applies
before any budget conversation: restricted Drive scopes are limited to backup-and-sync, productivity-and-education,
and reporting-and-security apps, and an app outside those categories cannot obtain them at any price.

Path **B** is a different product, not a cheaper configuration — the user hands over one file at a time through the
Google Picker. Worth knowing: the Docs API supports `drive.file` on **all three** of its methods, so path B loses no
Docs capability at all. Everything it loses, it loses on the Drive side: enumeration and search.

Path **C** is the one people forget, and for some integrations it is exactly right. A product that operates on a
document the user pasted a link to, or on documents it created itself and recorded the IDs of, never needs a Drive
scope and never needs a security assessment. Note that `documents.create` returns the created document — including its
`documentId` — so an app that creates its own documents can keep working without ever listing anything.

### The redundancy trap

**`drive.readonly` on its own already authorizes `documents.get`; `drive` on its own already authorizes
`documents.create` and `documents.batchUpdate`.** An app that has already accepted restricted Drive scopes gains
**nothing** by also declaring `documents.readonly` or `documents`. It adds a line to the consent screen, a scope to
justify in the verification review, and a scope to re-justify at every renewal — for zero new capability. Check this
against the actual call list before declaring; it is one of the few scope reductions that costs nothing.

The inverse trap is worse and quieter: **`documents.readonly` does not authorize `files.export`**, so an app that
declared Docs scopes only and then adds "download as PDF" discovers mid-sprint that the feature needs a Drive scope
and, unless it can use `drive.file`, a whole restricted-tier review.

### Switching later is a re-consent event

Moving an existing integration between these paths changes the requested scope set, which re-consents every connected
customer, and moving to path B adds a Picker UI. Say so plainly rather than proposing it as a configuration tweak.

## 3. The document model, and why index arithmetic breaks

You do not need to implement Docs to register its OAuth client, but the shape of the API explains most of the support
tickets that follow, and it is what a demo video for verification has to show working.

- A document is a tree, not a string: `Body` → a sequence of `StructuralElement`s (`Paragraph`, `Table`,
  `TableOfContents`, `SectionBreak`), and within a paragraph a sequence of `ParagraphElement`s (`TextRun`, `AutoText`,
  `PageBreak`, `InlineObjectElement`…). Reading text means walking that tree; there is no "plain text" field.
- Positions are **indexes into a segment**, where a segment is the body, a header, a footer or a footnote, each with
  its own index origin. Indexes are measured in **UTF-16 code units**, so an emoji or any astral-plane character
  consumes two. Naive character offsets computed in a language with a different string model silently misplace edits.
- `documents.batchUpdate` applies its requests **in order and atomically** — one invalid request fails the whole
  batch and applies none of it. Because each insert or delete shifts every later index, Google's own advice is to
  **order requests in descending index**, which removes the need to recompute offsets within a batch.
- **Across calls, indexes go stale.** Any collaborator — or a second process of your own — editing between your `get`
  and your `batchUpdate` invalidates the offsets you computed, and the API will happily apply them to the wrong place.
  The API's answer is `WriteControl` on `batchUpdate`: save `revisionId` from the `get`, then send either
  `requiredRevisionId` (fail the write if the document moved) or `targetRevisionId` (let the server merge your changes
  against the collaborator's). An integration that writes to shared documents and sets neither is not "usually fine";
  it is silently corrupting text under concurrency.
- **`documents.create` ignores any content in the request** — it creates a blank document with the given title, and
  everything else arrives via `batchUpdate`. Create-then-populate is two calls, always.
- **Published documents are unreachable.** A document published to the web gets a different, public ID, and
  `documents.get` returns **404** for it (as does `files.copy`). Only the original `documentId` works, and there is no
  documented way to derive it from a published URL.

## 4. Exporting vs reading structured content

Two genuinely different operations, with different scopes:

- **Structured read** — `documents.get` returns the JSON document tree. Authorized by a Docs scope *or* a Drive scope.
  This is what you use for text extraction, formatting-aware conversion, and anything that needs to know where a
  paragraph starts.
- **Export to a file** — `files.export` on the **Drive** API renders a Docs file to PDF, DOCX, ODT, RTF, TXT, EPUB,
  HTML, Markdown and so on. It is **not** a Docs API call and it **does not accept the Docs scopes**: `drive`,
  `drive.file`, `drive.readonly` or `drive.meet.readonly` only. Exported content is capped at **10 MB**; large
  documents fail on size rather than on permission, and the fix is chunking or a different format, not more scopes.

So "let users download the document as a PDF" is a Drive requirement dressed as a Docs feature. If the app is on path
C (§2), that feature alone moves it onto a Drive scope — `drive.file` if the document was picked or created by the
app, otherwise a restricted one.

## 5. Tabs, headers and footers, suggestions, comments

Only one of these changes the scope set. The rest change what a read returns, which is where "the API is missing half
our content" reports come from.

**Tabs — no scope effect, large correctness effect.** A document can hold multiple tabs, each with its own content
tree. `documents.get` takes `includeTabsContent`:

- `true` → content arrives under `document.tabs`, and the legacy top-level fields (`document.body`, `headers`,
  `footers`, `footnotes`, `namedRanges`, `inlineObjects`…) are left **empty**.
- omitted or `false` → the legacy fields are populated **from the first tab only**, and `document.tabs` is empty.
  Content in every other tab is silently absent. Nothing errors.

Child tabs nest, so reading a whole document means walking the tab tree, not iterating a flat list. On
`batchUpdate`, a request that does not name a tab is applied, in most cases, to the first tab. A field mask that
references `tabs` implicitly turns `includeTabsContent` on.

**Headers, footers and footnotes — no scope effect.** They are separate segments with their own index origins, hanging
off the document (or off each tab when `includeTabsContent` is `true`). No extra scope; just do not assume body
indexes address them.

**Suggestions — no scope effect, but the *access level* changes the answer.** `documents.get` takes
`suggestionsViewMode`, and when you omit it the API picks a default appropriate to the current user's privileges. The
documented consequence matters: **indexes differ between suggestion view modes**, and only `SUGGESTIONS_INLINE`
returns indexes valid for a subsequent `batchUpdate`. A read-only integration that later gains write capability, or a
connection authorized by a viewer rather than an editor, can compute offsets from a different view of the document
than the one it writes into. Set the mode explicitly rather than inheriting a default that varies per user.

**Comments — this one does change the scope set.** Docs comment and suggestion *threads* are not a Docs API resource
in general availability; reading them is the **Drive** comments API, which accepts only `drive`, `drive.file`,
`drive.readonly` or `drive.meet.readonly`. `documents.get` has a `commentsViewMode` parameter, but it is **Developer
Preview**, and it requires `includeTabsContent: true` (or a field mask referencing `tabs`). So "show comments" is a
Drive scope requirement today, and a preview-program dependency if you want it inline. Do not plan a launch on it.

## 6. Quotas

| | Per minute per project | Per minute per user per project |
| --- | --- | --- |
| **Read requests** | 3,000 | 300 |
| **Write requests** | 600 | 60 |

Points that matter when sizing an integration:

- **The per-user-per-project limits are the binding ones** for a multi-tenant connector doing an initial sync: 300
  reads/minute for one customer's user, regardless of how much project headroom is free.
- **A whole `batchUpdate` counts as one request**, however many subrequests it carries. Batching is a quota strategy,
  not only a latency one — and it is also how you keep an edit atomic.
- Exceeding a quota generally returns **429**, and the documented remedy is truncated exponential backoff. Google
  sometimes surfaces quota exhaustion as a **403** with a `rateLimitExceeded` / `userRateLimitExceeded` reason
  instead; treating that as an auth failure and re-authorizing is the classic wrong response. It is a backoff, not a
  token problem.
- These are Docs quotas only. Discovery, export and comments consume **Drive** quota, which is counted separately.

## Product fact — what the Docs connector asks for

> **As of 2026-09-20, Unified.to's Google Docs connector requests:**
>
> - **File read:** `openid`, `profile`, `email`, `https://www.googleapis.com/auth/drive.readonly`,
>   `https://www.googleapis.com/auth/drive.labels.readonly`, `https://www.googleapis.com/auth/documents.readonly`
> - **File write:** `openid`, `profile`, `email`, `https://www.googleapis.com/auth/drive`,
>   `https://www.googleapis.com/auth/drive.labels.readonly`, `https://www.googleapis.com/auth/documents`
> - **Employee-directory read:** `openid`, `profile`, `email`, `https://www.googleapis.com/auth/drive.readonly`
> - **Login/identity only:** `openid`, `profile`, `email`
>
> **Resulting tier: restricted.** `drive.readonly` and `drive` are restricted scopes, so every configuration except
> login-only puts the registering app in the restricted tier — verification **plus** an annual CASA assessment, plus
> the permitted-application-type gate in `google-drive-oauth-app` §2. The Docs scopes alongside them are sensitive and
> do not raise the tier; `drive.labels.readonly` is non-sensitive. Note that this is a **path A** connector (§2): it
> reaches documents through the Drive file surface, which is why Drive scopes are load-bearing rather than optional —
> and, per the redundancy trap in §2, the Docs scopes it also requests are already implied by the Drive ones it holds.
>
> There is **no `drive.file` variant**, so the non-sensitive path B is not available without a change on the connector
> side. The connector does support a **Google service-account credential** path as an alternative to the user OAuth
> flow, minting tokens for the documents, drive and drive-labels-read scopes — the domain-wide route discussed in
> `google-drive-oauth-app` §5.
>
> Two auth quirks worth carrying into the handoff: the authorize request is built with `access_type` and `prompt`
> parameters, so the refresh-token rules in the base skill's §5 are already wired in and should be verified rather
> than assumed; and the connector remaps Google's 403-with-quota-reason responses to 429 so reads back off instead of
> being mistaken for an authorization failure (§6).
>
> Scope sets change. This note is dated, not live — **confirm the current set with the connector's owner** before
> declaring scopes or submitting anything for review.

## 7. Docs-specific end-to-end checks

On top of the base skill's authorize → refresh round trip, and the Drive checks in `google-drive-oauth-app` §7 if a
Drive scope is in play:

1. **Enable both APIs you actually call.** Docs API always; Drive API if discovery, export or comments are in scope.
   A missing enablement fails at the first call with `403 accessNotConfigured`, long after consent looked fine.
2. `documents.get` one real document and confirm the JSON tree comes back — not just a 200 on the token exchange.
3. **Test discovery the way the product will do it.** If the app is on path B, confirm a Picker-selected document is
   readable **and** that a never-picked one is not. Testing only against a document the app itself created will pass
   on `drive.file` and prove nothing.
4. **Read a multi-tab document with `includeTabsContent` both ways** and confirm the code reads all tabs. A
   single-tab test document hides this defect completely.
5. If export is a feature, run a real `files.export` and confirm it is authorized by the Drive scope you declared —
   and test a document large enough to approach the 10 MB cap.
6. If the app writes, do a `get` → `batchUpdate` round trip **with `WriteControl` set**, and prove the failure path by
   editing the document between the two calls.
7. Inspect the granted `scope` in the token response against what was requested; a granular-consent screen lets the
   user drop scopes, and a partial grant fails later on a call, not at consent.

| Symptom | Cause |
| --- | --- |
| Auth succeeds, every Docs call fails | Google Docs API not enabled on the project |
| "We can't find any of the customer's documents" | The Docs API has no list method — discovery needs a Drive scope (§2) |
| `documents.get` works, PDF export 403s | `files.export` does not accept the Docs scopes; it needs a Drive scope (§4) |
| Only the first tab's content ever appears | `includeTabsContent` omitted — the legacy fields carry the first tab only (§5) |
| `404` on a document the user can clearly open | A published-to-the-web document ID; only the original `documentId` works (§3) |
| Edits land in the wrong place under collaboration | Stale indexes — no `WriteControl`, or offsets computed in a different suggestions view mode (§3, §5) |
| Offsets drift on documents containing emoji | Indexes are UTF-16 code units; astral characters consume two (§3) |
| Created document is empty despite content in the request | `documents.create` ignores supplied content — populate with `batchUpdate` (§3) |
| `403` with `rateLimitExceeded` on a working connection | Docs quota, not authorization — back off, do not re-authorize (§6) |
| Export fails only on long documents | 10 MB export cap (§4) |
| Comments never appear | Drive comments API and its Drive scopes; inline `commentsViewMode` is Developer Preview (§5) |

## Stop and ask

Beyond the base skill's list and the Drive skill's list:

- **Nobody has answered where document IDs come from.** Register nothing until §2 is settled — it decides sensitive
  versus restricted for the entire project, and it is close to irreversible once customers are connected.
- The product needs Drive-wide discovery and there is **no confirmed budget and named owner for the CASA assessment**,
  or the app may not qualify as a **permitted application type** for restricted Drive scopes.
- Someone proposes adding `documents` / `documents.readonly` to an app that already holds `drive` / `drive.readonly` —
  confirm against the real call list first; it is usually pure review surface for no capability (§2).
- A launch plan depends on **Developer Preview** comment/suggestion-thread functionality (§5).
- Someone wants to move live connections between §2's paths — that re-consents every customer and, for path B, changes
  the product's UI.
- The Docs scope tiers or the method-to-scope mapping do not match the **Docs platform state** section above.

## References

Docs-specific only; the base skill carries the generic Google OAuth references and `google-drive-oauth-app` carries
the Drive scope, CASA and shared-drive references. Verified to resolve on 2026-09-20.

- Choose Google Docs API scopes (the `documents` / `documents.readonly` tiers) — https://developers.google.com/workspace/docs/api/auth
- Google Docs API concepts: document, document ID, and managing Docs files through Drive — https://developers.google.com/workspace/docs/api/concepts/document
- Structure of a Google Docs document (structural elements, segments, UTF-16 indexes) — https://developers.google.com/workspace/docs/api/concepts/structure
- Requests and responses (the three methods, atomic batches, published-document 404) — https://developers.google.com/workspace/docs/api/concepts/request-response
- Best practices (edit backwards, plan for collaboration, `WriteControl`, take tabs into account) — https://developers.google.com/workspace/docs/api/how-tos/best-practices
- Batch requests (one batch = one request against quota) — https://developers.google.com/workspace/docs/api/how-tos/batch
- Work with tabs (`includeTabsContent` and the legacy first-tab representation) — https://developers.google.com/workspace/docs/api/how-tos/tabs
- Work with comments and suggestions (`SuggestionsViewMode`, `commentsViewMode`, index differences) — https://developers.google.com/workspace/docs/api/how-tos/suggestions
- `documents.get` reference (authorization scopes, `includeTabsContent`, `suggestionsViewMode`) — https://developers.google.com/workspace/docs/api/reference/rest/v1/documents/get
- `documents.create` reference (scopes; supplied content is ignored) — https://developers.google.com/workspace/docs/api/reference/rest/v1/documents/create
- `documents.batchUpdate` reference (scopes, `WriteControl`) — https://developers.google.com/workspace/docs/api/reference/rest/v1/documents/batchUpdate
- Docs API usage limits (per-minute read/write quotas, backoff) — https://developers.google.com/workspace/docs/api/limits
- Troubleshoot Docs authentication and authorization — https://developers.google.com/workspace/docs/api/troubleshoot-authentication-authorization
- Drive `files.export` reference (Drive-only scopes) — https://developers.google.com/workspace/drive/api/reference/rest/v3/files/export
- Download and export files (the 10 MB export cap) — https://developers.google.com/workspace/drive/api/guides/manage-downloads
- Drive export MIME types for Docs files — https://developers.google.com/workspace/drive/api/guides/ref-export-formats
- Search for files and folders (the `mimeType` query used for document discovery) — https://developers.google.com/workspace/drive/api/guides/search-files
- Drive `comments.list` reference (comments are a Drive scope) — https://developers.google.com/workspace/drive/api/reference/rest/v3/comments/list
- Google Workspace Developer Preview Program — https://developers.google.com/workspace/preview
