Skip to content

How to Integrate with the Microsoft Dynamics 365 CRM API (2026 Guide)

A technical guide to integrating with the Dynamics 365 Dataverse Web API — covering Azure AD OAuth setup, OData quirks, Service Protection rate limits, and architectural shortcuts.

Roopendra Talekar Roopendra Talekar · · 14 min read
How to Integrate with the Microsoft Dynamics 365 CRM API (2026 Guide)

If you need to integrate with Microsoft Dynamics 365 CRM, you are actually integrating with the Dataverse Web API — an OData v4 REST service that sits underneath Dynamics 365 Sales, Customer Service, and the broader Power Platform. You cannot simply send JSON payloads to a generic REST endpoint and expect it to work. You must navigate Azure Active Directory OAuth 2.0 authentication, strict Service Protection limits, polymorphic lookups, and deeply nested OData navigation properties.

If you are a product manager or engineering lead at a B2B SaaS company, your customers are demanding this integration. Enterprise sales teams live in Dynamics 365. If your product does not read from and write back to their system of record, it will be abandoned after the pilot phase.

This guide covers the real architectural decisions you will face: authentication via Microsoft Entra ID, surviving Service Protection throttling, handling heavily customized schemas, and deciding whether to build the integration yourself or use an abstraction layer.

Why Integrating with Microsoft Dynamics 365 is Non-Negotiable in 2026

If you are building B2B sales software, your product does not exist in isolation. It lives inside a stack of tools that sales representatives are already required to use.

Gartner reports that 50% of sellers feel overwhelmed by the number of platforms they must navigate daily. Every time a rep switches out of your application to manually log a call, update a deal stage, or enrich a contact record in their CRM, you lose product stickiness.

Dynamics 365 is not Salesforce, but it is the CRM that enterprise IT departments choose when they have already committed to the Microsoft ecosystem. Microsoft's Dynamics 365 CRM product had a revenue growth of 23% in FY25 Q4. Enlyft's research shows that Microsoft Dynamics 365 has a market share of about 10.32% in the CRM category. Over 97,653 companies use Microsoft Dynamics 365, and that number keeps climbing as organizations migrate off legacy Dynamics AX and CRM on-premise deployments. Meanwhile, the global CRM market reached $112.91 billion in 2025 and is projected to reach $262.74 billion by 2032, driven by AI and automation, according to SellersCommerce.

The most requested integrations for B2B sales tools always start with Salesforce and HubSpot, but Dynamics 365 is the undisputed third pillar. If you are selling into the mid-market or enterprise, lacking a native Dynamics 365 integration means losing deals — especially in competitive evaluations where the buyer's IT team runs on Microsoft 365, Azure AD, and Teams.

Understanding the Dynamics 365 Web API vs. Dataverse

Terminology in the Microsoft ecosystem is notoriously confusing. The documentation is vast, fragmented, and often references legacy systems. Here is the short version:

  • Dataverse (formerly Common Data Service / CDS) is the underlying cloud database and application platform that stores all Dynamics 365 data. When you query Dynamics 365 Sales, you are actually querying Dataverse tables.
  • The Dataverse Web API is the modern, RESTful OData v4 endpoint used to read and write that data. This is what you should use for all new integrations.
  • Dynamics 365 Sales / Customer Service / etc. are applications built on top of Dataverse.
  • Organization Service (SOAP): The legacy endpoint. Do not build new integrations against it. Microsoft 365 authentication for Dataverse is deprecated. If you are using anything other than .NET Framework, you must authenticate using OAuth and use the OData RESTful web services.
  • OData (Open Data Protocol): An OASIS standard that defines best practices for building and consuming RESTful APIs. Dynamics 365 relies heavily on OData syntax ($select, $filter, $expand, $orderby, $top) for all data retrieval.

Like the Microsoft Graph API, the Dataverse Web API is an OData RESTful service. Unlike the Microsoft Graph API, each environment has a different organization URL and includes custom tables and operations that can be unique to each environment.

Your base endpoint follows this structure:

https://yourorg.api.crm.dynamics.com/api/data/v9.2

Every request to this endpoint must include standard OData headers to ensure you receive clean JSON responses:

Accept: application/json
OData-MaxVersion: 4.0
OData-Version: 4.0
Prefer: odata.include-annotations="*"

If you have worked with OData before, the patterns will feel familiar. If you have not, prepare for a learning curve — OData filter syntax is more verbose than typical REST query parameters, and the relationship navigation (lookup columns, expand on navigation properties) has its own set of quirks we will cover later.

Dynamics 365 OAuth Authentication (Azure AD / Microsoft Entra ID)

Authentication is often the highest hurdle for engineering teams building their first Microsoft integration. Dynamics 365 does not use simple API keys. Dynamics 365 (built on Dataverse) supports OAuth 2.0 for authentication to its Web API. For Dataverse, the identity provider is Microsoft Entra ID.

You have two main authentication flows:

Flow Use Case Grant Type
Authorization Code Your SaaS lets end-users connect their own Dynamics 365 org authorization_code
Client Credentials Server-to-server sync with no user present client_credentials

For a multi-tenant B2B SaaS product, you will almost always need the authorization code flow so each customer can connect their own Dynamics 365 environment. The client credentials flow is for scenarios where you control both sides.

Step 1: App Registration in Azure AD

To get started, register an application in the Azure portal:

  1. Navigate to the Azure portal and open Microsoft Entra ID (formerly Azure AD).
  2. Go to App registrations and create a new registration.
  3. Choose Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant). This is critical if you are building a SaaS application that external customers will connect to.
  4. Add a Redirect URI (e.g., https://your-app.com/oauth/callback).
  5. Under API permissions, add the user_impersonation permission for Dynamics CRM.
  6. Generate a client secret and store the client ID, client secret, and tenant ID securely.

For a detailed walkthrough of the Azure portal setup, see our 3 steps to integrate Microsoft Dynamics 365 Sales using Web API guide (the Azure AD registration process is nearly identical in our Business Central step-by-step guide).

Step 2: The OAuth 2.0 Flow

Your application must direct the user to the Microsoft authorization endpoint to grant consent.

sequenceDiagram
    participant User
    participant SaaS App
    participant Azure AD
    participant Dynamics API
    
    User->>SaaS App: Clicks "Connect Dynamics 365"
    SaaS App->>Azure AD: Redirects to /oauth2/v2.0/authorize<br>with client_id, scope, redirect_uri
    Azure AD->>User: Prompts for login & consent
    User->>Azure AD: Grants consent
    Azure AD->>SaaS App: Redirects back with Authorization Code
    SaaS App->>Azure AD: POST /oauth2/v2.0/token<br>with code, client_id, client_secret
    Azure AD->>SaaS App: Returns Access Token & Refresh Token
    SaaS App->>Dynamics API: API calls with Bearer Access Token

Here is the token exchange as a concrete example using the client credentials flow:

curl -X POST \
  "https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id={client_id}" \
  -d "client_secret={client_secret}" \
  -d "scope=https://yourorg.crm.dynamics.com/.default" \
  -d "grant_type=client_credentials"

This returns an access_token with a default TTL of 3,599 seconds (roughly one hour). Use it as a Bearer token on all subsequent requests:

curl -X GET \
  "https://yourorg.crm.dynamics.com/api/data/v9.2/contacts?$select=fullname,emailaddress1&$top=10" \
  -H "Authorization: Bearer {access_token}" \
  -H "OData-MaxVersion: 4.0" \
  -H "OData-Version: 4.0" \
  -H "Accept: application/json"

Step 3: Managing Token Lifecycles

The number one production issue with Dynamics 365 integrations is stale tokens. Microsoft access tokens expire after one hour, and refresh tokens can be revoked by tenant admins at any time. If you do not proactively refresh tokens before they expire, your integration will start throwing 401 Unauthorized errors during business hours — exactly when your customer notices.

Token refresh failures are a leading cause of broken integrations. If your background job attempts to refresh a token and fails due to a network timeout, and you do not handle the retry logic correctly, the refresh token may be invalidated. This results in an invalid_grant error, forcing the customer to manually reconnect their account.

Warning

Authentication Pitfall: Always implement a proactive refresh strategy. Do not wait for a 401 Unauthorized response from Dynamics 365 to trigger a refresh. Schedule a background worker to refresh the token 5 to 10 minutes before its TTL expires. If you are managing multiple customer connections, you need a system that schedules token refreshes per-account and flags accounts that need re-authorization when a refresh fails.

Surviving Dynamics 365 Service Protection API Limits

This is where most teams get burned. Microsoft enforces strict Service Protection limits to prevent any single integration from degrading the performance of the Dataverse environment. If you poll the API aggressively or attempt to bulk-sync millions of records without a throttling strategy, your integration will fail.

These limits are designed to protect the system from extraordinary API request volumes. The default limits are enforced per user, per web server, on a 5-minute sliding window:

Measure Limit
Number of requests 6,000 per 5-minute window
Combined execution time 20 minutes (1,200 seconds) per 5-minute window
Concurrent requests 52 or higher

When you exceed these limits, the API returns an HTTP 429 Too Many Requests with a Retry-After header indicating how long to wait.

The tricky part: most Dynamics 365 environments have multiple web servers, but you cannot control or predict which server handles your request. And the execution time limit is sneaky — a single complex $expand query that takes 5 seconds to process on the server side counts the same as 5 simple queries that each take 1 second. Batch requests ($batch) reduce the request count but increase execution time per request.

Practical Throttling Strategy

flowchart TD
    A[Send API Request] --> B{HTTP 429?}
    B -- No --> C[Process Response]
    B -- Yes --> D[Read Retry-After Header]
    D --> E[Wait for specified duration + jitter]
    E --> A
    C --> F{More requests?}
    F -- Yes --> G{Approaching<br>6,000 req limit?}
    G -- No --> A
    G -- Yes --> H[Slow down request rate]
    H --> A
    F -- No --> I[Done]

The practical advice from Microsoft's own documentation is to let the server tell you how much it can handle: start with a low request rate, gradually increase, and back off when you get 429s. Do not try to pre-calculate a "safe" rate — each environment is different based on its license tier and current load.

Implementing Exponential Backoff

You cannot blindly retry a failed request. The 429 response includes a Retry-After header indicating the number of seconds you must wait. Your HTTP client must intercept the 429 status, read the header, pause execution, and retry. If the retry fails, apply exponential backoff with jitter to avoid thundering herd problems.

async function fetchWithDynamicsBackoff(
  url: string,
  options: RequestInit,
  maxRetries = 3
): Promise<Response> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, options);
 
    if (response.status === 429 && attempt < maxRetries) {
      const retryAfterStr = response.headers.get('Retry-After');
      const retryAfterSeconds = retryAfterStr ? parseInt(retryAfterStr, 10) : 5;
 
      // Add random jitter to prevent concurrent workers from retrying simultaneously
      const jitter = Math.random() * 1000;
      const delayMs = retryAfterSeconds * 1000 + jitter;
 
      console.warn(
        `Throttled by Dynamics 365. Waiting ${delayMs}ms (attempt ${attempt + 1})`
      );
      await new Promise((r) => setTimeout(r, delayMs));
      continue;
    }
 
    if (!response.ok) {
      throw new Error(`Dynamics 365 API error: ${response.status}`);
    }
 
    return response;
  }
  throw new Error('Max retries exceeded for Dynamics 365 API');
}
Warning

Do not ignore the Retry-After header. Some teams implement a fixed exponential backoff (2s, 4s, 8s) and ignore the server's recommendation. This will extend your total wait time. The Retry-After value is calculated based on your actual load profile and is usually shorter than generic backoff.

Batching Requests

To stay under the 6,000 request limit during initial data syncs, use OData batch requests. The $batch endpoint allows you to group up to 1,000 individual operations into a single HTTP request. This reduces the number of network round-trips, though keep in mind that batch requests increase server-side execution time per request — so you are trading one limit for another.

For more architectural patterns on handling strict throttling across multiple providers, read our guide on How to Handle Third-Party API Rate Limits When AI Agents Scrape Data.

Handling Custom Entities and Complex Data Models

Here is the part that makes Dynamics 365 integration fundamentally harder than HubSpot or Pipedrive: every Dynamics 365 org is different. Enterprise customers customize their Dataverse schemas aggressively — adding custom tables (entities), custom columns (attributes), custom option sets, and custom relationships.

A "contact" in one Dynamics 365 org might have 30 fields. In another, it has 150 fields including new_loyaltytier, cr5e2_preferredlanguage, and a dozen lookup columns pointing to custom tables you have never seen before. If your integration only supports standard fields like firstname and email on the contact table, it will be useless to enterprise buyers.

Because Microsoft Dataverse is a metadata-driven application, developers might need to query the system definitions at run-time to adapt to how an organization is configured.

Querying Metadata at Runtime

You can discover the full schema of any entity (including custom ones) via the EntityDefinitions endpoint:

GET /api/data/v9.2/EntityDefinitions(LogicalName='account')/Attributes
  ?$select=LogicalName,SchemaName,AttributeType,DisplayName

This returns a JSON payload describing every field on the table, including custom fields (which typically start with a publisher prefix like cr0a3_).

For picklist (option set) fields, you need to cast to the specific attribute type to get the available options:

GET /api/data/v9.2/EntityDefinitions(LogicalName='account')
  /Attributes(LogicalName='accountcategorycode')
  /Microsoft.Dynamics.CRM.PicklistAttributeMetadata
  ?$select=LogicalName
  &$expand=OptionSet($select=Options)

This metadata-first approach is how any serious integration handles the variation between Dynamics 365 orgs. You query the schema, discover what custom fields exist, and adapt your field mappings accordingly. But this also means your integration needs a layer of indirection between your data model and theirs — you cannot hardcode field names.

OData Query Construction

When fetching data, you must explicitly define which fields you want using the $select parameter. If you omit $select, Dynamics 365 returns every field on the record, which causes severe performance degradation and wastes bandwidth.

To fetch related records in a single request, use the $expand parameter:

GET /api/data/v9.2/opportunities?$select=name,estimatedvalue&$expand=parentcontactid($select=fullname,emailaddress1)

The OData Navigation Property Trap

One of the most common gotchas: lookup fields in OData do not behave like you expect. A lookup column named parentcustomerid on the contact entity is not queryable directly. You need to use the navigation property pattern:

# Wrong — will error
/contacts?$filter=parentcustomerid eq '{account-id}'
 
# Correct — use the navigation property
/contacts?$filter=parentcustomerid_account/accountid eq {account-id}

The underscore-then-entity-name suffix (_account) tells OData which entity the polymorphic lookup points to. This is documented, but buried deep enough that teams routinely waste hours debugging it.

Dynamics 365 limits the number of records returned in a single response to 5,000. To iterate through large datasets, you must follow the @odata.nextLink URL provided in the JSON response:

{
  "@odata.context": "https://[org].api.crm.dynamics.com/api/data/v9.2/$metadata#accounts",
  "value": [
    { "accountid": "123", "name": "Acme Corp" }
  ],
  "@odata.nextLink": "https://[org].api.crm.dynamics.com/api/data/v9.2/accounts?$skiptoken=[opaque-token]"
}

Follow the @odata.nextLink URL until it is no longer present in the response. Do not try to construct pagination parameters manually — the $skiptoken is opaque and server-generated.

How Truto Simplifies Dynamics 365 CRM API Integration

Building a production-grade Dynamics 365 integration from scratch is a multi-week effort at minimum: Azure AD app registration, OAuth token lifecycle management, OData query construction, metadata discovery for custom fields, rate limit handling with proper backoff, and cursor-based pagination. Multiply that effort if you also need to support Salesforce, HubSpot, and Pipedrive.

Truto's Unified CRM API abstracts these provider-specific details behind a single endpoint. You do not write if (provider === 'dynamics') in your codebase. The same API call that lists contacts in HubSpot also lists contacts in Dynamics 365:

# Same call, regardless of whether the customer uses
# Dynamics 365, Salesforce, HubSpot, or Pipedrive
curl -X GET \
  "https://api.truto.one/unified/crm/contacts?integrated_account_id={id}&limit=10" \
  -H "Authorization: Bearer {truto_api_token}"

The response comes back in a normalized schema:

{
  "result": [
    {
      "id": "a1b2c3d4-...",
      "first_name": "Jane",
      "last_name": "Chen",
      "email": "jane.chen@contoso.com",
      "phone": "+1-555-0147",
      "account": { "id": "x7y8z9...", "name": "Contoso Ltd" },
      "created_at": "2026-01-15T09:30:00Z",
      "remote_data": { /* raw Dataverse response preserved here */ }
    }
  ],
  "next_cursor": "eyJwYWdl..."
}

The remote_data field preserves the full original Dataverse response, so you can always access custom fields that fall outside the unified schema.

What the Platform Handles

Here is how Truto maps to the pain points we covered:

Pain Point What Truto Does
OAuth token lifecycle Refreshes tokens shortly before expiry; fires an integrated_account:authentication_error webhook if re-auth is needed
Service Protection limits (6,000 req / 5 min) Built-in rate limit detection with automatic backoff based on Retry-After headers. Your application never sees the 429 error.
OData query syntax Maps unified query parameters to Dataverse-native OData $filter and $select expressions via expression-based transformations
Custom fields and entities Accessible via Proxy API; unified schema covers standard CRM objects
Pagination Normalizes Dataverse's @odata.nextLink cursor into a standard next_cursor / prev_cursor pattern that works the same for every CRM

The architecture uses no integration-specific code. The same generic execution pipeline handles Dynamics 365, Salesforce, HubSpot, and every other CRM — the only difference is a configuration that describes each provider's endpoints, auth scheme, and field mappings. When Microsoft changes the Dataverse API (and they do — the v9.2 endpoint has seen breaking changes in how certain metadata annotations are returned), the fix is a config update, not a code deployment.

Proxy API for Custom Dataverse Tables

Unified models are perfect for standard CRM objects, but enterprise customers always have custom Dataverse tables. Truto's Proxy API allows you to make direct, authenticated requests to the Dynamics 365 Web API without dealing with token management or rate limiting. You pass the exact OData query you need, and Truto routes the request through the authenticated connection, returning the raw Dataverse response.

That said, a unified API is not a magic solution for every scenario. If you need deep Dynamics 365-specific functionality — like triggering Power Automate flows, interacting with Dynamics 365 Marketing segments, or calling custom Dataverse actions — you are writing provider-specific logic at that point. The unified layer shines for the 80% of CRM operations that are common across providers: listing contacts, creating opportunities, updating deal stages, and logging activities.

What Your Integration Architecture Should Look Like

Whether you build directly against the Dataverse Web API or use an abstraction layer, your architecture needs to account for:

  1. Token storage and refresh — Per-customer credential management with proactive refresh and re-auth detection.
  2. Rate limit resilience — Respect the 6,000 req / 5 min limit per user. Use Retry-After headers, not fixed backoff.
  3. Schema discovery — Query EntityDefinitions to handle custom fields. Cache metadata (it changes rarely) to avoid burning API requests.
  4. Pagination — Follow the @odata.nextLink URLs. Do not construct pagination parameters manually.
  5. Error handling — Dataverse error responses are verbose XML-wrapped JSON. Parse the error.message field for actionable info.
flowchart LR
    A[Your SaaS App] --> B{Build or Buy?}
    B -- Build Direct --> C[Azure AD OAuth<br>Token Management]
    C --> D[OData Query Builder]
    D --> E[Rate Limiter +<br>Retry Logic]
    E --> F[Dataverse Web API]
    B -- Use Unified API --> G[Truto Unified CRM API]
    G --> F
    G --> H[Salesforce API]
    G --> I[HubSpot API]
    G --> J[Pipedrive API]

The build-direct path works well if Dynamics 365 is your only CRM integration and you have dedicated engineering capacity to maintain it. The moment you need a second or third CRM provider, the unified API approach pays for itself in weeks of saved engineering time.

Next Steps

If you are scoping a Dynamics 365 integration today:

  • Start with the auth flow. Register an Azure AD app with multi-tenant support and user_impersonation delegated permissions. Test the full OAuth round-trip before writing a single line of business logic.
  • Prototype against the OData endpoint. Use Postman or curl to query /api/data/v9.2/contacts and /api/data/v9.2/EntityDefinitions to understand the response shapes and metadata model.
  • Plan for rate limits from day one. Bake Retry-After handling into your HTTP client layer. It is much harder to add later.
  • Evaluate whether you need multi-CRM support. If the answer is yes (and for most B2B SaaS products, it is), a unified API will save you months of engineering effort and ongoing maintenance.
  • Integrating with Microsoft's ERP? If your customers also need financial data sync, check out our guide on how to architect a Microsoft Dynamics 365 Business Central API integration.

For a deeper look at keeping CRM data in sync across systems, see our guide to architecting real-time CRM syncs.

FAQ

What API does Microsoft Dynamics 365 CRM use?
Dynamics 365 CRM uses the Dataverse Web API, which is an OData v4 REST endpoint. The base URL follows the pattern https://yourorg.api.crm.dynamics.com/api/data/v9.2. All modern integrations use this endpoint with OAuth 2.0 authentication via Microsoft Entra ID.
What are the API rate limits for Microsoft Dynamics 365?
Dynamics 365 enforces Service Protection limits of 6,000 requests per 5-minute sliding window, 20 minutes of combined execution time per 5-minute window, and 52 concurrent requests per user per web server. Exceeding these returns HTTP 429 with a Retry-After header.
How do I authenticate with the Dynamics 365 Web API?
Register an application in Microsoft Entra ID (Azure AD), configure it with Dynamics CRM permissions, and use OAuth 2.0 — either the authorization code flow for user-delegated access or client credentials for server-to-server integration. Access tokens expire after approximately one hour.
How do I query custom fields in Dynamics 365 via API?
Use the EntityDefinitions endpoint to discover custom tables and fields at runtime. For example, GET /api/data/v9.2/EntityDefinitions(LogicalName='contact')/Attributes returns all attributes including custom ones. Custom fields typically start with a publisher prefix like cr0a3_. This metadata-driven approach lets your integration adapt to each customer's schema.
Can I use one API to integrate with Dynamics 365 and Salesforce at the same time?
Yes. Unified CRM APIs like Truto normalize data from Dynamics 365, Salesforce, HubSpot, and other CRMs into a common schema. You write one integration and the platform handles the provider-specific OData queries, authentication, pagination, and rate limiting for each CRM.

More from our Blog