Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with Microsoft Dynamics 365

Microsoft Dynamics 365 integrates with Bolt.new through a Next.js API route using Azure AD OAuth 2.0. Register an Azure AD app, deploy your site to handle the OAuth callback (the WebContainer preview URL cannot be used as a redirect URI), store the access token in .env, and query the Dynamics 365 Web API using OData syntax to fetch contacts, accounts, and opportunities. Use $filter, $select, and $expand for targeted queries.

What you'll learn

  • How to register an Azure AD application and configure OAuth 2.0 permissions for Dynamics 365 API access
  • How to handle the Azure AD OAuth callback on a deployed site and exchange the authorization code for an access token
  • How to construct OData queries with $filter, $select, and $expand to retrieve Dynamics 365 entities efficiently
  • How to create and update Dynamics 365 contacts, accounts, and opportunities from Next.js API routes
  • How to build a sales pipeline dashboard displaying Dynamics 365 opportunity data in a React app
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate17 min read40 minutesMarketingApril 2026RapidDev Engineering Team
TL;DR

Microsoft Dynamics 365 integrates with Bolt.new through a Next.js API route using Azure AD OAuth 2.0. Register an Azure AD app, deploy your site to handle the OAuth callback (the WebContainer preview URL cannot be used as a redirect URI), store the access token in .env, and query the Dynamics 365 Web API using OData syntax to fetch contacts, accounts, and opportunities. Use $filter, $select, and $expand for targeted queries.

Microsoft Dynamics 365 CRM Integration for Bolt.new Apps

Microsoft Dynamics 365 is the enterprise CRM and ERP platform for organizations embedded in the Microsoft ecosystem — using Microsoft 365, Teams, SharePoint, and Azure. The platform provides a comprehensive Web API built on OData v4 standards, offering a well-structured REST interface for all Dynamics entities: contacts, accounts, leads, opportunities, cases, and custom entities defined by your organization. The OData protocol adds powerful querying capabilities — filtering, sorting, selecting specific fields, and expanding related records — that go beyond typical REST APIs.

For Bolt.new developers, Dynamics 365 integration typically arises when a client organization uses Dynamics 365 as their CRM of record and wants custom internal tools: a sales dashboard for team members without individual Dynamics licenses, a custom lead intake form that creates Dynamics contacts and leads from a marketing website, or a reporting view that combines Dynamics opportunity data with other business metrics. The Web API makes these scenarios practical through standard HTTP requests that work in Bolt's WebContainer.

The primary technical challenge is Azure AD authentication. Dynamics 365 uses Azure Active Directory (now called Microsoft Entra ID) as its identity provider — you cannot use a simple API key or Basic auth. Instead, you register an Azure AD application in the Azure Portal, configure it with Dynamics 365 API permissions, and complete an OAuth 2.0 authorization code flow. The OAuth callback requires a stable publicly accessible URL, which means completing the initial authentication setup on your deployed Netlify or Bolt Cloud site rather than in the Bolt WebContainer preview. Once you have an access token, all Dynamics 365 API operations work in both development and production. Azure AD also supports a client credentials flow (service-to-service auth without user interaction) for server-side integrations, which is often simpler for server-to-server scenarios.

Integration method

Bolt Chat + API Route

Dynamics 365 uses Azure Active Directory OAuth 2.0 for authentication — you register an Azure AD app, complete the OAuth flow on your deployed site to get an access token, and call the Dynamics 365 Web API using OData query syntax. The Web API is a standard REST API built on OData v4, supporting $filter, $select, $expand, and $orderby query parameters. All requests go through Next.js server-side API routes to keep the access token out of the browser.

