0Pricing
AI Agents · Lesson

Connecting to Gmail via the API

Google API client library, OAuth2 consent, and Gmail scope selection.

Connecting to Gmail via the API is a free AI Agents lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the AI Agents learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Use the Gmail API Instead of SMTP?

Traditional email automation uses SMTP/IMAP, but the Gmail API offers much more: read threads, search by query, manage labels, and send with full authentication. It also supports OAuth 2.0, so your agent never stores a password — just a limited-scope access token.

The Gmail API is part of the Google Workspace APIs, accessed via the google-api-python-client library.

# Install required libraries:
# pip install google-api-python-client google-auth google-auth-oauthlib

# The Gmail API lets agents:
# - List and search messages (labels, queries)
# - Read full message content and attachments
# - Send messages via OAuth (no password needed)
# - Manage labels and threads
# - Watch for new messages via push notifications

print('Gmail API is part of Google Workspace APIs')

Authentication: Service Account vs User Auth

Two authentication approaches exist for the Gmail API:

  • Service Account: best for workspace/organizational use with domain-wide delegation; no user interaction required
  • User OAuth (OAuth2 with consent screen): required for personal Gmail accounts; user grants access once, agent uses refresh token

For most agent automation, service accounts are preferred for reliability.

# Service Account approach:
# 1. Go to Google Cloud Console -> APIs & Services -> Credentials
# 2. Create a Service Account
# 3. Download the JSON key file
# 4. In Google Workspace Admin: enable domain-wide delegation
# 5. Grant required scopes to the service account

# User OAuth approach:
# 1. Create OAuth 2.0 Client ID (Desktop or Web App type)
# 2. Download credentials.json
# 3. First run: user sees consent screen and grants access
# 4. Agent stores token.json with refresh token for subsequent runs

print('Choose service account for org automation, OAuth for personal Gmail')

OAuth2 Credentials JSON Structure

Google provides credentials in a JSON file that your agent loads to authenticate. For User OAuth, this is a credentials.json file downloaded from Google Cloud Console. The file contains your client ID, secret, and redirect URI — never commit this to version control.

# credentials.json structure (User OAuth — Desktop app type):
# {
#   "installed": {
#     "client_id": "123456789.apps.googleusercontent.com",
#     "client_secret": "GOCSPX-abc123xyz",
#     "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"],
#     "auth_uri": "https://accounts.google.com/o/oauth2/auth",
#     "token_uri": "https://oauth2.googleapis.com/token"
#   }
# }

# service-account.json structure:
# {
#   "type": "service_account",
#   "project_id": "my-project",
#   "private_key_id": "abc123",
#   "private_key": "-----BEGIN PRIVATE KEY-----\n...",
#   "client_email": "agent@my-project.iam.gserviceaccount.com",
#   "client_id": "..."
# }

print('Store credential files outside your git repository')

Gmail API Scopes

OAuth scopes define exactly what your agent can access. Request only the scopes you need — this is the principle of least privilege. Gmail scopes range from read-only to full access.

  • gmail.readonly — read all mail
  • gmail.send — send only, no read
  • gmail.modify — read, send, modify labels
  • gmail.compose — create drafts only
# Gmail API scope constants
SCOPE_READONLY = 'https://www.googleapis.com/auth/gmail.readonly'
SCOPE_SEND = 'https://www.googleapis.com/auth/gmail.send'
SCOPE_MODIFY = 'https://www.googleapis.com/auth/gmail.modify'
SCOPE_COMPOSE = 'https://www.googleapis.com/auth/gmail.compose'

# Calendar scopes (often used alongside Gmail)
SCOPE_CALENDAR_READ = 'https://www.googleapis.com/auth/calendar.readonly'
SCOPE_CALENDAR_EVENTS = 'https://www.googleapis.com/auth/calendar.events'

# Combine scopes your agent actually needs
AGENT_SCOPES = [
    SCOPE_READONLY,
    SCOPE_SEND,
    SCOPE_CALENDAR_EVENTS
]
print(f'Using {len(AGENT_SCOPES)} scopes')

User OAuth: First-Time Authentication Flow

The first time a user runs the agent, it opens a browser for them to grant permission. The agent stores the resulting token in token.json. On subsequent runs, it loads the stored token and refreshes it automatically — no browser interaction needed.

from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
import os

SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']

def get_credentials(token_file='token.json', creds_file='credentials.json'):
    creds = None

    # Load existing token if available
    if os.path.exists(token_file):
        creds = Credentials.from_authorized_user_file(token_file, SCOPES)

    # Refresh or re-authenticate if needed
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())  # auto-refresh
        else:
            # Opens browser for user consent (first time only)
            flow = InstalledAppFlow.from_client_secrets_file(
                creds_file, SCOPES
            )
            creds = flow.run_local_server(port=0)
        # Save token for next run
        with open(token_file, 'w') as f:
            f.write(creds.to_json())

    return creds

Service Account Authentication

For automated agents that run without user interaction, service accounts are ideal. The agent authenticates with a private key, then impersonates a Google Workspace user via domain-wide delegation. No consent screen, no browser — just a JSON key file.

from google.oauth2 import service_account
import os

SCOPES = [
    'https://www.googleapis.com/auth/gmail.readonly',
    'https://www.googleapis.com/auth/gmail.send'
]

def get_service_account_credentials(impersonate_user):
    service_account_file = os.environ.get(
        'GOOGLE_SERVICE_ACCOUNT_JSON',
        'service-account.json'
    )

    credentials = service_account.Credentials.from_service_account_file(
        service_account_file,
        scopes=SCOPES
    )

    # Impersonate a real user (requires domain-wide delegation in Admin)
    delegated = credentials.with_subject(impersonate_user)
    return delegated

creds = get_service_account_credentials('agent@yourcompany.com')
print('Service account credentials ready')

Building the Gmail Service Object

Once you have credentials, use googleapiclient.discovery.build() to create the Gmail service object. This is the main interface for all Gmail API calls. Pass the service name 'gmail' and version 'v1'.

from googleapiclient.discovery import build

def build_gmail_service(credentials):
    service = build(
        'gmail',
        'v1',
        credentials=credentials,
        cache_discovery=False  # avoid file warnings in some environments
    )
    return service

# Full setup: credentials -> service
creds = get_credentials()          # or get_service_account_credentials()
gmail = build_gmail_service(creds)

# Test: get user profile
profile = gmail.users().getProfile(userId='me').execute()
print('Email:', profile['emailAddress'])
print('Total messages:', profile['messagesTotal'])

Building the Calendar Service Object

The same credential setup works for Google Calendar. Just build with 'calendar' and 'v3'. If you need both Gmail and Calendar in one agent, build both services from the same credentials object.

from googleapiclient.discovery import build

def build_google_services(credentials):
    gmail = build(
        'gmail', 'v1',
        credentials=credentials,
        cache_discovery=False
    )
    calendar = build(
        'calendar', 'v3',
        credentials=credentials,
        cache_discovery=False
    )
    return gmail, calendar

# Use both in one agent
creds = get_credentials()
gmail_service, calendar_service = build_google_services(creds)

# Test calendar access
cal_list = calendar_service.calendarList().list().execute()
for cal in cal_list.get('items', []):
    print(f'Calendar: {cal["summary"]}')

Handling Google API Errors

Google API errors are raised as googleapiclient.errors.HttpError. The error contains an HTTP status code and a JSON body with the error details. Always catch this and log the status and message for debugging.

from googleapiclient.errors import HttpError
import json

def safe_gmail_call(service, user_id='me'):
    try:
        profile = service.users().getProfile(userId=user_id).execute()
        return profile
    except HttpError as e:
        status = e.resp.status
        try:
            error_body = json.loads(e.content.decode())
            message = error_body.get('error', {}).get('message', str(e))
        except Exception:
            message = str(e)

        if status == 401:
            print('AUTH ERROR: Credentials invalid or expired')
        elif status == 403:
            print(f'PERMISSION ERROR: {message}')
            print('Check scopes and domain-wide delegation settings')
        elif status == 429:
            print('QUOTA EXCEEDED: Gmail API rate limit hit')
        else:
            print(f'Gmail API error {status}: {message}')
        return None

Google API Quota and Rate Limits

The Gmail API has usage quotas: 1 billion quota units per day by default, with each call costing 1-100 units depending on the operation. Reading messages costs more than listing. Use batch requests and exponential backoff on 429/503 errors to stay within limits.

import time
from googleapiclient.errors import HttpError

def gmail_call_with_retry(func, max_retries=5):
    for attempt in range(max_retries):
        try:
            return func()
        except HttpError as e:
            if e.resp.status in (429, 500, 503):
                wait = (2 ** attempt) + 1
                print(f'Quota/server error. Waiting {wait}s (attempt {attempt+1})')
                time.sleep(wait)
            elif e.resp.status == 403:
                # Check if it's a quota exceeded vs permission error
                import json
                body = json.loads(e.content.decode())
                reason = body.get('error', {}).get('errors', [{}])[0].get('reason', '')
                if reason == 'rateLimitExceeded':
                    time.sleep(2 ** attempt)
                else:
                    raise  # real permission error, don't retry
            else:
                raise
    raise Exception(f'Gmail API call failed after {max_retries} attempts')

Storing Credentials Securely

Never commit token.json, credentials.json, or service-account.json to version control. Add them to .gitignore. In production, store the service account JSON in an environment variable or a secrets manager and load it at runtime.

import json
import os
from google.oauth2 import service_account

SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']

def get_credentials_from_env():
    # Load service account JSON from environment variable
    sa_json = os.environ.get('GOOGLE_SERVICE_ACCOUNT_JSON')
    if not sa_json:
        raise EnvironmentError(
            'GOOGLE_SERVICE_ACCOUNT_JSON env var not set. '
            'Set it to the contents of your service-account.json'
        )

    sa_info = json.loads(sa_json)
    credentials = service_account.Credentials.from_service_account_info(
        sa_info,
        scopes=SCOPES
    )
    return credentials

# In production: export GOOGLE_SERVICE_ACCOUNT_JSON=$(cat service-account.json)
creds = get_credentials_from_env()
print('Service account loaded from env var')

Quick Check: Service Account vs User OAuth

Test your understanding of Gmail API authentication methods.

Gmail API Connection Recap

You can now connect an agent to Gmail:

  • User OAuth: use InstalledAppFlow + store token.json; auto-refresh on subsequent runs
  • Service Account: load JSON key, call .with_subject(user_email) for delegation
  • Scopes: request minimum needed (gmail.readonly, gmail.send, calendar.events)
  • Build service with build('gmail', 'v1', credentials=creds)
  • Catch HttpError for API errors; retry on 429/503
  • Never commit credential files — use env vars in production

Frequently asked questions

Is the “Connecting to Gmail via the API” lesson free?

Yes — the full text of “Connecting to Gmail via the API” is free to read here on the web, and the AI Agents course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the AI Agents course, upgrade to CoddyKit PRO.

What will I learn in “Connecting to Gmail via the API”?

Google API client library, OAuth2 consent, and Gmail scope selection. You practise AI Agents with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start AI Agents?

No prior experience is required. AI Agents on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Connecting to Gmail via the API” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this AI Agents lesson?

Yes. Every AI Agents lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Connecting to Gmail via the API
  2. Reading and Sending Emails Programmatically
  3. Calendar Event Creation and Querying
  4. Building a Simple Email Assistant Agent
← Back to AI Agents