> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deploybaseline.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Google

> OAuth 2.0 and OIDC, Gmail, Calendar, Drive, Sheets, Docs.

OAuth 2.0, OpenID Connect, and mutable Google Workspace-style surfaces for local inbox, calendar, drive, spreadsheet, and document flows.

## At a glance

| | |
| - | - |
| Package | `@deploybaseline/google` |
| Port | `4002` in the default set, `4000` when started alone |
| Config key | `google` |
| HTTP operations | 79, browsable at `http://localhost:3999/docs/google` while it runs ([API reference](/api-reference.md)) |

```bash CLI theme={null}
npx @deploybaseline/cli start --service google
```

```ts Programmatic theme={null}
import { createEmulator } from "@deploybaseline/cli";

const google = await createEmulator({ service: "google" });
// google.url is http://localhost:4000
```

```json baseline.config.json theme={null}
{
  "google": {
    "users": [
      {
        "email": "testuser@example.com",
        "name": "Test User",
        "picture": "https://lh3.googleusercontent.com/a/default-user",
        "email_verified": true
      }
    ],
    "oauth_clients": [
      {
        "client_id": "example-client-id.apps.googleusercontent.com",
        "client_secret": "GOCSPX-example_secret",
        "name": "Code App (Google)",
        "redirect_uris": ["http://localhost:3000/api/auth/callback/google"]
      }
    ],
    "labels": [
      {
        "id": "Label_ops",
        "user_email": "testuser@example.com",
        "name": "Ops/Review",
        "color_background": "#DDEEFF",
        "color_text": "#111111"
      }
    ],
    "messages": [
      {
        "id": "msg_welcome",
        "user_email": "testuser@example.com",
        "from": "welcome@example.com",
        "to": "testuser@example.com",
        "subject": "Welcome to the Gmail emulator",
        "body_text": "You can now test Gmail, Calendar, and Drive flows locally.",
        "label_ids": ["INBOX", "UNREAD", "CATEGORY_UPDATES"],
        "date": "2025-01-04T10:00:00.000Z"
      }
    ],
    "calendars": [
      {
        "id": "primary",
        "user_email": "testuser@example.com",
        "summary": "testuser@example.com",
        "primary": true,
        "selected": true,
        "time_zone": "UTC"
      }
    ],
    "calendar_events": [
      {
        "id": "evt_kickoff",
        "user_email": "testuser@example.com",
        "calendar_id": "primary",
        "summary": "Project Kickoff",
        "start_date_time": "2025-01-10T09:00:00.000Z",
        "end_date_time": "2025-01-10T09:30:00.000Z"
      }
    ],
    "drive_items": [
      {
        "id": "drv_docs",
        "user_email": "testuser@example.com",
        "name": "Docs",
        "mime_type": "application/vnd.google-apps.folder",
        "parent_ids": ["root"]
      }
    ]
  }
}
```

## Start

```bash theme={null}
npx @deploybaseline/cli start --service google
```

Google listens on port `4002` in the default set, or on `http://localhost:4000` when started alone. Without a seed config there is one user (`testuser@gmail.com`, Test User) with sample Gmail messages, a primary and a team calendar, and a Receipts folder in Drive.

Every API lives on the one Google port. With `googleapis`, pass the emulator URL as `rootUrl` to each client:

```ts theme={null}
import { google } from "googleapis";

const rootUrl = "http://localhost:4002/";
const auth = new google.auth.OAuth2({
  clientId,
  clientSecret,
  redirectUri,
  endpoints: {
    oauth2AuthBaseUrl: `${rootUrl}o/oauth2/v2/auth`,
    oauth2TokenUrl: `${rootUrl}oauth2/token`,
    oauth2RevokeUrl: `${rootUrl}oauth2/revoke`,
  },
});
const sheets = google.sheets({ version: "v4", auth, rootUrl });
const docs = google.docs({ version: "v1", auth, rootUrl });
const drive = google.drive({ version: "v3", auth, rootUrl });
```

Media uploads ignore a client-level `rootUrl`: `googleapis` builds the upload URL from the API's own root and sends the request to `googleapis.com`. Pass `rootUrl` again in the call options of every call that sends `media`:

