Gmail API with a delegated service account
Gmail is the most common reason people set up domain-wide delegation, and the API with the most delegation-specific gotchas. A service account has no mailbox of its own — every Gmail call must be made as somebody.
The minimum working example
from google.oauth2 import service_account
from googleapiclient.discovery import build
creds = service_account.Credentials.from_service_account_file(
"/secure/path/key.json",
scopes=["https://www.googleapis.com/auth/gmail.readonly"],
).with_subject("person@yourdomain.com")
gmail = build("gmail", "v1", credentials=creds)
print(gmail.users().getProfile(userId="me").execute())userId="me" means the impersonated subject, not the service account. This is the single most useful thing to internalise about the Gmail API under delegation.
Sending as an alias
A send-as alias belongs to a mailbox. To send from an alias, impersonate the mailbox that owns it and set the From header to the alias — impersonating the alias address directly fails, because it is not a user.
import base64
from email.message import EmailMessage
msg = EmailMessage()
msg["To"] = "someone@example.com"
msg["From"] = "alias@yourdomain.com" # alias on the impersonated mailbox
msg["Subject"] = "Hello"
msg.set_content("Body text")
gmail.users().messages().send(
userId="me",
body={"raw": base64.urlsafe_b64encode(msg.as_bytes()).decode()},
).execute()Three Gmail-specific traps
Drafts do not delete the way you expect
Deleting a draft through the drafts endpoint can return success while leaving the draft in place. Deleting the underlying message is what actually removes it. Whatever you call, verify with a follow-up fetch that expects a 404 — never trust the 200 alone.
Threading requires headers, not just a thread ID
Supplying threadId is not enough for a reply to nest properly in the recipient's client. Set In-Reply-To and References to the Message-ID of the message you are answering.
Search results lag reality
Gmail search is eventually consistent. Immediately after deleting or labelling, a query can still return the old state. When you need certainty, fetch by ID rather than trusting a search count.
Scopes for common Gmail jobs
| Job | Scope |
|---|---|
| Read and report on mail | gmail.readonly |
| Send notifications | gmail.send |
| Prepare drafts for a human to approve | gmail.compose |
| Label, archive, organise | gmail.modify |
| Manage signatures, filters, send-as | gmail.settings.basic |
Building something that touches every mailbox?
Automated triage, drafting, archiving and reporting across a domain is exactly what we build. We will set up the delegation and the Gmail pipeline, and verify it against real mailboxes before handover. $500 per hour.
Talk through your build