Google Workspace APIs from Python
The Google counterpart to ms-office-python. Written for the case that actually recurs: pulling
invoices out of a mailbox without anyone re-keying them.
1. Setup
- Google Cloud project → enable the specific APIs you need (Gmail, Drive, Sheets, Calendar). Enabling is per-API; a project with Gmail on does not have Drive on.
- Choose the auth model — §2. This is the decision that is expensive to change later.
- Configure the OAuth consent screen — internal or external, and the scopes.
- Create credentials — OAuth client ID (user-delegated) or a service account.
pip install google-api-python-client google-auth google-auth-oauthlib
2. OAuth vs service account — pick deliberately
| OAuth user credentials | Service account | |
|---|---|---|
| Acts as | A specific person, who consents | The application itself |
| Best for | One operator's own mailbox | Server-to-server, automated |
| Gmail access | Direct, after consent | Only via domain-wide delegation (Workspace, admin-granted) |
| Refresh | Refresh token, long-lived | Self-signed JWT, no user step |
| Gotcha | Tokens can be revoked or expire; needs a browser once | A plain service account cannot read a personal Gmail mailbox |
The trap worth stating plainly: people reach for a service account because it sounds like the "proper" automated route, then discover it cannot touch Gmail without Workspace domain-wide delegation. For one operator reading their own mailbox, OAuth user credentials are the correct answer, not a compromise.
Testing/internal consent screens are fine for a single operator. External + sensitive scopes triggers Google's verification review — plan for it or stay internal.
3. Scopes — least privilege, and it is enforced socially
| Scope | Grants |
|---|---|
gmail.readonly |
Read messages and download attachments |
gmail.modify |
Read plus label/mark — no delete |
gmail.send |
Send only |
drive.readonly / drive.file |
Read all / only files the app created |
spreadsheets.readonly |
Read Sheets |
Request the narrowest scope that does the job. For invoice capture that is gmail.readonly and
nothing else. Broader scopes make consent scarier, push you toward verification, and widen the blast
radius if a token leaks.
drive.file over drive wherever possible — it limits the app to files it created.
4. Gmail — the invoice-capture pattern
The reliable shape, and it is not "search the whole mailbox with clever heuristics":
- Filter at source. A Gmail filter applies a label (e.g.
invoices/) on arrival. - Query the label, not the world:
q="label:invoices has:attachment". - Paginate —
nextPageToken; never assume one page. - Fetch attachments by
attachmentId, decode base64url. - Record the message id so a re-run is idempotent.
Why the label beats a smart search: a heuristic search silently changes its result set as mail volume grows, so coverage drifts without anyone noticing. A label is deterministic and a human maintains it.
Store the original attachment and hash it — the PDF is the evidence, the email body is not
(financial-document-ingestion §6). Record the message id, received date and sender alongside it.
internalDate is the reliable timestamp; header dates are sender-supplied and can be wrong.
5. Tokens
- Store the refresh token in
~/.secrets/<project>.env(0600), never in the repo —secret-scanning/references/storage-standard.md. - The client library will refresh access tokens automatically; persist the refreshed credential or you will re-consent every run.
- A refresh token can be revoked by the user, by a password change, or by long disuse. Handle re-auth as a normal path, not an exception — for an unattended job, that means alerting rather than silently stopping.
- Put the re-auth risk in the tracker. An integration that quietly stops is discovered at a
deadline (
accounting-uk-ltd§2).
6. Quotas and batching
Google quotas are per-project and per-user, measured in units — different calls cost different amounts, so "requests per second" is the wrong mental model.
- Batch related requests rather than looping single calls.
- Exponential backoff with jitter on 429 and 5xx. The client libraries support it; use it.
- Fetch metadata first, full bodies only for what you actually need —
format=metadatais far cheaper thanformat=fullacross a large mailbox.
7. Anti-patterns
- Reaching for a service account for personal Gmail. It cannot, without Workspace delegation.
- Requesting
gmail.modifywhengmail.readonlywould do. - Heuristic mailbox search instead of a label maintained by a human.
- Not paginating and quietly processing only the first page.
- Trusting the header date over
internalDate. - Committing
credentials.jsonortoken.json. Both are secrets. - Treating re-auth as an error path for an unattended job — it is expected, and it needs an alert.
- Scraping the mailbox through IMAP hacks when the API exists.