The real problem
Why this is harder than it looks
Most developers look at the Gmail API and think the integration will take a day. The read and search endpoints are clean. The docs are thorough. The problems start the moment you move from a personal test account to real users in a multi-tenant product.
Gmail's OAuth scopes are split into three tiers: non-sensitive, sensitive, and restricted. Reading messages — the most basic thing an email agent does — falls under a sensitive or restricted scope depending on what access level you need. Sensitive scopes require Google to review and verify your app before users outside your test group can connect without seeing an "unverified app" warning. Restricted scopes require that review plus an annual third-party security assessment. This is before you've written a single line of agent logic. Under-scope and your agent fails at runtime; over-scope and enterprise IT admins will block your OAuth consent screen entirely under admin_policy_enforced.
Then there's the token revocation problem unique to Gmail. Google revokes all OAuth refresh tokens that contain mail scopes whenever the user changes their password — and in enterprise Workspace deployments, IT-enforced password rotation is routine. Your system needs to detect this, surface it gracefully per user, and prompt re-authorization without exposing an opaque invalid_grant error. Refresh tokens also expire after six months of inactivity, and each user account is capped at 100 live tokens per OAuth client — exceed that and Google silently invalidates the oldest one. In testing mode, tokens expire after just seven days.
None of this is unsolvable. But together it adds up to weeks of plumbing that has nothing to do with what your agent is supposed to accomplish. Scalekit handles scope verification flows, refresh token management, revocation detection, and per-user isolation — so your agent code only has to name a tool and pass parameters.
Capabilities
What your agent can do with Gmail
Once connected, your agent has 6 pre-built tools covering core Gmail workflows:
- Fetch and filter emails: search by sender, label, read status, date range, or any Gmail query syntax; paginate large results automatically
- Read full message content: retrieve the complete body, headers, and metadata for any message by ID
- Download attachments: pull attachment data by message ID and attachment ID for processing in your agent
- Access drafts: list a user's draft emails for review or editing workflows
- Search contacts: look up people in the user's Google Contacts and connected directory by name or email
- Browse the address book: fetch the full contacts list with pagination and field filtering
Setup context
What we're building
This guide connects an email assistant agent to Gmail — helping users triage inboxes, surface key messages, and look up contacts without leaving your product.
🤖
Example agent
Email assistant reading and searching Gmail on behalf of each user
🔐
Auth model
B2B SaaS — each user connects their own Gmail. identifier = your user ID
Setup
1 Setup: One SDK, One credential
Install the Scalekit SDK. The only credential your application manages is the Scalekit API key — no Gmail secrets, no user tokens, nothing belonging to your users.
pip install scalekit-sdk-python
import scalekit.client
import os
from dotenv import load_dotenv
load_dotenv()
scalekit = scalekit.client.ScalekitClient(
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
env_url=os.getenv("SCALEKIT_ENV_URL"),
)
actions = scalekit.actions
Connected Accounts
2 Per-User Auth: Creating connected accounts
Each user gets their own Connected Account, giving them a dedicated auth context. The identifier is any unique string from your system — a UUID, email, whatever you use internally.
response = actions.get_or_create_connected_account(
connection_name="gmail",
identifier="user_gmail_456" # your internal user ID
)
connected_account = response.connected_account
print(f"Status: {connected_account.status}")
# Status: PENDING — user hasn't authorized yet
This call is idempotent — safe to call on every session start. Returns the existing account if one already exists.
Authorization Flow
3 The authorization flow
The user authorizes your agent once. Scalekit generates the OAuth URL with correct scopes, PKCE challenge, and redirect handling pre-configured. After approval, you never see the token.
if connected_account.status != "ACTIVE":
link = actions.get_authorization_link(
connection_name="gmail",
identifier="user_gmail_456"
)
# Redirect user → Google's native OAuth consent screen
# Scalekit captures the token on callback
return redirect(link.link)
Token management is automatic
After the user approves, Scalekit stores encrypted tokens and the connected account moves to ACTIVE. Access
tokens refresh before expiry. If a user's token is revoked — by password change, admin policy, or manual
revocation — the account moves to REVOKED. No silent invalid_grant failures. Check account.status before
critical operations.
Bring Your Own Credentials — required for production
Gmail requires you to register your own Google Cloud OAuth app and supply your Client ID and Secret.
Scalekit does not provide a managed Google app for Gmail. In the Scalekit dashboard, go to Agent Auth →
Connections → your Gmail connection, click "Use your own credentials," and paste your Google credentials.
Token management stays fully handled.
Already have credentials?
Calling Gmail
4 Calling Gmail: What your agent writes
With the connected account active, your agent calls Gmail actions using actions.execute_tool(). Name the tool, pass parameters. Scalekit handles token retrieval and request construction.
Search and fetch emails
Search with Gmail's native query syntax. The query parameter accepts the same strings you'd type in the Gmail search bar — from:, is:unread, subject:, date ranges, labels, and more.
result = actions.execute_tool(
identifier="user_gmail_456",
tool_name="gmail_fetch_mails",
tool_input={
"query": "is:unread from:boss@company.com after:2026/03/01",
"max_results": 20,
"label_ids": ["INBOX"],
"format": "metadata"
}
)
# Returns list of messages with id, threadId, labelIds, snippet
Read a full message
Fetch the complete content of a specific message by ID. Use format: "full" to get the decoded body and all headers.
message = actions.execute_tool(
identifier="user_gmail_456",
tool_name="gmail_get_message_by_id",
tool_input={
"message_id": "18e4a2f1b3d9c8a0",
"format": "full"
}
)
# { "id": "...", "payload": { "headers": [...], "body": { "data": "..." } } }
Download an attachment
Retrieve attachment data for processing — invoice PDFs, images, or any other file. Requires the message ID and attachment ID from the message payload.
attachment = actions.execute_tool(
identifier="user_gmail_456",
tool_name="gmail_get_attachment_by_id",
tool_input={
"message_id": "18e4a2f1b3d9c8a0",
"attachment_id": "ANGjdJ8kQo2_3...",
"file_name": "invoice_march_2026.pdf"
}
)
# Returns base64-encoded attachment data
Look up a contact
Search the user's Google Contacts by name or email. Useful when your agent needs to resolve a person's details from a partial name or infer relationships between senders.
people = actions.execute_tool(
identifier="user_gmail_456",
tool_name="gmail_search_people",
tool_input={
"query": "Alice Johnson",
"person_fields": ["names", "emailAddresses", "organizations"],
"page_size": 5
}
)
# Returns matching people with names, emails, and org metadata
Framework wiring
5 Wiring into your agent framework
Scalekit integrates directly with LangChain. The agent decides what to look up or summarize; Scalekit handles auth on every invocation. No token plumbing in your agent logic.
from langchain_anthropic import ChatAnthropic
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from scalekit.langchain import get_tools
gmail_tools = get_tools(
connection_name="gmail",
identifier="user_gmail_456"
)
prompt = ChatPromptTemplate.from_messages([
("system", "You are an email assistant. Use the available tools to help the user manage their Gmail inbox."),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
agent = create_tool_calling_agent(ChatAnthropic(model="claude-sonnet-4-6"), gmail_tools, prompt)
result = AgentExecutor(agent=agent, tools=gmail_tools).invoke({
"input": "Summarize my unread emails from the last 48 hours and flag anything that looks urgent"
})
Other frameworks supported
Tool reference
All 6 Gmail tools
Grouped by capability. Your agent calls tools by name — no API wrappers to write.
Fetch emails using Gmail search syntax — filter by sender, label, read status, date, or any query string. Supports pagination and label filtering
Retrieve a specific message by ID. Control response format: metadata, minimal, or full (includes decoded body and all headers)
gmail_get_attachment_by_id
Download a specific attachment from a message using the message ID and attachment ID. Returns base64-encoded file data
List draft emails in the connected Gmail account with pagination support
Search Google Contacts and directory by name or email. Supports including "Other Contacts" and specifying which person fields to return
Fetch the full contacts list with pagination. Specify which fields to return — names, email addresses, organizations, phone numbers, and more
Connector notes
Gmail-specific behavior
Tokens are revoked on password change — by design
Google revokes all refresh tokens that contain Gmail scopes when the user changes their password. In
enterprise Workspace deployments with mandatory password rotation, this happens routinely. The account will
move to REVOKED in Scalekit. Check account.status before operations and surface a re-authorization prompt
rather than returning a generic error. This is a Google security policy, not an edge case.
Workspace admins can block scope access entirely
Google Workspace admins can restrict specific OAuth scopes at the domain level via admin_policy_enforced.
If a user's token stops working after successful authorization, check whether the customer's Workspace admin
has restricted third-party OAuth app access. This surfaces as an authorization error on the first API call
after the policy is set — it cannot be detected programmatically in advance.
Scope selection shapes your Google verification path
Gmail scopes are tiered. Read-access scopes like gmail.readonly are sensitive and require Google app
verification before users outside your test group can connect. Broader scopes are restricted and also
require an annual security assessment. Use the narrowest scope set that covers your agent's actual
functionality — over-scoping slows down verification and causes enterprise IT admins to reject the consent
screen. Configure your approved scope set in the Scalekit dashboard under your Gmail connection.
Testing mode tokens expire after 7 days
If your Google Cloud project's OAuth consent screen is set to "Testing" with "External" user type, Google
revokes refresh tokens after 7 days. Promote your app to "Production" status in the Google Cloud Console
before building anything multi-session. You'll see the "unverified app" warning until verification
completes, but tokens will persist normally.
Infrastructure decision
Why not build this yourself
The Gmail OAuth flow is documented. Token storage isn't technically hard. But here's what you're actually signing up for:
PROBLEM 01
Token revocation on every user password change — a routine event in enterprise deployments — requires per-user detection and re-authorization prompts your backend must handle gracefully
PROBLEM 02
Scope tiers (sensitive vs. restricted) determine whether you need Google's verification process, an annual third-party security assessment, or both — getting the scope selection wrong blocks enterprise IT admins from approving your app
PROBLEM 03
Per-user token isolation across a multi-tenant system — one user's Gmail token must never be accessible to another — is an architectural constraint that requires deliberate design, not just a database index
PROBLEM 04
Workspace admin_policy_enforced errors, the 100 refresh token per account cap, and 7-day expiry in testing mode are all silent failure modes that only surface in production with real users
That's one connector. Your agent product will eventually need Salesforce, Slack, HubSpot, Notion, and whatever else your customers ask for. Each has its own OAuth quirks and failure modes.
Scalekit maintains every connector. You maintain none of them.