Connect Bubble to Microsoft Dynamics 365 using Azure AD OAuth 2.0 and OData v4 query syntax. The key challenge: Azure AD requires admin consent for the 'Dynamics CRM user_impersonation' permission — without it, OAuth silently fails regardless of correct credentials. Queries use URL parameters ($filter, $select, $expand) instead of SQL, and all four OData protocol headers are required on every call.
| Fact | Value |
|---|---|
| Tool | Microsoft Dynamics 365 |
| Category | CRM & Sales |
| Method | Bubble API Connector |
| Difficulty | Advanced |
| Time required | 4–6 hours |
| Last updated | July 2026 |
The Azure AD admin consent requirement that silently blocks Dynamics 365 integrations in Bubble
Microsoft Dynamics 365 integration in Bubble involves two layers of complexity that many tutorials skip: Azure AD's permission model and OData v4's query syntax.
On the authentication side, Dynamics 365 uses Azure Active Directory for all API access. To connect a Bubble app to Dynamics, you register an App Registration in the Azure portal and request the 'Dynamics CRM user_impersonation' delegated permission. Here is the critical point: individual users cannot grant themselves this permission. An Azure AD administrator must click 'Grant admin consent' in the Azure portal. Without admin consent, the OAuth authorization flow appears to work — users can log in and grant permissions — but the resulting access token is rejected by Dynamics 365's API with an AADSTS error. Many developers spend hours troubleshooting credentials that are actually correct, when the real issue is missing admin consent.
If you are building this integration for your own organization, get admin consent first before writing any Bubble workflows. If you are building for a client's organization, you need their IT department or Azure AD admin to grant consent on their tenant.
On the query side, Dynamics 365 uses OData v4 — a REST protocol where query operations are expressed as URL parameters, not a query body. Instead of SQL or SOQL, you write: `$filter=statuscode eq 1 and revenue gt 1000000` and `$select=name,emailaddress1,telephone1` as separate URL parameters. Every Dynamics API call also requires four specific OData protocol headers (`OData-MaxVersion: 4.0`, `OData-Version: 4.0`, `Accept: application/json`, `Content-Type: application/json`) — missing any of them causes request failures.
Integration method
API Connector with Azure AD OAuth 2.0 Bearer tokens (client credentials or authorization code), four required OData protocol headers, and OData v4 URL parameters ($filter, $select, $expand, $top) for querying Dynamics entities.
Prerequisites
- A Microsoft Dynamics 365 subscription (Sales, Service, or Customer Insights) with API access enabled
- Access to the Azure portal (portal.azure.com) to create an App Registration — or a relationship with the organization's Azure AD administrator who can do this for you
- Azure AD administrator permission to grant admin consent for Dynamics CRM user_impersonation — required before any OAuth flow works
- A Bubble app on Starter plan or above — Backend Workflows are required for OAuth token refresh
- The API Connector plugin installed in your Bubble app (Plugins tab → Add plugins → search 'API Connector' by Bubble)
Step-by-step guide
Create an Azure AD App Registration and grant admin consent
Open portal.azure.com and sign in with a Microsoft account that has access to the Azure Active Directory tenant for your organization. In the left sidebar, search for 'App registrations' and click it. Click '+ New registration.' Fill in the registration form: - Name: `Bubble Dynamics Integration` - Supported account types: 'Accounts in this organizational directory only' (single-tenant) - Redirect URI: select 'Web' and enter your Bubble app's OAuth callback URL: `https://yourapp.bubbleapps.io/oauth_callback` Click 'Register.' You will see the Overview page with your Application (client) ID and Directory (tenant) ID — copy both. Now add the Dynamics API permission. Click 'API permissions' in the left sidebar. Click '+ Add a permission.' Click 'Dynamics CRM' (scroll down or search for it). Select 'Delegated permissions.' Check `user_impersonation`. Click 'Add permissions.' You should now see 'Dynamics CRM user_impersonation' in the permissions list with status 'Not granted for [your tenant].' Here is the critical step: click 'Grant admin consent for [your organization].' Only a Global Administrator or Application Administrator can click this button. If you see it greyed out, you do not have this role — contact your Azure AD admin. After admin consent is granted, the status changes to 'Granted for [your tenant]' with a green checkmark. Finally, create a client secret. Click 'Certificates & secrets' → '+ New client secret.' Set an expiry (24 months is recommended). Click 'Add' and IMMEDIATELY copy the secret Value — it is only shown once.
1// Azure AD App Registration details to collect:2// Application (client) ID → used as client_id in OAuth3// Directory (tenant) ID → used in token URL4// Client Secret Value → used as client_secret (copy immediately)56// Token endpoint URL format:7// https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token89// Authorization URL format (for authorization_code flow):10// https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize11// ?client_id={client_id}12// &response_type=code13// &redirect_uri=https://yourapp.bubbleapps.io/oauth_callback14// &scope=https://orgname.crm.dynamics.com/user_impersonation offline_access15// &response_mode=query1617// Required permission:18// API: Dynamics CRM19// Permission: user_impersonation (delegated)20// Admin consent: REQUIRED — must be granted by Global Admin or App AdminPro tip: The Dynamics CRM 'user_impersonation' permission allows your app to access Dynamics 365 on behalf of a signed-in user. Without admin consent, even a correctly configured OAuth flow produces tokens that the Dynamics API rejects. This is the single most common reason Dynamics 365 integrations fail silently.
Expected result: Azure AD App Registration is created with client_id, tenant_id, and client_secret. The 'Dynamics CRM user_impersonation' delegated permission shows 'Granted for [your organization]' status with a green checkmark. Admin consent is confirmed.
Set up the Dynamics 365 data type and token storage in Bubble
Before configuring the API Connector, create the data infrastructure for storing Azure AD tokens in Bubble. In the Data tab, add a data type named `Dynamics_Auth` with these fields: - `access_token` (text) - `refresh_token` (text) - `org_url` (text) — the Dynamics 365 environment URL (e.g., `https://yourorg.crm.dynamics.com`) - `token_expiry` (date) - `user` (User) Apply a privacy rule to Dynamics_Auth: 'Everyone else → no fields visible.' Enable 'Ignore privacy rules when using workflows.' Create an `oauth_callback` page in Bubble if you do not have one from another integration. This page will receive the `?code=` URL parameter from Azure AD after user authentication. Now go to Backend Workflows and create two workflows: Workflow 1 — `Exchange Dynamics Code`: Input parameter `code` (text). Steps: (1) Call API Connector → Dynamics → Exchange Code for Token (create in next step). (2) Create or update Dynamics_Auth record: access_token, refresh_token, org_url (from response), token_expiry = Current date/time + 3540 seconds, user = Current User. Workflow 2 — `Refresh Dynamics Token`: No inputs. Steps: (1) Call API Connector → Dynamics → Refresh Token using Current User's Dynamics_Auth refresh_token. (2) Update Current User's Dynamics_Auth: new access_token and new token_expiry.
1// Dynamics_Auth data type:2// access_token → text3// refresh_token → text4// org_url → text (e.g., 'https://yourorg.crm.dynamics.com')5// token_expiry → date6// user → User78// Privacy: Everyone else → no fields visible9// ✓ Ignore privacy rules when using workflows1011// Backend Workflow: Exchange Dynamics Code12// Input: code (text)13// Step 1: API Connector → Dynamics → Exchange Code for Token14// code: input code15// client_id: [Private]16// client_secret: [Private]17// tenant_id: [Private]18// redirect_uri: https://yourapp.bubbleapps.io/oauth_callback19// Step 2: Create Dynamics_Auth20// access_token = Step1.access_token21// refresh_token = Step1.refresh_token22// org_url = Step1.resource (or hardcode your org URL)23// token_expiry = Current date/time + 3540 seconds24// user = Current UserPro tip: Your Dynamics 365 org URL (e.g., `https://yourorg.crm.dynamics.com`) is found in Dynamics 365 → Settings → Customization → Developer Resources. It is different from the Microsoft 365 admin center URL. Store it as a hardcoded config value in Bubble's App Settings rather than extracting it from the OAuth response, since it never changes for a given org.
Expected result: Dynamics_Auth data type exists with privacy rules applied. Exchange Dynamics Code and Refresh Dynamics Token Backend Workflows are created with the appropriate steps outlined.
Configure the Dynamics 365 API Connector with all required OData headers
In Plugins → API Connector, click 'Add another API' and name it `Microsoft Dynamics 365`. Set the base URL to your Dynamics 365 environment: `https://yourorg.crm.dynamics.com/api/data/v9.2`. Replace `yourorg` with your actual organization name (the subdomain of your Dynamics URL). Add four shared headers — ALL FOUR are required for every OData call: 1. `OData-MaxVersion: 4.0` 2. `OData-Version: 4.0` 3. `Accept: application/json` 4. `Content-Type: application/json` Add a fifth shared header for authentication: 5. `Authorization: Bearer <access_token>` — mark as Private dynamic parameter Now add the key API calls: Call 1 — Exchange Code for Token: Method POST. URL: `https://login.microsoftonline.com/<tenant_id>/oauth2/v2.0/token`. Body (form-encoded): `grant_type=authorization_code`, `code=<code>`, `client_id=<client_id>` (Private), `client_secret=<client_secret>` (Private), `scope=https://yourorg.crm.dynamics.com/user_impersonation offline_access`, `redirect_uri=https://yourapp.bubbleapps.io/oauth_callback`. Set as Action. Call 2 — Refresh Token: Method POST. Same token URL. Body: `grant_type=refresh_token`, `refresh_token=<refresh_token>` (Private), `client_id=<client_id>` (Private), `client_secret=<client_secret>` (Private). Set as Action. Call 3 — Get Contacts: Method GET. URL: `/contacts`. URL parameters: `$select=fullname,emailaddress1,telephone1,accountid`, `$filter=<filter_expression>` (dynamic), `$top=50`, `$orderby=createdon desc`. Set as Data. Initialize call — should return a response with a `value` array.
1// Shared headers (ALL REQUIRED on every Dynamics call):2// OData-MaxVersion: 4.03// OData-Version: 4.04// Accept: application/json5// Content-Type: application/json6// Authorization: Bearer <access_token> [Private dynamic param]78// Call 3: Get Contacts9{10 "method": "GET",11 "url": "https://yourorg.crm.dynamics.com/api/data/v9.2/contacts",12 "params": {13 "$select": "fullname,emailaddress1,telephone1,telephone2,accountid",14 "$filter": "<filter_expression>", // e.g., contains(fullname,'Smith')15 "$top": "50",16 "$orderby": "createdon desc"17 }18}19// Response: { "@odata.context": "...", "value": [ { "fullname": "...", ... } ] }20// Bubble binding: use 'value' array as list data source2122// OAuth scope for Dynamics:23// https://yourorg.crm.dynamics.com/user_impersonation offline_access24// The org URL MUST match your actual Dynamics environment URL exactly25// Generic Microsoft scopes (like https://graph.microsoft.com) do NOT work for DynamicsPro tip: The OAuth scope MUST use your Dynamics org URL as the resource: `https://yourorg.crm.dynamics.com/user_impersonation`. Using generic Microsoft scopes like `https://management.azure.com/.default` or `https://graph.microsoft.com/.default` returns tokens that Dynamics 365 immediately rejects with a 401.
Expected result: The Dynamics 365 API Connector is configured with all five required headers. The Get Contacts call initializes successfully and shows the `value` array in the response. Exchange Code and Refresh Token calls are set as Actions.
Build OData queries for Dynamics entities and display in Bubble
OData v4 query syntax expresses filtering, field selection, sorting, and joins entirely through URL parameters — not a query body like SQL or SOQL. Here is how to build common Dynamics queries in Bubble's API Connector: **$select** — specify which fields to return (always include this to avoid huge responses): `$select=name,emailaddress1,revenue,statuscode` **$filter** — filter records (OData operators: `eq`, `ne`, `gt`, `lt`, `ge`, `le`, `and`, `or`, `contains`, `startswith`, `endswith`): `$filter=statuscode eq 1 and revenue gt 1000000` `$filter=contains(name,'Microsoft')` `$filter=statecode eq 0` (0 = active, 1 = inactive for most entities) **$expand** — join related entities (like SQL JOIN): `$expand=primarycontactid($select=fullname,emailaddress1)` — expands the primary contact on an account **$orderby** and **$top**: `$orderby=createdon desc&$top=50` Dynamics 365 entity names (used in the URL path): - Contacts: `/contacts` - Accounts: `/accounts` - Leads: `/leads` - Opportunities: `/opportunities` - Cases: `/incidents` In Bubble, set each OData parameter as a URL parameter in the API Connector call. Bubble's text composition in parameter values supports building dynamic filter expressions. For PATCH (update) operations: the URL is `/contacts(guid)` where the GUID is in parentheses, NOT as a path segment. Dynamics GUIDs look like `3d86a63b-a5a4-ed11-81a9-002248e89db3`. PATCH returns HTTP 204 with an empty body — Bubble will not throw an error but you get no confirmation JSON. Handle this with a subsequent GET call to verify the update if needed.
1// Common OData queries for Dynamics 365 entities:23// Get active accounts with $expand for primary contact:4// GET /accounts?$select=name,telephone1,revenue,websiteurl&$filter=statecode eq 0&$expand=primarycontactid($select=fullname,emailaddress1)&$top=50&$orderby=revenue desc56// Get opportunities in active stage:7// GET /opportunities?$select=name,estimatedvalue,stepname,closeddateestimate,parentaccountid&$filter=statecode eq 0&$orderby=closeddateestimate asc&$top=10089// Search contacts by name:10// GET /contacts?$select=fullname,emailaddress1,telephone1&$filter=contains(fullname,'<search_term>')&$top=201112// Create a new Lead:13// POST /leads14// Body: { "firstname": "Jane", "lastname": "Smith", "emailaddress1": "jane@company.com", "companyname": "Acme Corp" }1516// Update contact email (PATCH returns 204, no body):17// PATCH /contacts(3d86a63b-a5a4-ed11-81a9-002248e89db3)18// Body: { "emailaddress1": "new@email.com" }1920// Dynamics entity plural names:21// contacts, accounts, leads, opportunities, incidents (cases), tasks, phonecallsPro tip: When using $expand to join related entities, Bubble's Initialize call must return a non-empty record that includes the expanded relationship. If the first matching record has a null relationship (e.g., an account with no primary contact), Bubble cannot detect the expanded fields. Use a $filter that selects a record you know has all relationships populated for initialization.
Expected result: Repeating Groups in your Bubble app display live Dynamics 365 contacts, accounts, and opportunities filtered by OData expressions. Create and PATCH operations successfully modify records in Dynamics 365. The dashboard updates to reflect changes without page reloads.
Wire the Azure AD login flow and token refresh
Create the user-facing login flow that connects each Bubble user to their Dynamics 365 account. Add a 'Connect Dynamics 365' button to your onboarding or settings page. When clicked, it redirects to the Azure AD authorization URL: ``` https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize?client_id={client_id}&response_type=code&redirect_uri=https://yourapp.bubbleapps.io/oauth_callback&scope=https://yourorg.crm.dynamics.com/user_impersonation+offline_access&response_mode=query ``` On your `oauth_callback` page, add a Page is loaded workflow: - Condition: Get URL parameter 'code' is not empty - Action: Schedule API Workflow → Exchange Dynamics Code (at Current date/time), passing `code` = Get URL parameter 'code' - After workflow: Navigate to main dashboard page For token refresh in every workflow that calls Dynamics: 1. Check: Is Current User's Dynamics_Auth token_expiry < Current date/time? OR Is Dynamics_Auth empty? 2. If yes: Schedule API Workflow → Refresh Dynamics Token (at Current date/time) 3. Then proceed with the Dynamics API call using the stored access_token For the `oauth_callback` page to also handle errors (user cancelled or consent denied), add a second condition: - Condition: Get URL parameter 'error' is not empty - Action: Navigate to login page, passing error message as URL parameter for display RapidDev's team has experience with enterprise Azure AD integrations in Bubble — visit rapidevelopers.com/contact if you need help navigating your organization's Azure AD tenant requirements.
1// Azure AD Authorization URL:2https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize3 ?client_id={client_id}4 &response_type=code5 &redirect_uri=https://yourapp.bubbleapps.io/oauth_callback6 &scope=https://yourorg.crm.dynamics.com/user_impersonation+offline_access7 &response_mode=query89// oauth_callback page workflow:10// Trigger: Page is loaded11// Case 1: code param exists12// Action: Schedule API Workflow → Exchange Dynamics Code13// code = Get URL parameter 'code'14// Then: Navigate to /dashboard15// Case 2: error param exists16// Action: Navigate to /login?msg=dynamics_auth_failed1718// Token refresh check in all Dynamics workflows:19// Condition: Dynamics_Auth is empty OR token_expiry < Current date/time20// Action: Schedule API Workflow → Refresh Dynamics Token21// Wait: Add a 2-second pause or chain the next action after the Backend Workflow2223// Then call Dynamics API with:24// Authorization header: Bearer + Current User's Dynamics_Auth access_tokenPro tip: Azure AD access tokens typically expire in 3,600 seconds (1 hour), but this can be configured differently by your organization's Azure AD admin. Store token_expiry with a 60-second buffer (3540 seconds) to ensure proactive refresh. If users report unexpected 401 errors, check the token lifetime policy in Azure AD.
Expected result: Clicking 'Connect Dynamics 365' takes users through Azure AD authentication and returns them to your Bubble app with tokens stored. Dynamics 365 data appears in your Bubble pages. Token refresh runs automatically when needed.
Common use cases
Dynamics 365 contact lookup portal for field teams
Build a mobile-optimized Bubble app that lets field teams search Dynamics 365 contacts by name, company, or phone number without navigating the full Dynamics UI. Results load in a clean list with click-to-call links and quick-update forms for adding interaction notes.
Create a Bubble search page with a Text Input for contact name. On input change, call the Dynamics API Connector with $filter=contains(fullname, 'search_term') and $select=fullname,emailaddress1,telephone1,accountid. Show results in a Repeating Group. Each row has a 'Log Call' button that PATCHes the contact record with a note and last-contacted date.
Copy this prompt to try it in Bubble
Sales pipeline reporting dashboard
Pull live Dynamics 365 opportunity data into a Bubble analytics dashboard that calculates pipeline value by stage, average deal size, and win rate — without requiring Dynamics Power BI licensing. The OData $expand parameter joins opportunity records with account data in a single API call.
On page load, query Dynamics Opportunities with $filter=statecode eq 0 and $expand=customerid_account($select=name,revenue) and $select=name,estimatedvalue,stepname,closeddateestimate. Group by stepname in Bubble using :filtered and display total value per stage in a Repeating Group with :sum.
Copy this prompt to try it in Bubble
New account creation form for Microsoft-ecosystem teams
Provide non-technical team members a simple Bubble form to create new Account records in Dynamics 365 — without Dynamics licenses for every user. The form validates required fields before submission and provides immediate confirmation with the new account ID.
When a user submits the new account form, POST to Dynamics /accounts with name, telephone1, emailaddress1, websiteurl, and industrycode. On success, show the new account's full name and link back to the Dynamics portal for the admin to complete additional fields.
Copy this prompt to try it in Bubble
Troubleshooting
OAuth flow completes but Dynamics API calls return AADSTS or 401 errors immediately
Cause: Admin consent was not granted for the Dynamics CRM user_impersonation permission in the Azure AD App Registration. The OAuth flow can complete and issue tokens even without admin consent, but those tokens are rejected by the Dynamics 365 API with AADSTS permission errors.
Solution: Go to Azure portal → App Registrations → your Bubble app → API permissions. Check the status of the 'Dynamics CRM user_impersonation' permission. If it shows 'Not granted for [tenant]' or a warning icon, click 'Grant admin consent for [your org].' This requires Global Administrator or Application Administrator role. If you see the button greyed out, contact your organization's Azure AD administrator.
'There was an issue setting up your call' when initializing the Get Contacts call
Cause: The Initialize call requires a valid access token in the Authorization header. Placeholder text, expired tokens, or incorrect token format cause Dynamics to return 401, which Bubble interprets as a setup failure. Also, missing OData headers can cause the initialization to fail.
Solution: Ensure all four OData protocol headers are added as shared headers (OData-MaxVersion, OData-Version, Accept, Content-Type). Temporarily paste a fresh valid access token directly into the Authorization header value for initialization. Test with a simple query: `GET /contacts?$select=fullname&$top=1`. Once initialized, switch back to the dynamic access_token parameter.
1// Required shared headers (missing any causes failures):2// OData-MaxVersion: 4.03// OData-Version: 4.04// Accept: application/json5// Content-Type: application/json6// Authorization: Bearer <access_token> [Private]PATCH update returns 204 but changes do not appear in Dynamics
Cause: HTTP 204 means the request was accepted but the changes may not have propagated to Dynamics 365 instantly, especially in organizations with business rules or plugins that process updates asynchronously. Alternatively, the GUID in the URL was malformatted.
Solution: Verify the GUID format in the URL: Dynamics requires GUIDs in parentheses — `/contacts(3d86a63b-a5a4-ed11-81a9-002248e89db3)` — not as a path segment or query parameter. If the format is correct, add a brief delay (2-3 seconds) before re-querying the record to confirm the update. Check Dynamics 365 Server-Side Plugins in the system settings if records consistently fail to update despite 204 responses.
1// Correct GUID format in URL:2// PATCH /contacts(3d86a63b-a5a4-ed11-81a9-002248e89db3)34// Wrong formats:5// PATCH /contacts/3d86a63b-a5a4-ed11-81a9-002248e89db3 (no parentheses)6// PATCH /contacts?id=3d86a63b-a5a4-ed11-81a9-002248e89db3 (query param)OData $filter query returns empty results despite matching records existing in Dynamics
Cause: OData filter expressions are case-sensitive for string values, and the logged-in Azure AD user may not have permission to view the records being queried due to Dynamics security roles. Also, the filter operator syntax may be incorrect (using SQL operators like '=' instead of OData `eq`).
Solution: Verify OData operator syntax: equality is `eq` (not `=`), not-equal is `ne`, greater than is `gt`. String operators use function syntax: `contains(name,'value')`, `startswith(name,'value')`. Check the logged-in user's Dynamics 365 security role — they may not have read access to all records. Test the same filter in the Dynamics 365 Web API directly (Settings → Developer Resources → Organization Service) to verify the query syntax independently of Bubble.
1// OData filter operators:2// eq (equal): $filter=statuscode eq 13// ne (not eq): $filter=statecode ne 14// gt / lt: $filter=revenue gt 10000005// contains: $filter=contains(name,'Acme')6// startswith: $filter=startswith(emailaddress1,'admin')7// and / or: $filter=statecode eq 0 and statuscode eq 1Best practices
- Always include $select in every Dynamics API call to specify only the fields you need. Dynamics 365 entities have 100+ fields by default — fetching all of them is extremely wasteful. A contact with no $select returns over 100 fields; with $select=fullname,emailaddress1,telephone1 it returns 3.
- Store the access token in a Bubble data type with an expiry timestamp, and refresh proactively. Azure AD tokens expire in 3,600 seconds (1 hour) by default. A mid-session 401 disrupts user experience — checking expiry before each call and refreshing is far better than handling errors reactively.
- Apply Bubble privacy rules to Dynamics_Auth so regular users cannot read access tokens. Set 'Everyone else → no fields visible' and use 'Ignore privacy rules when using workflows' for Backend Workflows that need token access.
- All four OData protocol headers must be present on every API call. Missing OData-MaxVersion, OData-Version, Accept, or Content-Type causes intermittent and hard-to-diagnose failures. Add them as shared headers at the API Connector group level so they apply automatically to every call.
- Use $expand for related entity data rather than making separate API calls for each relationship. One call with $expand=customerid_account($select=name,revenue) returns opportunity data plus the related account name in a single request — saving Bubble WU and Dynamics API credits.
- Handle 204 responses for PATCH updates by design. Dynamics 365 returns HTTP 204 with no body on successful PATCH — do not expect confirmation JSON. If you need to verify an update, make a separate GET call to the updated record.
- Coordinate admin consent with your organization's IT department before starting development. Admin consent is a security gate that only Azure AD administrators can unlock — discovering this late in development can block deployment for days.
Alternatives
Salesforce uses Connected App OAuth (not Azure AD) and SOQL query language instead of OData v4. If your organization is not Microsoft-centric, Salesforce has a larger ecosystem of pre-built Bubble plugins and more API documentation for no-code integration. Choose Dynamics 365 if your organization already runs on Microsoft 365 and Azure AD, since Azure AD authentication integrates with existing identity.
Zoho CRM uses self-client OAuth (simpler, no browser redirect, no admin consent required) and COQL queries. It is significantly lower cost and faster to integrate in Bubble. Dynamics 365 is the better choice for Microsoft-ecosystem enterprises where Dynamics is already deployed and Azure AD is the identity provider.
Airtable uses simple API key authentication and has an intuitive REST API — dramatically easier to integrate with Bubble than Dynamics 365. For teams without an existing Dynamics investment, Airtable provides a highly customizable database that can mirror CRM functionality at a fraction of the complexity and cost.
Frequently asked questions
Can I use a service principal (client credentials flow) instead of user authentication?
Yes — for background processes or apps where individual user login is not required, the client credentials flow is possible. Instead of authorization_code with user login, POST to the token endpoint with grant_type=client_credentials, client_id, client_secret, and scope=https://yourorg.crm.dynamics.com/.default. The resulting token acts as a service account. This requires that a Dynamics 365 application user is created in Dynamics linked to your Azure AD app — your Dynamics admin must set this up in Dynamics 365 Settings → Security → Application Users.
Why does Dynamics 365 refuse my token even though Azure AD issued it successfully?
There are two common reasons: (1) Missing admin consent — the Dynamics CRM user_impersonation permission was not granted by an Azure AD admin, causing token acceptance by Azure AD but rejection by Dynamics 365. (2) Wrong scope — the OAuth scope in your authorization request must be `https://yourorg.crm.dynamics.com/user_impersonation`, not a generic Microsoft scope. The org URL in the scope must match your exact Dynamics environment URL.
What are Dynamics 365 entity names for API calls?
Common Dynamics 365 entity names used in API URL paths: contacts, accounts, leads, opportunities, incidents (cases), tasks, phonecalls, emails, systemusers (team members), teams, queues. Note that the URL uses plural entity names. Custom entities created by your organization end in `s` but use the Dynamics-generated logical name — find these in Dynamics 365 Settings → Customization → Developer Resources.
Can I use Dynamics 365 on Bubble's Free plan?
No. The Azure AD OAuth flow requires a Backend Workflow to handle the authorization code callback and token storage. Backend Workflows are only available on Bubble Starter plan and above. Without them, there is no way to securely capture and store the Azure AD tokens after the OAuth redirect.
How do I find a Dynamics 365 record's GUID for use in PATCH or DELETE calls?
Every Dynamics 365 record has a unique GUID returned in API responses as the entity's ID field (e.g., `contactid` for contacts, `accountid` for accounts, `opportunityid` for opportunities). When you display records in a Bubble Repeating Group, store the GUID in a Custom State when the user clicks a record. Use this stored GUID in the URL for subsequent PATCH or DELETE calls: `/contacts(<stored_guid>)` in parenthesis notation.
Does this integration work with both Dynamics 365 Sales and Dynamics 365 Customer Service?
Yes. Both modules share the same Dataverse API endpoint and Azure AD authentication. The entity types available depend on which Dynamics modules are licensed: Sales adds opportunities and leads; Customer Service adds cases (incidents) and entitlements. The OData query pattern, authentication, and Bubble API Connector setup are identical — only the entity names and available fields differ.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation