Dynamics 365 Web API (OData v4) Capabilities & Rate Limits

Type: ERP Integration System: Dynamics 365 F&O (10.0.x) + Business Central (API v2.0) Confidence: 0.93 Sources: 8 Verified: 2026-03-01 Freshness: 2026-03-01

TL;DR

System Profile

This card covers the OData v4 Web API surfaces for two distinct Microsoft Dynamics 365 products: Finance & Operations (F&O, also called Finance & Supply Chain Management / F&SCM) and Business Central (BC). While both use OData v4, they have different API architectures, endpoint structures, rate limits, and throttling behaviors. F&O exposes data entities at /data/ endpoints with server-driven paging. BC exposes standard API v2.0 entities at /api/v2.0/ and OData web services at /ODataV4/. This card does not cover the Dataverse Web API, Virtual Entities, or Dual Write. [src1, src2, src3]

PropertyDynamics 365 F&ODynamics 365 Business Central
VendorMicrosoftMicrosoft
SystemFinance & Operations 10.0.xBusiness Central (SaaS)
API SurfaceOData v4 (/data/)OData v4 (/api/v2.0/, /ODataV4/)
Current API VersionContinuous release (10.0.x)API v2.0 (stable)
Editions CoveredAll cloud editionsEssentials, Premium
DeploymentCloudCloud (SaaS only)
API DocsF&O OData docsBC API v2.0 docs
StatusGAGA

API Surfaces & Capabilities

API SurfaceProductProtocolBest ForMax Records/RequestRate LimitReal-time?Bulk?
OData v4 Data EntitiesF&OHTTPS/JSONCRUD on individual records, queries10,000 (server paging)6,000 req/5min/user/serverYesLimited
OData $batchF&OHTTPS/JSON multipartAtomic multi-record operationsMultiple changesetsCounts toward 6,000 limitYesModerate
Custom Service EndpointsF&OHTTPS/JSONCustom X++ business logicN/ACounts toward 6,000 limitYesNo
Data Management REST APIF&OHTTPS/JSON + filePackage-based import/exportMillions (file-based)ExemptNo (async)Yes
Recurring Integrations APIF&OHTTPS/JSON + fileScheduled file-based syncUnlimited (file-based)ExemptNo (queued)Yes
Standard API v2.0BCHTTPS/JSONCRUD on ~55 standard entities20,000 (max page size)6,000 req/5min/userYesLimited
Custom API Pages (AL)BCHTTPS/JSONCustom tables/codeunits via AL20,000 (max page size)6,000 req/5min/userYesNo
OData Web ServicesBCHTTPS/JSONPublished pages/queries/codeunits20,000 (max page size)6,000 req/5min/userYesNo

[src1, src2, src3, src6]

Rate Limits & Quotas

F&O: User-Based Service Protection Limits (per user, per app ID, per web server)

Limit TypeValueWindowNotes
Request count6,0005-min sliding windowPer user per app ID per web server
Combined execution time1,200 seconds (20 min)5-min sliding windowAggregate of all request durations
Concurrent requests52InstantaneousAcross all endpoints for that user

Important: As of version 10.0.36, user-based limits are disabled by default and the option to enable them has been removed. Resource-based limits remain mandatory. [src1]

F&O: Resource-Based Limits (environment-wide, always on)

Limit TypeTriggerScopeNotes
CPU utilizationExceeds thresholdAll users on web serverReturns 429 with resource message
Memory utilizationExceeds thresholdAll users on web serverPrioritized throttling applies
Aggregate loadCombined resource pressureEnvironment-wideCan throttle even low-usage users

[src1]

F&O: Exempt Services

The following are exempt from OData service protection limits: DIXF/DMF, Recurring Integrations, Power Platform Virtual Tables (when PP integration enabled), F&O Connector, Warehouse Mobile App, Retail Server API, Office Integration, Document Routing Agent. [src1]

BC: Per-User Operational Limits

