| name | gmail-api |
| description | Integrate with the Gmail API for reading, sending, syncing, and managing email programmatically. Use when building email clients, automating email workflows, syncing mailboxes, processing attachments, managing labels/filters, or setting up push notifications via Pub/Sub. Covers OAuth 2.0 authentication (including localhost/dev and Cloudflare tunnels), full and partial sync, Pub/Sub push, search/query operators, batch requests, drafts, attachments, labels, filters, and performance best practices. Supports Python and Node.js SDKs. |
Gmail API Integration Skill
When to Read What
Critical Rules
-
Always use the narrowest OAuth scope needed. Don't request mail.google.com (full access) when gmail.readonly suffices. Changing scopes later forces users to re-consent — delete stored token.json when scopes change.
-
Store refresh tokens securely and persistently. The refresh token is only returned on the first authorization (or when prompt=consent). Losing it means the user must re-authorize. Store in a database or encrypted file — never in source control.
-
Never refresh the access token on every API call. Cache it and only refresh when expired. Refreshing on every call is slow and may be throttled by Google.
-
Use historyId for incremental sync — never re-list the entire mailbox. Store the historyId from your last sync. Call history.list with it to get only changes. If it returns 404, fall back to full sync.
-
Renew Pub/Sub watch before it expires (~7 days). Set up a recurring job (e.g., every 3 days). Missing renewal = missed notifications with no warning.
-
Batch API calls aggressively. Group messages.get calls (up to 50–100 per batch) instead of making sequential HTTP requests. Use format=MINIMAL for already-cached messages.
-
Messages are immutable — only labels change. Cache message content locally. On subsequent checks, use format=MINIMAL to detect label changes without re-downloading the body.
-
Use fields parameter to request partial responses. Only fetch the fields you need to minimize bandwidth and latency.
-
Handle invalid_grant gracefully. Delete stored tokens and re-initiate the OAuth flow. Common causes: Testing mode (7-day token expiry), password change on Gmail-scoped tokens, user revocation, 6-month inactivity.
-
Implement exponential backoff for rate limits (429/403). Start at 1s, double each retry, cap at ~32s. Add jitter to avoid thundering herd.
Core Concepts (Quick Reference)
The Gmail API is RESTful. Base URL: https://gmail.googleapis.com/gmail/v1
Resources:
| Resource | Description | Key Fields |
|---|
| Message | An email. Immutable once created. | id, threadId, labelIds, payload (MIME tree), historyId, internalDate, snippet |
| Thread | A conversation — a collection of related messages. | id, messages[], historyId |
| Label | Tag for organizing messages. System labels (INBOX, SENT, DRAFT, SPAM, TRASH, STARRED, UNREAD, IMPORTANT, CATEGORY_*) + user labels. | id, name, type, color |
| Draft | An unsent message container. Stable ID, but inner message ID changes on update. | id, message |
| History | A change record for incremental sync. | id, messagesAdded, messagesDeleted, labelsAdded, labelsRemoved |
| Filter | A server-side rule applied to incoming messages (not threads). | id, criteria, action |
Message format options (on messages.get):
| Format | Returns | When to Use |
|---|
FULL | Parsed MIME payload + headers + body | First-time fetch, full content needed |
MINIMAL | Only id, threadId, labelIds, snippet, historyId | Sync checks on cached messages |
RAW | Entire RFC 2822 as base64url in raw field | Re-parsing, forwarding, archiving |
METADATA | id, threadId, labelIds, snippet + headers (no body) | Header inspection without body |
Authentication: OAuth 2.0 only. Access tokens (~1hr), refresh tokens (long-lived). See AUTH.md.
Sync strategies: Full sync (first connect), partial sync via history.list, push via Cloud Pub/Sub. See SYNC.md and PUSH.md.
userId: Always use "me" for the authenticated user.
SDK Setup
Python
pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
from googleapiclient.discovery import build
service = build('gmail', 'v1', credentials=creds)
Node.js
npm install googleapis @google-cloud/local-auth
import { google } from 'googleapis';
const gmail = google.gmail({ version: 'v1', auth });
Common Pitfalls
| Pitfall | Solution |
|---|
| Refresh token not returned | Set access_type=offline AND prompt=consent in auth URL |
| Token expires after 7 days | Your OAuth consent screen is in "Testing" mode — publish to "Production" |
messages.list returns only IDs | By design — call messages.get per message (use batch) |
| Search doesn't find alias emails | API doesn't expand aliases like the Gmail UI does |
| Labels added to thread don't apply to future messages | Thread label operations only affect existing messages |
history.list returns 404 | historyId is too old — fall back to full sync |
| Push notifications stop | watch expired (7-day max) — renew proactively |
| Slow API calls | You're refreshing the access token every call — cache it |
| Batch request returns 401 | Auth header must be on the outer request, not inner parts |
format=FULL with gmail.metadata scope | Scope doesn't permit body access — use METADATA format |