# Gmail API

- Tool: FlutterFlow
- Difficulty: Advanced
- Time required: 60 minutes
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to Gmail API by extending Google sign-in with the `gmail.send` scope via a Custom Action, then building a Gmail API Group that sends email via `POST /users/me/messages/send`. The critical FlutterFlow-specific step: Gmail requires the entire email as a base64url-encoded RFC 2822 message in a single `raw` field — a Custom Function must build and encode that string before the API Call.

## Send Gmail from Your FlutterFlow App Using the Signed-In User's Account

Gmail API lets your FlutterFlow app send email as the signed-in user — from their own Gmail address, with their email identity. This is fundamentally different from transactional email tools like Mailgun or SendGrid, which send from a domain you own. Gmail API sends from the user's personal or Workspace address, which means the recipient sees the user's real name and address in the From field. Typical use-cases: a coaching app that lets clients send recap emails, a CRM that sends follow-up messages directly from the salesperson's Gmail, or an onboarding flow that sends a welcome email from the founder's own inbox.

The Gmail API is free with a Google account. The sending quota is 500 emails per day for standard Gmail accounts and around 2,000 per day for Google Workspace accounts (verify current limits in the Google documentation). These are per-user-per-day limits, not per-app limits, which makes Gmail suitable for lightweight sending but not bulk campaigns — for those, use Mailgun or SendGrid. The rate limit is 250 quota units per user per second, and a `messages.send` call costs 100 units, so you have headroom for occasional sends.

The integration architecture in FlutterFlow has two mandatory pieces that often trip builders: First, FlutterFlow's built-in Google Sign-In only grants basic profile scopes — you must add `gmail.send` via a Custom Action. Second, the send endpoint (`POST /users/me/messages/send`) does not accept a simple JSON body with `to`, `subject`, and `body` fields. It requires the entire email formatted as an RFC 2822 MIME message, then base64url-encoded (not standard base64 — use URL-safe encoding, no padding). A FlutterFlow Custom Function handles this encoding step before the API Call fires.

## Before you start

- A Google account (Gmail API is free; no billing required for the API itself)
- A Google Cloud project with the Gmail API enabled (console.cloud.google.com → APIs & Services → Enable APIs → 'Gmail API')
- OAuth 2.0 configured: OAuth consent screen with `https://www.googleapis.com/auth/gmail.send` scope (and optionally `gmail.readonly` for reading), and an OAuth client ID
- A FlutterFlow project with Firebase Auth configured and Google Sign-In enabled
- A paid FlutterFlow plan for Custom Actions and Custom Functions (Standard or higher)

## Step-by-step guide

### 1. Enable Gmail API and configure the gmail.send OAuth scope

Open console.cloud.google.com and navigate to your Firebase project. In the left menu, click APIs & Services, then Library. Search for 'Gmail API' and click Enable. Next, click OAuth consent screen. You'll need to add the Gmail scope to your app's authorized scopes. Click Add or Remove Scopes and find `https://www.googleapis.com/auth/gmail.send` — it appears as 'Send email on your behalf'. Add it. If you want to also read inbox messages, add `https://www.googleapis.com/auth/gmail.readonly` as well. The `gmail.send` scope is classified as sensitive by Google. During development you can use it with test users (added to the Test Users list on the consent screen). Before releasing to the public, you must submit your app for Google's OAuth verification — this review process can take 1-4 weeks and requires a privacy policy URL, an explanation of why you need the scope, and a demo video. For now, add your own Google account to the Test Users list and proceed with development. Click Save and Continue through the consent screen steps.

**Expected result:** The Gmail API is enabled in your project and the `gmail.send` scope is listed in your OAuth consent screen's authorized scopes.

### 2. Create a Custom Action to sign in with the Gmail scope

In FlutterFlow, click Custom Code in the left navigation panel. Click + Add and choose Action. Name it 'signInWithGmailScope'. In the Dependencies field, add `google_sign_in: ^6.2.1` (verify the latest version on pub.dev). Set the Return Value to String — the action returns the OAuth access token. Paste the code below. This Custom Action signs the user in with Google, explicitly requesting the `gmail.send` scope in addition to the basic profile scopes. It returns the access token as a string. After calling this action in FlutterFlow's Action Flow Editor, store the returned value in an App State or Page State variable named `gmailAccessToken`. If the user was previously signed in without the Gmail scope, calling `signOut()` first ensures they get the full permission prompt. Custom Actions that use `google_sign_in` cannot run in FlutterFlow's web Run/Test mode — use Test on Device (Android or iOS) or download the project and run `flutter run` locally to test this step. The access token expires in approximately 1 hour — for a production app, implement token refresh logic or prompt re-sign-in when a 401 is returned from the API.

