To integrate Microsoft Dynamics 365 with Lovable, register an Azure AD application to get OAuth2 client credentials, store them in Cloud → Secrets, then build Edge Functions that use client credentials flow to call the Dynamics 365 Web API for accounts, contacts, opportunities, and custom entities using OData query syntax. Dynamics 365 is the right choice when your organization is standardized on Microsoft Azure and Office 365.
Connect Lovable to Microsoft Dynamics 365 with Azure AD OAuth2 and the Web API
Microsoft Dynamics 365 is the enterprise CRM/ERP platform of choice for organizations standardized on the Microsoft ecosystem — Azure, Office 365, Teams, Power BI, and SharePoint. For Lovable apps serving enterprise customers or building internal tools for Microsoft-centric organizations, Dynamics 365 integration is often non-negotiable. The platform's deep Azure Active Directory integration means authentication goes through Azure AD OAuth2, adding complexity but providing enterprise-grade security.
Dynamics 365 Web API is OData v4-based REST API. Unlike the simple REST APIs of Pipedrive or Freshdesk, Dynamics 365 uses OData query syntax: $select for field projection, $filter for filtering, $expand for related entity joins, $orderby for sorting, and $top for pagination limits. Mastering OData queries is the key skill for building effective Dynamics 365 integrations.
Authentication uses Azure AD OAuth2 client credentials flow — your Azure AD application (registered in the Azure portal) obtains access tokens using its own client ID and secret, without requiring a user to log in. This is the correct approach for server-side Edge Function integrations. Access tokens expire after one hour and must be refreshed using the same client credentials flow.
The most important prerequisite is having the correct Azure AD application permissions. Dynamics 365 uses 'user_impersonation' scope under the Dynamics CRM API resource, and the application must be granted a security role in the Dynamics 365 instance itself (not just Azure AD permissions). Both steps are required — many integration failures stem from completing one but not the other.
Integration method
Microsoft Dynamics 365 has no native Lovable connector. All CRM operations are proxied through Lovable Edge Functions that call the Dynamics 365 Web API using Azure AD OAuth2 client credentials flow. The Azure AD Application ID, Client Secret, Tenant ID, and Dynamics instance URL are stored in Cloud → Secrets and accessed via Deno.env.get(). The Edge Function handles token acquisition and OData query construction.
Prerequisites
- A Lovable project with Lovable Cloud enabled (Edge Functions require a deployed app)
- An active Microsoft Dynamics 365 instance (Dynamics 365 Sales, Customer Service, or similar — trial available at dynamics.microsoft.com)
- Azure Active Directory admin access (or a global admin who can create and configure app registrations)
- Your Dynamics 365 instance URL (found in your Dynamics 365 environment — format: https://yourorg.crm.dynamics.com)
- Familiarity with Azure Portal navigation for app registration and API permission configuration
Step-by-step guide
Register an Azure AD application with Dynamics 365 API permissions
Register an Azure AD application with Dynamics 365 API permissions
Dynamics 365 authentication is handled through Azure Active Directory. You must register an application in Azure AD and grant it access to your Dynamics 365 instance. Go to portal.azure.com and sign in with your Azure account. Navigate to Azure Active Directory → App registrations → New registration. Fill in: - Name: 'Lovable Dynamics 365 Integration' - Supported account types: 'Accounts in this organizational directory only' - Redirect URI: leave blank (not needed for client credentials flow) Click Register. On the app overview page, copy the Application (client) ID and Directory (tenant) ID — you need both. Next, create a client secret. Go to Certificates & secrets → Client secrets → New client secret. Set an expiry (24 months recommended) and click Add. Copy the secret Value immediately — it is shown only once. Now configure API permissions. Go to API permissions → Add a permission → APIs my organization uses → search for 'Dynamics CRM'. Select 'Dynamics CRM' and then 'Application permissions'. Add the 'user_impersonation' scope. Click 'Grant admin consent for [your tenant]'. Wait for the green checkmark confirming consent. Critical step that many guides miss: you must also add the application as a user in your Dynamics 365 instance. In Dynamics 365, go to Settings → Security → Users → switch to 'Application Users' view → New Application User. Enter the Application ID from Azure AD, and assign a security role (e.g., 'System Administrator' for full access, or a custom role with minimum required permissions). Without this Dynamics-side step, API calls return 403 even with valid Azure AD tokens.
Pro tip: The most common Dynamics 365 integration failure is missing the Application User setup in Dynamics 365 itself. Azure AD permissions alone are insufficient — the app registration must also have a Dynamics 365 Application User with an assigned security role. Both Azure AD and Dynamics 365 configurations are required.
Expected result: An Azure AD app registration is created with Application ID, Tenant ID, and Client Secret. The 'Dynamics CRM user_impersonation' permission has admin consent. An Application User exists in Dynamics 365 with a security role assigned.
Store Azure AD credentials in Cloud Secrets
Store Azure AD credentials in Cloud Secrets
Add all Azure AD and Dynamics 365 credentials to Lovable's Cloud Secrets panel. In Lovable, click '+' → Cloud panel → Secrets. Add four secrets: - DYNAMICS_CLIENT_ID — the Application (client) ID from Azure AD app registration - DYNAMICS_CLIENT_SECRET — the client secret value you copied - DYNAMICS_TENANT_ID — the Directory (tenant) ID from Azure AD - DYNAMICS_INSTANCE_URL — your Dynamics 365 instance URL (e.g., https://yourorg.crm.dynamics.com) The instance URL format matters: it must be the base URL without any path suffix. For US environments, it typically ends in .crm.dynamics.com. For other regions: .crm4.dynamics.com (EMEA), .crm5.dynamics.com (APAC), .crm3.dynamics.com (Canada). The Azure AD token endpoint uses your tenant ID: https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token. The scope for Dynamics 365 API access is: https://yourorg.crm.dynamics.com/.default (your instance URL with /.default appended). This scope format tells Azure AD to grant all permissions pre-configured for the Dynamics CRM API. Access tokens from Azure AD expire after one hour (3600 seconds). The Edge Function requests a new token for each request using client credentials flow — since tokens are not stored, there is no refresh token management required. Simply re-request a new token when needed.
Pro tip: The OData scope for Azure AD token requests must end with '/.default' — it should be your full Dynamics instance URL followed by '/.default', e.g., 'https://yourorg.crm.dynamics.com/.default'. Missing the /.default suffix returns an invalid_scope error from Azure AD.
Expected result: Four secrets (DYNAMICS_CLIENT_ID, DYNAMICS_CLIENT_SECRET, DYNAMICS_TENANT_ID, DYNAMICS_INSTANCE_URL) appear in Cloud → Secrets with masked values.
Create the Dynamics 365 Web API Edge Function with token acquisition
Create the Dynamics 365 Web API Edge Function with token acquisition
Build the Edge Function that acquires Azure AD access tokens and proxies OData requests to the Dynamics 365 Web API. The token acquisition uses the OAuth2 client credentials flow: POST to the Azure AD token endpoint with client_id, client_secret, and scope. Dynamics 365 Web API base path: {instanceUrl}/api/data/v9.2/ OData endpoint examples: - GET /api/data/v9.2/accounts — list all accounts - GET /api/data/v9.2/accounts?$select=name,revenue&$filter=statecode eq 0&$top=50 — filtered, projected - POST /api/data/v9.2/leads — create a lead - PATCH /api/data/v9.2/accounts({id}) — update an account Important Dynamics 365 API headers: Requests must include 'OData-MaxVersion: 4.0' and 'OData-Version: 4.0'. The 'Prefer: odata.include-annotations=OData.Community.Display.V1.FormattedValue' header returns human-readable values for option sets (e.g., the text label for a status code number). Dynamics entity IDs are GUIDs (e.g., '00000000-0000-0000-0000-000000000000'). When creating records (POST), Dynamics returns the new record ID in the OData-EntityId response header — not in the response body. Parse this header to get the created record's ID. OData $filter syntax for common operations: - equals: name eq 'Acme Corp' - contains: contains(name,'Acme') - active records: statecode eq 0 - date after: estimatedclosedate ge 2026-01-01
Create an Edge Function called dynamics-crm at supabase/functions/dynamics-crm/index.ts. Accept POST with { action, entityType, params }. Support actions: list_records, get_record, create_record, update_record. Acquire an Azure AD access token using client credentials flow with DYNAMICS_CLIENT_ID, DYNAMICS_CLIENT_SECRET, DYNAMICS_TENANT_ID. Build OData requests to DYNAMICS_INSTANCE_URL/api/data/v9.2/{entityType}. Include required OData headers. Include CORS headers.
Paste this in Lovable chat
1import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'23const corsHeaders = {4 'Access-Control-Allow-Origin': '*',5 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',6}78async function getAccessToken(): Promise<string> {9 const clientId = Deno.env.get('DYNAMICS_CLIENT_ID')!10 const clientSecret = Deno.env.get('DYNAMICS_CLIENT_SECRET')!11 const tenantId = Deno.env.get('DYNAMICS_TENANT_ID')!12 const instanceUrl = Deno.env.get('DYNAMICS_INSTANCE_URL')!1314 const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`15 const resp = await fetch(tokenUrl, {16 method: 'POST',17 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },18 body: new URLSearchParams({19 grant_type: 'client_credentials',20 client_id: clientId,21 client_secret: clientSecret,22 scope: `${instanceUrl}/.default`,23 }),24 })25 const data = await resp.json()26 if (!data.access_token) throw new Error('Azure AD token acquisition failed: ' + JSON.stringify(data.error_description || data))27 return data.access_token28}2930serve(async (req) => {31 if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })3233 try {34 const { action, entityType = 'accounts', params = {} } = await req.json()35 const instanceUrl = Deno.env.get('DYNAMICS_INSTANCE_URL')!36 const accessToken = await getAccessToken()3738 const odataHeaders = {39 Authorization: `Bearer ${accessToken}`,40 'Content-Type': 'application/json',41 'OData-MaxVersion': '4.0',42 'OData-Version': '4.0',43 Accept: 'application/json',44 'Prefer': 'odata.include-annotations=OData.Community.Display.V1.FormattedValue',45 }4647 const apiBase = `${instanceUrl}/api/data/v9.2`48 let resp: Response4950 switch (action) {51 case 'list_records': {52 const query = new URLSearchParams()53 if (params.select) query.set('$select', params.select)54 if (params.filter) query.set('$filter', params.filter)55 if (params.top) query.set('$top', String(params.top))56 if (params.orderby) query.set('$orderby', params.orderby)57 if (params.expand) query.set('$expand', params.expand)58 resp = await fetch(`${apiBase}/${entityType}?${query.toString()}`, { headers: odataHeaders })59 break60 }6162 case 'get_record':63 resp = await fetch(`${apiBase}/${entityType}(${params.id})${params.select ? '?$select=' + params.select : ''}`, { headers: odataHeaders })64 break6566 case 'create_record':67 resp = await fetch(`${apiBase}/${entityType}`, {68 method: 'POST',69 headers: odataHeaders,70 body: JSON.stringify(params.data),71 })72 // For create, Dynamics returns the ID in the OData-EntityId header73 if (resp.status === 204) {74 const entityId = resp.headers.get('OData-EntityId')75 return new Response(JSON.stringify({ success: true, entityId }), {76 headers: { ...corsHeaders, 'Content-Type': 'application/json' },77 })78 }79 break8081 case 'update_record':82 resp = await fetch(`${apiBase}/${entityType}(${params.id})`, {83 method: 'PATCH',84 headers: odataHeaders,85 body: JSON.stringify(params.data),86 })87 if (resp.status === 204) {88 return new Response(JSON.stringify({ success: true }), {89 headers: { ...corsHeaders, 'Content-Type': 'application/json' },90 })91 }92 break9394 default:95 return new Response(JSON.stringify({ error: 'Unknown action' }), {96 status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' },97 })98 }99100 const data = await resp.json()101 return new Response(JSON.stringify(data), {102 status: resp.ok ? 200 : resp.status,103 headers: { ...corsHeaders, 'Content-Type': 'application/json' },104 })105 } catch (error) {106 return new Response(JSON.stringify({ error: error.message }), {107 status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' },108 })109 }110})Pro tip: Dynamics 365 record creation (POST) returns HTTP 204 No Content with the new record's ID in the OData-EntityId response header, not in the response body. Always parse the OData-EntityId header to get the created record's GUID, as shown in the code above.
Expected result: The dynamics-crm Edge Function deploys in Cloud → Edge Functions. Test with action 'list_records', entityType 'accounts', and params { top: 5, select: 'name,revenue' } — account records from your Dynamics 365 instance should appear in the response.
Build the Dynamics 365 dashboard in Lovable
Build the Dynamics 365 dashboard in Lovable
With the Edge Function deployed, use Lovable's chat to build dashboards that surface Dynamics 365 data. The OData query parameters are your primary tool for shaping the data returned. For an account dashboard, invoke list_records with entityType 'accounts' and OData parameters: filter 'statecode eq 0' (active accounts only), select 'name,revenue,statecode,customertypecode', top 50, orderby 'revenue desc'. This returns active accounts sorted by revenue with only the fields you need. For an opportunity pipeline, use entityType 'opportunities' with filter 'statecode eq 0' (open opportunities), expand 'parentaccountid($select=name)' to include the account name, and select 'name,estimatedvalue,estimatedclosedate,stepname,parentaccountid'. The stepname field contains the deal stage description. For creating leads from your app, call create_record with entityType 'leads' and a data object using Dynamics 365 lead attribute names: firstname, lastname, emailaddress1, telephone1, companyname. The lead entity in Dynamics uses 'emailaddress1' not 'email'. Check the Dynamics 365 field schema in your environment for exact attribute names. For complex Dynamics 365 integrations involving custom entities, complex lookups, or bulk data operations, RapidDev's team can help design the correct OData query patterns and entity relationship navigation.
Build a CRM dashboard page using the dynamics-crm Edge Function. Fetch active accounts (action: list_records, entityType: accounts, filter: 'statecode eq 0', top: 50, select: 'name,revenue,accountid'). Show in a sortable table with account name and revenue. Add a separate section for open opportunities (entityType: opportunities, filter: 'statecode eq 0', select: 'name,estimatedvalue,estimatedclosedate,stepname'). Show total pipeline value as a summary metric.
Paste this in Lovable chat
Pro tip: Dynamics 365 OData responses wrap data in a 'value' array: { value: [...], '@odata.context': '...' }. Always access data.value to get the records array, not data directly. For single record fetches (get_record), the response is the record object directly without a value wrapper.
Expected result: The CRM dashboard displays Dynamics 365 account and opportunity data. The total pipeline value summary shows correctly. Creating records from the UI creates corresponding records in your Dynamics 365 instance visible in the CRM.
Common use cases
Build a custom account and opportunity dashboard for sales managers
Fetch Dynamics 365 accounts with their associated open opportunities and display in a custom dashboard showing pipeline value by account, deal stages, and estimated close dates. Sales managers see their territory's entire pipeline without full Dynamics 365 licenses for every user. The dashboard supports filtering by territory, account type, and opportunity stage using OData query parameters.
Create an Edge Function called dynamics-pipeline that fetches accounts with open opportunities from Dynamics 365 using DYNAMICS_CLIENT_ID, DYNAMICS_CLIENT_SECRET, DYNAMICS_TENANT_ID, and DYNAMICS_INSTANCE_URL from secrets. Use OData $filter to get opportunities where statecode eq 0 (active), $expand to include account details, and $select for specific fields. Build a dashboard showing pipeline value by account with stage breakdown.
Copy this prompt to try it in Lovable
Create Dynamics 365 leads from Lovable contact forms
When visitors fill out a lead generation form in your Lovable app, create a Dynamics 365 lead record with their information, source campaign, and custom scoring fields. The lead appears immediately in the Dynamics 365 sales rep's queue for follow-up. Custom entity fields on the Dynamics lead record can capture product interest, budget range, and authority level for better lead qualification.
Create an Edge Function called dynamics-create-lead that accepts { firstName, lastName, email, company, phone, campaignSource, productInterest } and creates a Lead entity in Dynamics 365 using DYNAMICS_CLIENT_ID, DYNAMICS_CLIENT_SECRET, DYNAMICS_TENANT_ID, and DYNAMICS_INSTANCE_URL from secrets. Map fields to Dynamics 365 lead entity attributes. Return the leadid. Call this from my lead magnet form after Supabase insertion.
Copy this prompt to try it in Lovable
Real-time account health dashboard for customer success teams
Build an internal dashboard that shows key account health metrics from Dynamics 365 — renewal dates, support ticket counts, usage levels, and relationship scores. The account data from Dynamics is combined with usage data from your Supabase database to give customer success managers a single-pane view of their accounts, enabling proactive outreach before renewals or issues escalate.
Create an Edge Function called dynamics-accounts that fetches accounts from Dynamics 365 with OData $select for name, revenue, statecode, customertypecode, and custom fields. Use $filter for accounts where statecode eq 0 (active) and $orderby for estimated_value descending. Build a customer success dashboard combining this data with Supabase usage metrics for each account.
Copy this prompt to try it in Lovable
Troubleshooting
Azure AD returns 'AADSTS700016: Application not found in directory' during token acquisition
Cause: The DYNAMICS_CLIENT_ID or DYNAMICS_TENANT_ID is incorrect, or the app registration is in a different Azure AD tenant than expected.
Solution: In Azure Portal → Azure Active Directory → App registrations, locate your app and verify the Application (client) ID and Directory (tenant) ID. Ensure you are in the correct Azure AD tenant — if your organization uses multiple tenants, verify you are viewing the tenant where Dynamics 365 is deployed.
Azure AD token acquisition succeeds but Dynamics 365 API returns 403 Forbidden
Cause: The Azure AD application has not been added as an Application User in Dynamics 365 with an assigned security role.
Solution: In Dynamics 365, go to Settings → Security → Users → switch the view dropdown to 'Application Users'. Create a new Application User with your app's Application ID and assign an appropriate security role. The security role must have read/write permissions for the entities you are accessing. This Dynamics-side configuration is required in addition to Azure AD permissions.
OData filter returns 'Could not find a property named X' error
Cause: The OData $filter or $select uses an attribute name that does not exist in the Dynamics 365 entity schema.
Solution: Dynamics 365 attribute names are lowercase with no spaces. Common attribute names: accounts (name, revenue, statecode), contacts (firstname, lastname, emailaddress1, telephone1), opportunities (name, estimatedvalue, estimatedclosedate). Check the exact attribute names in Dynamics 365 → Settings → Customizations → Customize the System → Entities → select entity → Fields. Custom fields have names prefixed with your publisher prefix.
Record creation returns 204 but the OData-EntityId header value is missing or null
Cause: The fetch API in Deno may not expose all response headers by default, or the response is not being read as a 204.
Solution: Explicitly read the OData-EntityId header before consuming the response body. Verify the response status is 204 (successful creation). If the Edge Function logs show a different status code, check for validation errors by reading the response body on non-204 responses.
1// Read OData-EntityId for created records:2if (resp.status === 204) {3 const entityId = resp.headers.get('OData-EntityId') || resp.headers.get('odata-entityid')4 return new Response(JSON.stringify({ success: true, entityId }), { headers: ... })5}Best practices
- Acquire a fresh Azure AD access token for each Edge Function invocation rather than caching — the complexity of cache invalidation is not worth the small performance gain for typical CRM operation frequency.
- Use OData $select to specify only the fields you need — Dynamics 365 entities can have hundreds of attributes, and fetching all fields creates large unnecessary payloads.
- Test OData filter syntax in Dynamics 365's built-in API browser (yourorg.crm.dynamics.com/api/data/v9.2/) before embedding filters in Edge Function code.
- Create a dedicated Azure AD app registration for each environment (development, production) — sharing credentials across environments creates security risk and complicates debugging.
- Assign the minimum required Dynamics 365 security role to your Application User — System Administrator access is convenient but violates least-privilege principles.
- Store your Dynamics 365 instance URL without trailing slash in DYNAMICS_INSTANCE_URL — the Edge Function appends the API path, and a trailing slash creates double-slash URLs.
- Use Dynamics 365 solution layers to track customizations made for your integration — this simplifies upgrades and environment migrations.
Alternatives
Choose Salesforce if your organization is not Microsoft-centric — Salesforce has a larger third-party integration ecosystem and is more common in B2B SaaS companies outside the Microsoft stack.
Choose HubSpot if you want a simpler CRM with a more developer-friendly API and a generous free tier — HubSpot's Private App authentication is significantly easier than Azure AD OAuth2.
Choose Zoho CRM if you need a full CRM suite at lower cost than Dynamics 365 and are not constrained by Microsoft ecosystem requirements.
Frequently asked questions
Do I need a Dynamics 365 license to use the API for integration?
Yes — you need an active Dynamics 365 subscription and a valid Dynamics 365 environment (instance). The API integration (client credentials flow) runs as an Application User, which requires a Dynamics 365 app license in addition to the Azure AD app registration. Dynamics 365 Sales Professional starts at $65/user/month. If you are building an integration for an enterprise client, their existing Dynamics 365 subscription is used.
What is OData and why does Dynamics 365 use it?
OData (Open Data Protocol) is a standard for building and consuming RESTful APIs that supports rich query capabilities through URL parameters. Dynamics 365 uses OData v4 because it enables powerful server-side filtering, field selection, and entity expansion without building custom query endpoints. The $filter, $select, $expand, and $orderby parameters let you precisely specify what data you need in a single API call rather than fetching everything and filtering client-side.
How do I work with custom Dynamics 365 entities in Lovable?
Custom entities created in Dynamics 365 are accessible via the Web API using their schema name. Custom entity schema names include your publisher prefix: for example, a custom entity named 'ServiceContract' from publisher 'contoso' has the schema name 'contoso_servicecontract'. The API path would be /api/data/v9.2/contoso_servicecontracts. Find custom entity names in Dynamics 365 → Settings → Customizations → Customize the System → Entities.
Can I use the same Azure AD app for multiple Dynamics 365 environments?
Yes — one Azure AD app registration can be granted access to multiple Dynamics 365 environments (instances). Each environment has its own DYNAMICS_INSTANCE_URL. Configure an Application User in each Dynamics 365 environment using the same Application ID. In your Lovable app, store separate secrets for each environment's instance URL (DYNAMICS_INSTANCE_URL_PROD, DYNAMICS_INSTANCE_URL_DEV) and select the appropriate one based on your deployment context.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation