Skip to main content
RapidDev - Software Development Agency
bubble-integrationsBubble API Connector

Microsoft Dynamics 365

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.

What you'll learn

  • How to create an Azure AD App Registration with the Dynamics CRM user_impersonation permission and admin consent
  • Why Azure AD admin consent is mandatory and how to request it from your IT administrator
  • How to configure all four required OData v4 headers in Bubble's API Connector
  • How to construct OData $filter, $select, $expand, and $orderby queries as URL parameters in Bubble
  • How to handle Dynamics 365's `value` array wrapper in API responses and the 204 empty response on PATCH
  • How to use GUIDs correctly in Dynamics 365 URL paths using parenthesis notation
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced18 min read4–6 hoursCRM & SalesLast updated July 2026RapidDev Engineering Team
TL;DR

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.

Quick facts about this guide
FactValue
ToolMicrosoft Dynamics 365
CategoryCRM & Sales
MethodBubble API Connector
DifficultyAdvanced
Time required4–6 hours
Last updatedJuly 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

Bubble API Connector

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

1

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.

azure_ad_app_registration.txt
1// Azure AD App Registration details to collect:
2// Application (client) ID → used as client_id in OAuth
3// Directory (tenant) ID → used in token URL
4// Client Secret Value → used as client_secret (copy immediately)
5
6// Token endpoint URL format:
7// https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token
8
9// Authorization URL format (for authorization_code flow):
10// https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize
11// ?client_id={client_id}
12// &response_type=code
13// &redirect_uri=https://yourapp.bubbleapps.io/oauth_callback
14// &scope=https://orgname.crm.dynamics.com/user_impersonation offline_access
15// &response_mode=query
16
17// Required permission:
18// API: Dynamics CRM
19// Permission: user_impersonation (delegated)
20// Admin consent: REQUIRED — must be granted by Global Admin or App Admin

Pro 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.

2

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.

dynamics_auth_setup.txt
1// Dynamics_Auth data type:
2// access_token → text
3// refresh_token → text
4// org_url → text (e.g., 'https://yourorg.crm.dynamics.com')
5// token_expiry → date
6// user → User
7
8// Privacy: Everyone else → no fields visible
9// ✓ Ignore privacy rules when using workflows
10
11// Backend Workflow: Exchange Dynamics Code
12// Input: code (text)
13// Step 1: API Connector → Dynamics → Exchange Code for Token
14// code: input code
15// client_id: [Private]
16// client_secret: [Private]
17// tenant_id: [Private]
18// redirect_uri: https://yourapp.bubbleapps.io/oauth_callback
19// Step 2: Create Dynamics_Auth
20// access_token = Step1.access_token
21// refresh_token = Step1.refresh_token
22// org_url = Step1.resource (or hardcode your org URL)
23// token_expiry = Current date/time + 3540 seconds
24// user = Current User

Pro 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.

3

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.

dynamics_api_connector_config.json
1// Shared headers (ALL REQUIRED on every Dynamics call):
2// OData-MaxVersion: 4.0
3// OData-Version: 4.0
4// Accept: application/json
5// Content-Type: application/json
6// Authorization: Bearer <access_token> [Private dynamic param]
7
8// Call 3: Get Contacts
9{
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 source
21
22// OAuth scope for Dynamics:
23// https://yourorg.crm.dynamics.com/user_impersonation offline_access
24// The org URL MUST match your actual Dynamics environment URL exactly
25// Generic Microsoft scopes (like https://graph.microsoft.com) do NOT work for Dynamics

Pro 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.

4

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.

dynamics_odata_queries.json
1// Common OData queries for Dynamics 365 entities:
2
3// 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 desc
5
6// Get opportunities in active stage:
7// GET /opportunities?$select=name,estimatedvalue,stepname,closeddateestimate,parentaccountid&$filter=statecode eq 0&$orderby=closeddateestimate asc&$top=100
8
9// Search contacts by name:
10// GET /contacts?$select=fullname,emailaddress1,telephone1&$filter=contains(fullname,'<search_term>')&$top=20
11
12// Create a new Lead:
13// POST /leads
14// Body: { "firstname": "Jane", "lastname": "Smith", "emailaddress1": "jane@company.com", "companyname": "Acme Corp" }
15
16// Update contact email (PATCH returns 204, no body):
17// PATCH /contacts(3d86a63b-a5a4-ed11-81a9-002248e89db3)
18// Body: { "emailaddress1": "new@email.com" }
19
20// Dynamics entity plural names:
21// contacts, accounts, leads, opportunities, incidents (cases), tasks, phonecalls

Pro 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.

5

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.

dynamics_login_flow.txt
1// Azure AD Authorization URL:
2https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize
3 ?client_id={client_id}
4 &response_type=code
5 &redirect_uri=https://yourapp.bubbleapps.io/oauth_callback
6 &scope=https://yourorg.crm.dynamics.com/user_impersonation+offline_access
7 &response_mode=query
8
9// oauth_callback page workflow:
10// Trigger: Page is loaded
11// Case 1: code param exists
12// Action: Schedule API Workflow → Exchange Dynamics Code
13// code = Get URL parameter 'code'
14// Then: Navigate to /dashboard
15// Case 2: error param exists
16// Action: Navigate to /login?msg=dynamics_auth_failed
17
18// Token refresh check in all Dynamics workflows:
19// Condition: Dynamics_Auth is empty OR token_expiry < Current date/time
20// Action: Schedule API Workflow → Refresh Dynamics Token
21// Wait: Add a 2-second pause or chain the next action after the Backend Workflow
22
23// Then call Dynamics API with:
24// Authorization header: Bearer + Current User's Dynamics_Auth access_token

Pro 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.

Bubble Prompt

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.

Bubble Prompt

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.

Bubble Prompt

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.

typescript
1// Required shared headers (missing any causes failures):
2// OData-MaxVersion: 4.0
3// OData-Version: 4.0
4// Accept: application/json
5// Content-Type: application/json
6// 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.

typescript
1// Correct GUID format in URL:
2// PATCH /contacts(3d86a63b-a5a4-ed11-81a9-002248e89db3)
3
4// 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.

typescript
1// OData filter operators:
2// eq (equal): $filter=statuscode eq 1
3// ne (not eq): $filter=statecode ne 1
4// gt / lt: $filter=revenue gt 1000000
5// contains: $filter=contains(name,'Acme')
6// startswith: $filter=startswith(emailaddress1,'admin')
7// and / or: $filter=statecode eq 0 and statuscode eq 1

Best 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

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Integrations are where projects stall

We wire Bubble integrations like this one every week — auth, webhooks, edge cases included. Fixed-price builds from $13K, delivered in 6–10 weeks.

Talk to an integration engineer

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.