Skip to main content
RapidDev - Software Development Agency
API AutomationsGmailOAuth 2.0

How to Automate Gmail Templated Replies & Follow-ups using the API

Automate Gmail templated replies and follow-up sequences using the Gmail API's users.messages.send endpoint. Messages must be RFC 2822-formatted and base64url-encoded (not standard base64). Threading requires matching In-Reply-To and References headers plus the original threadId. Hard sending cap: 500 messages/day for @gmail.com, 2,000/day for Google Workspace accounts.

Need help automating? Talk to an expert
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate7 min read30-60 minutesGmailMay 2026RapidDev Engineering Team
TL;DR

Automate Gmail templated replies and follow-up sequences using the Gmail API's users.messages.send endpoint. Messages must be RFC 2822-formatted and base64url-encoded (not standard base64). Threading requires matching In-Reply-To and References headers plus the original threadId. Hard sending cap: 500 messages/day for @gmail.com, 2,000/day for Google Workspace accounts.

API Quick Reference

Auth

OAuth 2.0

Rate limit

15,000 quota units/minute per user; 500 or 2,000 emails/day hard cap

Format

JSON

SDK

Available

Understanding the Gmail API

The Gmail API is a RESTful service for programmatic Gmail access. The messages.send endpoint accepts RFC 2822-formatted email encoded as a base64url string in the 'raw' field — this is the primary gotcha that trips up most developers. Standard base64 uses '+' and '/' characters that are URL-unsafe; base64url replaces them with '-' and '_' respectively.

For threaded replies (essential for follow-up sequences), you must include three components: the In-Reply-To header with the original message's Message-ID, the References header with the full chain of Message-IDs, and the threadId field in the API request body. Missing any one of these causes Gmail to create a new thread instead of appending to the existing conversation.

The gmail.send scope is listed as Sensitive in Google's scope table, but in practice Gmail content scopes follow Restricted-level policy and require OAuth verification plus a security assessment if you store email content server-side. Official docs: https://developers.google.com/workspace/gmail/api

Base URLhttps://gmail.googleapis.com/gmail/v1

Setting Up Gmail API Authentication

Gmail uses OAuth 2.0 with user consent for sending email. The gmail.send scope allows sending but not reading messages — add gmail.readonly or gmail.modify if you need to fetch thread data for constructing replies. Access tokens expire after 1 hour; the client libraries handle refresh automatically using the stored refresh token.

  1. 1Go to https://console.cloud.google.com and create or select a project
  2. 2Navigate to APIs & Services → Library → search 'Gmail API' → Enable
  3. 3Go to APIs & Services → OAuth consent screen. Configure app name, support email, and scopes
  4. 4Add scopes: https://www.googleapis.com/auth/gmail.send and https://www.googleapis.com/auth/gmail.readonly
  5. 5Go to Credentials → Create Credentials → OAuth Client ID → select Desktop app
  6. 6Download credentials JSON as credentials.json
  7. 7Install dependencies: pip install google-api-python-client google-auth-oauthlib google-auth-httplib2
  8. 8Run the authentication script once to complete OAuth flow and save token.json
auth.py
1import os
2from google.oauth2.credentials import Credentials
3from google_auth_oauthlib.flow import InstalledAppFlow
4from google.auth.transport.requests import Request
5from googleapiclient.discovery import build
6
7SCOPES = [
8 'https://www.googleapis.com/auth/gmail.send',
9 'https://www.googleapis.com/auth/gmail.readonly'
10]
11
12def get_gmail_service():
13 creds = None
14 if os.path.exists('token.json'):
15 creds = Credentials.from_authorized_user_file('token.json', SCOPES)
16 if not creds or not creds.valid:
17 if creds and creds.expired and creds.refresh_token:
18 creds.refresh(Request())
19 else:
20 flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)
21 creds = flow.run_local_server(port=0)
22 with open('token.json', 'w') as f:
23 f.write(creds.to_json())
24 return build('gmail', 'v1', credentials=creds)

Security notes

  • Never commit credentials.json or token.json to version control — add both to .gitignore
  • The gmail.send scope requires OAuth verification before your app can be used by external users
  • Do not share refresh tokens across users — each user must have their own OAuth token
  • For Google Workspace server automation, use Service Account + Domain-Wide Delegation
  • Monitor sending patterns — Gmail's abuse detection may suspend accounts with unusual bulk sending behavior

Key endpoints

POST/gmail/v1/users/{userId}/messages/send

Sends a new email or reply. The message body must be a complete RFC 2822 email encoded as base64url. For replies, include the threadId field to keep messages in the same thread.

ParameterTypeRequiredDescription
userIdstringrequiredThe user's email address or 'me' for authenticated user
rawstringrequiredComplete RFC 2822 message encoded as base64url (NOT standard base64)
threadIdstringoptionalThread ID to reply into — required for proper threading

Request

json
1{"raw":"RnJvbTogc2VuZGVyQGV4YW1wbGUuY29tClRvOiByZWNpcGllbnRAZXhhbXBsZS5jb20KU3ViamVjdDogSGVsbG8KCkJvZHkgdGV4dA==","threadId":"18c3a2b1d4e5f6a7"}

Response

json
1{"id":"18d1f2e3c4b5a697","threadId":"18c3a2b1d4e5f6a7","labelIds":["SENT"]}
GET/gmail/v1/users/{userId}/messages/{id}

Retrieves a full message including headers needed for constructing a reply — Message-ID, In-Reply-To, References, and threadId.

ParameterTypeRequiredDescription
idstringrequiredThe message ID from messages.list
formatstringoptionalMessage format: 'full' (default), 'minimal', 'raw', or 'metadata'
metadataHeadersarrayoptionalWhen format=metadata, list of headers to include

Response

json
1{"id":"18c3a2b1d4e5f6a7","threadId":"18c3a2b1d4e5f6a7","labelIds":["INBOX","UNREAD"],"payload":{"headers":[{"name":"Message-ID","value":"<abc123@mail.gmail.com>"},{"name":"From","value":"sender@example.com"},{"name":"Subject","value":"Re: Your question"}]}}
GET/gmail/v1/users/{userId}/messages

Lists messages matching a search query. Returns only {id, threadId} — use messages.get for full content. Use this to find the original message for constructing replies.

ParameterTypeRequiredDescription
qstringoptionalGmail search query (e.g., 'from:customer@example.com is:unread')
maxResultsnumberoptionalMaximum messages to return, 1-500
pageTokenstringoptionalPagination token from previous response

Response

json
1{"messages":[{"id":"18c3a2b1d4e5f6a7","threadId":"18c3a2b1d4e5f6a7"}],"resultSizeEstimate":1}
POST/gmail/v1/users/{userId}/drafts

Creates a draft message that can be sent later. Useful for scheduling follow-ups — create the draft now, send via drafts.send at the right time.

ParameterTypeRequiredDescription
message.rawstringrequiredRFC 2822 message encoded as base64url
message.threadIdstringoptionalThread ID to associate the draft with

Request

json
1{"message":{"raw":"RnJvbTogc2VuZGVyQGV4YW1wbGUuY29tClRvOiByZWNpcGllbnRAZXhhbXBsZS5jb20KU3ViamVjdDogRm9sbG93IFVwCgpCb2R5IHRleHQ="}}

Response

json
1{"id":"r8765432109876543","message":{"id":"18d5e6f7a8b9c0d1","threadId":"18d5e6f7a8b9c0d1","labelIds":["DRAFT"]}}

Step-by-step automation

1

Build RFC 2822 Message with Correct base64url Encoding

Why: Gmail's API rejects standard base64 — the '+' and '/' characters cause request parsing failures or garbled messages.

Construct a properly-formatted RFC 2822 email string with required headers (From, To, Subject, MIME-Version, Content-Type), then encode it using base64url encoding specifically. Python's base64.urlsafe_b64encode() handles this correctly. For UTF-8 subjects with non-ASCII characters, use RFC 2047 encoding: =?UTF-8?B?<base64>?=.

request.sh
1# Build and encode a message
2MESSAGE=$(python3 -c "
3import base64
4msg = '''From: you@gmail.com
5To: recipient@example.com
6Subject: Following up
7MIME-Version: 1.0
8Content-Type: text/plain; charset=UTF-8
9
10Hi, just following up on my previous message.
11'''
12encoded = base64.urlsafe_b64encode(msg.encode()).decode()
13print(encoded)
14")
15
16curl -X POST \
17 -H "Authorization: Bearer $ACCESS_TOKEN" \
18 -H "Content-Type: application/json" \
19 -d "{\"raw\":\"$MESSAGE\"}" \
20 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send'

Pro tip: Always test your base64url encoding by decoding it back and checking the result. A common mistake is using standard base64 which works for ASCII-only content but silently corrupts non-ASCII characters in subjects or bodies.

Expected result: A message object with the 'raw' field containing the base64url-encoded RFC 2822 email, ready to pass to messages.send.

2

Fetch Original Message for Threading Data

Why: Proper email threading requires the original message's Message-ID header — without it, your reply starts a new conversation thread in the recipient's inbox.

When replying to an existing message, first fetch it using messages.get to extract the Message-ID header, existing References header chain, and the threadId. Request format=metadata with specific headers to minimize quota cost (5 units vs 5 units — same, but less data transferred). The threadId is in the message object at the top level, not in headers.

request.sh
1# Fetch message headers for threading (cost: 5 quota units)
2curl -H "Authorization: Bearer $ACCESS_TOKEN" \
3 'https://gmail.googleapis.com/gmail/v1/users/me/messages/18c3a2b1d4e5f6a7?format=metadata&metadataHeaders=Message-ID&metadataHeaders=References&metadataHeaders=Subject&metadataHeaders=From'

Pro tip: The 'Re: ' prefix in Subject is cosmetic in Gmail — Gmail threads by Message-ID/References chains, not Subject matching. Still add 'Re: ' for compatibility with other email clients.

Expected result: Returns an object with threadId, messageId (for In-Reply-To header), references chain, and the original subject — all needed for constructing a properly threaded reply.

3

Send the Templated Reply or Initial Outreach

Why: This is the actual send call — quota cost is 100 units, and each send counts against your daily sending limit.

Call messages.send with the built message object. The daily sending limits are hard caps enforced by Gmail infrastructure: 500 messages/day for @gmail.com accounts and 2,000/day for Google Workspace users. Exceeding these returns a 429 error. Via the API, each message can have a maximum of 100 recipients. Plan your sending schedule to stay within limits — for bulk outreach, Workspace accounts give you 4x the headroom.

request.sh
1# Send the message
2curl -X POST \
3 -H "Authorization: Bearer $ACCESS_TOKEN" \
4 -H "Content-Type: application/json" \
5 -d "$MESSAGE_JSON" \
6 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send'

Pro tip: HTTP 200 does not guarantee delivery — Gmail may still silently fail near the daily sending limit. Track sent message IDs and verify they appear in the Sent folder via messages.list.

Expected result: Returns {id, threadId, labelIds: ['SENT']}. The message appears in Gmail's Sent folder immediately.

4

Schedule Follow-up Sequences with Drafts

Why: Creating drafts now and sending them later via drafts.send is more reliable than keeping messages in memory — drafts survive server restarts and process crashes.

For multi-day follow-up sequences, create all drafts upfront with their scheduled send dates stored in your database. Use a cron job or scheduler to call drafts.send at the right time. The drafts.create endpoint costs 10 quota units; drafts.send costs 100 units (same as messages.send). Store the draft ID and scheduled_at timestamp, then poll for due drafts in your scheduler.

request.sh
1# Create a draft for later sending
2curl -X POST \
3 -H "Authorization: Bearer $ACCESS_TOKEN" \
4 -H "Content-Type: application/json" \
5 -d '{"message":{"raw":"<base64url-encoded-RFC2822>"}}' \
6 'https://gmail.googleapis.com/gmail/v1/users/me/drafts'
7
8# Send a draft by ID
9curl -X POST \
10 -H "Authorization: Bearer $ACCESS_TOKEN" \
11 -H "Content-Type: application/json" \
12 -d '{"id":"r8765432109876543"}' \
13 'https://gmail.googleapis.com/gmail/v1/users/me/drafts/send'

Pro tip: Drafts don't auto-send — you must call drafts.send explicitly. Consider using a simple cron job with a database table of {draft_id, send_at, lead_id} rather than building complex in-memory scheduling.

Expected result: Returns an array of draft objects with IDs and scheduled send timestamps. Store these in your database and use a cron job to send drafts when their scheduled time arrives.

Complete working code

This script implements a 3-touch follow-up sequence: sends an initial outreach email, then creates two follow-up drafts scheduled for days 3 and 7. It reads leads from a CSV file, respects the daily sending limit with a counter, and properly threads all follow-ups in the original conversation.

automate_followups.py
1#!/usr/bin/env python3
2"""Gmail Follow-up Sequence Automator."""
3import os
4import csv
5import base64
6import time
7import logging
8from datetime import datetime, timedelta
9from email.mime.text import MIMEText
10from email.mime.multipart import MIMEMultipart
11from email.header import Header
12from googleapiclient.discovery import build
13from google.oauth2.credentials import Credentials
14from google_auth_oauthlib.flow import InstalledAppFlow
15from google.auth.transport.requests import Request
16
17logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
18log = logging.getLogger(__name__)
19
20SCOPES = ['https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/gmail.readonly']
21DAILY_LIMIT = 450 # Stay safely under the 500/day @gmail.com cap
22
23TEMPLATES = {
24 'initial': 'Hi {name},\n\nI noticed {company} and thought we might be able to help with {pain_point}.\n\nOpen to a quick 15-minute chat?\n\nBest, Me',
25 'followup_3': 'Hi {name},\n\nJust following up on my last note. Still happy to connect if timing works.\n\nBest, Me',
26 'followup_7': 'Hi {name},\n\nLast note from my end — reach out any time if this becomes relevant.\n\nBest, Me'
27}
28
29def get_service():
30 creds = None
31 if os.path.exists('token.json'):
32 creds = Credentials.from_authorized_user_file('token.json', SCOPES)
33 if not creds or not creds.valid:
34 if creds and creds.expired and creds.refresh_token:
35 creds.refresh(Request())
36 else:
37 flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)
38 creds = flow.run_local_server(port=0)
39 with open('token.json', 'w') as f: f.write(creds.to_json())
40 return build('gmail', 'v1', credentials=creds)
41
42def encode_message(sender, to, subject, body, reply_to=None, references=None, thread_id=None):
43 msg = MIMEText(body, 'plain', 'utf-8')
44 msg['From'] = sender
45 msg['To'] = to
46 msg['Subject'] = str(Header(subject, 'utf-8'))
47 if reply_to:
48 msg['In-Reply-To'] = reply_to
49 msg['References'] = references or reply_to
50 raw = base64.urlsafe_b64encode(msg.as_bytes()).decode()
51 result = {'raw': raw}
52 if thread_id: result['threadId'] = thread_id
53 return result
54
55def main():
56 service = get_service()
57 sent_today = 0
58
59 with open('leads.csv') as f:
60 for lead in csv.DictReader(f):
61 if sent_today >= DAILY_LIMIT:
62 log.warning('Daily limit reached, stopping')
63 break
64 try:
65 # Send initial email
66 body = TEMPLATES['initial'].format(**lead)
67 msg = encode_message('me@gmail.com', lead['email'],
68 f"Quick question for {lead['company']}", body)
69 sent = service.users().messages().send(userId='me', body=msg).execute()
70 thread_id = sent['threadId']
71 sent_today += 1
72 log.info(f'Sent initial to {lead["email"]} (thread: {thread_id})')
73
74 # Create follow-up drafts
75 for days, key in [(3, 'followup_3'), (7, 'followup_7')]:
76 follow_body = TEMPLATES[key].format(**lead)
77 follow_msg = encode_message(
78 'me@gmail.com', lead['email'],
79 f"Re: Quick question for {lead['company']}",
80 follow_body, thread_id=thread_id
81 )
82 service.users().drafts().create(userId='me', body={'message': follow_msg}).execute()
83 log.info(f'Draft created for day {days} follow-up to {lead["email"]}')
84
85 time.sleep(1.5) # ~40 sends/min max for safety
86 except Exception as e:
87 log.error(f'Failed for {lead["email"]}: {e}')
88 continue
89
90if __name__ == '__main__':
91 main()

Error handling

400Invalid base64 for 'raw'
Cause

The 'raw' field contains standard base64 instead of base64url encoding. Characters '+' and '/' are URL-unsafe and rejected by the Gmail API parser.

Fix

Use base64.urlsafe_b64encode() in Python or replace + with - and / with _ after standard base64 encoding in JavaScript.

Retry strategy

No retry — fix the encoding in your code and resubmit.

429User-rate limit exceeded (Mail sending)
Cause

Hit the daily sending cap: 500 messages/day for @gmail.com or 2,000/day for Workspace. This is a hard infrastructure limit, not a quota unit limit.

Fix

Stop sending for the day — the limit resets at midnight in the account's timezone. For higher volume, upgrade to Google Workspace. Build a send counter into your automation.

Retry strategy

Wait until next day. Do not retry immediately — the limit is per-day, not per-minute.

403Request had insufficient authentication scopes
Cause

OAuth token lacks gmail.send scope. If you also need to read threads for reply threading, you need gmail.readonly as well.

Fix

Delete token.json, add the required scopes to your SCOPES list, and re-run the OAuth flow to get a new token.

Retry strategy

No retry — user must re-authorize.

400Failed to parse 'From' header
Cause

The From header in the RFC 2822 message doesn't match the authenticated user's email address, or the header format is malformed.

Fix

Ensure the From address exactly matches the Google account being used. Use 'Name <email@domain.com>' format or bare email address — both are valid.

Retry strategy

No retry — fix the From header and resubmit.

500Backend Error
Cause

Transient Gmail infrastructure error.

Fix

Implement exponential backoff and retry. These errors are temporary and typically resolve within a few seconds.

Retry strategy

Exponential backoff: 2^n seconds (1s, 2s, 4s, 8s, 16s, 32s, max 64s) with jitter.

Rate Limits for Gmail API

ScopeLimitWindow
Per user15,000 quota unitsper minute
Per project1,200,000 quota unitsper minute
Daily send cap (@gmail.com)500 messagesper day (resets midnight)
Daily send cap (Workspace)2,000 messagesper day per user
Recipients per message (API)100 recipientsper message
retry-handler.ts
1import time
2import random
3from googleapiclient.errors import HttpError
4
5def send_with_backoff(service, message_body, max_retries=5):
6 for attempt in range(max_retries):
7 try:
8 return service.users().messages().send(
9 userId='me', body=message_body
10 ).execute()
11 except HttpError as e:
12 if e.resp.status in [500, 503]:
13 wait = min((2 ** attempt) + random.uniform(0, 1), 64)
14 time.sleep(wait)
15 elif e.resp.status == 429:
16 # Daily limit hit don't retry
17 raise Exception('Daily sending limit exceeded')
18 else:
19 raise
20 raise Exception('Max retries exceeded')
  • Track daily sent count in your database and stop at 450/day for @gmail.com (10% buffer below the 500 cap)
  • Add 1-2 second delays between sends to avoid hitting per-minute quota limits
  • Batch follow-up drafts creation rather than sending immediately — separate creation from delivery timing
  • For outreach at scale, use a Google Workspace account (2,000/day vs 500/day for personal accounts)
  • messages.send costs 100 quota units — at 15,000 units/minute user limit, max theoretical send rate is 150 messages/minute, but daily limits kick in first

Security checklist

  • Store OAuth credentials outside your repository with .gitignore — credentials.json and token.json contain sensitive keys
  • Never send from an email address that doesn't match the authenticated Google account
  • Add unsubscribe headers to outreach sequences to comply with CAN-SPAM and GDPR
  • Implement a suppression list to prevent sending to contacts who have replied, unsubscribed, or bounced
  • The gmail.send scope requires OAuth verification for external apps — complete this before production use
  • Rate-limit your automation to stay well under daily sending caps — sudden spikes trigger Gmail's abuse detection
  • Log all sent message IDs for audit trail and bounce tracking
  • Avoid sending identical messages to many recipients — Gmail's spam filters flag repetitive bulk patterns

Automation use cases

Sales Outreach Drip

intermediate

Send a personalized cold email followed by 2-3 follow-ups over 14 days to a prospect list, with all messages threaded in the same conversation.

Customer Onboarding Sequence

intermediate

Trigger a multi-step onboarding email series when a new user signs up, sending tips and check-ins at day 1, 3, and 7.

Support Ticket Auto-Reply

advanced

Send an immediate templated acknowledgment when a support email arrives, then follow up automatically if no human response is sent within 24 hours.

Invoice Follow-up Reminder

beginner

Automatically send payment reminder emails at net-30, net-45, and net-60 days for unpaid invoices, threaded in the original invoice email chain.

No-code alternatives

Don't want to write code? These platforms can automate the same workflows visually.

Zapier

Free tier (100 tasks/month); Starter from $19.99/month

Zapier's Gmail + delay actions allow building basic follow-up sequences triggered by new email or form submissions without code.

Pros
  • + No code needed
  • + Easy template editing
  • + Integrates with 500+ apps for triggers
Cons
  • - No native send delay (requires paid Paths + delay steps)
  • - Limited templating compared to code
  • - 500/1500 task limits on lower tiers

Make (Integromat)

Free (1,000 ops/month); Core from $9/month

Make supports Gmail send actions with variable substitution and can schedule follow-ups using sleep modules and Google Sheets as a state store.

Pros
  • + Visual sequence builder
  • + Better pricing than Zapier at scale
  • + Native sleep/delay support
Cons
  • - More complex to configure
  • - Free tier limited to 1,000 ops/month
  • - Steeper learning curve

n8n

Free self-hosted; Cloud from €20/month

Self-hosted n8n provides Gmail nodes for constructing and sending RFC 2822 messages with full control over threading headers and scheduling.

Pros
  • + Free self-hosted
  • + Full RFC 2822 header control
  • + Schedule workflows as cron jobs
Cons
  • - Requires server setup
  • - More technical configuration
  • - Gmail credentials still need OAuth setup

Best practices

  • Always use base64url encoding for the 'raw' field — test by decoding your encoded string before the first production send
  • Include proper threading headers (In-Reply-To, References) for all follow-up messages — this is what keeps replies in the same thread
  • Build a suppression list: before each send, check if the recipient has replied or unsubscribed, and skip them
  • Cap your automation at 80% of the daily limit to leave headroom for manual emails sent from the same account
  • Store draft IDs in a database rather than memory — your follow-up scheduler needs to survive restarts
  • Add the List-Unsubscribe header to comply with CAN-SPAM: List-Unsubscribe: <mailto:unsub@yourdomain.com>
  • Test RFC 2822 messages with a single recipient before bulk runs — encoding errors don't show up until the API call

Ask AI to help

Copy one of these prompts to get a personalized, working implementation.

ChatGPT / Claude Prompt

I'm using the Gmail API in Python to send threaded follow-up emails. My initial send works but follow-up emails start new threads instead of replying to the original. I'm passing threadId in the message body and adding In-Reply-To header. Here's my message construction code: [paste code]. What am I missing to make the replies actually thread correctly in Gmail?

Lovable / V0 Prompt

Build a Gmail follow-up sequence manager dashboard. Features: 1) Upload a CSV of leads with name, email, company fields, 2) Create and preview email templates with merge fields, 3) Configure sequence timing (day 1, day 3, day 7), 4) Dashboard showing sequence status per lead (sent/pending/replied), 5) One-click pause for leads who reply. Use Supabase for state management and a Supabase Edge Function to call the Gmail API with OAuth 2.0.

Frequently asked questions

Why do my follow-up emails start new threads instead of replying to the original?

You need three things for proper threading: the In-Reply-To header set to the original message's Message-ID value, the References header containing the chain of all previous Message-IDs, and the threadId field in the API request body. Missing any one of them causes Gmail to create a new thread. Fetch the original message with format=metadata to get its Message-ID header.

What is the Gmail API daily sending limit?

Personal @gmail.com accounts: 500 messages per day, max 500 recipients per email. Google Workspace accounts: 2,000 messages per day per user, max 100 recipients per message via API (not 500 for API calls). Exceeding these limits returns a 429 error. The limits reset at midnight in the account's timezone. These are hard infrastructure limits that cannot be raised.

Why does Gmail reject my message with a 400 error about base64?

The Gmail API requires base64url encoding, not standard base64. The difference: base64url replaces '+' with '-' and '/' with '_'. In Python, use base64.urlsafe_b64encode(). In JavaScript, after standard btoa(), replace all + with - and / with _. Standard base64 encoding of ASCII-only content happens to work sometimes because those characters don't appear, but non-ASCII content will fail.

Can I send from a different email address (alias) than the authenticated account?

Only if the alias is configured in Gmail Settings → Accounts → Send mail as. The From header must match either the primary account or a verified send-as alias. Using an unverified From address results in a 400 error. You can fetch the list of valid send-as addresses with users.settings.sendAs.list.

What happens when I hit the rate limit on messages.send?

You'll receive a 429 or 403 error. If it's the daily sending cap (500/day for Gmail, 2,000/day for Workspace), stop sending for the day — the limit resets at midnight. If it's the per-minute quota (15,000 units/min, with send costing 100 units = max 150 sends/minute), implement exponential backoff starting at 1 second, doubling each attempt, max 64 seconds.

Is the Gmail API free for sending email?

Yes, the Gmail API is free with no per-message charges. You pay only for any Google Cloud infrastructure (like Pub/Sub if you use push notifications). The sending limits (500 or 2,000/day) are the practical constraint, not cost.

Can RapidDev help build a custom Gmail email automation system?

Yes. RapidDev has built 600+ apps including sales automation and support systems using the Gmail API. We handle OAuth setup, RFC 2822 message construction, threading, rate limit management, and building the dashboard to manage sequences. Contact us for a free consultation at rapidevelopers.com.

RapidDev

Need this automated?

Our team has built 600+ apps with API automations. We can build this for you.

Book a free consultation

Skip the coding — we'll build it for you

Our experts have built 600+ API automations. From prototype to production in days, not weeks.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.