At a glance
Start
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:
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. Setgoogle.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— AuthorizePOST /oauth2/token— Exchange tokenGET /oauth2/v2/userinfo— Get user infoGET /.well-known/openid-configuration— Get OIDC discovery documentGET /oauth2/v3/certs— Get JSON Web Key Set (JWKS): with the RSA public key for ID token verification
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,rawformats)GET /gmail/v1/users/:userId/messages/:messageId/attachments/:id— Get attachmentPOST /gmail/v1/users/:userId/messages/send— Send message (raw MIME or structured fields)POST /gmail/v1/users/:userId/messages/import— Import inbox mailPOST /gmail/v1/users/:userId/messages— Insert message: directlyPOST /gmail/v1/users/:userId/messages/:id/modify— Modify message labels: add or removePOST /gmail/v1/users/:userId/messages/batchModify— Batch modify messages: label changesPOST /gmail/v1/users/:userId/messages/:id/trash— Trash messagePOST /gmail/v1/users/:userId/messages/:id/untrash— Untrash message
Gmail drafts
GET /gmail/v1/users/:userId/drafts— List draftsPOST /gmail/v1/users/:userId/drafts— Create draftGET /gmail/v1/users/:userId/drafts/:id— Get draftPUT /gmail/v1/users/:userId/drafts/:id— Update draftPOST /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 threadsGET /gmail/v1/users/:userId/threads/:id— Get threadPOST /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 labelsPOST /gmail/v1/users/:userId/labels— Create labelPATCH /gmail/v1/users/:userId/labels/:id— Update labelDELETE /gmail/v1/users/:userId/labels/:id— Delete label
Gmail history and push
GET /gmail/v1/users/:userId/history— List history changesPOST /gmail/v1/users/:userId/watch— Set up push notificationsPOST /gmail/v1/users/:userId/stop— Stop push notifications
Gmail settings
GET /gmail/v1/users/:userId/settings/filters— List filtersPOST /gmail/v1/users/:userId/settings/filters— Create filterDELETE /gmail/v1/users/:userId/settings/filters/:id— Delete filterGET /gmail/v1/users/:userId/settings/forwardingAddresses— List forwarding addressesGET /gmail/v1/users/:userId/settings/sendAs— List send-as aliases
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 RESTGET /calendar/v3/users/:userId/calendarList— List calendarsGET /calendar/v3/calendars/:calendarId/events— List eventsPOST /calendar/v3/calendars/:calendarId/events— Create eventGET /calendar/v3/calendars/:calendarId/events/:eventId— Get eventPATCH /calendar/v3/calendars/:calendarId/events/:eventId— Patch eventPUT /calendar/v3/calendars/:calendarId/events/:eventId— Replace eventDELETE /calendar/v3/calendars/:calendarId/events/:eventId— Delete eventPOST /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 filesGET /drive/v3/files/:fileId— Get file metadataPOST /drive/v3/files— Create file metadataPATCH /drive/v3/files/:fileId— Update file metadataPUT /drive/v3/files/:fileId— Update file contentPOST /upload/drive/v3/files— Upload fileGET /drive/v3/files/:fileId/export— Export file: a spreadsheet astext/csv(first sheet) ortext/tab-separated-values, or a document astext/plain
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 dataGET /v4/spreadsheets/:spreadsheetId— Get a spreadsheet (ranges,includeGridData,fields)POST /v4/spreadsheets/:spreadsheetId:batchUpdate— Batch update spreadsheet: withrepliesandincludeSpreadsheetInResponseGET /v4/spreadsheets/:spreadsheetId/values/:range— Read valuesPUT /v4/spreadsheets/:spreadsheetId/values/:range— Write valuesPOST /v4/spreadsheets/:spreadsheetId/values/:range:append— Append values: after the table in a range (insertDataOptionOVERWRITEorINSERT_ROWS)POST /v4/spreadsheets/:spreadsheetId/values/:range:clear— Clear values, keeping formatsGET /v4/spreadsheets/:spreadsheetId/values:batchGet— Read several rangesPOST /v4/spreadsheets/:spreadsheetId/values:batchUpdate— Write several ranges, all or nothingPOST /v4/spreadsheets/:spreadsheetId/values:batchClear— Clear several ranges
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 (onlytitleis read, as on Docs)GET /v1/documents/:documentId— Get a document (fields,includeTabsContent,suggestionsViewMode)POST /v1/documents/:documentId:batchUpdate— Batch update document: withrepliesandwriteControl
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:
insertTextatlocation.indexorendOfSegmentLocation; a newline splits the paragraph, inserted text takes the style of the text before it, and control characters are droppeddeleteContentRange; deleting a newline joins two paragraphsreplaceAllTextwithcontainsText.matchCase; the reply holdsoccurrencesChangedupdateTextStyleandupdateParagraphStylewith a field mask infields(bold,italic,namedStyleType,*); a heading gets aheadingId, and a link turns text blue and underlinedcreateParagraphBulletswith anybulletPreset; leading tabs set the nesting level and are removed.deleteParagraphBulletsremoves theminsertTablewithrowsandcolumns, andinsertPageBreak
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
Onegoogle: block seeds sign-in and every API. A full example:
rows: read as USER_ENTERED inputparagraphs: a string, or{ "text": ..., "style": ... }with a named style; ortextwith one line per paragraph
strict_scopes (see Auth) and signing_key.private_jwk (see Signing key).
Hosted domain (hd) claim
Google Workspace accounts include anhd 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.