Limit TypeValueWindowHTTP Error
OData rate limit6,000 requests5-min sliding window429 Too Many Requests
Max concurrent OData requests5 processingInstantaneous503 (queued then timeout)
Max OData connections100 simultaneousInstantaneous429 Too Many Requests
Max request queue95 queued requestsInstantaneous429 Too Many Requests
Request execution timeout10 minutesPer request504 Gateway Timeout

[src3, src4]

BC: Per-Environment Limits

Limit TypeValueHTTP Error
Max OData page size20,000 entities413 Request Entity Too Large
Max $batch operations100 per batchN/A
Max request body size350 MB413 Request Entity Too Large
OData operation timeout8 minutes408 Request Timeout
Max webhook subscriptions200N/A

[src3]

F&O: OData v4 Query Capabilities

FeatureSupportedNotes
$filterYeseq, ne, gt, ge, lt, le, and, or, not, add, sub, mul, div, mod
$orderbyYesStandard sorting
$top / $skipYesPagination
$selectYesProject specific fields
$countYesInclude total count
$expandFirst-level onlyNo nested expansion
Cross-company queriesYesUse ?cross-company=true
$batchYesChangesets for atomic operations
Bound actionsYesCustom X++ methods on entities
has / in operatorsNoNot supported in $filter
Array fieldsNoNot supported in OData entities

[src2]

Authentication

FlowUse WhenToken LifetimeRefresh?Notes
OAuth 2.0 Authorization CodeUser-context operations, interactive appsAccess: 1h, Refresh: 90 daysYesUser must sign in; requires redirect URI
OAuth 2.0 Client CredentialsServer-to-server, daemon apps, integrationsAccess: 1hNew token per requestRecommended for integrations; no user context
OAuth 2.0 On-Behalf-OfMiddle-tier services acting as userAccess: 1hYesPreserves user identity through service chain

[src1, src2]

Authentication Gotchas

Constraints

Integration Pattern Decision Tree

START -- Integrate with Dynamics 365 F&O or Business Central
|-- Which D365 product?
|   |-- Finance & Operations (F&O)
|   |   |-- What volume?
|   |   |   |-- < 1,000 records/day
|   |   |   |   |-- Real-time needed?
|   |   |   |   |   |-- YES -> OData v4 data entities (individual CRUD)
|   |   |   |   |   +-- NO -> OData v4 with scheduled polling
|   |   |   |-- 1,000-100,000 records/day
|   |   |   |   |-- Need atomic transactions?
|   |   |   |   |   |-- YES -> OData $batch with changesets
|   |   |   |   |   +-- NO -> Data Management REST API (package-based)
|   |   |   +-- > 100,000 records/day
|   |   |       +-- Data Management Framework (DMF) package import
|   |   |           (exempt from service protection limits)
|   |   +-- Need event-driven notifications?
|   |       |-- YES -> Business Events + Azure Service Bus / Event Grid
|   |       +-- NO -> OData polling with $filter on ModifiedDateTime
|   +-- Business Central (BC)
|       |-- What volume?
|       |   |-- < 1,000 records/day -> Standard API v2.0 (direct CRUD)
|       |   |-- 1,000-10,000 records/day -> API v2.0 with $batch (max 100 ops)
|       |   +-- > 10,000 records/day -> Distribute across multiple users/SPs
|       +-- Custom business logic needed?
|           |-- YES -> Custom API pages (AL) or OData unbound actions
|           +-- NO -> Standard API v2.0 endpoints (~55 entities)
|-- Which direction?
|   |-- Inbound (writing to D365) -> check concurrent request limits
|   |-- Outbound (reading from D365) -> check page size + execution timeout
|   +-- Bidirectional -> design conflict resolution + use ModifiedDateTime tracking
+-- Error tolerance?
    |-- Zero-loss required -> implement idempotency + Retry-After logic + DLQ
    +-- Best-effort acceptable -> exponential backoff on 429 responses

Quick Reference

F&O Key OData Endpoints

OperationMethodEndpointNotes
List entitiesGET{baseUrl}/data/{EntityCollection}Server-driven paging, max 10K/page
Get single entityGET{baseUrl}/data/{EntityCollection}("{key}")All key fields required
Create recordPOST{baseUrl}/data/{EntityCollection}Returns created entity
Update recordPATCH{baseUrl}/data/{EntityCollection}("{key}")Partial update
Delete recordDELETE{baseUrl}/data/{EntityCollection}("{key}")Returns 204 No Content
Cross-company queryGET{baseUrl}/data/{Entity}?cross-company=trueReturns data across legal entities
Batch requestPOST{baseUrl}/data/$batchChangesets for atomicity
Entity metadataGET{baseUrl}/data/$metadataRead-only, includes EnumType
Bound actionPOST{baseUrl}/data/{Entity}("{key}")/.../ {ActionName}Custom X++ methods

[src2]

BC Key API Endpoints

OperationMethodEndpointNotes
List entitiesGET{baseUrl}/api/v2.0/companies({companyId})/{entity}~55 standard entities
Get single entityGET{baseUrl}/api/v2.0/companies({companyId})/{entity}({id})By system ID
Create recordPOST{baseUrl}/api/v2.0/companies({companyId})/{entity}Returns created entity
Update recordPATCH{baseUrl}/api/v2.0/companies({companyId})/{entity}({id})Requires If-Match ETag
Delete recordDELETE{baseUrl}/api/v2.0/companies({companyId})/{entity}({id})Returns 204
Batch requestPOST{baseUrl}/api/v2.0/$batchMax 100 operations
Custom APIGET/POST{baseUrl}/api/{publisher}/{group}/{version}/{entity}Defined in AL code

[src7]

Step-by-Step Integration Guide

1. Register an application in Microsoft Entra ID

Create an app registration in Azure Portal. For F&O, add the Dynamics ERP API permission. For BC, add Dynamics 365 Business Central API permission. [src1, src7]

# Azure CLI: Create app registration
az ad app create \
  --display-name "D365-Integration-App" \
  --sign-in-audience AzureADMyOrg

# Create a client secret
az ad app credential reset --id <app-id> --append

Verify: az ad app show --id <app-id> -> returns app registration details.

2. Obtain an OAuth 2.0 access token

Use Client Credentials flow for server-to-server integration. The scope differs between F&O and BC. [src1, src7]

# F&O: Get access token
curl -X POST "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token" \
  -d "client_id={app-id}&client_secret={secret}&scope=https://{env}.operations.dynamics.com/.default&grant_type=client_credentials"

# BC: Get access token
curl -X POST "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token" \
  -d "client_id={app-id}&client_secret={secret}&scope=https://api.businesscentral.dynamics.com/.default&grant_type=client_credentials"

Verify: Response contains access_token field. Decode at jwt.ms to confirm audience and scopes.

3. Query data entities via OData

Use the access token to make authenticated OData requests. [src2, src7]

# F&O: List customers
curl -H "Authorization: Bearer {token}" \
  "https://{env}.operations.dynamics.com/data/CustomersV3?$top=10&cross-company=true"

# BC: List customers
curl -H "Authorization: Bearer {token}" \
  "https://api.businesscentral.dynamics.com/v2.0/{tenant}/{env}/api/v2.0/companies({id})/customers?$top=10"

Verify: Response returns 200 OK with value array containing entity records.

4. Implement rate limit handling with Retry-After

When the service returns 429, read the Retry-After header and wait before retrying. [src1, src5]

import requests, time

def d365_api_call(url, headers, max_retries=5):
    for attempt in range(max_retries):
        response = requests.get(url, headers=headers)
        if response.status_code == 200:
            return response.json()
        elif response.status_code == 429:
            retry_after = int(response.headers.get('Retry-After', 30))
            time.sleep(retry_after)
        elif response.status_code == 503:
            time.sleep(10 * (attempt + 1))
        else:
            response.raise_for_status()
    raise Exception(f"Max retries exceeded for {url}")

Verify: Call succeeds on retry; Retry-After header value should be respected.

Code Examples

Python: Paginated OData query for F&O with cross-company support

# Input:  F&O environment URL, entity name, OAuth token
# Output: All records across pages as a list of dicts

import requests, time

def fetch_all_fo_records(base_url, entity, token, filter_expr=None):
    headers = {"Authorization": f"Bearer {token}", "Accept": "application/json"}
    url = f"{base_url}/data/{entity}?cross-company=true&$count=true"
    if filter_expr:
        url += f"&$filter={filter_expr}"

    all_records = []
    while url:
        response = requests.get(url, headers=headers)
        if response.status_code == 429:
            time.sleep(int(response.headers.get("Retry-After", 30)))
            continue
        response.raise_for_status()
        data = response.json()
        all_records.extend(data.get("value", []))
        url = data.get("@odata.nextLink")  # Server-driven paging
    return all_records

JavaScript/Node.js: BC API v2.0 CRUD with ETag handling

// Input:  BC environment URL, company ID, OAuth token
// Output: Customer records with proper ETag concurrency

const axios = require('axios'); // v1.6+

async function updateBCCustomer(baseUrl, companyId, customerId, data, token) {
  // Step 1: Get current record to obtain ETag
  const current = await axios.get(
    `${baseUrl}/api/v2.0/companies(${companyId})/customers(${customerId})`,
    { headers: { 'Authorization': `Bearer ${token}` } }
  );
  const etag = current.data['@odata.etag'];

  // Step 2: Update with If-Match header
  return await axios.patch(
    `${baseUrl}/api/v2.0/companies(${companyId})/customers(${customerId})`,
    data,
    { headers: { 'Authorization': `Bearer ${token}`, 'If-Match': etag } }
  );
}

cURL: F&O $batch request with changeset

# Input:  F&O access token, entity data for batch create
# Output: Batch response with individual operation results

curl -X POST "https://{env}.operations.dynamics.com/data/$batch" \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: multipart/mixed; boundary=batch_boundary" \
  -H "OData-Version: 4.0" \
  --data-binary '
--batch_boundary
Content-Type: multipart/mixed; boundary=changeset_boundary

--changeset_boundary
Content-Type: application/http
Content-ID: 1

POST /data/CustomersV3 HTTP/1.1
Content-Type: application/json

{"CustomerAccount":"CUST001","OrganizationName":"Contoso Ltd","dataAreaId":"usmf"}

--changeset_boundary--
--batch_boundary--'

Data Mapping

F&O OData Data Entity Model

ConceptDescriptionGotcha
Data EntityDe-normalized view of underlying tablesNot all table fields are exposed; check IsPublic property
dataAreaIdLegal entity (company) identifierRequired for cross-company queries; defaults to user's company
Composite keyMulti-field keys common in F&OMust provide ALL key fields in URL
Enum fieldsInteger values in JSONUse $metadata to map enum integer to label
Navigation propertiesLinks between entitiesOnly first-level $expand supported
Date/Time fieldsUTC datetime stringsAlways UTC; no timezone conversion in OData layer

[src2]

BC API v2.0 Data Model

ConceptDescriptionGotcha
System IDGUID identifier for each recordUse systemId for API operations, not the No. field
ETagOptimistic concurrency controlRequired in If-Match header for PATCH/DELETE
Company scopingAll entities scoped to a companyMust include companies({companyId}) in URL path
DateTimeEdm.DateTimeOffsetISO 8601 format with timezone offset

[src7]

Data Type Gotchas

Error Handling & Failure Points

Common Error Codes

CodeMeaningProductResolution
429Too Many RequestsF&O, BCRead Retry-After header, wait, then retry
503Service Temporarily UnavailableBCQueue timeout; wait 10s, distribute across users
504Gateway TimeoutBCExceeded 10-min timeout; optimize query, paginate
408Request TimeoutBCExceeded 8-min OData timeout; break into smaller ops
413Request Entity Too LargeBCExceeded 20K entities or 350 MB; use $top + paginate
412Precondition FailedBCStale ETag; re-fetch record, get current ETag, retry
400Bad RequestF&OMissing key fields or invalid $filter; check $metadata

[src1, src3, src7]

Failure Points in Production

Anti-Patterns

Wrong: Polling all records to detect changes

# BAD -- Fetches all records every time to find what changed
all_customers = fetch_all_fo_records(base_url, "CustomersV3", token)
# Compare each against local cache... wastes API calls, hits rate limits

Correct: Use $filter on ModifiedDateTime for incremental sync

# GOOD -- Only fetches records modified since last sync
filter_expr = f"ModifiedDateTime gt {last_sync_utc}"
changed = fetch_all_fo_records(base_url, "CustomersV3", token, filter_expr)

Wrong: Single service principal for all F&O traffic

# BAD -- All requests share one user's 6,000/5min budget
sp_token = get_token(client_id="single-app-id", ...)
for batch in all_batches:
    call_api(batch, token=sp_token)  # Hits 429 quickly at scale

Correct: Distribute requests across multiple service principals

# GOOD -- Each SP has its own 6,000/5min budget per web server
sps = [{"client_id": "app-1", ...}, {"client_id": "app-2", ...}]
for i, batch in enumerate(all_batches):
    token = get_token(**sps[i % len(sps)])
    call_api(batch, token=token)  # 2x+ throughput

Wrong: Omitting ETag in BC update operations

// BAD -- Missing If-Match header causes 412 errors
await axios.patch(`${baseUrl}/customers(${id})`, data, {
  headers: { 'Authorization': `Bearer ${token}` }
}); // Returns 412 Precondition Failed

Correct: Always include ETag from the most recent GET

// GOOD -- Fetch current ETag, include in update
const current = await axios.get(`${baseUrl}/customers(${id})`, { headers });
const etag = current.data['@odata.etag'];
await axios.patch(`${baseUrl}/customers(${id})`, data, {
  headers: { ...headers, 'If-Match': etag }
});

Common Pitfalls

Diagnostic Commands

# F&O: Check OData service availability
curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer {token}" \
  "https://{env}.operations.dynamics.com/data/$metadata"
# Expected: 200

# F&O: List all available data entities
curl -s -H "Authorization: Bearer {token}" \
  "https://{env}.operations.dynamics.com/data/"

# F&O: Test simple query
curl -s -H "Authorization: Bearer {token}" \
  "https://{env}.operations.dynamics.com/data/CustomersV3?$top=1&cross-company=true"

# BC: Check API availability
curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer {token}" \
  "https://api.businesscentral.dynamics.com/v2.0/{tenant}/{env}/api/v2.0/companies"

# BC: List companies
curl -s -H "Authorization: Bearer {token}" \
  "https://api.businesscentral.dynamics.com/v2.0/{tenant}/{env}/api/v2.0/companies"

# BC: Check webhook subscriptions
curl -s -H "Authorization: Bearer {token}" \
  "https://api.businesscentral.dynamics.com/v2.0/{tenant}/{env}/api/v2.0/subscriptions"

Version History & Compatibility

Version / ReleaseDateStatusKey ChangesMigration Notes
F&O 10.0.36+2024 Wave 1CurrentUser-based limits disabled by defaultResource-based limits remain mandatory
F&O 10.0.332023 Wave 1PreviousUser-based limits announced mandatoryWas rolled back; now optional only
F&O 10.0.192021HistoricalResource-based protection introducedFirst version with API throttling
BC API v2.02022+Current55 standard endpoints, custom API pagesPreferred over v1.0 and OData services
BC API v1.02020SupportedOriginal API versionMigrate to v2.0 for new integrations
BC OData page endpoints2015+Deprecating 2027Published pages/queries at /ODataV4/Migrate to API v2.0 or custom API pages

[src1, src3, src7]

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Real-time CRUD on individual records (<1K)Bulk data migration >10K recordsData Management Framework (DMF) -- exempt from rate limits
Interactive apps needing instant responseLarge scheduled ETL jobsRecurring Integrations API (F&O) or multi-SP distribution (BC)
OData queries with filters and paginationCross-system real-time sync <1s latencyDual Write (F&O to Dataverse)
Custom business logic via bound actionsAccessing F&O data from Power PlatformVirtual Entities
Standard entity operationsComplex multi-object transactionsCustom X++ services (F&O) or codeunit APIs (BC)

Important Caveats

Related Units