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.
| Fact | Value |
|---|---|
| Tool | Gmail API |
| Category | Communication |
| Method | FlutterFlow API Call |
| Difficulty | Advanced |
| Time required | 60 minutes |
| Last updated | July 2026 |
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.
Integration method
FlutterFlow accesses the Gmail API through a user's OAuth token obtained via a Custom Action that extends Google sign-in with the `gmail.send` scope. The fundamental challenge is Gmail's send endpoint: it does not accept plain to/subject/body fields — it requires a base64url-encoded RFC 2822 MIME message in a single `raw` field. A Custom Function in FlutterFlow builds the MIME string and encodes it. For reading large inboxes or sending on behalf of other users, use a Cloud Function with service account credentials instead.
Prerequisites
- 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
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.
Pro tip: The OAuth consent screen App name and logo are what users see when they authorize the Gmail scope. Use your app's real name and logo so the permission prompt looks trustworthy, not like a generic test app.
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.
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.
1// Custom Action: signInWithGmailScope2// Add dependency: google_sign_in: ^6.2.13// Return type: String45import 'package:google_sign_in/google_sign_in.dart';67Future<String> signInWithGmailScope() async {8 final GoogleSignIn googleSignIn = GoogleSignIn(9 scopes: [10 'email',11 'profile',12 'https://www.googleapis.com/auth/gmail.send',13 ],14 );1516 try {17 await googleSignIn.signOut(); // Force re-consent to add new scope18 final GoogleSignInAccount? account = await googleSignIn.signIn();19 if (account == null) return '';2021 final GoogleSignInAuthentication auth = await account.authentication;22 final token = auth.accessToken ?? '';23 return token;24 } catch (e) {25 return '';26 }27}Pro tip: Store the signed-in user's email address (from `account.email`) as a separate App State variable if you want to show it in the UI or use it as the From address confirmation.
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.
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.
1// Custom Function: buildGmailRaw2// Arguments: toEmail (String), fromEmail (String), subject (String), body (String)3// Return type: String4// No external dependencies needed - dart:convert is always available56import 'dart:convert';78String buildGmailRaw(9 String toEmail,10 String fromEmail,11 String subject,12 String body,13) {14 // Build RFC 2822 MIME message15 final mimeMessage = [16 'To: $toEmail',17 'From: $fromEmail',18 'Subject: $subject',19 'Content-Type: text/plain; charset=utf-8',20 'MIME-Version: 1.0',21 '', // blank line separates headers from body22 body,23 ].join('\r\n');2425 // Base64url encode (URL-safe, no padding)26 final encoded = base64Url.encode(utf8.encode(mimeMessage));27 // Strip padding '=' characters28 return encoded.replaceAll('=', '');29}Pro tip: To send HTML email instead of plain text, change `Content-Type: text/plain` to `Content-Type: text/html; charset=utf-8` and put HTML markup in the body parameter.
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.
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).
1{2 "raw": "[rawMessage]"3}Pro tip: The Gmail API base URL is `https://gmail.googleapis.com/gmail/v1` (NOT `https://www.googleapis.com/gmail/v1`) — both work but the first is the canonical current URL shown in Google's documentation.
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.
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.
Pro tip: Set the recipient email field's keyboard type to 'Email Address' in FlutterFlow's widget settings to show the @ keyboard on mobile and improve input accuracy.
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.
Common 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.
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.
Copy this prompt to try it in FlutterFlow
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.
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.
Copy this prompt to try it in FlutterFlow
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.
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.
Copy this prompt to try it in FlutterFlow
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.
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.
Alternatives
Choose Mailgun if you want to send transactional email from a domain you own (not the user's Gmail) — Mailgun is a domain-level bulk sender with no per-user limits and simple form-urlencoded sends.
Choose Outlook 365 API if your users are on Microsoft 365 — the Microsoft Graph send email endpoint (`/me/sendMail`) accepts a simple JSON body with to/subject/body, which is easier than Gmail's base64url MIME requirement.
Choose AWS SES if you need high-volume transactional email from a domain you own at very low cost — SES charges per email rather than per month and has no per-user limits.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation