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

At a glance

Start

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 ([email protected], 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:
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:

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.

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:

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):

Seed config

One google: block seeds sign-in and every API. A full example:
  • 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) and signing_key.private_jwk (see 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