Connect FlutterFlow to Microsoft Dynamics 365 using the Dataverse Web API (OData v4). Because the Azure AD client secret cannot live in a mobile app, route OAuth token minting through a Firebase Cloud Function, then call Dynamics REST endpoints from a FlutterFlow API Group using the short-lived bearer token — enabling read/write of Leads, Accounts, and Opportunities from your Flutter app.
| Fact | Value |
|---|---|
| Tool | Microsoft Dynamics 365 |
| Category | CRM & Sales |
| Method | FlutterFlow API Call |
| Difficulty | Advanced |
| Time required | 60 minutes |
| Last updated | July 2026 |
Why Dynamics 365 Needs a Proxy Before FlutterFlow Can Talk to It
Microsoft Dynamics 365 is powered by the Dataverse Web API — a standard OData v4 REST interface at `https://yourorg.crm.dynamics.com/api/data/v9.2/`. Every request must carry a short-lived Azure AD bearer token. To obtain that token you need an Azure app-registration client secret — a server-to-server credential that must never be embedded inside an iOS or Android app bundle, where it can be extracted by anyone who decompiles the app.
The correct FlutterFlow architecture is a two-layer setup. A Firebase Cloud Function holds the Azure tenant ID, client ID, and client secret, calls `https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token`, and returns the resulting access token to FlutterFlow. FlutterFlow then stores the token in an App State variable and passes it as the `Authorization: Bearer` header on every Dataverse API Call. The token is valid for approximately one hour; when it expires, your app silently re-calls the Cloud Function to get a fresh one.
Dynamics 365 Sales starts at around $65 per user per month (verify current pricing on the Microsoft website). Dataverse applies service-protection limits — approximately 6,000 API requests per 5-minute window per user — and returns HTTP 429 with a `Retry-After` header when you exceed them. Because FlutterFlow compiles a client app that can run on many devices simultaneously, be thoughtful about how frequently you poll Dynamics data: prefer on-demand fetches and local App State caching over aggressive auto-refresh.
Integration method
FlutterFlow sends REST calls to the Dataverse Web API (`/api/data/v9.2/`) using OData v4 query parameters. Because Azure AD requires a client secret to mint tokens — and that secret cannot safely live inside a compiled Flutter app — a Firebase Cloud Function handles the Azure AD OAuth client-credentials flow and returns short-lived bearer tokens to FlutterFlow. FlutterFlow then stores the token in App State and attaches it as an `Authorization: Bearer` header on every API Call.
Prerequisites
- A Microsoft Dynamics 365 organization (trial or licensed) with a known org URL, e.g. https://yourorg.crm.dynamics.com
- Access to Microsoft Azure portal (portal.azure.com) to create an app registration
- A Firebase project with Cloud Functions enabled (Blaze pay-as-you-go plan required for Cloud Functions)
- A FlutterFlow project on a paid plan (Starter or above) to use API Calls and App State
- Basic familiarity with FlutterFlow's left-nav panels and API Calls section
Step-by-step guide
Register an Azure AD app and grant Dataverse API permission
Open the Azure portal at portal.azure.com and sign in with your Microsoft 365 admin account. In the left navigation, click 'Azure Active Directory' (now called Microsoft Entra ID). Select 'App registrations' → '+ New registration'. Give the app a name like 'FlutterFlow Dynamics Connector', leave the Redirect URI blank for now (you're using client credentials, not a user-interactive flow), and click 'Register'. Once created, copy the Application (client) ID and the Directory (tenant) ID — you'll need both for the Cloud Function. Next, click 'API permissions' → '+ Add a permission' → 'Dynamics CRM' → 'Application permissions' → tick 'user_impersonation' (or for true app-only access, add the Dataverse application permission). Click 'Add permissions', then click 'Grant admin consent for [Your Org]' and confirm. Without admin consent, the client-credentials flow will fail. Finally, click 'Certificates & secrets' → '+ New client secret', give it a description like 'FlutterFlow Proxy', set an expiry (24 months recommended), and click 'Add'. Copy the secret VALUE immediately — Azure only shows it once. You now have three values: Tenant ID, Client ID, Client Secret. Store them securely; they go into the Firebase Cloud Function environment, not into FlutterFlow.
Pro tip: Use an App Registration rather than a user account credential. App Registrations use the client-credentials grant, which does not require a human to log in — ideal for a background token fetch that runs every hour.
Expected result: Your Azure app registration page shows 'Grant status: Granted' for the Dynamics CRM permission. You have Tenant ID, Client ID, and Client Secret copied somewhere safe.
Deploy a Firebase Cloud Function to mint Azure AD tokens
The Firebase Cloud Function is the only place your Azure client secret will live. When FlutterFlow calls this function, it returns a short-lived Dynamics 365 access token. Your FlutterFlow app never sees the client secret. In the Firebase Console, open your project, go to 'Build → Functions', and click 'Get started' if you haven't already (Blaze plan required). You'll write the function using the Firebase Console inline editor or your local Firebase CLI environment. Create a file called `index.js` inside your functions folder with the code below. The function receives an HTTPS GET request (from FlutterFlow), calls Microsoft's token endpoint with your stored credentials, and returns the access token as JSON. Store your Azure credentials as Firebase environment variables — in the Firebase CLI, set them with `firebase functions:config:set azure.tenant_id="..." azure.client_id="..." azure.client_secret="..." azure.org_url="https://yourorg.crm.dynamics.com"`. After deploying (`firebase deploy --only functions`), copy the Function's HTTPS trigger URL — that's what FlutterFlow will call.
1const functions = require('firebase-functions');2const axios = require('axios');34exports.getDynamicsToken = functions.https.onRequest(async (req, res) => {5 // CORS for FlutterFlow web builds6 res.set('Access-Control-Allow-Origin', '*');7 if (req.method === 'OPTIONS') {8 res.set('Access-Control-Allow-Methods', 'GET');9 res.set('Access-Control-Allow-Headers', 'Content-Type');10 return res.status(204).send('');11 }1213 const config = functions.config().azure;14 const tokenUrl = `https://login.microsoftonline.com/${config.tenant_id}/oauth2/v2.0/token`;1516 try {17 const params = new URLSearchParams();18 params.append('client_id', config.client_id);19 params.append('client_secret', config.client_secret);20 params.append('scope', `${config.org_url}/.default`);21 params.append('grant_type', 'client_credentials');2223 const response = await axios.post(tokenUrl, params);24 return res.json({25 access_token: response.data.access_token,26 expires_in: response.data.expires_in27 });28 } catch (err) {29 console.error('Token error:', err.response?.data || err.message);30 return res.status(500).json({ error: 'Failed to obtain token' });31 }32});33Pro tip: The OAuth scope must match your exact Dynamics org URL with `/.default` appended — e.g. `https://contoso.crm.dynamics.com/.default`. A wrong or mismatched scope returns `invalid_scope` and the token request fails silently.
Expected result: Calling the Cloud Function URL in a browser returns a JSON object with `access_token` and `expires_in`. The token is approximately 3,600 seconds (1 hour).
Create the Dataverse API Group in FlutterFlow
Open your FlutterFlow project. In the left navigation panel, click 'API Calls'. At the top right of the API Calls panel, click '+ Add' → 'Create API Group'. Name the group 'Dynamics365' and set the Base URL to your Dynamics org's Dataverse endpoint: `https://yourorg.crm.dynamics.com/api/data/v9.2`. Under 'Headers', add three shared headers that every Dataverse call needs: - `Authorization`: leave the value as a variable for now — you'll pass the token here - `Accept`: `application/json` - `OData-MaxVersion`: `4.0` To make the bearer token injectable, click 'Variables' in the API Group header section, add a variable named `bearerToken` of type String. Then update the Authorization header value to `Bearer [bearerToken]` using FlutterFlow's double-bracket variable syntax. Now add individual API Calls inside this group: 1. Click '+ Add API Call' → name it `getAccounts`, Method GET, Endpoint Path `/accounts?$select=name,accountid,telephone1&$top=50`. 2. Add another → `createLead`, Method POST, Endpoint Path `/leads`. In the Body tab, set Content-Type `application/json` and add a JSON body template with variables for `firstname`, `lastname`, `emailaddress1`, `companyname`. 3. Add `getLeads`, Method GET, Endpoint `/leads?$select=fullname,emailaddress1,leadid&$orderby=createdon desc&$top=25`. Click the 'Response & Test' tab on `getAccounts`. Paste a sample Dynamics response (with the `value` array) and click 'Generate JSON Paths'. FlutterFlow will auto-detect paths like `$.value[0].name` and `$.value[0].accountid`.
1{2 "method": "GET",3 "endpoint": "/accounts?$select=name,accountid,telephone1,emailaddress1&$top=50",4 "headers": {5 "Authorization": "Bearer [bearerToken]",6 "Accept": "application/json",7 "OData-MaxVersion": "4.0"8 }9}10Pro tip: Dynamics wraps all collection responses in a `value` array — so your JSON Path for Account names is `$.value[*].name`, not `$.name`. Always set the `OData-MaxVersion: 4.0` header or Dataverse may fall back to legacy OData v3 behavior.
Expected result: The FlutterFlow API Calls panel shows a 'Dynamics365' group with getAccounts, createLead, and getLeads calls. Testing getAccounts in the Response & Test tab returns a 200 with account records under the `value` key.
Wire token fetching to App State and call the Cloud Function on app start
Because the bearer token expires every hour, your FlutterFlow app needs to fetch a fresh token from your Firebase Cloud Function when it starts, and optionally refresh it before expiry. First, add a separate API Call for the token endpoint. In the API Calls panel, click '+ Add' → 'Create API Group' → name it 'TokenService'. Set the Base URL to your Firebase Cloud Function's HTTPS trigger URL (e.g. `https://us-central1-your-project.cloudfunctions.net/getDynamicsToken`). Add a single API Call named `getToken` with Method GET and no additional parameters. Next, set up App State. Go to 'App Values' in the left nav (the puzzle-piece icon) → 'App State'. Add a String variable named `dynamicsToken` (initial value empty). This variable will hold the current bearer token. Now wire the token fetch to app launch. Click on your initial page (e.g. HomePage) in the left nav. Open the Actions panel. Add an Action on the 'On Page Load' trigger. Choose 'Backend/API Call' → select the `getToken` API Call from TokenService group. In the 'Action Output Variable' section, map the response JSON Path `$.access_token` to the App State variable `dynamicsToken`. Finally, go back to your Dynamics365 API Group. In the group-level 'Variables' section, the `bearerToken` variable should be set to 'App State → dynamicsToken' when you make any API call. In the Action Flow Editor for any widget that calls a Dynamics endpoint, pass `App State → dynamicsToken` as the `bearerToken` argument to the API Call.
Pro tip: If the user navigates away and returns after more than an hour, the token may have expired. Add a second 'On Page Load' action on frequently-used screens that re-calls `getToken` and updates App State — this costs just one Cloud Function invocation and avoids 401 errors mid-session.
Expected result: When you click 'Test' in FlutterFlow Run mode, the app fetches a token on load and stores it in App State. Inspecting the App State variable shows a non-empty JWT string.
Build an Accounts ListView and a create-lead form
Now wire the Dynamics data to your UI. On your main page, drag a 'ListView' widget from the widget panel onto the canvas. Select the ListView and open its 'Backend Query' panel. Set the 'Query Type' to 'API Call', choose the `getAccounts` call from the Dynamics365 group. Under 'Variables', pass `App State → dynamicsToken` as the `bearerToken`. Under 'Response JSON Path', select `$.value` so the ListView iterates over the accounts array. Inside the ListView, add child widgets: a Text widget bound to `$.name` for the account name and a second Text for `$.telephone1`. Run the app in FlutterFlow Run mode — you should see your Dynamics Accounts populate the list. For the create-lead form, add a new page called 'CreateLeadPage'. Drag a Column widget onto the canvas and add TextField widgets for First Name, Last Name, Email, and Company Name. Add a Button widget labeled 'Submit'. In the Button's Actions panel, add a 'Backend/API Call' action pointing to the `createLead` API Call. Map each TextField's text value to the corresponding API Call variable: `firstname`, `lastname`, `emailaddress1`, `companyname`. Pass `App State → dynamicsToken` as `bearerToken`. In the Action Flow Editor, after the API Call succeeds, add a 'Show Snack Bar' action with the message 'Lead created in Dynamics 365!' and a 'Navigate Back' action to return to the main page. For error handling, add a Conditional Action — if the API response status code is not 200–204, show an Alert Dialog with the error message. The Dataverse API returns 204 No Content on successful POST (not 200), so check for status 204 rather than expecting a response body.
Pro tip: Dynamics 365 returns lookup fields (like owner or account) as `_fieldname_value` GUID strings rather than human-readable names. If you need the display name, use OData `$expand` or `$select` with formatted values by adding the header `Prefer: odata.include-annotations="OData.Community.Display.V1.FormattedValue"`.
Expected result: The Accounts list populates with real Dynamics data in Run mode. Submitting the create-lead form results in a 204 response and a new lead visible in your Dynamics 365 Sales Hub Leads view.
Handle 401 token expiry and 429 service-protection limits
Two error conditions will affect production usage of the Dynamics 365 integration: token expiry (401 Unauthorized) and service-protection throttling (429 Too Many Requests). For 401 handling: in the Action Flow Editor on any API Call that reads Dynamics data, add a Conditional Action after the API Call. Check 'API Call Response → Status Code equals 401'. If true, call the `getToken` API Call to refresh the token, update `App State → dynamicsToken`, then retry the original Dynamics API Call. This silent refresh pattern keeps the user experience smooth without a visible login screen. For 429 handling: Dataverse returns HTTP 429 with a `Retry-After` header when you exceed roughly 6,000 requests per 5-minute window per user (verify current limits in Microsoft's documentation). In FlutterFlow, you can't read HTTP headers directly from an API Call response, so the practical approach is to avoid high-frequency polling. Set auto-refresh intervals no faster than every 30 seconds on any ListView that calls Dynamics, and show a user-friendly 'Too many requests — please wait a moment' message when you detect a 429 status code. For web builds of your FlutterFlow app, note that CORS must be handled at the server level — the Dataverse API itself may block cross-origin requests from a browser. Your Firebase Cloud Function already sets CORS headers in the example above, so token fetching is fine. However, if your Dynamics API Calls go directly from the browser to Dataverse, you may hit a CORS 'XMLHttpRequest error'. In that case, proxy ALL Dataverse calls through the Cloud Function (not just the token endpoint) for web builds. If this is a mobile-only app (iOS/Android), CORS is not a concern. If you'd rather skip building and maintaining the Cloud Function proxy yourself, RapidDev's team builds FlutterFlow integrations like this every week — get a free scoping call at rapidevelopers.com/contact.
Pro tip: Track token expiry by storing the `expires_in` value from the token response alongside `dynamicsToken` in App State. Add a simple timestamp comparison on each API call to proactively refresh tokens before they expire, avoiding the reactive 401 path entirely.
Expected result: When a 401 occurs, the app silently re-fetches a token and retries the call without user intervention. When a 429 occurs, the app shows a friendly message rather than crashing or showing a raw error.
Common use cases
Field sales rep mobile CRM — view and create leads on the go
A FlutterFlow mobile app lets outside sales reps browse their Account list, drill into Account details, and tap a button to create a new Lead — all synced live to Dynamics 365. The Cloud Function handles authentication silently so reps never log in twice.
Build a Flutter mobile app with a home screen showing a scrollable list of Dynamics 365 Accounts pulled from the Dataverse API. Tapping an Account opens a detail view. A floating action button opens a form to create a new Lead that posts to Dynamics 365.
Copy this prompt to try it in FlutterFlow
Service technician dispatch app — read open cases
A Flutter app for field technicians reads open Service Cases from Dynamics 365 Customer Service, filtered by `$filter=statuscode eq 1` and ordered by scheduled date. Technicians can update the status to 'In Progress' directly from the app, reducing dispatcher calls.
Build a Flutter app that fetches open Dynamics 365 service cases using a Dataverse OData query, displays them in a card list sorted by due date, and lets the technician tap 'Start' to PATCH the case status to In Progress.
Copy this prompt to try it in FlutterFlow
Conference lead-capture app — push scanned contacts into Dynamics
A FlutterFlow app at trade shows lets staff enter lead information into a form; on submission, the app POSTs a new Lead record to Dynamics 365 via the Dataverse API, with the source field automatically set to 'Trade Show'. All leads appear instantly in the sales team's Dynamics pipeline.
Build a lead-capture Flutter form with fields for first name, last name, email, company, and phone. On submit, POST the data as a new Lead to Dynamics 365 Dataverse and show a success confirmation screen.
Copy this prompt to try it in FlutterFlow
Troubleshooting
Token request returns `invalid_scope` or empty `access_token`
Cause: The OAuth scope in the Cloud Function does not exactly match your Dynamics 365 org URL. The scope must be the org URL with `/.default` appended.
Solution: In your Cloud Function, verify the scope is set to `https://yourorg.crm.dynamics.com/.default` where `yourorg.crm.dynamics.com` exactly matches your organization URL shown in the Dynamics 365 Power Platform admin center. A trailing slash or wrong subdomain will cause this error.
Dataverse API returns 403 Forbidden even with a valid token
Cause: The Azure app registration has not been granted admin consent, or the Dynamics 365 user associated with the app does not have the required security roles in the Dynamics environment.
Solution: In Azure portal → App registrations → your app → API permissions, confirm 'Grant status' shows 'Granted' (green tick) for the Dynamics CRM permission. Then in Dynamics 365 Admin Center, go to Environments → your org → Settings → Security → Application Users, add your Azure app as an Application User, and assign it the 'System Administrator' or a custom security role with the necessary read/write privileges.
FlutterFlow ListView shows empty — accounts/leads not appearing
Cause: The JSON Path in the Backend Query is set to `$` or `$.results` instead of `$.value`. Dataverse wraps all collection responses in a `value` array, not `results` or the entity name.
Solution: In the FlutterFlow API Call → Response & Test tab, paste a real Dynamics response and click 'Generate JSON Paths'. Set the ListView's Response JSON Path to `$.value`. For individual field bindings inside the ListView child, use paths like `$.name`, `$.accountid`, etc. (FlutterFlow applies these relative to each `value` item).
PATCH or POST to Dataverse returns 412 Precondition Failed
Cause: Dataverse requires an `If-Match: *` header for PATCH updates, or you are sending a PATCH without the correct Content-Type.
Solution: For PATCH (update) calls, add the header `If-Match: *` to the API Call in FlutterFlow. Set Content-Type to `application/json`. For POST (create), no If-Match is needed. Also verify the request body does not include read-only fields like `createdon` or `modifiedon` — Dataverse rejects writes to system-managed fields.
Best practices
- Never embed the Azure AD client secret or tenant ID in a FlutterFlow API Call, App State, or custom action — always keep them in Firebase Cloud Function environment variables where they cannot be extracted from the app bundle.
- Cache the bearer token in App State and reuse it for the full ~60-minute lifespan rather than fetching a new one on every API call — each token request counts against Azure AD rate limits and adds 200-400ms of latency.
- Use OData `$select` on every Dataverse query to fetch only the columns you display. Dynamics tables often have 200+ fields; pulling all of them wastes bandwidth and hits service-protection limits faster.
- Store the token expiry timestamp in App State alongside the token and check it before each API call so you can proactively refresh rather than reactively handling 401 errors mid-user-flow.
- Respect the Dataverse service-protection 429 Retry-After value — implement exponential backoff or a simple 'please wait' UI rather than immediately retrying, especially for operations triggered by user taps that could fire rapidly.
- Test on an actual device (iOS or Android) rather than only in FlutterFlow Run mode — Run mode can mask platform-level networking behaviors, and some OData header handling differs between the web preview and a real Flutter app.
- Use a dedicated Dynamics 365 application user (system account) for the Cloud Function's client-credentials token rather than a licensed human user account — application users don't consume named licenses and their credentials don't expire with password resets.
- For navigation-property lookups (e.g. resolving an account's owner to a user name), use `$expand` in a single API call rather than making a second round-trip lookup — this reduces both latency and API call count.
Alternatives
Choose Salesforce if your organization already uses Sales Cloud and you need SOQL-based querying and a large ecosystem of integrations — Salesforce has a larger third-party app marketplace than Dynamics 365.
Choose Zoho CRM if cost is a priority — Zoho CRM's free tier and lower paid plans make it significantly more affordable than Dynamics 365 for small teams, with a similarly straightforward REST API.
Choose HubSpot if you want a simpler API with a generous free tier and no need for a proxy layer — HubSpot's API uses a plain Bearer token with no Azure AD complexity.
Frequently asked questions
Does FlutterFlow have a native Dynamics 365 connector?
No. FlutterFlow does not have a built-in Dynamics 365 integration. You connect using FlutterFlow's API Calls panel, which lets you configure REST endpoints manually. Because Azure AD OAuth requires a client secret, you'll also need a Firebase Cloud Function to handle the token exchange securely.
Can I use Dynamics 365 with a FlutterFlow web app (not just mobile)?
Yes, but you need to be careful about CORS. Native mobile apps (iOS/Android) bypass CORS entirely. FlutterFlow web builds run in a browser, which enforces CORS. If the Dataverse API blocks your browser's origin, route all Dataverse calls through the Firebase Cloud Function, not just the token endpoint. The Cloud Function sets CORS headers and acts as a full proxy.
What happens when the bearer token expires mid-session?
Azure AD tokens expire after approximately 3,600 seconds (1 hour). When a Dynamics API Call returns 401, build an Action Flow in FlutterFlow that re-calls your `getToken` Cloud Function endpoint, updates the `dynamicsToken` App State variable, and retries the original API Call. Storing the token expiry timestamp lets you proactively refresh before the error occurs.
How do I filter Dynamics 365 records in FlutterFlow?
The Dataverse API uses OData v4 query parameters. Add `$filter` to your API Call endpoint path — for example, `/leads?$filter=statuscode eq 1&$select=fullname,emailaddress1`. In FlutterFlow, you can make the filter value a Variable so the user can change it dynamically (e.g. filtering by status or assigned user). Use `$orderby` for sorting and `$top` to limit results.
Can I use Dynamics 365 on FlutterFlow's free plan?
FlutterFlow's free plan does not include API Calls, which are required for this integration. You need at least the Starter plan. Firebase Cloud Functions also require the Blaze (pay-as-you-go) plan, though usage within free quotas is generally sufficient for low-traffic integrations.
What Dynamics 365 license do I need for API access?
You need a Dynamics 365 application that includes the Dataverse — for example, Dynamics 365 Sales, Customer Service, or a Power Platform license with Dataverse capacity. Dynamics 365 Sales starts at around $65 per user per month; verify current pricing at microsoft.com/en-us/dynamics-365/pricing. A Dynamics 365 trial environment works for development and testing.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation