Gmail
Messages, threads, drafts, labels, history, filters, forwarding, send-as, and settings.
79 methods · v1 · revision 20260921
grr gmail
Every service command grr runs is generated from a machine-readable description of Google’s APIs, distilled into an index and compiled into the binary. grr api is the same index read flat, by method id.
A hand-written command tree can never keep up with a Google Workspace API. There are 308 methods across ten services, and Google adds more without asking.
So grr does not hand-write one. The committed Discovery index is the single source of truth for the entire surface: a build-time generator compiles it into the namespaced command tree (grr gmail users messages list — see Commands), and grr api reads the same index flat by method id. A method released after this build of grr existed reaches the tree through the daily refresh PR — no code change, no new release, no upgrade step.
Two properties make this more than a thin passthrough:
grr api list and grr api describe need no network, no cache directory, and no API key, so an agent can discover what is available before it has a credential.The distilled index is about 360 KiB across eleven files, against several megabytes for the raw documents — a meaningful difference in a binary whose size is a selling point.
Two read-only commands that need neither a network connection nor a login — and one that pulls the index forward.
grr api list prints an array of { id, http, summary }. The count goes to stderr and the array to stdout, so grr api list | jq stays valid JSON. --service narrows to one service, --filter is a case-insensitive substring match over both method ids and descriptions, and --grouped nests the methods under their service with a per-service count.
$ grr api list
308 methods # to stderr
[{"id":"calendar.acl.delete","http":"DELETE","summary":"Deletes an access control rule."}, ...]
$ grr api list --service sheets
$ grr api list --filter list --grouped
$ grr api list --service gmail --filter "batch" grr api describe takes one method id and returns everything needed to call it: the HTTP verb, the raw path template, the full scope list, the narrowest scope, and every parameter with its type, required flag, repetition, and enum values. The hint field is a ready-made invocation using the method’s first parameter.
$ grr api describe gmail.users.messages.list
{
"id": "gmail.users.messages.list",
"service": "gmail",
"apiVersion": "v1",
"discoveryRevision": "20260921",
"httpMethod": "GET",
"path": "gmail/v1/users/{userId}/messages",
"urlTemplate": "https://gmail.googleapis.com/gmail/v1/users/{userId}/messages",
"description": "Lists the messages in the user's mailbox.",
"scopes": ["https://mail.google.com/", "..."],
"leastPrivilegeScope": "https://www.googleapis.com/auth/gmail.readonly",
"parameters": [
{"name":"userId","type":"string","required":true,"repeated":false,...},
{"name":"q","type":"string","required":false,"repeated":false,...},
{"name":"maxResults","type":"integer","required":false,...}
],
"hint": "grr api call gmail.users.messages.list --params '{\"userId\": "..."}'"
} grr api refresh [--service X] fetches fresh Discovery documents into the local cache between releases, for the rare method you need before the next daily PR lands. The embedded index keeps working offline regardless.
Counts are what grr api list reports at runtime. Revisions come from the committed index at src/discovery/manifest.json.
Messages, threads, drafts, labels, history, filters, forwarding, send-as, and settings.
79 methods · v1 · revision 20260921
grr gmail
Calendars, events, instances, ACL, free/busy, colors, and settings.
38 methods · v3 · revision 20260826
grr calendar
Files, permissions, comments, revisions, changes, drives, and team drives.
64 methods · v3 · revision 20260923
grr drive
Contacts, connections, other contacts, contact groups, and directory people.
24 methods · v1 · revision 20260923
grr people
Spaces, messages, memberships, reactions, attachments, sections, and availability.
54 methods · v1 · revision 20260922
grr chat
Form bodies, responses, publish settings, and push-notification watches.
10 methods · v1 · revision 20260922
grr forms
Spreadsheet values, batch updates, data filters, and developer metadata.
17 methods · v4 · revision 20260921
grr sheets
Task lists and the tasks inside them. See the scope caveat below.
14 methods · v1 · revision 20260922
grr tasks
Presentation batch updates, page reads, and thumbnails.
5 methods · v1 · revision 20260921
grr slides
Document create, read, and batch update.
3 methods · v1 · revision 20260921
grr docs
Every service in the index has a full generated command namespace now — the cards above are the namespaces. grr api is not the only way into any of them; it is the flat alternative when you would rather compose an id than walk a path.
One rule covers every method: path placeholders are substituted into the URL, and everything else becomes a query parameter or the request body.
A missing required parameter is caught before anything is sent, and the error names the parameter and lists the rest, rather than letting Google answer with a 404 or a confusing 400.
--params JSON takes a whole object. Must be a JSON object; an array or a bare value is rejected with a message saying so.--param KEY=VALUE is repeatable and wins over --params, so you can override one field of a large blob. Values are coerced to real JSON types: --param pageSize=10 is a number, not the string "10".= splits, so --param 'q=name=a.pdf' and Gmail search strings survive intact.--body-file PATH reads the body verbatim from a file, or from stdin with -, and wins over anything --params would have synthesised. This is the path for nested bodies.--query KEY=VALUE appends raw query pairs for parameters Discovery does not describe, such as alt=json or fields.--method-override VERB replaces the verb from the Discovery document, for the rare endpoint where the published method is behind the live API.For POST, PATCH, and PUT without --body-file, the non-path parameters are collected into a JSON body automatically — and they are also appended as query pairs, which the dry-run output above shows honestly. For read verbs no body is sent at all. Path values are percent-encoded, so Drive ids and Gmail labels containing spaces or slashes are safe.
# Path parameters are substituted into the URL; the rest become
# query parameters (GET) or the request body (POST/PATCH/PUT).
$ grr api call gmail.users.messages.list --param userId=me --param 'q=is:unread' --param maxResults=5
$ grr api call drive.files.list --params '{"pageSize": 10, "q": "name contains \'report\'"}'
# Values are coerced to real JSON types, so numbers and booleans need no quoting.
$ grr api call sheets.spreadsheets.values.get \
--param spreadsheetId=1a2b3c --param range=A1:D10
# A body from a file ("-" reads stdin), sent verbatim.
$ grr api call chat.spaces.messages.create \
--param parent=spaces/AAAA --body-file ./message.json
# Endpoints whose parameters Discovery does not describe take raw query pairs.
$ grr api call drive.files.list --query alt=json --query fields=files(id,name)
# Override the verb when the Discovery document is behind the live API.
$ grr api call tasks.tasks.list --method-override GET --dry-run prints the request that would be sent and sends nothing. It is the cheapest way to find out whether a call is shaped correctly.
The output includes the resolved method, the fully built url, the body that would be serialised, the scopes the method declares, the narrowest of them, and dryRun: true. Because URL construction and parameter validation both happen before the dry-run branch, a dry run surfaces exactly the errors a real call would.
$ grr api call calendar.events.insert \
--param calendarId=primary \
--params '{"summary":"Standup","start":{"dateTime":"2026-10-05T09:30:00Z"},"end":{"dateTime":"2026-10-05T09:45:00Z"}}' \
--dry-run
{
"method": "POST",
"url": "https://www.googleapis.com/calendar/v3/calendars/primary/events?summary=Standup",
"body": {"summary":"Standup","start":{...},"end":{...}},
"scopesRequested": ["https://www.googleapis.com/auth/calendar", "..."],
"leastPrivilegeScope": "https://www.googleapis.com/auth/calendar.events.owned",
"dryRun": true
}
# With --body-file the body is sent verbatim and the URL stays clean:
$ grr api call calendar.events.insert --param calendarId=primary \
--body-file ./event.json --dry-run
# Every generated leaf has the same --dry-run, so the same inspection works
# without composing the id by hand:
$ grr calendar events insert --calendar-id primary --dry-run The flag exists on every generated leaf as well as on grr api call, and both print JSON — so a jq -e '.url' assertion works on either.
A method id is the dotted service.resource.method path, taken verbatim from the Discovery document — resource segments included, so gmail.users.messages.list is the id, not gmail.messages.list. The service prefix is the same key --service takes, and slashes are accepted in place of dots. The same id, with dots as spaces, is the generated command; the tree and grr api are two views of one index.
# Canonical: <service>.<resource>.<method>
$ grr api call drive.files.list
# Slashes work too, and a leading dot is tolerated.
$ grr api call gmail/users/messages/list
$ grr api call .gmail.users.messages.list
# A typo gets ranked suggestions rather than a bare "not found".
$ grr api call gmail.users.messages.lst
unknown method 'users.messages.lst'; did you mean: users.messages.list, ... An unknown service lists the ten known keys. An unknown method is matched against the real ids and returns ranked suggestions, preferring candidates that share the most of the resource path — which is what tells users.messages.list apart from users.drafts.list when both are one typo away.
Three APIs that no CLI used to wrap. Generated from the same index, they now have full command paths.
# Sheets: read a range, write a range, append a row.
$ grr sheets spreadsheets values get --spreadsheet-id 1a2b3c --range Sheet1!A1:D10
$ grr sheets spreadsheets values update --spreadsheet-id 1a2b3c --range Sheet1!A1 \
--body-file ./values.json
$ grr sheets spreadsheets values append --spreadsheet-id 1a2b3c --range Sheet1!A:A \
--value-input-option USER_ENTERED
# Docs: create, read, and batch-update.
$ grr docs documents get --document-id 1a2b3c
$ grr docs documents create --body-file ./new-doc.json
$ grr docs documents batchUpdate --document-id 1a2b3c --body-file ./requests.json
# Slides: presentations, pages, and thumbnails.
$ grr slides presentations get --presentation-id 1a2b3c
$ grr slides presentations pages getThumbnail \
--presentation-id 1a2b3c --page-object-id g1 Note the colon-verb path templates — values/{range}:append, documents/{documentId}:batchUpdate. The braces are path placeholders, so --range Sheet1!A1:D10 substitutes into the URL and is percent-encoded for you, colons and all.
Sheets and Slides writes need the request body as a nested structure, which is what --body-file is for. Generating that JSON in a heredoc and piping it through stdin (--body-file -) keeps it out of shell-quoting hell. And camelCase methods keep their casing as leaf aliases: getThumbnail, not get-thumbnail.
grr consents to sixteen scopes. Most of the index falls inside that set; a small tail does not.
Discovery lists each method’s accepted scopes from broadest to narrowest, usually with https://mail.google.com/ or .../auth/drive first. grr api describe reports the narrowest entry as leastPrivilegeScope, which is the right thing to show a human deciding what a call touches.
Before sending, grr compares that narrowest scope against the consented set. When it is not there, it prints a note to stderr and attempts the call anyway, because the narrow scope is often a sibling of a broader one the build does hold — drive.readonly and drive.file are both satisfied by the consented auth/drive. A note on stderr is a heads-up, not a failure. Treat the exit status as the answer.
Where a method genuinely needs a scope nobody consented to, grr names it. A 403 comes back with the specific scope and the file to edit, rather than a bare permission error:
$ grr api list --service tasks
14 methods
$ grr api describe tasks.tasks.list
# leastPrivilegeScope: https://www.googleapis.com/auth/tasks.readonly
# -> outside the scopes this build consented to
$ grr tasks tasks list --tasklist @default
note: tasks.tasks.list wants 'https://www.googleapis.com/auth/tasks.readonly',
which is outside the scopes this build consented to. Attempting anyway;
if Google answers 403, re-run 'grr auth login' after extending the scope
set.
... HTTP 403 ... Tasks is the one service that does not work out of the box. Its fourteen methods need .../auth/tasks or .../auth/tasks.readonly, and neither is in the sixteen consented scopes, nor is any broader scope that would cover them. They list and describe correctly, and --dry-run is accurate, but a live call returns 403 until the scope set in src/core/auth/mod.rs is extended and grr auth login is re-run.
A smaller tail behaves the same way for narrower reasons: Gmail CSE keypairs and S/MIME settings need gmail.settings.* scopes, directory people need directory.readonly, and most of the Chat admin surface (custom emoji, space import) needs scopes outside the consented set. Docs, Sheets, and Slides all work, because their methods accept the Drive scope the build already holds.
The index is committed at src/discovery/*.json so builds stay hermetic and grr api list works offline. Something has to notice upstream drift, and that is a scheduled workflow: a daily run refetches the ten Discovery documents, distils them with scripts/fetch-discovery.ts, regenerates the command tree with scripts/generate-commands.ts, and opens a pull request when either differs. An unchanged upstream produces an empty diff and no PR.
Both outputs are deterministic (sorted keys) precisely so that comparison is meaningful. When the index does change, the workflow runs the Rust test suite against the new data before proposing the change, so a method whose shape broke incompatibly fails the build instead of shipping. The tree and the index move in the same PR, so the commands and the index can never drift apart.
Treat the counts on this page as a snapshot of the embedded index, not a promise about what Google publishes tomorrow.
grr api list is always the live answer.