Connect FlutterFlow to Outlook 365 via Microsoft Graph API using a Firebase Cloud Function as an OAuth2 token proxy. Register an Azure AD app for Mail.Send and Mail.Read scopes, run the OAuth flow through your backend, and store only the short-lived access token in FlutterFlow App State — the Azure client secret never touches the compiled app.
| Fact | Value |
|---|---|
| Tool | Outlook 365 API (Microsoft Graph) |
| Category | Communication |
| Method | FlutterFlow API Call |
| Difficulty | Advanced |
| Time required | 90 minutes |
| Last updated | July 2026 |
Microsoft Graph + FlutterFlow: The Full OAuth2 Path for Work Mailboxes
Microsoft Graph is the authoritative API for Outlook 365 — it lets you read a user's inbox (`GET /me/messages`), send email (`POST /me/sendMail`), access calendar events, and more. Unlike consumer IMAP/SMTP, Graph works for work and school accounts managed by Azure Active Directory, and it supports modern OAuth2 delegated permissions so users can authorize your app without sharing their password.
The complexity comes from two things. First, Microsoft Graph requires an **Azure AD app registration** — you create an app in the Azure portal, declare the scopes you need (Mail.Send, Mail.Read, offline_access), and get a Client ID and Client Secret. Second, the Client Secret is a full-privilege credential that cannot live inside your compiled FlutterFlow app. FlutterFlow compiles to a Flutter binary running on the user's device, so any string you place in an API Call header or Dart variable is decompilable. The correct architecture is: Azure client secret + token exchange logic live in a Firebase Cloud Function; FlutterFlow triggers the Microsoft login page (Launch URL action), receives the auth code via deep-link redirect, sends it to the Cloud Function, gets back a short-lived access token, stores it in secure App State, and attaches it as a Bearer header on subsequent Graph calls.
The Microsoft 365 API itself is free — you only need a valid Microsoft 365 or Outlook account. Graph throttles around 10,000 requests per 10 minutes per app-plus-mailbox combination, which is more than enough for typical app usage. Access tokens expire in about 60 minutes; your Cloud Function must also handle refresh-token exchanges so users aren't forced to log in again mid-session.
Integration method
FlutterFlow communicates with Microsoft Graph through an API Group pointed at graph.microsoft.com/v1.0. Because Microsoft Graph uses OAuth2 delegated auth with an Azure AD client secret, you deploy a Firebase Cloud Function (or Supabase Edge Function) to handle the token exchange and refresh cycle — the secret never enters the FlutterFlow binary. FlutterFlow holds only the short-lived access token in App State and passes it as a Bearer header on every Graph call.
Prerequisites
- A Microsoft 365 or Outlook work/school account (consumer @outlook.com accounts behave differently — this tutorial targets Azure AD / Microsoft 365 tenant accounts)
- Access to the Azure Portal (portal.azure.com) to register an app — free with any Microsoft 365 subscription
- A Firebase project with Cloud Functions enabled (Blaze pay-as-you-go plan required for external network calls) OR a Supabase project with Edge Functions
- A FlutterFlow project on the Standard plan or higher (API Calls and App State are available on all paid plans)
- Basic familiarity with FlutterFlow's API Calls panel and Action Flow Editor
Step-by-step guide
Register an Azure AD application and configure Graph permissions
Open the Azure Portal at portal.azure.com and sign in with your Microsoft 365 admin or developer account. In the left sidebar, click 'Azure Active Directory', then 'App registrations', then '+ New registration'. Give your app a name (e.g., 'FlutterFlow Mail App'), set the Supported account type to 'Accounts in this organizational directory only' (or 'Accounts in any organizational directory' if you want multi-tenant), and paste a Redirect URI — for now use `https://your-cloud-function-url/oauth/callback` (you'll fill this in properly after deploying the Cloud Function in Step 2). Click Register. Once created, note your **Application (client) ID** — you'll need this throughout. Now click 'API permissions' in the left menu → '+ Add a permission' → 'Microsoft Graph' → 'Delegated permissions'. Search for and add: `Mail.Read`, `Mail.Send`, and `offline_access`. Click 'Add permissions'. Then grant admin consent if your tenant requires it (the blue 'Grant admin consent' button). Next, click 'Certificates & secrets' → '+ New client secret'. Set an expiry (12 or 24 months), click Add, and immediately copy the **Value** — this is your Client Secret and Azure will never show it again. Store it securely (you'll paste it into your Cloud Function environment variables in the next step, never into FlutterFlow).
Pro tip: If you see 'Need admin approval' during the OAuth test flow, your Azure tenant has user-consent restrictions enabled. Ask your Microsoft 365 admin to grant consent, or switch to a developer sandbox tenant at developer.microsoft.com.
Expected result: You have an Azure AD app with Client ID and Client Secret noted, Mail.Read / Mail.Send / offline_access permissions configured, and a placeholder Redirect URI ready to update.
Deploy a Firebase Cloud Function as the OAuth2 token proxy
The Azure Client Secret must never be placed in FlutterFlow — it needs to live in a backend function that handles the token exchange. Open your Firebase project, navigate to Functions, and create a new Cloud Function. You'll implement two endpoints: `/oauth/callback` (receives the auth code from Microsoft's login redirect, exchanges it for access + refresh tokens, returns them to the app) and `/oauth/refresh` (accepts a refresh token, returns a new access token). In the Cloud Function code, load the Client Secret from Firebase environment config (`functions.config().azure.client_secret`) — not hardcoded. Use the `axios` or built-in `https` module to POST to `https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token` with grant_type `authorization_code` or `refresh_token`. Return the token response to FlutterFlow as JSON. After deploying, copy the callback endpoint URL (e.g., `https://us-central1-your-project.cloudfunctions.net/oauth/callback`) and go back to Azure Portal → your App Registration → Authentication → add this as a Redirect URI. Update the Redirect URI entry you set in Step 1 to this real URL. Set your Firebase config: `firebase functions:config:set azure.client_id='YOUR_ID' azure.client_secret='YOUR_SECRET' azure.tenant_id='YOUR_TENANT'`
1// index.js (Firebase Cloud Function)2const functions = require('firebase-functions');3const axios = require('axios');45const CLIENT_ID = functions.config().azure.client_id;6const CLIENT_SECRET = functions.config().azure.client_secret;7const TENANT_ID = functions.config().azure.tenant_id;8const REDIRECT_URI = functions.config().azure.redirect_uri;9const TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token`;1011// Exchange auth code for tokens12exports.oauthCallback = functions.https.onRequest(async (req, res) => {13 const { code } = req.query;14 try {15 const params = new URLSearchParams({16 client_id: CLIENT_ID,17 client_secret: CLIENT_SECRET,18 code,19 redirect_uri: REDIRECT_URI,20 grant_type: 'authorization_code',21 });22 const { data } = await axios.post(TOKEN_URL, params.toString(), {23 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }24 });25 // Return tokens to app (deep link or response)26 res.json({ access_token: data.access_token, refresh_token: data.refresh_token, expires_in: data.expires_in });27 } catch (e) {28 res.status(500).json({ error: e.message });29 }30});3132// Refresh access token33exports.oauthRefresh = functions.https.onRequest(async (req, res) => {34 const { refresh_token } = req.body;35 try {36 const params = new URLSearchParams({37 client_id: CLIENT_ID,38 client_secret: CLIENT_SECRET,39 refresh_token,40 grant_type: 'refresh_token',41 });42 const { data } = await axios.post(TOKEN_URL, params.toString(), {43 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }44 });45 res.json({ access_token: data.access_token, expires_in: data.expires_in });46 } catch (e) {47 res.status(500).json({ error: e.message });48 }49});Pro tip: Use Firebase environment config (not hardcoded strings) for all secrets. If you prefer Supabase, the same logic works as a Deno Edge Function — use Deno.env.get('AZURE_CLIENT_SECRET') and the standard fetch API.
Expected result: Two Cloud Function endpoints are live: one for auth-code exchange, one for token refresh. The Azure Redirect URI points to the callback endpoint.
Trigger the Microsoft OAuth login flow from FlutterFlow
Back in FlutterFlow, you need to send the user to Microsoft's login page and capture the auth code when they return. First, set up two App State variables: `msAccessToken` (String, persisted) and `msRefreshToken` (String, persisted). These will hold the tokens after the OAuth dance. In the Action Flow Editor of your 'Sign in with Microsoft' button, add a **Launch URL** action. Build the authorization URL dynamically using a Set from Custom Function that constructs: `https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize?client_id={clientId}&response_type=code&redirect_uri={encodedCallbackUrl}&scope=Mail.Read%20Mail.Send%20offline_access&response_mode=query`. Set the Launch URL to open in an in-app browser (use the `url_launcher` custom action or FlutterFlow's built-in Launch URL with External Browser option). When Microsoft redirects back to your Cloud Function callback, the function exchanges the code for tokens and returns them. Your Cloud Function can then redirect to your app's deep link (e.g., `yourapp://oauth/callback?access_token=...`) or you can handle this with a separate API Call from the app that polls a Firestore document the Cloud Function writes to. The deep-link approach requires configuring a custom URL scheme in FlutterFlow's App Settings → URL Scheme. Once the access token arrives in the app, store it in `msAccessToken` App State via a Set App State action. Store the refresh token in `msRefreshToken`. Add a separate action chain on app startup to check if the access token is expired (compare stored expiry timestamp to current time) and call the `/oauth/refresh` Cloud Function if needed.
Pro tip: The Microsoft OAuth login page will not work correctly inside the FlutterFlow web preview iframe because iframe-embedded OAuth flows are blocked by Microsoft for security reasons. Always test the full login flow on a device or using FlutterFlow's Test Mode build (not web Run mode).
Expected result: Users can tap 'Sign in with Microsoft', complete the login in a browser, and return to the app with access and refresh tokens stored in App State.
Create the Microsoft Graph API Group in FlutterFlow
Click **API Calls** in the FlutterFlow left nav, then click **+ Add** → **Create API Group**. Name it 'Microsoft Graph'. Set the Base URL to `https://graph.microsoft.com/v1.0`. In the Headers section, add one shared header: Key = `Authorization`, Value = `Bearer [accessToken]` where `[accessToken]` is an API Group variable. Click the Variables tab, add a variable named `accessToken` (String type). Back in the Headers section, set the Authorization value to `Bearer {{accessToken}}`. Now add the first API Call inside this group: click **+ Add API Call**, name it 'Get Inbox Messages'. Set method to GET, path to `/me/messages`. Add query parameters: `$top=20`, `$select=subject,from,receivedDateTime,bodyPreview`, `$orderby=receivedDateTime desc`. Click **Test** — you'll need to paste a valid access token in the variable field. If you get a 200 response, click 'Generate JSON Paths' and create paths for: `$.value[*].subject`, `$.value[*].from.emailAddress.address`, `$.value[*].receivedDateTime`, `$.value[*].bodyPreview`. Add a second API Call: name it 'Send Mail'. Method = POST, path = `/me/sendMail`. In the Body section, set type to JSON and use this template: ```json { "message": { "subject": "{{subject}}", "body": { "contentType": "HTML", "content": "{{body}}" }, "toRecipients": [{ "emailAddress": { "address": "{{toEmail}}" } }] } } ``` Add variables: `subject`, `body`, `toEmail` (all String).
1{2 "group": "Microsoft Graph",3 "baseUrl": "https://graph.microsoft.com/v1.0",4 "headers": {5 "Authorization": "Bearer {{accessToken}}"6 },7 "calls": [8 {9 "name": "Get Inbox Messages",10 "method": "GET",11 "path": "/me/messages",12 "queryParams": {13 "$top": "20",14 "$select": "subject,from,receivedDateTime,bodyPreview",15 "$orderby": "receivedDateTime desc"16 }17 },18 {19 "name": "Send Mail",20 "method": "POST",21 "path": "/me/sendMail",22 "body": {23 "message": {24 "subject": "{{subject}}",25 "body": { "contentType": "HTML", "content": "{{body}}" },26 "toRecipients": [{ "emailAddress": { "address": "{{toEmail}}" } }]27 }28 }29 }30 ]31}Pro tip: The sendMail endpoint returns HTTP 202 Accepted with no body on success — don't expect a JSON response. Treat any 2xx as success and show the user a confirmation SnackBar.
Expected result: Your Microsoft Graph API Group appears in FlutterFlow with two calls — 'Get Inbox Messages' and 'Send Mail' — both sharing the Bearer token header. Test calls return 200/202 when a valid token is provided.
Build the inbox ListView and compose form
On your inbox screen, add a ListView widget. In the Backend Query tab, set the data source to your 'Get Inbox Messages' API Call. Pass the `msAccessToken` App State variable as the `accessToken` argument. Map the JSON path results to child widgets: a Text widget for the subject, another for the sender address, and a subtitle Text for `bodyPreview`. Add a refresh button that re-runs the query — useful for pulling new messages. For the compose screen, add a Column with three TextFields: To Email, Subject, and Body (use a multiline TextField for the body). Add a Send button. In its Action Flow Editor, add an **API Request** action pointing at your 'Send Mail' API Call. Pass `msAccessToken` as the `accessToken` variable, and bind the TextField values to `toEmail`, `subject`, and `body` respectively. After the API Call action, add a **Conditional Action**: if the API response code is 202, show a SnackBar 'Email sent!', clear the form, and navigate back. If the response code is 401, trigger your token-refresh flow (call the `/oauth/refresh` Cloud Function, update `msAccessToken` in App State, retry the send). This 401 handler prevents silent failures when the access token expires mid-session. If you'd rather skip building the token refresh logic yourself, RapidDev's team handles FlutterFlow integrations like this every week — free scoping call at rapidevelopers.com/contact.
Pro tip: FlutterFlow's ListView backend query runs once when the screen loads. To poll for new emails automatically, wrap the backend query in a Timer action (Custom Action using `Timer.periodic` in Dart) or use a Refresh Indicator widget to let the user pull-to-refresh.
Expected result: The inbox screen lists the user's 20 most recent Outlook emails with sender, subject, and preview. The compose screen successfully sends emails and shows confirmation. A 401 mid-session triggers a token refresh without forcing re-login.
Handle token expiry and test the full flow on a device
Microsoft access tokens expire after approximately 60 minutes. You need a proactive check on every screen that calls Graph. Create a custom Dart function (left nav → Custom Code → + Add → Function) named `isTokenExpired` that compares a stored expiry timestamp (in App State as `msTokenExpiry`, an integer UNIX timestamp) to `DateTime.now().millisecondsSinceEpoch`. If within 5 minutes of expiry, return true. In the Action Flow Editor of any screen that makes Graph calls, add an **If/Else** condition at the start: if `isTokenExpired()` returns true, first run the Refresh API Call (point a separate API Call at your Cloud Function's `/oauth/refresh` endpoint, pass `msRefreshToken`, update `msAccessToken` and `msTokenExpiry` in App State), then proceed to the actual Graph call. This ensures users never hit a 401 mid-flow. To test the complete OAuth flow, use FlutterFlow's Test Mode (Ctrl/Cmd + R → 'Test' in the top bar) which produces a real device build. The Microsoft login page, deep-link redirect, and IMAP/SMTP operations all require a real device build — they won't work in FlutterFlow's web Run mode. Install the TestFlight (iOS) or APK (Android) build on your device, tap 'Sign in with Microsoft', complete the login, and verify that emails appear in the ListView and that sending works.
1// Custom Dart Function: isTokenExpired2// Arguments: tokenExpiry (int — UNIX ms timestamp)3// Return type: bool4bool isTokenExpired(int tokenExpiry) {5 final now = DateTime.now().millisecondsSinceEpoch;6 // Return true if token expires within 5 minutes7 return now >= tokenExpiry - (5 * 60 * 1000);8}Pro tip: Personal @outlook.com consumer accounts and Azure AD / Microsoft 365 work accounts have different OAuth endpoints and behave differently. This tutorial targets work/school accounts (Azure AD). For personal accounts, the tenant in the OAuth URL should be 'consumers' rather than your tenant ID.
Expected result: The app silently refreshes the access token when it's near expiry, preventing 401 errors. The full flow — sign in, view inbox, send email — works end-to-end on a test device build.
Common use cases
CRM app that reads a salesperson's Outlook inbox
A FlutterFlow sales CRM fetches the user's recent Outlook messages via `/me/messages` and surfaces customer emails alongside their CRM record. The rep sees email history without leaving the app. Replies are sent via `/me/sendMail` pre-filled with CRM context.
Build a screen that lists the 20 most recent emails from the signed-in user's Outlook inbox, showing sender, subject, and received date, with a tap-to-expand body view.
Copy this prompt to try it in FlutterFlow
Automated notification sender for a booking platform
A FlutterFlow booking app sends confirmation emails from the business's Microsoft 365 mailbox whenever a booking is created. The email is assembled in the app, passed to the Cloud Function, and dispatched via `/me/sendMail` on the company account — keeping all booking correspondence in Outlook.
When a booking is confirmed, send a confirmation email from the business Outlook account to the customer's email address with booking details.
Copy this prompt to try it in FlutterFlow
Internal HR app for sending templated emails to employees
An HR team uses a FlutterFlow internal tool to select employees and send templated Outlook emails — welcome messages, policy updates, or event invitations. The Graph API call is secured through the Cloud Function proxy so the company's Azure credentials remain private.
Build a form where HR selects a recipient from a list, chooses a template, and sends a formatted Outlook email with one tap.
Copy this prompt to try it in FlutterFlow
Troubleshooting
Microsoft OAuth login page shows 'The redirect URI specified in the request does not match' error
Cause: The Redirect URI in your Azure AD app registration doesn't exactly match the URL your Cloud Function is using, including scheme and trailing slashes.
Solution: Go to Azure Portal → your App Registration → Authentication → Redirect URIs. Copy the exact URL from your deployed Cloud Function callback endpoint (including https://, no trailing slash) and paste it here. Even a single character difference causes this error. Save the registration, wait 30 seconds for propagation, and retry.
API Call returns 401 Unauthorized even with a freshly obtained access token
Cause: The Bearer token in the Authorization header may have extra whitespace, the token may be from the wrong scope, or the Azure app's permissions haven't been consented to.
Solution: In FlutterFlow's API Call test panel, manually paste the raw access token (no 'Bearer ' prefix — FlutterFlow adds it) and verify you get a 200. If still 401, check that the scope in the OAuth authorize URL includes `Mail.Read` and `Mail.Send` without URL encoding errors. Confirm admin consent was granted in Azure Portal → API permissions (should show a green checkmark).
'XMLHttpRequest error' when testing Graph API Calls in FlutterFlow web Run mode
Cause: Microsoft Graph blocks cross-origin requests from browser origins that aren't registered as allowed. FlutterFlow's web preview origin is not registered in your Azure app, and CORS blocks the call.
Solution: This is expected for web builds — Graph doesn't support arbitrary browser CORS origins. Route all Graph calls through your Cloud Function proxy (which runs server-side and isn't subject to browser CORS). For FlutterFlow testing, use the device-based Test Mode build instead of web Run mode, or add your Cloud Function as the actual destination for Graph calls.
Token refresh returns 400 Bad Request with 'invalid_grant'
Cause: The refresh token has expired (Microsoft refresh tokens expire after 90 days of inactivity or if the user revokes access), the token was already used once, or the client secret in the Cloud Function has expired.
Solution: If the refresh token itself is expired, the user must complete the full OAuth login flow again. In FlutterFlow, detect `invalid_grant` in the Cloud Function response, clear the stored tokens from App State, and redirect to the sign-in screen. Also check your Azure client secret expiry date — if it expired, generate a new one in Azure Portal and update Firebase config.
Best practices
- Never put the Azure Client Secret in FlutterFlow API Call headers or Dart code — it ships inside the compiled binary; always keep it in your Cloud Function environment config
- Store access tokens in FlutterFlow App State with 'Persisted' enabled so users stay signed in across app restarts; store the expiry timestamp alongside so you can refresh proactively
- Request only the minimum Graph scopes your app needs — Mail.Read and Mail.Send for basic email, not Calendars.ReadWrite unless you're building calendar features; minimal scopes reduce user friction on the consent screen
- Always test the Microsoft OAuth login flow on a real device or Test Mode build, not in FlutterFlow's web Run mode — Microsoft blocks OAuth in iframes
- Handle 429 (throttling) responses from Graph by reading the Retry-After header and retrying after the specified delay; Graph can throttle at ~10,000 requests per 10 minutes per mailbox
- Target Microsoft 365 / Azure AD tenant accounts with this tutorial; personal @outlook.com consumer accounts use a different OAuth tenant ('consumers') and may have different Graph behavior
- When displaying email body content (HTML), be cautious about rendering raw HTML in your Flutter app — consider stripping to plain text for safety unless you're using a sandboxed WebView widget
Alternatives
Gmail API uses OAuth2 through Google Cloud Console with the same token-proxy pattern, but is simpler to set up — Google's OAuth flow is more widely documented and has no Azure AD tenant complexity.
SES is better for transactional email at scale (cheapest at $0.10/1,000 emails) but only sends — it can't read a user's inbox; use Outlook 365 API when you need both send and read from a specific mailbox.
SendGrid is the easiest transactional email to call from FlutterFlow via a proxy — no OAuth dance, just an API key — but it sends from your domain, not from a specific user's Microsoft 365 mailbox.
Frequently asked questions
Can I use this with a personal @outlook.com account (not a Microsoft 365 work account)?
Yes, but the setup differs slightly. For personal Microsoft accounts, use `consumers` as the tenant ID in the OAuth authorize URL instead of your organization's tenant ID. The Graph endpoints (`/me/messages`, `/me/sendMail`) work for both account types, but personal accounts don't have an Azure AD admin to grant consent — users grant permissions individually during the OAuth flow.
Why can't I just put the access token directly in an App Value constant instead of App State?
App Values in FlutterFlow are baked into the app at build time and are visible to anyone who decompiles the binary — they're effectively public constants. Access tokens are user-specific and change every 60 minutes, so they need to live in App State (runtime variables) that are set dynamically after the OAuth flow completes. Additionally, access tokens are sensitive user credentials; treat them like passwords.
Does this work for sending email on behalf of another user (not the signed-in user)?
Sending on behalf of another user requires the `Mail.Send.Shared` scope and the delegating user to have explicitly granted send-on-behalf permission in their Outlook settings. This is an advanced Microsoft 365 permission scenario usually handled by IT admins. For most app use cases, you'll send as the signed-in user using `/me/sendMail`.
Will the inbox update in real-time when new emails arrive?
Microsoft Graph doesn't push real-time notifications to FlutterFlow directly. For near-real-time inbox updates, you can use Microsoft Graph's change notifications (webhooks) — which require a Cloud Function endpoint to receive them — and relay new messages to Firestore, which FlutterFlow can stream in real-time. For most apps, a pull-to-refresh button or a periodic refresh every 60 seconds is sufficient.
How long do Azure client secrets last, and what happens when one expires?
Azure client secrets can be set to expire after 6 months, 12 months, 24 months, or a custom period (up to 2 years). When a client secret expires, your Cloud Function's token exchange calls will fail with an authentication error, effectively breaking sign-in for all users. Set a calendar reminder before expiry, generate a new secret in Azure Portal, and update your Cloud Function environment config before the old one expires.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation