# Microsoft Dynamics 365

- Tool: Bubble
- Difficulty: Advanced
- Time required: 4–6 hours
- Last updated: July 2026

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

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

## Before you start

- 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 details to collect:
// Application (client) ID  → used as client_id in OAuth
// Directory (tenant) ID    → used in token URL
// Client Secret Value      → used as client_secret (copy immediately)

// Token endpoint URL format:
// https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token

// Authorization URL format (for authorization_code flow):
// 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://orgname.crm.dynamics.com/user_impersonation offline_access
//   &response_mode=query

// Required permission:
// API: Dynamics CRM
// Permission: user_impersonation (delegated)
// Admin consent: REQUIRED — must be granted by Global Admin or App Admin
```

**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 data type:
// access_token   → text
// refresh_token  → text
// org_url        → text  (e.g., 'https://yourorg.crm.dynamics.com')
// token_expiry   → date
// user           → User

// Privacy: Everyone else → no fields visible
// ✓ Ignore privacy rules when using workflows

// Backend Workflow: Exchange Dynamics Code
// Input: code (text)
// Step 1: API Connector → Dynamics → Exchange Code for Token
//   code: input code
//   client_id: [Private]
//   client_secret: [Private]
//   tenant_id: [Private]
//   redirect_uri: https://yourapp.bubbleapps.io/oauth_callback
// Step 2: Create Dynamics_Auth
//   access_token = Step1.access_token
//   refresh_token = Step1.refresh_token
//   org_url = Step1.resource (or hardcode your org URL)
//   token_expiry = Current date/time + 3540 seconds
//   user = Current User
```

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

```
// Shared headers (ALL REQUIRED on every Dynamics call):
// OData-MaxVersion: 4.0
// OData-Version: 4.0
// Accept: application/json
// Content-Type: application/json
// Authorization: Bearer <access_token>  [Private dynamic param]

// Call 3: Get Contacts
{
  "method": "GET",
  "url": "https://yourorg.crm.dynamics.com/api/data/v9.2/contacts",
  "params": {
    "$select": "fullname,emailaddress1,telephone1,telephone2,accountid",
    "$filter": "<filter_expression>",  // e.g., contains(fullname,'Smith')
    "$top": "50",
    "$orderby": "createdon desc"
  }
}
// Response: { "@odata.context": "...", "value": [ { "fullname": "...", ... } ] }
// Bubble binding: use 'value' array as list data source

// OAuth scope for Dynamics:
// https://yourorg.crm.dynamics.com/user_impersonation offline_access
// The org URL MUST match your actual Dynamics environment URL exactly
// Generic Microsoft scopes (like https://graph.microsoft.com) do NOT work for Dynamics
```

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

```
// Common OData queries for Dynamics 365 entities:

// Get active accounts with $expand for primary contact:
// GET /accounts?$select=name,telephone1,revenue,websiteurl&$filter=statecode eq 0&$expand=primarycontactid($select=fullname,emailaddress1)&$top=50&$orderby=revenue desc

// Get opportunities in active stage:
// GET /opportunities?$select=name,estimatedvalue,stepname,closeddateestimate,parentaccountid&$filter=statecode eq 0&$orderby=closeddateestimate asc&$top=100

// Search contacts by name:
// GET /contacts?$select=fullname,emailaddress1,telephone1&$filter=contains(fullname,'<search_term>')&$top=20

// Create a new Lead:
// POST /leads
// Body: { "firstname": "Jane", "lastname": "Smith", "emailaddress1": "jane@company.com", "companyname": "Acme Corp" }

// Update contact email (PATCH returns 204, no body):
// PATCH /contacts(3d86a63b-a5a4-ed11-81a9-002248e89db3)
// Body: { "emailaddress1": "new@email.com" }

// Dynamics entity plural names:
// contacts, accounts, leads, opportunities, incidents (cases), tasks, phonecalls
```

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

```
// 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

// oauth_callback page workflow:
// Trigger: Page is loaded
// Case 1: code param exists
//   Action: Schedule API Workflow → Exchange Dynamics Code
//     code = Get URL parameter 'code'
//   Then: Navigate to /dashboard
// Case 2: error param exists
//   Action: Navigate to /login?msg=dynamics_auth_failed

// Token refresh check in all Dynamics workflows:
// Condition: Dynamics_Auth is empty OR token_expiry < Current date/time
// Action: Schedule API Workflow → Refresh Dynamics Token
// Wait: Add a 2-second pause or chain the next action after the Backend Workflow

// Then call Dynamics API with:
// Authorization header: Bearer + Current User's Dynamics_Auth access_token
```

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

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

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

Prompt example:

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

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

Prompt example:

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

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

Prompt example:

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

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

```
// Required shared headers (missing any causes failures):
// OData-MaxVersion: 4.0
// OData-Version: 4.0
// Accept: application/json
// Content-Type: application/json
// 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.

```
// Correct GUID format in URL:
// PATCH /contacts(3d86a63b-a5a4-ed11-81a9-002248e89db3)

// Wrong formats:
// PATCH /contacts/3d86a63b-a5a4-ed11-81a9-002248e89db3  (no parentheses)
// 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.

```
// OData filter operators:
// eq (equal):    $filter=statuscode eq 1
// ne (not eq):   $filter=statecode ne 1
// gt / lt:       $filter=revenue gt 1000000
// contains:      $filter=contains(name,'Acme')
// startswith:    $filter=startswith(emailaddress1,'admin')
// and / or:      $filter=statecode eq 0 and statuscode eq 1
```

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

---

Source: https://www.rapidevelopers.com/bubble-integrations/microsoft-dynamics-365
© RapidDev — https://www.rapidevelopers.com/bubble-integrations/microsoft-dynamics-365