Prerequisites

  • A Microsoft Azure account (portal.azure.com) with permission to register Azure AD applications — your organization's Azure AD administrator may need to grant app registration permissions
  • An active Dynamics 365 subscription — Dynamics 365 has no free tier; it requires a paid Microsoft license. Common plans: Sales Professional ($65/user/month), Sales Enterprise ($95/user/month)
  • Your Dynamics 365 organization URL (found in Dynamics Settings → Customizations → Developer Resources — format: https://yourorg.crm.dynamics.com)
  • A deployed Bolt.new app on Netlify or Bolt Cloud to use as the Azure AD OAuth redirect URI — Bolt's WebContainer preview URL is dynamic and cannot be registered as a stable redirect URI
  • Dynamics 365 system administrator or customizer role in your Dynamics org to grant API permissions in Azure AD

Step-by-step guide

1

Register an Azure AD application for Dynamics 365 API access

Dynamics 365 uses Azure Active Directory (Microsoft Entra ID) for authentication — there is no standalone developer portal or API key. You register an application in the Azure Portal, grant it permission to access Dynamics 365, and use OAuth 2.0 to obtain access tokens. Go to portal.azure.com and sign in with your organizational Microsoft account. Navigate to 'Azure Active Directory' (or search for 'Microsoft Entra ID' — they are the same service). Click 'App registrations' → 'New registration.' Give your app a name (e.g., 'My Bolt App'). For the Redirect URI, select 'Web' and enter your deployed site URL: https://your-app.netlify.app/api/auth/callback/dynamics. This cannot be the Bolt WebContainer preview URL — register the deployed Netlify or Bolt Cloud URL. After creating the app, go to 'Certificates & secrets' → 'New client secret.' Give it a description and choose an expiration period. Copy the Value immediately — it is only shown once. Store it as AZURE_CLIENT_SECRET in .env. Next, configure API permissions. Click 'API permissions' → 'Add a permission' → 'Dynamics CRM' → 'Delegated permissions.' Check 'user_impersonation' (allows the app to access Dynamics 365 on behalf of the signed-in user). For service-to-service access (no user interaction), use 'Application permissions' instead of 'Delegated.' Click 'Grant admin consent' to authorize the permissions for your Azure AD tenant — this requires an Azure AD Global Administrator. Note your Application (client) ID and Directory (tenant) ID from the app's Overview page. Store them as AZURE_CLIENT_ID and AZURE_TENANT_ID in .env. Your Dynamics 365 organization URL (e.g., https://yourorg.crm.dynamics.com) goes in as DYNAMICS_ORG_URL. The OAuth authorization URL format is: https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize. The token endpoint is: https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token. The Dynamics 365 API scope is your org URL followed by /.default: https://yourorg.crm.dynamics.com/.default.

Bolt.new Prompt

Add Azure AD and Dynamics 365 credentials to .env. Create the file with: AZURE_CLIENT_ID=your_app_registration_client_id, AZURE_TENANT_ID=your_azure_ad_tenant_id, AZURE_CLIENT_SECRET=your_client_secret (server-side only, never NEXT_PUBLIC_), DYNAMICS_ORG_URL=https://yourorg.crm.dynamics.com, and NEXT_PUBLIC_APP_URL=https://your-app.netlify.app. Add comments explaining where to find each value in the Azure Portal.

Paste this in Bolt.new chat

.env
1# .env
2# Azure AD app registration credentials
3# Get from portal.azure.com Azure Active Directory App registrations your app
4AZURE_CLIENT_ID=your-app-client-id-here
5AZURE_TENANT_ID=your-azure-ad-tenant-id-here
6AZURE_CLIENT_SECRET=your-client-secret-value-here
7
8# Dynamics 365 organization URL
9# Find in Dynamics Settings Customizations Developer Resources
10DYNAMICS_ORG_URL=https://yourorg.crm.dynamics.com
11
12# Access token (populated after completing OAuth flow)
13# Server-side only never NEXT_PUBLIC_
14DYNAMICS_ACCESS_TOKEN=your_access_token_here
15
16# Your deployed app URL for OAuth redirect URI
17NEXT_PUBLIC_APP_URL=https://your-app.netlify.app

Pro tip: Azure AD app registrations are tenant-specific by default — they only work with accounts in your organization's Azure AD tenant. To allow accounts from other tenants, change 'Supported account types' to 'Accounts in any organizational directory' in the app registration settings.

Expected result: An Azure AD app registration exists with Dynamics CRM user_impersonation permission granted and admin consent provided. Client ID, tenant ID, and client secret are stored in .env. The redirect URI is registered to your deployed site.

2

Implement Azure AD OAuth 2.0 and build the Dynamics API service

Build the OAuth callback handler and the Dynamics 365 API service. Azure AD OAuth 2.0 follows the standard authorization code flow but uses Microsoft's identity endpoints rather than generic OAuth. The authorization URL structure: https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize?client_id={client_id}&response_type=code&redirect_uri={redirect_uri}&scope={scope}&response_mode=query. The scope for Dynamics 365 is your org URL with /.default appended: https://yourorg.crm.dynamics.com/.default. This tells Azure AD to grant all the Dynamics 365 permissions you configured in the app registration. The token endpoint is https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token. The token response includes access_token (Bearer token for API calls), expires_in (seconds until expiry, typically 3600 = 1 hour), and refresh_token (for obtaining new tokens without re-authorization). For service-to-service access without user interaction (when your Bolt app accesses Dynamics on behalf of the application itself, not a specific user), use the client credentials flow: POST to the token endpoint with grant_type=client_credentials, client_id, client_secret, and scope. This is simpler and more appropriate for background integrations — you do not need a redirect URI or user authorization. Note that client credentials require Application permissions in Azure AD, not Delegated permissions. For the Dynamics 365 Web API service, every request needs the access token in the Authorization header and an OData-Version: 4.0 header. Include Accept: application/json for JSON responses. The Prefer: odata.include-annotations='*' header enables rich metadata in responses including option set labels.

Bolt.new Prompt

Create a Dynamics 365 OAuth and API service. (1) Create lib/dynamics.ts with a dynamicsFetch(entity, options, queryParams) utility using DYNAMICS_ACCESS_TOKEN Bearer auth and DYNAMICS_ORG_URL. Add OData-Version: 4.0 and Accept: application/json headers. Export: getContacts(filter, select), getAccounts(filter, select), getOpportunities(filter, select), createRecord(entity, data), and updateRecord(entity, id, data). (2) Create app/api/auth/dynamics/route.ts that redirects to Azure AD authorization URL using AZURE_CLIENT_ID, AZURE_TENANT_ID, DYNAMICS_ORG_URL for scope. (3) Create app/api/auth/callback/dynamics/route.ts that exchanges the code for tokens. Include TypeScript types for Dynamics entities.

Paste this in Bolt.new chat

lib/dynamics.ts
1// lib/dynamics.ts
2const getDynamicsBase = () => {
3 const orgUrl = process.env.DYNAMICS_ORG_URL;
4 if (!orgUrl) throw new Error('DYNAMICS_ORG_URL not configured');
5 return `${orgUrl}/api/data/v9.2`;
6};
7
8async function dynamicsFetch<T>(
9 path: string,
10 options: RequestInit = {}
11): Promise<T> {
12 const token = process.env.DYNAMICS_ACCESS_TOKEN;
13 if (!token) throw new Error('DYNAMICS_ACCESS_TOKEN not configured');
14
15 const res = await fetch(`${getDynamicsBase()}${path}`, {
16 ...options,
17 headers: {
18 Authorization: `Bearer ${token}`,
19 'OData-Version': '4.0',
20 'OData-MaxVersion': '4.0',
21 Accept: 'application/json',
22 'Content-Type': 'application/json',
23 ...options.headers,
24 },
25 });
26
27 if (!res.ok) {
28 const body = await res.json().catch(() => ({ error: { message: res.statusText } }));
29 throw Object.assign(new Error(body.error?.message ?? 'Dynamics API error'), {
30 status: res.status,
31 });
32 }
33
34 // DELETE returns 204 No Content
35 if (res.status === 204) return {} as T;
36 return res.json();
37}
38
39export async function getOpportunities(
40 filter?: string,
41 select = 'name,estimatedvalue,salesstage,estimatedclosedate,statecode'
42) {
43 const params = new URLSearchParams({ '$select': select });
44 if (filter) params.set('$filter', filter);
45 params.set('$orderby', 'estimatedvalue desc');
46 return dynamicsFetch<{ value: Record<string, unknown>[] }>(
47 `/opportunities?${params}`
48 );
49}
50
51export async function getContacts(
52 filter?: string,
53 select = 'fullname,emailaddress1,telephone1,parentcustomerid'
54) {
55 const params = new URLSearchParams({ '$select': select });
56 if (filter) params.set('$filter', filter);
57 return dynamicsFetch<{ value: Record<string, unknown>[] }>(`/contacts?${params}`);
58}
59
60export async function createRecord(
61 entitySetName: string,
62 data: Record<string, unknown>
63) {
64 return dynamicsFetch<Record<string, unknown>>(`/${entitySetName}`, {
65 method: 'POST',
66 body: JSON.stringify(data),
67 });
68}
69
70export async function updateRecord(
71 entitySetName: string,
72 id: string,
73 data: Record<string, unknown>
74) {
75 return dynamicsFetch<void>(`/${entitySetName}(${id})`, {
76 method: 'PATCH',
77 body: JSON.stringify(data),
78 });
79}

Pro tip: Dynamics 365 entity names in the Web API are pluralized lowercase versions of the logical name: 'contacts', 'accounts', 'opportunities', 'leads', 'systemusers'. Custom entities follow the format 'new_entitynames' (with publisher prefix). Check your Dynamics instance's entity set names via GET /api/data/v9.2/ to list all available entities.

Expected result: lib/dynamics.ts provides authenticated access to the Dynamics 365 Web API with OData headers. The OAuth routes handle authorization and token exchange on the deployed site. All Dynamics entity operations (contacts, accounts, opportunities) use the correct OData endpoint paths.

3

Handle Azure AD OAuth callback and token management

Build the OAuth callback handler that completes the Azure AD authorization flow on your deployed site. The callback receives the authorization code from Azure AD after user consent and exchanges it for access and refresh tokens. Azure AD OAuth returns the code in a query parameter and also includes a state parameter you can use to prevent CSRF attacks. The token exchange POSTs to https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token with grant_type=authorization_code, code, redirect_uri (must match registration exactly), client_id, client_secret, and scope. Azure AD access tokens expire after one hour (3600 seconds). The refresh token has a longer lifetime (typically 90 days for interactive flows). Implement automatic token refresh by catching 401 responses in dynamicsFetch and refreshing before retrying. For serverless deployments on Netlify, store tokens in Supabase rather than process.env — serverless function instances do not share memory, so process.env assignments do not persist across invocations. For service-to-service integrations where no user interaction is needed (the most common pattern for Bolt.new dashboards reading company CRM data), the client credentials flow is simpler. POST to the token endpoint with grant_type=client_credentials and client_id/client_secret — you get a token immediately without any redirect or user authorization. This requires Application permissions in Azure AD (not Delegated) and is supported for Dynamics 365. The client credentials approach is ideal for dashboards and reporting tools that do not need to impersonate specific users.

Bolt.new Prompt

Create Azure AD OAuth routes. (1) app/api/auth/dynamics/route.ts generates the Azure AD authorization URL: https://login.microsoftonline.com/{AZURE_TENANT_ID}/oauth2/v2.0/authorize with client_id, redirect_uri ({NEXT_PUBLIC_APP_URL}/api/auth/callback/dynamics), response_type=code, scope (DYNAMICS_ORG_URL + '/.default'), and response_mode=query. Redirect user to this URL. (2) app/api/auth/callback/dynamics/route.ts exchanges code by POSTing to https://login.microsoftonline.com/{AZURE_TENANT_ID}/oauth2/v2.0/token with grant_type=authorization_code, code, redirect_uri, client_id, client_secret. Log the access_token (truncated) and redirect to /dashboard. (3) app/api/auth/dynamics/client-credentials/route.ts POSTs to token endpoint with grant_type=client_credentials for service-to-service auth.

Paste this in Bolt.new chat

app/api/auth/callback/dynamics/route.ts
1// app/api/auth/callback/dynamics/route.ts
2import { NextResponse } from 'next/server';
3
4export async function GET(request: Request) {
5 const { searchParams } = new URL(request.url);
6 const code = searchParams.get('code');
7 if (!code) {
8 return NextResponse.json({ error: 'Missing authorization code' }, { status: 400 });
9 }
10
11 const tenantId = process.env.AZURE_TENANT_ID!;
12 const appUrl = process.env.NEXT_PUBLIC_APP_URL!;
13
14 const tokenRes = await fetch(
15 `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
16 {
17 method: 'POST',
18 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
19 body: new URLSearchParams({
20 grant_type: 'authorization_code',
21 code,
22 redirect_uri: `${appUrl}/api/auth/callback/dynamics`,
23 client_id: process.env.AZURE_CLIENT_ID!,
24 client_secret: process.env.AZURE_CLIENT_SECRET!,
25 scope: `${process.env.DYNAMICS_ORG_URL}/.default`,
26 }),
27 }
28 );
29
30 if (!tokenRes.ok) {
31 const err = await tokenRes.json();
32 return NextResponse.json({ error: err }, { status: 400 });
33 }
34
35 const tokens = await tokenRes.json();
36 console.log('Dynamics tokens:', {
37 access_token_preview: tokens.access_token?.substring(0, 30) + '...',
38 expires_in: tokens.expires_in,
39 has_refresh_token: !!tokens.refresh_token,
40 });
41
42 // Store access_token as DYNAMICS_ACCESS_TOKEN in hosting env vars
43 return NextResponse.redirect(new URL('/dashboard', request.url));
44}

Pro tip: For the client credentials flow (service-to-service, no user interaction), use scope='{DYNAMICS_ORG_URL}/.default' and grant_type=client_credentials. The token endpoint is the same, but you get a token immediately without any redirect URI or user authorization code. This requires Application permissions in Azure AD.

Expected result: The OAuth callback route exchanges the Azure AD authorization code for a Dynamics 365 access token. The flow completes on the deployed site (cannot work in the WebContainer preview). The access token is ready for storage in .env or Supabase for production use.

4

Build the Dynamics 365 sales pipeline dashboard with OData queries

Build the opportunity pipeline dashboard using Dynamics 365's OData query capabilities. OData enables server-side filtering, sorting, and field selection that reduces data transfer and API processing time compared to fetching all records and filtering client-side. The Dynamics 365 Web API supports standard OData v4 query options: $select (specify which fields to return — always use this to avoid fetching all entity fields), $filter (server-side filtering using OData expressions), $orderby (sort results), $top (limit result count), $expand (include related entity data in one request), and $count (include a total record count). Key OData filter expressions for Dynamics: statecode eq 0 (open/active records), estimatedclosedate gt 2024-01-01T00:00:00Z (date comparisons use ISO format), contains(name, 'Acme') (string contains), and _ownerid_value eq {guid} (filter by owner using lookup field). For the pipeline dashboard, fetch opportunities with statecode eq 0 (open) and $select=name,estimatedvalue,salesstage,estimatedclosedate,_ownerid_value&$expand=ownerid($select=fullname). The $expand fetches the related owner's full name in a single request rather than requiring separate lookup calls. Dynamics 365 returns lookup field values (foreign keys) with a special naming convention: the field name is prefixed with underscore and suffixed with _value. For example, the opportunity owner lookup is _ownerid_value (returns a GUID), while $expand=ownerid($select=fullname) returns the actual name. Always use $expand for related record display to avoid N+1 query patterns.

Bolt.new Prompt

Build a Dynamics 365 sales pipeline dashboard at app/dashboard/dynamics/page.tsx. Fetch from /api/dynamics/opportunities which calls getOpportunities with filter 'statecode eq 0' and select 'name,estimatedvalue,salesstage,estimatedclosedate,_ownerid_value'. Group opportunities by salesstage. Display as a pipeline table with columns: Opportunity Name, Estimated Value (formatted currency), Sales Stage, Expected Close Date, Owner. Add a summary row showing total pipeline value and weighted forecast (sum of estimatedvalue * 0.5 for each stage as a rough estimate). Add a Recharts BarChart showing opportunity count by stage. Use Tailwind CSS with alternating row colors.

Paste this in Bolt.new chat

app/api/dynamics/opportunities/route.ts
1// app/api/dynamics/opportunities/route.ts
2import { NextResponse } from 'next/server';
3import { getOpportunities } from '@/lib/dynamics';
4
5export async function GET(request: Request) {
6 const { searchParams } = new URL(request.url);
7 const filter = searchParams.get('filter') ?? 'statecode eq 0';
8
9 try {
10 const data = await getOpportunities(
11 filter,
12 'name,estimatedvalue,salesstage,estimatedclosedate,statecode,_ownerid_value'
13 );
14
15 const opportunities = (data.value ?? []).map((opp) => ({
16 id: opp.opportunityid,
17 name: opp.name,
18 value: typeof opp.estimatedvalue === 'number' ? opp.estimatedvalue : 0,
19 stage: opp.salesstage ?? 'Unknown',
20 closeDate: opp.estimatedclosedate,
21 ownerId: opp._ownerid_value,
22 }));
23
24 return NextResponse.json({
25 opportunities,
26 totalValue: opportunities.reduce((sum, o) => sum + o.value, 0),
27 });
28 } catch (error) {
29 const message = error instanceof Error ? error.message : 'Unknown error';
30 return NextResponse.json({ error: message }, { status: 500 });
31 }
32}

Pro tip: OData $filter strings are case-sensitive for field names in Dynamics 365. Logical names (used in the API) are lowercase: 'statecode', 'estimatedvalue', 'estimatedclosedate'. Display names in the Dynamics UI may differ — always use the logical name in OData queries, which you can find in Dynamics Settings → Customizations → Customize the System → Entities → Fields.

Expected result: The Dynamics 365 pipeline dashboard displays open opportunities grouped by sales stage with total pipeline value and a bar chart. OData $select limits the response to only required fields, and $filter restricts results to open opportunities, making queries efficient.

Common use cases

Dynamics 365 CRM Sales Dashboard

An internal sales dashboard showing Dynamics 365 opportunities, accounts, and contacts in a custom interface for team members who do not have individual Dynamics 365 licenses. The dashboard displays the sales pipeline by stage, opportunity values, and close date projections using Dynamics Web API OData queries.

Bolt.new Prompt

Build a Dynamics 365 sales dashboard. Create /api/dynamics/opportunities that GETs https://{DYNAMICS_ORG_URL}/api/data/v9.2/opportunities?$select=name,estimatedvalue,salesstage,estimatedclosedate,_ownerid_value&$filter=statecode eq 0&$orderby=estimatedvalue desc using Bearer DYNAMICS_ACCESS_TOKEN from process.env. Also create /api/dynamics/accounts that GETs accounts with $select=name,revenue,numberofemployees,address1_city. In React, display opportunities as a Kanban board grouped by salesstage. Show account info in a side panel. Use Tailwind CSS.

Copy this prompt to try it in Bolt.new

Lead Capture Form Creating Dynamics 365 Contacts and Leads

A website contact form that creates a Dynamics 365 lead record when submitted, triggering the organization's existing lead qualification workflow in Dynamics. The lead includes all standard fields (first name, last name, email, company, phone, topic) and is assigned to the appropriate queue or team based on form metadata.

Bolt.new Prompt

Create a Dynamics 365 lead capture form. Build a form with firstName, lastName, email, phone, company, and message fields. On submit, POST to /api/dynamics/leads which calls POST https://{DYNAMICS_ORG_URL}/api/data/v9.2/leads using Bearer DYNAMICS_ACCESS_TOKEN. Map fields: firstname, lastname, emailaddress1, telephone1, companyname, subject (use 'Website Inquiry'), and description (the message). Return the lead ID from the OData-EntityId response header. Show confirmation with the lead ID.

Copy this prompt to try it in Bolt.new

Opportunity Forecasting Report

A quarterly revenue forecast report that aggregates Dynamics 365 opportunities by close date quarter, showing expected revenue, weighted pipeline value, and conversion rates by sales stage. Finance and leadership teams access this report without needing Dynamics 365 licenses, and it can be shared as a standalone dashboard.

Bolt.new Prompt

Create a Dynamics 365 opportunity forecast report. Build /api/dynamics/forecast that GETs opportunities with $select=name,estimatedvalue,probability,estimatedclosedate,salesstage,_ownerid_value&$filter=statecode eq 0 (open opportunities). Group opportunities by quarter (Q1-Q4 of current fiscal year). For each quarter, calculate: total expected revenue, probability-weighted pipeline value (estimatedvalue * probability / 100), and deal count by stage. Return this as structured JSON. In React, display a Recharts BarChart showing expected vs weighted revenue by quarter. Add a table with all opportunities sortable by value, stage, and close date.

Copy this prompt to try it in Bolt.new

Troubleshooting

Azure AD OAuth returns 'AADSTS50011: The redirect URI specified in the request does not match the redirect URIs configured' error

Cause: The redirect URI in the authorization request does not exactly match the URI registered in the Azure AD app registration. Azure AD requires an exact match including protocol, domain, path, and trailing slash.

Solution: Go to Azure Portal → Azure Active Directory → App registrations → your app → Authentication. Check the Redirect URIs list. Ensure your deployed URL (e.g., https://your-app.netlify.app/api/auth/callback/dynamics) is listed exactly as you send it in the request. Remember that Bolt's WebContainer preview URL cannot be used — register the deployed Netlify or Bolt Cloud URL.

Dynamics 365 Web API returns 401 Unauthorized with 'Bearer error=invalid_token'

Cause: The Azure AD access token has expired (tokens expire after 1 hour) or the token's audience does not match the Dynamics 365 org URL. Tokens issued for one Dynamics org cannot be used with a different org.

Solution: Use the refresh token to obtain a new access token. POST to https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token with grant_type=refresh_token. Verify that the scope used when obtaining the token includes your exact DYNAMICS_ORG_URL — a token scoped to https://org1.crm.dynamics.com will not work for https://org2.crm.dynamics.com.

typescript
1// Refresh an expired Dynamics 365 access token
2async function refreshDynamicsToken(refreshToken: string): Promise<string> {
3 const res = await fetch(
4 `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/oauth2/v2.0/token`,
5 {
6 method: 'POST',
7 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
8 body: new URLSearchParams({
9 grant_type: 'refresh_token',
10 refresh_token: refreshToken,
11 client_id: process.env.AZURE_CLIENT_ID!,
12 client_secret: process.env.AZURE_CLIENT_SECRET!,
13 scope: `${process.env.DYNAMICS_ORG_URL}/.default`,
14 }),
15 }
16 );
17 const { access_token } = await res.json();
18 return access_token;
19}

OData $filter query returns 400 Bad Request with 'Could not find a property named...' error

Cause: The field name used in the $filter expression is not the logical name of the field in Dynamics 365. OData requires the exact logical name (lowercase, no spaces), not the display name shown in the Dynamics UI.

Solution: Find the logical field names in Dynamics 365 Settings → Customizations → Customize the System → select the entity → Fields. The 'Name' column shows display names; the 'Field Name' column shows logical names used in API queries. Common mistakes: 'Subject' (display) vs 'subject' (logical), 'EstimatedValue' vs 'estimatedvalue', 'CloseDate' vs 'estimatedclosedate'.

Best practices

  • Store all Azure AD credentials (client ID, tenant ID, client secret) and the Dynamics access token in server-side .env with no NEXT_PUBLIC_ prefix — they provide access to your organization's entire CRM database
  • Always use $select in OData queries to request only the fields you need — fetching all entity fields by default returns dozens of columns and increases response size significantly, slowing your dashboard
  • Complete Azure AD OAuth flows on your deployed site, not in Bolt's WebContainer preview — the preview uses dynamic StackBlitz URLs that cannot be registered as stable Azure AD redirect URIs
  • Use $filter with statecode eq 0 for active records and statecode eq 1 for inactive — always filter by state unless you intentionally want all records including deleted ones
  • Implement access token refresh logic for production — Azure AD tokens expire after one hour, and failed API calls due to token expiry cause confusing user-facing errors
  • Use $expand sparingly and only for the related fields you actually display — each expanded entity adds to response size; for large datasets, fetch related records only in detail views, not list views
  • For service-to-service integrations (dashboards, reporting tools that access CRM data without acting as a specific user), use the client credentials OAuth flow rather than authorization code — it is simpler and does not require a redirect URI or user interaction

Alternatives

Frequently asked questions

Does Dynamics 365 work with Bolt.new?

Yes. Dynamics 365's Web API uses standard HTTPS requests with OData query syntax that work in Bolt's WebContainer via Next.js API routes. The key constraint is Azure AD OAuth 2.0 authentication — the initial OAuth flow requires a stable redirect URI (your deployed Netlify or Bolt Cloud site), but once you have an access token, all Dynamics API operations work in both development and production.

How do I connect Bolt.new to Dynamics 365?

Register an Azure AD application in the Azure Portal with Dynamics CRM user_impersonation permissions. Deploy your app to Netlify or Bolt Cloud and register the deployed URL as the Azure AD redirect URI. Complete the OAuth 2.0 authorization flow to get an access token. Store the token as DYNAMICS_ACCESS_TOKEN in .env alongside your Azure client ID, tenant ID, and Dynamics org URL. Create Next.js API routes that include both the Bearer token and OData headers on requests to {DYNAMICS_ORG_URL}/api/data/v9.2.

What is OData and why does Dynamics 365 use it?

OData (Open Data Protocol) is a standard REST protocol that adds a rich query language on top of HTTP. Dynamics 365 uses OData v4 for its Web API, enabling server-side filtering ($filter), field selection ($select), sorting ($orderby), related record expansion ($expand), and pagination ($top, $skip). OData is particularly powerful for CRM use cases because it lets you fetch exactly the data you need in a single request without custom API endpoints.

Is there a free tier for Dynamics 365 API access?

No. Dynamics 365 requires an active paid license — there is no free tier or developer trial that includes CRM functionality. Business plan pricing starts at $65/user/month for Sales Professional. Microsoft does offer limited free trials (30 days) and a Power Apps Developer Plan that includes some Dynamics functionality for individual developers, but these are not suitable for production applications.

Why can I not complete the Dynamics 365 OAuth flow in Bolt's WebContainer preview?

Bolt's WebContainer runs in a browser sandbox with dynamic session-specific URLs generated by StackBlitz (e.g., https://[hash].local.webcontainer-api.io/). These URLs change between sessions and cannot be registered as stable redirect URIs in Azure AD app registrations. Deploy to Netlify or Bolt Cloud, register that stable domain, and complete OAuth there. After getting an access token, store it in .env and continue developing in the preview.

How do I query Dynamics 365 contacts filtered by account or date?

Use OData $filter syntax in the Web API query string. To filter contacts by account: $filter=_parentcustomerid_value eq {account_guid}. To filter opportunities by close date: $filter=estimatedclosedate gt 2024-01-01T00:00:00Z. To filter by owner: $filter=_ownerid_value eq {user_guid}. Combine filters with 'and': $filter=statecode eq 0 and estimatedclosedate gt 2024-01-01T00:00:00Z. Field names are always lowercase logical names.

RapidDev

Talk to an Expert

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

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

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.