```
// Custom Action: signInWithGmailScope
// Add dependency: google_sign_in: ^6.2.1
// Return type: String

import 'package:google_sign_in/google_sign_in.dart';

Future<String> signInWithGmailScope() async {
  final GoogleSignIn googleSignIn = GoogleSignIn(
    scopes: [
      'email',
      'profile',
      'https://www.googleapis.com/auth/gmail.send',
    ],
  );

  try {
    await googleSignIn.signOut(); // Force re-consent to add new scope
    final GoogleSignInAccount? account = await googleSignIn.signIn();
    if (account == null) return '';

    final GoogleSignInAuthentication auth = await account.authentication;
    final token = auth.accessToken ?? '';
    return token;
  } catch (e) {
    return '';
  }
}
```

**Expected result:** The Custom Action appears in FlutterFlow's Custom Code panel. When tested on a device, it shows the Google account picker, requests Gmail permission, and returns an access token string.

### 3. Create a Custom Function to build the RFC 2822 MIME message

This is the step where most FlutterFlow builders get stuck: Gmail's send endpoint does not accept a JSON body with `to`, `subject`, and `body` fields. It accepts only a single field named `raw` containing the entire email formatted as an RFC 2822 MIME message and then base64url-encoded. You need a Custom Function to build this string. In FlutterFlow, click Custom Code → + Add → Function. Name it 'buildGmailRaw'. Set Arguments: `toEmail` (String), `fromEmail` (String), `subject` (String), `body` (String). Set the Return Value to String. Paste the Dart code below. This function assembles the MIME headers and body into the correct RFC 2822 format and encodes it as base64url (NOT standard base64 — the URL-safe variant replaces `+` with `-` and `/` with `_`, and strips padding `=` characters). Getting the encoding wrong — for example, using standard base64 or leaving padding — returns a 400 error from the Gmail API with message 'Invalid value for ByteString field'. The `utf8.encode` call ensures non-ASCII characters in the subject or body are handled correctly. Custom Functions (as opposed to Custom Actions) CAN run in FlutterFlow's web preview because they are pure Dart with no native platform calls.

```
// Custom Function: buildGmailRaw
// Arguments: toEmail (String), fromEmail (String), subject (String), body (String)
// Return type: String
// No external dependencies needed - dart:convert is always available

import 'dart:convert';

String buildGmailRaw(
  String toEmail,
  String fromEmail,
  String subject,
  String body,
) {
  // Build RFC 2822 MIME message
  final mimeMessage = [
    'To: $toEmail',
    'From: $fromEmail',
    'Subject: $subject',
    'Content-Type: text/plain; charset=utf-8',
    'MIME-Version: 1.0',
    '',  // blank line separates headers from body
    body,
  ].join('\r\n');

  // Base64url encode (URL-safe, no padding)
  final encoded = base64Url.encode(utf8.encode(mimeMessage));
  // Strip padding '=' characters
  return encoded.replaceAll('=', '');
}
```

**Expected result:** The buildGmailRaw Custom Function appears in FlutterFlow's Custom Code panel and can be tested immediately in the web editor by passing test values — no device needed for Custom Functions.

### 4. Build the Gmail API Group and sendMessage API Call

Click API Calls in the left navigation panel. Click + Add and choose Create API Group. Name it 'GmailAPI'. Set the Base URL to `https://gmail.googleapis.com/gmail/v1`. Click the Headers tab and add a shared header: key `Authorization`, value `Bearer [accessToken]`. In the Variables tab, add `accessToken` (String). Now add an API Call inside this group: click + Add API Call, name it 'SendMessage'. Set the method to POST. Set the API URL to `/users/me/messages/send`. In the Body tab, set body type to JSON. Add one field: key `raw`, value `[rawMessage]`. In the Variables tab, add `rawMessage` (String) — this will hold the output of the buildGmailRaw Custom Function. In the Response & Test tab, enter test values: a real access token from a signed-in session (copy it temporarily from a debug print), a base64url-encoded test message. Click Test API Call — a successful send returns a 200 with the sent message's ID. Add a second API Call: 'ListMessages', method GET, URL `/users/me/messages?maxResults=10`, Variable `q` (String, default `is:unread`) — add it as a query param `q=[q]`. Add a third: 'GetMessage', method GET, URL `/users/me/messages/[messageId]`, Variable `messageId` (String).

```
{
  "raw": "[rawMessage]"
}
```

**Expected result:** The GmailAPI group appears in the API Calls panel with SendMessage, ListMessages, and GetMessage calls. A test send in the Response & Test tab delivers an email to the target inbox.

### 5. Wire the full send email action chain and test

Open the page with your email compose UI — at minimum a Subject TextField, a Body TextField (multiline), a recipient address TextField, and a Send button. Wire the Send button's Actions panel in this order: (1) Call the signInWithGmailScope Custom Action and store the result in `gmailAccessToken` Page State. If the returned value is empty, show a Snackbar 'Could not sign in with Google — please try again' and stop. (2) Call the buildGmailRaw Custom Function, passing `toEmail` from the recipient field, `fromEmail` from the signed-in user's email App State variable, `subject` from the subject field, and `body` from the body field. Store the result in a `rawMessage` Page State variable. (3) Call the SendMessage API Call, binding `accessToken` to `gmailAccessToken` and `rawMessage` to the Custom Function output. (4) On success (check for a 200 response or non-empty `$.id` JSON Path), show a Snackbar 'Email sent!', clear the form fields, and optionally navigate back. On failure, show the error. Add a loading indicator (Page State boolean `isSending`) that shows a CircularProgressIndicator during the API call and disables the Send button. Test first in the web preview for the Custom Function step (buildGmailRaw), then on a real device for the full end-to-end flow including Google sign-in. If you'd rather skip the custom code setup, RapidDev's team builds FlutterFlow integrations like this every week — free scoping call at rapidevelopers.com/contact.

**Expected result:** Tapping Send on a real device signs in with Google, builds the RFC 2822 message, sends it via Gmail API, and shows 'Email sent!' confirmation. The email appears in the recipient's Gmail inbox from the sender's real Gmail address.

## Best practices

- Request only the `gmail.send` scope if you only need to send — do not request broader scopes like `gmail.modify` or `mail.google.com` unless you have a specific need for them. Google's scope review process is stricter for broader scopes.
- Always use base64url encoding (not standard base64) with padding stripped for the `raw` field — this is the single most common error in Gmail API integrations. Dart's `base64Url.encode()` from `dart:convert` handles this correctly.
- The Gmail API is for per-user transactional email only — do not use it for bulk sends, marketing campaigns, or sending to users who have not explicitly consented. High-volume sending gets accounts flagged. Use Mailgun or SendGrid for bulk email.
- Store the signed-in user's email address alongside the access token so the From field is accurate and you can display it in the UI to confirm whose account is being used.
- Handle the 401 Unauthorized error gracefully in the API Call's error branch by calling signInWithGmailScope again to silently refresh the token rather than showing a raw error to the user.
- For the OAuth consent screen, prepare a clear explanation of why your app needs Gmail access — Google's reviewers look for a specific, legitimate use case in the app's description.
- Validate the recipient email address format in FlutterFlow before building the MIME message — a malformed address causes a 400 error that is harder to diagnose than a client-side format check.

## Use cases

### CRM app that sends follow-up emails directly from the salesperson's Gmail

A FlutterFlow CRM shows a list of customer contacts. When the salesperson taps a contact and fills in a message, the app calls Gmail API to send the email from the salesperson's own Gmail address. The recipient sees the email coming from the salesperson's real email identity, not a no-reply address.

Prompt example:

```
On the contact detail page, add a 'Send Email' button that opens a form with Subject and Body text fields. When sent, call the Gmail API using the signed-in user's OAuth token to send from their Gmail address to the contact's email.
```

### Client portal app that sends meeting recap emails to the user themselves

After a client completes a session in the FlutterFlow app, an action chain automatically drafts a recap email with key points and sends it to the signed-in user's own Gmail inbox. The user opted in during onboarding and the app sends on their behalf from their address.

Prompt example:

```
After the session summary is saved, use the Gmail API to send an email to the signed-in user's email address with subject 'Your Session Recap' and the session notes as the email body.
```

### Inbox reader that shows unread Gmail messages in a FlutterFlow ListView

A FlutterFlow page shows the user's unread Gmail messages in a ListView, pulled via the Gmail list API with the query `is:unread`. Tapping a message loads the full content. This gives the user a focused inbox view inside the app for messages relevant to the app's context.

Prompt example:

```
Add a page with a ListView that loads unread Gmail messages using the Gmail API list call with q=is:unread. Show the sender name, subject, and preview snippet. Tapping opens the full message.
```

## Troubleshooting

### Gmail API returns 400 'Invalid value for ByteString field'

Cause: The `raw` field contains incorrectly encoded content — either using standard base64 (with `+` and `/`) instead of base64url (with `-` and `_`), or the padding `=` characters were not stripped.

Solution: Verify the buildGmailRaw Custom Function uses `base64Url.encode()` (not `base64.encode()`) and removes `=` padding with `.replaceAll('=', '')`. Standard base64 from Dart's `base64.encode()` will produce invalid output for Gmail's `raw` field.

### Gmail API returns 403 with `insufficientPermissions`

Cause: The OAuth token was obtained without the `gmail.send` scope — either the user was signed in before the scope was added to the consent screen, or FlutterFlow's built-in Google login was used instead of the Custom Action.

Solution: Call the signInWithGmailScope Custom Action (Step 2) rather than FlutterFlow's Google Sign-In widget. If the user has an existing sign-in session without the Gmail scope, the `signOut()` call in the action forces re-consent. Verify the `gmail.send` scope is listed in your Google Cloud Console OAuth consent screen.

### The API Call works in the FlutterFlow editor test but fails with 401 Unauthorized on the device after some time

Cause: The OAuth access token has expired. Google access tokens typically expire in 3600 seconds (1 hour).

Solution: Call the signInWithGmailScope Custom Action again to get a fresh token before each send operation. For a smoother UX, call `googleSignIn.signInSilently()` first (which returns a cached account without showing the picker), then get a fresh authentication token — this refreshes the token without interrupting the user.

### Emails send successfully but the To name shows as the email address only, not a display name

Cause: The MIME `To` field only has the email address, not the `Name <email>` format.

Solution: Update the buildGmailRaw function to accept a `toName` argument and format the To header as `Name <email>`: `'To: $toName <$toEmail>'`. If no display name is available, the email address alone is also valid.

## Frequently asked questions

### Why does Gmail API need a base64url-encoded MIME message instead of simple to/subject/body fields?

The Gmail API models email as the raw RFC 2822 format that email protocols actually use — including all headers, MIME parts, and proper encoding. This makes it more powerful (you can send HTML, attachments, custom headers) but also more complex to build. The Custom Function in Step 3 abstracts this complexity so the rest of your FlutterFlow app just passes simple string parameters.

### Can I use Gmail API to send from a shared inbox or alias, not just the primary Gmail address?

Yes, if the signed-in user has send-as aliases configured in their Gmail settings (Settings → Accounts and Import → Send mail as), you can use those addresses in the From field of the MIME message. However, Gmail will reject the send if the From address is not a verified alias or the primary address for the authenticated account.

### What's the daily sending limit for Gmail API?

Standard Gmail accounts can send approximately 500 emails per day via the Gmail API. Google Workspace (paid) accounts can send approximately 2,000 per day (verify current limits in Google's documentation, as these can change). These are per-user limits — they count against the same quota as emails sent through the Gmail web interface.

### Can I send attachments through Gmail API from FlutterFlow?

Yes, but it requires building a multipart MIME message instead of a plain text message. You'd need to extend the buildGmailRaw Custom Function to handle multipart/mixed content type, encode the attachment as base64, and assemble the multipart boundary format. This is doable in Dart but significantly more complex — consider it an advanced enhancement after the basic send flow works.

### Is there a simpler way to send email from FlutterFlow without dealing with Gmail's encoding?

Yes — if you don't need the email to come from the user's own Gmail address, use Mailgun, SendGrid, or AWS SES via a Cloud Function proxy. These services accept simple JSON fields (to, subject, body) and handle the email formatting themselves. Gmail API is specifically for when you need the email to appear as coming from the signed-in user's Gmail account.

---

Source: https://www.rapidevelopers.com/flutterflow-integrations/gmail-api
© RapidDev — https://www.rapidevelopers.com/flutterflow-integrations/gmail-api
