The Gmail API: the complete developer guide
Everything about the Gmail API in one place: OAuth setup, scopes, sending, reading, push notifications, quotas, common errors - and when it's the wrong tool.
Dvir Atias
Founder
The Gmail API is Google's REST interface to Gmail mailboxes: read, send, search, label, and watch messages in a user's Gmail account. It is the right tool when your software needs to act on a real person's Gmail. It is routinely the wrong tool when developers reach for it because "we need to send/receive email from code" - a job it makes surprisingly painful. This guide covers both sides honestly: how to use it well, and when to use something else.
What the Gmail API actually is
The Gmail API (gmail.googleapis.com) operates on a single Google account's mailbox. Every request acts as a user who granted your app permission via OAuth 2.0. That single fact drives everything that follows:
- There are no service accounts for consumer Gmail. Domain-wide delegation exists only for Google Workspace, configured by a Workspace admin.
- Tokens expire and users revoke. Your app must store refresh tokens and survive re-consent flows.
- Unverified apps hit a wall. Sensitive scopes (
gmail.send, and especially the restrictedgmail.readonly/gmail.modify) require Google's OAuth app verification - a security review process that can take weeks and, for restricted scopes, an annual third-party assessment for production use.
If your use case is "a user connects their own Gmail to my product," that cost is justified. If it's "my app or agent needs an email address," it isn't - skip to the last section.
Setup: project, consent screen, credentials
- Create a project in the Google Cloud console and enable the Gmail API under APIs & Services.
- Configure the OAuth consent screen: app name, support email, and the scopes you need. In Testing mode you can add up to 100 test users without verification.
- Create an OAuth client ID (type: Web application or Desktop) and download the client secret JSON.
Scopes: request the minimum
| Scope | Grants | Verification tier |
|---|---|---|
gmail.send | Send only, no reading | Sensitive |
gmail.readonly | Read everything, no changes | Restricted |
gmail.modify | Read, labels, trash - not permanent delete | Restricted |
gmail.compose | Create drafts and send | Sensitive |
https://mail.google.com/ | Full access incl. delete | Restricted |
Restricted scopes trigger the heaviest review. A classic mistake is requesting mail.google.com when gmail.send would do - it can add months to your launch.
Sending email
The API takes a full RFC 2822 message, base64url-encoded, in a field called raw:
import { google } from "googleapis";
const gmail = google.gmail({ version: "v1", auth: oauth2Client });
const message = [
"From: me@gmail.com",
"To: dest@example.com",
"Subject: Hello from the Gmail API",
"Content-Type: text/plain; charset=utf-8",
"",
"It works.",
].join("\r\n");
await gmail.users.messages.send({
userId: "me",
requestBody: {
raw: Buffer.from(message).toString("base64url"),
},
});Yes - you are assembling MIME by hand (or with a library like nodemailer's MailComposer). HTML bodies, attachments, and threading headers (In-Reply-To, References) are all your responsibility. We cover the send path in more depth - including Python - in Gmail API: send email in Python and Node.js.
Reading and searching mail
Reading is a two-step affair: list returns IDs; you fetch each message separately.
const { data } = await gmail.users.messages.list({
userId: "me",
q: "is:unread from:stripe.com", // full Gmail search syntax
maxResults: 20,
});
for (const { id } of data.messages ?? []) {
const msg = await gmail.users.messages.get({ userId: "me", id, format: "full" });
// msg.data.payload is a raw MIME tree: parts, nested parts,
// base64url bodies, headers as an array of {name, value}...
}The payload is a MIME tree you must walk yourself: multipart/alternative inside multipart/mixed, base64url-encoded bodies, attachments fetched via a third call (attachments.get). Budget real time for this - or see our guide to receiving email as JSON for what parsed inbound email can look like.
Push notifications (watch + Pub/Sub)
The Gmail API doesn't have webhooks. To react to new mail you must:
- Create a Google Cloud Pub/Sub topic and grant
gmail-api-push@system.gserviceaccount.compublish rights. - Call
users.watchwith the topic name. - Handle Pub/Sub pushes - which contain only a
historyId, not the message. - Call
users.history.listto diff what changed, then fetch messages. - Renew the watch at least every 7 days - it silently expires.
We walk the full setup in Gmail webhook: real-time push when email arrives. It works, but it is five moving parts to get "email arrived → my code runs."
Quotas and limits
- 250 quota units per user per second (messages.send costs 100 units; messages.get costs 5; list costs 5).
- 1 billion units/day per project by default - rarely the binding limit.
- Sending is also capped by the Gmail account's limits: ~500 recipients/day for consumer Gmail, 2,000 for Workspace. The API does not raise these.
429/rateLimitExceededand403/userRateLimitExceededdemand exponential backoff.
That last point surprises teams: the Gmail API is not a bulk sender. For product email volumes you need a transactional provider - see our transactional email API guide.
Common errors
| Error | Cause | Fix |
|---|---|---|
403 accessNotConfigured | API not enabled on the project | Enable Gmail API in the console |
401 invalid_grant | Refresh token revoked/expired (testing-mode tokens expire in 7 days) | Re-run consent; publish the app |
403 insufficientPermissions | Missing scope for the call | Re-consent with the right scope |
429 rateLimitExceeded | Per-user quota burst | Exponential backoff |
400 failedPrecondition | Consumer account calling a delegation-only flow | Use OAuth user consent |
When the Gmail API is the wrong tool
Use the Gmail API when the mailbox belongs to a human who connects their account to your product: an email client, a CRM syncing correspondence, a scheduling assistant reading a user's inbox with permission.
Do not use it to give your application or AI agent an email address. You would be building OAuth consent, token refresh, MIME assembly and parsing, Pub/Sub plumbing, and watch renewals - then submitting to a restricted-scope security review - to end up with a mailbox that is rate-limited for humans and owned by a Google account you have to babysit.
For that job, an email API built for programmatic inboxes is one call:
curl -X POST https://api.agenticemail.dev/v1/inboxes \
-H "Authorization: Bearer am_..." \
-d '{"username": "agent"}'The inbox can send, receive, and reply; inbound mail arrives as parsed JSON over a signed webhook or WebSocket - no Pub/Sub, no MIME tree, no OAuth review. Read why agents need their own email infrastructure or start with the quickstart.