```ts theme={null}
await drive.files.create(
  { requestBody: { name: "notes.txt" }, media: { mimeType: "text/plain", body: "hello" } },
  { rootUrl },
);
```

## Auth

Google authentication is permissive by default for local bearer-token workflows. Set `google.strict_scopes: true` in seed config to require emulator-issued, unexpired access tokens on Gmail, Calendar, Drive, Sheets, Docs, and userinfo routes. Invalid credentials return `401 UNAUTHENTICATED`; missing method scopes return `403` with `insufficientPermissions`. Strict mode distinguishes read, write, send, settings, and metadata scopes, and userinfo includes only granted identity claims. Revoking an access or refresh token invalidates the linked access and refresh credentials. Discovery, signing keys, and the sign-in page remain public. This mode checks method scopes; resource-specific grants such as `drive.file` and Gmail add-on scopes are not modeled.

## OAuth and OpenID Connect

* `GET /o/oauth2/v2/auth` — Authorize
* `POST /oauth2/token` — Exchange token
* `GET /oauth2/v2/userinfo` — Get user info
* `GET /.well-known/openid-configuration` — Get OIDC discovery document
* `GET /oauth2/v3/certs` — Get JSON Web Key Set (JWKS): with the RSA public key for ID token verification

Google ID tokens are RS256-signed JWTs. The discovery document advertises RS256, and the JWKS endpoint returns the matching RSA public key used to verify issued tokens.

### Signing key

ID tokens are signed with a key stored in emulator state (`kid`: `baseline-google-1`), so they stay valid across snapshot restores and persisted restarts. Set `google.signing_key.private_jwk` to an RSA private JWK to pin the key. See [OIDC signing keys](/configuration.md#oidc-signing-keys).

## Gmail

* `GET /gmail/v1/users/:userId/messages` — List messages (`q`, `labelIds`, `maxResults`, `pageToken`)
* `GET /gmail/v1/users/:userId/messages/:id` — Get message (`full`, `metadata`, `minimal`, `raw` formats)
* `GET /gmail/v1/users/:userId/messages/:messageId/attachments/:id` — Get attachment
* `POST /gmail/v1/users/:userId/messages/send` — Send message (raw MIME or structured fields)
* `POST /gmail/v1/users/:userId/messages/import` — Import inbox mail
* `POST /gmail/v1/users/:userId/messages` — Insert message: directly
* `POST /gmail/v1/users/:userId/messages/:id/modify` — Modify message labels: add or remove
* `POST /gmail/v1/users/:userId/messages/batchModify` — Batch modify messages: label changes
* `POST /gmail/v1/users/:userId/messages/:id/trash` — Trash message
* `POST /gmail/v1/users/:userId/messages/:id/untrash` — Untrash message

## Gmail drafts

* `GET /gmail/v1/users/:userId/drafts` — List drafts
* `POST /gmail/v1/users/:userId/drafts` — Create draft
* `GET /gmail/v1/users/:userId/drafts/:id` — Get draft
* `PUT /gmail/v1/users/:userId/drafts/:id` — Update draft
* `POST /gmail/v1/users/:userId/drafts/send` — Send a draft (`{ "id": "<draft id>" }` in the body)
* `DELETE /gmail/v1/users/:userId/drafts/:id` — Delete draft

## Gmail threads

* `GET /gmail/v1/users/:userId/threads` — List threads
* `GET /gmail/v1/users/:userId/threads/:id` — Get thread
* `POST /gmail/v1/users/:userId/threads/:id/modify` — Modify thread labels: add or remove across a thread

## Gmail labels

* `GET /gmail/v1/users/:userId/labels` — List labels
* `POST /gmail/v1/users/:userId/labels` — Create label
* `PATCH /gmail/v1/users/:userId/labels/:id` — Update label
* `DELETE /gmail/v1/users/:userId/labels/:id` — Delete label

## Gmail history and push

* `GET /gmail/v1/users/:userId/history` — List history changes
* `POST /gmail/v1/users/:userId/watch` — Set up push notifications
* `POST /gmail/v1/users/:userId/stop` — Stop push notifications

## Gmail settings

* `GET /gmail/v1/users/:userId/settings/filters` — List filters
* `POST /gmail/v1/users/:userId/settings/filters` — Create filter
* `DELETE /gmail/v1/users/:userId/settings/filters/:id` — Delete filter
* `GET /gmail/v1/users/:userId/settings/forwardingAddresses` — List forwarding addresses
* `GET /gmail/v1/users/:userId/settings/sendAs` — List send-as aliases

On send (and on drafts), a `From` whose address is one of the user's send-as addresses is kept as written, display name included. Any other `From`, or none, becomes the default send-as identity (`Name <address>`), in the stored headers and in the raw MIME, as Gmail does.

## Calendar

* `GET /discovery/v1/apis/calendar/v3/rest` — Get Calendar discovery document: public Calendar v3 REST
* `GET /calendar/v3/users/:userId/calendarList` — List calendars
* `GET /calendar/v3/calendars/:calendarId/events` — List events
* `POST /calendar/v3/calendars/:calendarId/events` — Create event
* `GET /calendar/v3/calendars/:calendarId/events/:eventId` — Get event
* `PATCH /calendar/v3/calendars/:calendarId/events/:eventId` — Patch event
* `PUT /calendar/v3/calendars/:calendarId/events/:eventId` — Replace event
* `DELETE /calendar/v3/calendars/:calendarId/events/:eventId` — Delete event
* `POST /calendar/v3/freeBusy` — Query free/busy

`events.patch` (`PATCH`) changes only the fields sent; a `start` or `end` object replaces that time. `events.update` (`PUT`) replaces the event and needs `start` and `end`. Writes check that the end is not before the start (`400`, `The specified time range is empty.`), and the `etag` changes with every write.

## Drive

* `GET /drive/v3/files` — List files
* `GET /drive/v3/files/:fileId` — Get file metadata
* `POST /drive/v3/files` — Create file metadata
* `PATCH /drive/v3/files/:fileId` — Update file metadata
* `PUT /drive/v3/files/:fileId` — Update file content
* `POST /upload/drive/v3/files` — Upload file
* `GET /drive/v3/files/:fileId/export` — Export file: a spreadsheet as `text/csv` (first sheet) or `text/tab-separated-values`, or a document as `text/plain`

Spreadsheets are Drive files with the `application/vnd.google-apps.spreadsheet` type, so `files.list` finds them, a Drive rename renames them, and their `webViewLink` is the editor URL. Creating a file with that type through Drive creates a spreadsheet with one empty `Sheet1`; uploading `text/csv` (or `text/tab-separated-values`) with that type imports the rows into the first sheet as `USER_ENTERED` input. They have no binary content: `alt=media` answers `403 fileNotDownloadable`, and exporting a file that is not a Docs Editors file answers `403 fileNotExportable`. Any other export format is `400 The requested conversion is not supported.`

Documents work the same way with the `application/vnd.google-apps.document` type: a Drive created document is blank, a `text/plain` upload with that type becomes one paragraph per line, and `text/plain` export gives the text with a byte order mark and CRLF line ends, as Drive does.

`q` follows Drive's search language: `name` (`=`, `!=`, `contains`, which matches the start of the name or of a word in it), `fullText contains` (name and text content), `mimeType` (`=`, `!=`, `contains`), `createdTime` and `modifiedTime` comparisons, `trashed`, `starred`, `sharedWithMe`, `'<id>' in parents`, and `'<email>' in owners` (or `writers`, `readers`), joined with `and`, `or`, `not`, and parentheses. Values are single-quoted with `\'` escapes. A field or operator outside that list is `400 Invalid Value`, as on Drive, instead of being ignored. Trashed files are listed unless the query says `trashed = false`.

## Sheets

Sheets API v4 at the same root (`google.sheets({ version: "v4", rootUrl })`).

* `POST /v4/spreadsheets` — Create a spreadsheet, with sheets and grid data
* `GET /v4/spreadsheets/:spreadsheetId` — Get a spreadsheet (`ranges`, `includeGridData`, `fields`)
* `POST /v4/spreadsheets/:spreadsheetId:batchUpdate` — Batch update spreadsheet: with `replies` and `includeSpreadsheetInResponse`
* `GET /v4/spreadsheets/:spreadsheetId/values/:range` — Read values
* `PUT /v4/spreadsheets/:spreadsheetId/values/:range` — Write values
* `POST /v4/spreadsheets/:spreadsheetId/values/:range:append` — Append values: after the table in a range (`insertDataOption` `OVERWRITE` or `INSERT_ROWS`)
* `POST /v4/spreadsheets/:spreadsheetId/values/:range:clear` — Clear values, keeping formats
* `GET /v4/spreadsheets/:spreadsheetId/values:batchGet` — Read several ranges
* `POST /v4/spreadsheets/:spreadsheetId/values:batchUpdate` — Write several ranges, all or nothing
* `POST /v4/spreadsheets/:spreadsheetId/values:batchClear` — Clear several ranges

**Ranges.** A1 notation as Sheets reads it: `Sheet1!A1:B2`, `'My Sheet'!A1` (quotes doubled inside quoted names), `Sheet1` (the whole sheet), `A1:B2` (the first visible sheet), whole columns `A:B`, whole rows `2:5`, open ended `A2:B`, `$` markers, and lowercase. Sheet names match case-insensitively. Responses name the range clipped to the grid (`A:A` reads as `Sheet1!A1:A1000`) and quote sheet names when Sheets would. R1C1 notation and named ranges are not supported. Anything else is `400 INVALID_ARGUMENT` `Unable to parse range: <range>`, and a range past the grid is `Range ('Sheet1'!A1001) exceeds grid limits. Max rows: 1000, max columns: 26`.

**Writing values.** `valueInputOption` is required (`Invalid valueInputOption: INPUT_VALUE_OPTION_UNSPECIFIED`). `RAW` stores strings, numbers, and booleans as given. `USER_ENTERED` reads numbers (`1,234.5`, `12%`, `$5`), booleans, dates (`2024-01-05`, `1/5/2024`, `2024-01-05 13:30`), and `=` formulas, and a leading `'` keeps text as text; an inferred number format is stored with the cell. `null` leaves a cell alone and `""` clears it. A single cell range is where the write starts; a larger range must hold the values (`Requested writing within range [Sheet1!A1:B2], but tried writing to row [3]`). `includeValuesInResponse` returns `updatedData`.

**Reading values.** `valueRenderOption` is `FORMATTED_VALUE` (the default: strings as displayed), `UNFORMATTED_VALUE` (numbers and booleans typed; dates as serial numbers unless `dateTimeRenderOption=FORMATTED_STRING`), or `FORMULA`; `majorDimension` is `ROWS` or `COLUMNS`. Trailing empty cells and rows are trimmed as Sheets trims them, and `values` is left out of an empty range.

**Formulas** are stored as written and evaluated when read, for simple cases: arithmetic, comparison, `&`, percent, cell and range references (also `'Other sheet'!A1:B2` and whole columns), and `SUM`, `AVERAGE`, `MIN`, `MAX`, `PRODUCT`, `COUNT`, `COUNTA`, `IF`, `AND`, `OR`, `NOT`, `CONCATENATE`, `CONCAT`, `LEN`, `UPPER`, `LOWER`, `TRIM`, `ABS`, and `ROUND`. Errors show as Sheets shows them (`#DIV/0!`, `#NAME?` for an unknown function, `#REF!` for a circular reference or unknown sheet, `#VALUE!`, `#ERROR!` for a parse error), with `errorValue` in grid data. Array results do not spill, and references are not rewritten when rows or columns move.

**batchUpdate requests:** `addSheet`, `deleteSheet`, `duplicateSheet`, `updateSheetProperties`, `updateSpreadsheetProperties`, `updateCells` (with `start` or `range`), `appendCells`, `repeatCell`, `insertDimension`, `deleteDimension`, `appendDimension`, `findReplace` (`matchCase`, `matchEntireCell`, `searchByRegex`, `includeFormulas`), and `sortRange`. Update requests take a field mask in `fields` (`title,gridProperties.rowCount`, `userEnteredFormat(numberFormat,textFormat.bold)`, `*`). Requests run in order and the batch is all or nothing; errors carry Sheets' `Invalid requests[i].<request>: ...` prefix (for example `You can't remove all the sheets in a document.`). Layout only requests (`updateDimensionProperties`, `autoResizeDimensions`, `updateBorders`, `mergeCells`, `unmergeCells`) are accepted without effect; other request kinds answer `400` naming the request as not emulated.

**Scopes** under `strict_scopes` follow the Sheets reference: reads take `spreadsheets`, `spreadsheets.readonly`, `drive`, `drive.readonly`, or `drive.file`; writes take `spreadsheets`, `drive`, or `drive.file`.

Errors use the Sheets body: `{ "error": { "code": 400, "message": "...", "status": "INVALID_ARGUMENT" } }`. A missing spreadsheet is `404 NOT_FOUND`, and a Drive file of another type is `400 This operation is not supported for this document`.

Seed spreadsheets under `google.spreadsheets`; rows are read as `USER_ENTERED` input:

```json theme={null}
{
  "google": {
    "spreadsheets": [
      {
        "id": "budget-2025",
        "title": "Budget",
        "sheets": [
          {
            "title": "Q1",
            "rows": [
              ["Item", "Cost"],
              ["Rent", "1,200"],
              ["Total", "=SUM(B2:B2)"]
            ]
          }
        ]
      }
    ]
  }
}
```

## Docs

Docs API v1 at the same root (`google.docs({ version: "v1", rootUrl })`).

* `POST /v1/documents` — Create a blank document (only `title` is read, as on Docs)
* `GET /v1/documents/:documentId` — Get a document (`fields`, `includeTabsContent`, `suggestionsViewMode`)
* `POST /v1/documents/:documentId:batchUpdate` — Batch update document: with `replies` and `writeControl`

**Structure.** A document is a section break at index 0 and paragraphs after it, each ending in a newline. Indexes count UTF-16 code units, as Docs counts them, so a character outside the Basic Multilingual Plane takes two. A table takes one index for its start, one per row, one per cell, and one for its end, so a 2x2 table inserted at index 1 puts its cell paragraphs at 5, 7, 10, and 12. Responses carry `body.content` with `startIndex` and `endIndex` on every element, text runs split by style, `paragraphStyle`, `bullet`, `lists`, `documentStyle`, `namedStyles`, and `revisionId`. `includeTabsContent=true` moves the content under `tabs[0].documentTab`.

**batchUpdate requests:**

* `insertText` at `location.index` or `endOfSegmentLocation`; a newline splits the paragraph, inserted text takes the style of the text before it, and control characters are dropped
* `deleteContentRange`; deleting a newline joins two paragraphs
* `replaceAllText` with `containsText.matchCase`; the reply holds `occurrencesChanged`
* `updateTextStyle` and `updateParagraphStyle` with a field mask in `fields` (`bold,italic`, `namedStyleType`, `*`); a heading gets a `headingId`, and a link turns text blue and underlined
* `createParagraphBullets` with any `bulletPreset`; leading tabs set the nesting level and are removed. `deleteParagraphBullets` removes them
* `insertTable` with `rows` and `columns`, and `insertPageBreak`

Requests run in order and the batch is all or nothing. Errors carry Docs' `Invalid requests[i].<request>: ...` prefix: an index past the segment (`Index 12 must be less than the end index of the referenced segment, 12.`), an insertion outside a paragraph or inside a surrogate pair, an empty range, a range over index 0 or the last newline, a partial table delete, or a page break inside a table. Other request kinds (named ranges, images, table row and column edits, headers, footers, footnotes, sections, tabs) answer `400` naming the request as not emulated.

**Revisions.** Every successful batch makes a new `revisionId`, returned as `writeControl.requiredRevisionId`. `writeControl.requiredRevisionId` refuses a batch when the document has moved on (`400 FAILED_PRECONDITION`); `targetRevisionId` accepts the current or an earlier revision and applies the batch to the current text without merging.

**Scopes** under `strict_scopes` follow the Docs reference: `documents.get` takes `documents`, `documents.readonly`, `drive`, `drive.readonly`, or `drive.file`; `create` and `batchUpdate` take `documents`, `drive`, or `drive.file`.

Errors use the same body as Sheets. A missing document is `404 NOT_FOUND`, and a Drive file of another type is `400 This operation is not supported for this document`.

Seed documents under `google.documents` with `text` (one paragraph per line) or `paragraphs` (each a string or `{ text, style }` with a named style such as `HEADING_1`):

```json theme={null}
{
  "google": {
    "documents": [
      {
        "id": "launch-plan",
        "title": "Launch plan",
        "paragraphs": [
          {
            "text": "Launch plan",
            "style": "HEADING_1"
          },
          "Ship the beta."
        ]
      }
    ]
  }
}
```

## Seed config

One `google:` block seeds sign-in and every API. A full example:

```json theme={null}
{
  "google": {
    "users": [
      {
        "email": "testuser@example.com",
        "name": "Test User"
      },
      {
        "email": "admin@acme.com",
        "name": "Admin",
        "hd": "acme.com"
      }
    ],
    "oauth_clients": [
      {
        "client_id": "my-client-id.apps.googleusercontent.com",
        "client_secret": "GOCSPX-secret",
        "redirect_uris": ["http://localhost:3000/api/auth/callback/google"]
      }
    ],
    "labels": [
      {
        "id": "Label_ops",
        "user_email": "testuser@example.com",
        "name": "Ops/Review",
        "color_background": "#DDEEFF",
        "color_text": "#111111"
      }
    ],
    "messages": [
      {
        "id": "msg_welcome",
        "user_email": "testuser@example.com",
        "from": "welcome@example.com",
        "to": "testuser@example.com",
        "subject": "Welcome to the Gmail emulator",
        "body_text": "You can now test Gmail, Calendar, and Drive flows locally.",
        "label_ids": ["INBOX", "UNREAD", "CATEGORY_UPDATES"]
      }
    ],
    "calendars": [
      {
        "id": "primary",
        "user_email": "testuser@example.com",
        "summary": "testuser@example.com",
        "primary": true,
        "selected": true,
        "time_zone": "UTC"
      }
    ],
    "calendar_events": [
      {
        "id": "evt_kickoff",
        "user_email": "testuser@example.com",
        "calendar_id": "primary",
        "summary": "Project Kickoff",
        "start_date_time": "2025-01-10T09:00:00.000Z",
        "end_date_time": "2025-01-10T09:30:00.000Z"
      }
    ],
    "drive_items": [
      {
        "id": "drv_docs",
        "user_email": "testuser@example.com",
        "name": "Docs",
        "mime_type": "application/vnd.google-apps.folder",
        "parent_ids": ["root"]
      }
    ],
    "spreadsheets": [
      {
        "id": "budget-2025",
        "user_email": "testuser@example.com",
        "title": "Budget",
        "sheets": [
          {
            "title": "Q1",
            "rows": [
              ["Item", "Cost"],
              ["Rent", "1,200"],
              ["Total", "=SUM(B2:B2)"]
            ]
          }
        ]
      }
    ],
    "documents": [
      {
        "id": "launch-plan",
        "user_email": "testuser@example.com",
        "title": "Launch plan",
        "paragraphs": [
          {
            "text": "Launch plan",
            "style": "HEADING_1"
          },
          "Ship the beta."
        ]
      }
    ]
  }
}
```

* `rows`: read as USER\_ENTERED input
* `paragraphs`: a string, or `{ "text": ..., "style": ... }` with a named style; or `text` with one line per paragraph

Other keys: `strict_scopes` (see [Auth](#auth)) and `signing_key.private_jwk` (see [Signing key](#signing-key)).

### Hosted domain (hd) claim

Google Workspace accounts include an `hd` claim in ID tokens and userinfo responses identifying the user's hosted domain. The emulator derives this automatically from the user's email domain. Consumer domains (`gmail.com`, `googlemail.com`) omit the claim, matching real Google behavior.

To override the derived value, set `hd` on a seeded user. To suppress the claim entirely, set `hd` to an empty string.

## Inspector

The inspector at `/` shows Drive files and each spreadsheet's sheets with their values.

The inspector at `/?tab=docs` shows each document's paragraphs, styles, and revision.

## Conformance


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.