---
# === IDENTITY ===
id: business/erp-integration/dynamics-365-web-api-capabilities/2026
canonical_question: "What are Dynamics 365 F&O and Business Central Web API (OData v4) capabilities and rate limits?"
aliases:
  - "Dynamics 365 Finance Operations OData API rate limits"
  - "Dynamics 365 Business Central API v2.0 capabilities"
  - "D365 F&O service protection API limits"
  - "Dynamics 365 Web API throttling and retry-after behavior"
  - "D365 OData v4 endpoints and query options"
  - "Dynamics 365 concurrent request limits 52"
entity_type: erp_integration
domain: business > erp-integration > Dynamics 365 Web API
region: global
jurisdiction: global
temporal_scope: 2025-2026

# === SYSTEM PROFILE ===
systems:
  - name: "Dynamics 365 Finance & Operations"
    vendor: "Microsoft"
    version: "10.0.x (continuous release)"
    edition: "All cloud editions"
    deployment: cloud
    api_surface: "OData v4"
  - name: "Dynamics 365 Business Central"
    vendor: "Microsoft"
    version: "API v2.0"
    edition: "Essentials/Premium"
    deployment: cloud
    api_surface: "OData v4 / REST"

# === VERIFICATION ===
last_verified: 2026-03-01
confidence: 0.93
version: 1.0
first_published: 2026-03-01

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: volatile
  last_breaking_change: "10.0.36 (2024) - User-based service protection limits removed as mandatory; now optional"
  next_review: 2026-08-28
  change_sensitivity: high

# === CONSTRAINTS ===
constraints:
  - "F&O: 6,000 API requests per 5-minute sliding window per user per web server"
  - "F&O: 52 max concurrent requests per user per web server"
  - "F&O: 1,200 seconds (20 min) combined execution time per 5-minute window per user"
  - "F&O: OData page size capped at 10,000 records per page"
  - "F&O: $expand supports only first-level expansion (no nested $expand)"
  - "BC: 5 max concurrent OData requests per user (with 95 in queue)"
  - "BC: 6,000 OData requests per 5-minute sliding window per user"
  - "BC: 10-minute max execution time per request (504 Gateway Timeout)"
  - "BC: Max OData $batch size of 100 operations per request"
  - "BC: Max page size of 20,000 entities per OData response"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User needs Dynamics 365 Dataverse/CRM Web API (not F&O or BC)"
    use_instead: "business/erp-integration/dynamics-365-dataverse-api-capabilities/2026"
  - condition: "User needs Dual Write configuration between F&O and Dataverse"
    use_instead: "business/erp-integration/dynamics-365-dual-write-capabilities/2026"
  - condition: "User needs DMF/DIXF file-based import/export for large data migration"
    use_instead: "business/erp-integration/dynamics-365-dmf-import-export/2026"
  - condition: "User needs Virtual Entities to access F&O data from Power Platform"
    use_instead: "business/erp-integration/dynamics-365-virtual-entities/2026"
  - condition: "User needs Dynamics 365 authentication flow details"
    use_instead: "business/erp-integration/dynamics-365-authentication/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: integration_pattern
    question: "What integration pattern do you need?"
    type: choice
    options:
      - "real-time sync (individual records, <1s latency)"
      - "batch/bulk (scheduled, high volume)"
      - "event-driven (webhook, CDC, platform events)"
      - "file-based (CSV/XML import/export)"
  - key: data_volume
    question: "What's your daily data volume?"
    type: choice
    options:
      - "< 1,000 records/day"
      - "1,000-100,000 records/day"
      - "> 100,000 records/day"
  - key: direction
    question: "What's the data flow direction?"
    type: choice
    options:
      - "inbound (writing to this ERP)"
      - "outbound (reading from this ERP)"
      - "bidirectional sync"
  - key: d365_product
    question: "Which Dynamics 365 product?"
    type: choice
    options:
      - "Finance & Operations (F&O / F&SCM)"
      - "Business Central (BC)"
      - "Both"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/dynamics-365-web-api-capabilities/2026"
suggested_citation: "Source: knowledgelib.io - AI Knowledge Library (verified 2026-03-01)"

# === RELATED UNITS ===
related_kos:
  depends_on:
    - id: "business/erp-integration/dynamics-365-authentication/2026"
      label: "Dynamics 365 authentication flows (OAuth 2.0, app registration)"
  related_to:
    - id: "business/erp-integration/dynamics-365-dataverse-api-capabilities/2026"
      label: "Dataverse Web API capabilities and limits"
    - id: "business/erp-integration/dynamics-365-dmf-import-export/2026"
      label: "Data Management Framework for file-based import/export"
  solves:
    - id: "business/erp-integration/dynamics-365-dual-write-capabilities/2026"
      label: "Dual Write real-time sync between F&O and Dataverse"
  alternative_to:
    - id: "business/erp-integration/dynamics-365-virtual-entities/2026"
      label: "Virtual Entities for cross-platform data access"
  often_confused_with:
    - id: "business/erp-integration/dynamics-365-dataverse-api-capabilities/2026"
      label: "Dataverse Web API (different product, different limits)"

# === SOURCES ===
sources:
  - id: src1
    title: "Service protection API limits - Finance & Operations"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/data-entities/service-protection-api-limits
    type: official_docs
    published: 2026-01-20
    reliability: authoritative
  - id: src2
    title: "Open Data Protocol (OData) - Finance & Operations"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/data-entities/odata
    type: official_docs
    published: 2026-01-14
    reliability: authoritative
  - id: src3
    title: "Operation Limits in Dynamics 365 Business Central"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/operational-limits-online
    type: official_docs
    published: 2025-04-08
    reliability: authoritative
  - id: src4
    title: "Current API Limits in Dynamics 365 Business Central"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/dynamics-current-limits
    type: official_docs
    published: 2024-01-22
    reliability: authoritative
  - id: src5
    title: "Maximize API throughput - Finance & Operations"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/data-entities/service-protection-maximizing-api-throughput
    type: official_docs
    published: 2025-01-15
    reliability: authoritative
  - id: src6
    title: "Data management package REST API - Finance & Operations"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/data-entities/data-management-api
    type: official_docs
    published: 2025-06-15
    reliability: authoritative
  - id: src7
    title: "API (v2.0) for Dynamics 365 Business Central"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/
    type: official_docs
    published: 2025-01-10
    reliability: authoritative
  - id: src8
    title: "Navigating service protection API limits in Dynamics 365"
    author: Confiz
    url: https://www.confiz.com/blog/service-protection-api-limits-in-dynamics-365-best-practices-for-optimization/
    type: technical_blog
    published: 2024-08-15
    reliability: moderate_high
---

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

## TL;DR

- **Bottom line**: Both D365 F&O and Business Central expose OData v4 endpoints with full CRUD, but they have **completely different** service protection limits, throttling mechanisms, and API architectures -- treat them as separate integration targets.
- **Key limit**: F&O allows 6,000 requests per 5-minute sliding window per user per web server, with 52 max concurrent requests; BC allows 5 concurrent OData requests per user with a 6,000/5min rate limit.
- **Watch out for**: F&O has **two layers** of service protection -- user-based (optional since 10.0.36) and resource-based (always on). Resource-based limits trigger when aggregate server CPU/memory exceeds thresholds, not just your user's requests.
- **Best for**: Real-time CRUD operations on individual records or small batches via OData v4; use Data Management Framework (DMF) for bulk file-based loads >10K records.
- **Authentication**: OAuth 2.0 via Microsoft Entra ID (Azure AD) -- both Authorization Code flow (user context) and Client Credentials flow (service-to-service / app-only).

## 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 (used by Dynamics 365 CE/CRM apps), Virtual Entities, or Dual Write -- see related cards for those. [src1, src2, src3]

| Property | Dynamics 365 F&O | Dynamics 365 Business Central |
|---|---|---|
| **Vendor** | Microsoft | Microsoft |
| **System** | Finance & Operations 10.0.x | Business Central (SaaS) |
| **API Surface** | OData v4 (`/data/`) | OData v4 (`/api/v2.0/`, `/ODataV4/`) |
| **Current API Version** | Continuous release (10.0.x) | API v2.0 (stable) |
| **Editions Covered** | All cloud editions | Essentials, Premium |
| **Deployment** | Cloud (on-premises has no service protection limits) | Cloud (SaaS only for v2.0 API) |
| **API Docs URL** | [F&O OData docs](https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/data-entities/odata) | [BC API v2.0 docs](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/api-reference/v2.0/) |
| **Status** | GA | GA |

## API Surfaces & Capabilities

| API Surface | Product | Protocol | Best For | Max Records/Request | Rate Limit | Real-time? | Bulk? |
|---|---|---|---|---|---|---|---|
| OData v4 Data Entities | F&O | HTTPS/JSON | CRUD on individual records, queries | 10,000 (server-driven paging) | 6,000 req/5min/user/server | Yes | Limited |
| OData $batch | F&O | HTTPS/JSON multipart | Atomic multi-record operations | Multiple changesets | Counts toward 6,000 limit | Yes | Moderate |
| Custom Service Endpoints | F&O | HTTPS/JSON | Custom X++ business logic | N/A | Counts toward 6,000 limit | Yes | No |
| Data Management REST API | F&O | HTTPS/JSON + file | Package-based import/export | Millions (file-based) | Exempt from service protection | No (async) | Yes |
| Recurring Integrations API | F&O | HTTPS/JSON + file | Scheduled file-based sync | Unlimited (file-based) | Exempt from service protection | No (queued) | Yes |
| Standard API v2.0 | BC | HTTPS/JSON | CRUD on ~55 standard entities | 20,000 (max page size) | 6,000 req/5min/user | Yes | Limited |
| Custom API Pages (AL) | BC | HTTPS/JSON | Custom tables/codeunits via AL | 20,000 (max page size) | 6,000 req/5min/user | Yes | No |
| OData Web Services | BC | HTTPS/JSON | Published pages/queries/codeunits | 20,000 (max page size) | 6,000 req/5min/user | Yes | No |

[src1, src2, src3, src6]

## Rate Limits & Quotas

### Dynamics 365 Finance & Operations -- Service Protection Limits

#### User-Based Limits (per user, per application ID, per web server)

| Limit Type | Value | Window | Notes |
|---|---|---|---|
| Request count | 6,000 | 5-minute sliding window | Per user per app ID per web server |
| Combined execution time | 1,200 seconds (20 min) | 5-minute sliding window | Aggregate of all request durations |
| Concurrent requests | 52 | Instantaneous | Across 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]

#### Resource-Based Limits (environment-wide)

| Limit Type | Trigger | Scope | Notes |
|---|---|---|---|
| CPU utilization | Exceeds threshold | All users on web server | Returns 429 with generic resource message |
| Memory utilization | Exceeds threshold | All users on web server | Prioritized throttling applies |
| Aggregate load | Combined resource pressure | Environment-wide | Can throttle even low-usage users |

Resource-based limits cannot be disabled. They protect the environment when cumulative utilization from all integrations exceeds safe thresholds. Use Throttling Prioritization to control which integrations get throttled first. [src1]

#### F&O Exempt Services

The following are **exempt** from OData service protection limits: Data Import/Export Framework (DIXF/DMF), Recurring Integrations, Power Platform Virtual Tables (when PP integration is enabled), Finance and Operations Connector, Warehouse Mobile App, Retail Server API, Office Integration, Document Routing Agent. [src1]

### Dynamics 365 Business Central -- Operational Limits

#### Per-User Limits

| Limit Type | Value | Window | HTTP Error |
|---|---|---|---|
| OData rate limit | 6,000 requests | 5-minute sliding window | 429 Too Many Requests |
| Max concurrent OData requests | 5 processing | Instantaneous | 503 (queued until timeout) |
| Max OData connections | 100 simultaneous | Instantaneous | 429 Too Many Requests |
| Max request queue | 95 queued requests | Instantaneous | 429 Too Many Requests |
| Request execution timeout | 10 minutes | Per request | 504 Gateway Timeout |

[src3, src4]

#### Per-Environment Limits

| Limit Type | Value | HTTP Error |
|---|---|---|
| Max OData page size | 20,000 entities | 413 Request Entity Too Large |
| Max $batch operations | 100 per batch | N/A |
| Max request body size | 350 MB | 413 Request Entity Too Large |
| OData operation timeout | 8 minutes | 408 Request Timeout |
| Max webhook subscriptions | 200 | N/A |

[src3]

### OData v4 Query Capabilities (F&O)

| Feature | Supported | Notes |
|---|---|---|
| $filter | Yes | eq, ne, gt, ge, lt, le, and, or, not, add, sub, mul, div, mod |
| $orderby | Yes | Standard sorting |
| $top | Yes | Limit result count |
| $skip | Yes | Pagination offset |
| $select | Yes | Project specific fields |
| $count | Yes | Include total count |
| $expand | First-level only | No nested expansion |
| $search | No | Not supported |
| Cross-company queries | Yes | Use `?cross-company=true` |
| Server-driven paging | Yes | Max 10,000 records per page |
| Batch requests ($batch) | Yes | Changesets for atomic operations |
| Bound actions | Yes | Custom X++ methods on entities |
| EnumType metadata | Yes | In $metadata endpoint |
| `has` / `in` operators | No | Not supported in $filter |
| Array fields | No | Not supported in OData entities |

[src2]

## Authentication

| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| OAuth 2.0 Authorization Code | User-context operations, interactive apps | Access: 1h, Refresh: 90 days | Yes | User must sign in; requires redirect URI |
| OAuth 2.0 Client Credentials | Server-to-server, daemon apps, integrations | Access: 1h | New token per request | Recommended for integrations; no user context |
| OAuth 2.0 On-Behalf-Of | Middle-tier services acting as user | Access: 1h | Yes | Preserves user identity through service chain |

[src1, src2]

### Authentication Gotchas

- F&O and BC use **different** Microsoft Entra ID app registrations -- an app registered for F&O cannot directly access BC APIs and vice versa. Each requires its own API permissions. [src1, src7]
- The throttling key for F&O service protection combines **user object ID + application client ID**. If you use a single service principal for all API traffic, all requests count toward that one user's limits. Distribute across multiple service principals for higher throughput. [src1]
- Client Credentials flow in F&O uses the **application's object ID** as the "user" for throttling purposes. This means all requests from that app share the same 6,000/5min budget. [src1]
- BC access tokens issued via OAuth 2.0 are scoped to a specific environment (production vs sandbox). Tokens for sandbox cannot access production and vice versa. [src7]

## Constraints

- F&O OData `$expand` supports **first-level only** -- you cannot nest `$expand` queries (e.g., `$expand=Lines($expand=Details)` will fail). [src2]
- F&O OData does **not** support `$search`, the `has` operator, or the `in` operator in `$filter`. [src2]
- F&O OData does **not** support array fields in data entities. [src2]
- F&O resource-based service protection limits **cannot be disabled** -- they trigger based on aggregate server resource utilization, meaning even well-behaved integrations may be throttled during peak load from other tenants sharing the same infrastructure. [src1]
- BC has a **strict 10-minute execution timeout** per request -- long-running queries that exceed this return 504 Gateway Timeout with no retry mechanism. [src4]
- BC OData page endpoints (published pages/queries) will be **deprecated in 2027** -- migrate to API v2.0 or custom API pages. [src7]
- BC `$batch` is limited to **100 operations** per request. [src3]
- BC SOAP endpoints are being deprecated -- use OData/API instead for all new integrations. [src3]
- The first OData call after an F&O AOS restart takes significantly longer due to metadata cache building. Warm up OData at AOS startup to avoid latency. [src2]

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

| Operation | Method | Endpoint | Payload | Notes |
|---|---|---|---|---|
| List entities | GET | `{baseUrl}/data/{EntityCollection}` | N/A | Server-driven paging, max 10K/page |
| Get single entity | GET | `{baseUrl}/data/{EntityCollection}("{key}")` | N/A | All key fields required |
| Create record | POST | `{baseUrl}/data/{EntityCollection}` | JSON | Returns created entity |
| Update record | PATCH | `{baseUrl}/data/{EntityCollection}("{key}")` | JSON | Partial update |
| Delete record | DELETE | `{baseUrl}/data/{EntityCollection}("{key}")` | N/A | Returns 204 No Content |
| Cross-company query | GET | `{baseUrl}/data/{EntityCollection}?cross-company=true` | N/A | Returns data across legal entities |
| Filter by company | GET | `{baseUrl}/data/{EntityCollection}?$filter=dataAreaId eq 'usmf'&cross-company=true` | N/A | Combine with cross-company |
| Batch request | POST | `{baseUrl}/data/$batch` | Multipart | Changesets for atomicity |
| Entity metadata | GET | `{baseUrl}/data/$metadata` | N/A | Read-only, includes EnumType |
| Bound action | POST | `{baseUrl}/data/{EntityCollection}("{key}")/Microsoft.Dynamics.DataEntities.{ActionName}` | JSON | Custom X++ methods |

[src2]

### BC Key API Endpoints

| Operation | Method | Endpoint | Payload | Notes |
|---|---|---|---|---|
| List standard entities | GET | `{baseUrl}/api/v2.0/companies({companyId})/{entity}` | N/A | ~55 standard entities |
| Get single entity | GET | `{baseUrl}/api/v2.0/companies({companyId})/{entity}({id})` | N/A | By system ID |
| Create record | POST | `{baseUrl}/api/v2.0/companies({companyId})/{entity}` | JSON | Returns created entity |
| Update record | PATCH | `{baseUrl}/api/v2.0/companies({companyId})/{entity}({id})` | JSON | Requires If-Match ETag |
| Delete record | DELETE | `{baseUrl}/api/v2.0/companies({companyId})/{entity}({id})` | N/A | Returns 204 |
| Batch request | POST | `{baseUrl}/api/v2.0/$batch` | Multipart | Max 100 operations |
| Custom API | GET/POST | `{baseUrl}/api/{publisher}/{group}/{version}/{entity}` | JSON | Defined in AL code |
| OData web service | GET | `{baseUrl}/ODataV4/Company('{name}')/{service}` | N/A | Published pages/queries (deprecating 2027) |

[src7]

## Step-by-Step Integration Guide

### 1. Register an application in Microsoft Entra ID

Create an app registration in Azure Portal for your integration. For F&O, add the `Dynamics ERP` API permission. For BC, add `Dynamics 365 Business Central` API permission with the appropriate scopes. [src1, src7]

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

# Note the appId and create a client secret
az ad app credential reset \
  --id <app-id> \
  --append
```

**Verify**: `az ad app show --id <app-id>` -> should return the app registration details.

### 2. Obtain an OAuth 2.0 access token

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

```bash
# F&O: Get access token (Client Credentials)
curl -X POST "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id={app-id}" \
  -d "client_secret={client-secret}" \
  -d "scope=https://{f&o-environment}.operations.dynamics.com/.default" \
  -d "grant_type=client_credentials"

# BC: Get access token (Client Credentials)
curl -X POST "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id={app-id}" \
  -d "client_secret={client-secret}" \
  -d "scope=https://api.businesscentral.dynamics.com/.default" \
  -d "grant_type=client_credentials"
```

**Verify**: Response contains `access_token` field with a JWT token. 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]

```bash
# F&O: List customers with filter and select
curl -X GET "https://{environment}.operations.dynamics.com/data/CustomersV3?\$select=CustomerAccount,OrganizationName&\$filter=dataAreaId eq 'usmf'&\$top=10&cross-company=true" \
  -H "Authorization: Bearer {access_token}" \
  -H "Accept: application/json"

# BC: List customers with filter
curl -X GET "https://api.businesscentral.dynamics.com/v2.0/{tenant-id}/{environment}/api/v2.0/companies({company-id})/customers?\$select=number,displayName&\$top=10" \
  -H "Authorization: Bearer {access_token}" \
  -H "Accept: application/json"
```

**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 that many seconds before retrying. [src1, src5]

```python
# Python: Retry-After handler for D365 API calls
import requests
import time

def d365_api_call(url, headers, max_retries=5):
    """Make D365 API call with automatic rate limit handling."""
    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))
            print(f"Rate limited. Waiting {retry_after}s (attempt {attempt + 1})")
            time.sleep(retry_after)
        elif response.status_code == 503:
            # BC: request queued but timed out
            time.sleep(10 * (attempt + 1))
        else:
            response.raise_for_status()

    raise Exception(f"Max retries ({max_retries}) exceeded for {url}")
```

**Verify**: Call should succeed on retry; check that `Retry-After` header value decreases on subsequent 429 responses.

## Code Examples

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

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

import requests
import time

def fetch_all_fo_records(base_url, entity, token, filter_expr=None):
    """Fetch all records from F&O OData with pagination and throttling."""
    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/json",
        "OData-MaxVersion": "4.0",
        "OData-Version": "4.0"
    }

    url = f"{base_url}/data/{entity}?cross-company=true&$count=true"
    if filter_expr:
        url += f"&$filter={filter_expr}"

    all_records = []
    page = 0

    while url:
        response = requests.get(url, headers=headers)

        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 30))
            time.sleep(retry_after)
            continue

        response.raise_for_status()
        data = response.json()

        records = data.get("value", [])
        all_records.extend(records)
        page += 1
        print(f"Page {page}: fetched {len(records)} records (total: {len(all_records)})")

        # Follow @odata.nextLink for server-driven paging
        url = data.get("@odata.nextLink")

    return all_records
```

### JavaScript/Node.js: BC API v2.0 CRUD operations

```javascript
// Input:  BC environment URL, company ID, OAuth token
// Output: Created/updated customer record

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

class BCApiClient {
  constructor(tenantId, environment, companyId, token) {
    this.baseUrl = `https://api.businesscentral.dynamics.com/v2.0/${tenantId}/${environment}/api/v2.0/companies(${companyId})`;
    this.headers = {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    };
  }

  async getCustomers(filter = '', top = 100) {
    let url = `${this.baseUrl}/customers?$top=${top}`;
    if (filter) url += `&$filter=${filter}`;
    const res = await this._request('GET', url);
    return res.data.value;
  }

  async createCustomer(data) {
    const res = await this._request('POST', `${this.baseUrl}/customers`, data);
    return res.data;
  }

  async updateCustomer(id, data, etag) {
    const headers = { ...this.headers, 'If-Match': etag };
    const res = await this._request('PATCH',
      `${this.baseUrl}/customers(${id})`, data, headers);
    return res.data;
  }

  async _request(method, url, data = null, headers = null, retries = 5) {
    for (let i = 0; i < retries; i++) {
      try {
        return await axios({ method, url, data, headers: headers || this.headers });
      } catch (err) {
        if (err.response?.status === 429) {
          const wait = parseInt(err.response.headers['retry-after'] || '30', 10);
          console.log(`Rate limited. Waiting ${wait}s...`);
          await new Promise(r => setTimeout(r, wait * 1000));
        } else {
          throw err;
        }
      }
    }
    throw new Error(`Max retries exceeded for ${method} ${url}`);
  }
}
```

### cURL: F&O $batch request with changeset

```bash
# Input:  F&O access token, entity data
# Output: Batch response with individual operation results

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

--changeset_boundary
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 1

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

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

--changeset_boundary
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 2

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

{"CustomerAccount":"CUST002","OrganizationName":"Fabrikam Inc","dataAreaId":"usmf"}

--changeset_boundary--
--batch_boundary--
EOF
```

## Data Mapping

### F&O OData Data Entity Model

| Concept | Description | Gotcha |
|---|---|---|
| Data Entity | De-normalized view of underlying tables | Not all table fields are exposed; check `IsPublic` property |
| dataAreaId | Legal entity (company) identifier | Required for cross-company queries; defaults to user's company |
| Composite key | Multi-field keys common in F&O | Must provide ALL key fields in URL for GET/PATCH/DELETE |
| Enum fields | Represented as integer values in JSON | Use `$metadata` to map enum integer to label |
| Navigation properties | Links between entities | Only first-level `$expand` supported |
| Date/Time fields | UTC datetime strings | Always UTC; no timezone conversion in OData layer |

[src2]

### BC API v2.0 Data Model

| Concept | Description | Gotcha |
|---|---|---|
| System ID | GUID identifier for each record | Use systemId for API operations, not the No. field |
| ETag (@odata.etag) | Optimistic concurrency control | Required in `If-Match` header for PATCH/DELETE |
| Company scoping | All entities scoped to a company | Must include `companies({companyId})` in URL path |
| Nested entities | Sub-pages under parent entities | E.g., `salesOrders({id})/salesOrderLines` |
| Complex types | Address, dimension values | Represented as embedded JSON objects |
| DateTime | Edm.DateTimeOffset | ISO 8601 format with timezone offset |

[src7]

### Data Type Gotchas

- F&O `DateTimeOffset` fields are always in **UTC**. Client applications must handle timezone conversion. There is no server-side timezone conversion in the OData layer. [src2]
- F&O enum fields return **integer values** in OData responses, not labels. Use `$metadata` endpoint to get the `EnumType` definitions for label mapping. [src2]
- BC requires `@odata.etag` in the `If-Match` header for **every** PATCH and DELETE operation. Omitting it returns `412 Precondition Failed`. Always fetch the record first to get the current ETag. [src7]
- BC `systemId` (GUID) is the primary identifier for API operations, but many entities also have a human-readable `number` field (e.g., customer number). These are different -- use `systemId` for API calls. [src7]

## Error Handling & Failure Points

### Common Error Codes

| Code | Meaning | Product | Cause | Resolution |
|---|---|---|---|---|
| 429 | Too Many Requests | F&O, BC | Rate limit or resource threshold exceeded | Read `Retry-After` header, wait, then retry [src1] |
| 503 | Service Temporarily Unavailable | BC | Max concurrent requests reached, queue timeout | Wait 10s, retry; distribute across users [src3] |
| 504 | Gateway Timeout | BC | Request exceeded 10-minute execution time | Optimize query, reduce result set, use pagination [src4] |
| 408 | Request Timeout | BC | Operation exceeded 8-minute OData timeout | Break into smaller operations [src3] |
| 413 | Request Entity Too Large | BC | Response > 20,000 entities or body > 350 MB | Use `$top` to limit page size, paginate [src3] |
| 412 | Precondition Failed | BC | Missing or stale `If-Match` ETag header | Re-fetch record, get current ETag, retry [src7] |
| 400 | Bad Request | F&O | Missing key fields, invalid $filter syntax | Check entity key composition in $metadata [src2] |

### Failure Points in Production

- **F&O metadata cache cold start**: The first OData call after AOS restart takes 30-60 seconds due to metadata cache building. Fix: `Enable OData metadata warmup in AOS startup configuration`. [src2]
- **F&O cross-company performance**: Queries with `?cross-company=true` across many legal entities can be extremely slow and hit execution time limits. Fix: `Add $filter=dataAreaId eq '{company}' to target specific companies, or split into per-company queries`. [src2]
- **BC ETag race conditions**: Concurrent updates to the same record fail with 412 because ETags change on every modification. Fix: `Implement optimistic concurrency retry loop: fetch -> update -> on 412, re-fetch and re-apply changes`. [src7]
- **F&O resource-based throttling during month-end**: Even well-designed integrations get throttled when other workloads (financial close, MRP runs) consume server resources. Fix: `Schedule integration workloads outside peak business hours; use throttling prioritization to protect critical integrations`. [src1]
- **BC webhook subscription loss**: Webhook subscriptions expire and are silently dropped if the callback URL returns errors 10 consecutive times. Fix: `Monitor subscription status via GET /api/v2.0/subscriptions; implement subscription renewal logic`. [src3]
- **F&O $batch partial failures**: By default, a changeset is atomic (all-or-nothing), but individual requests outside changesets can partially succeed. Fix: `Always wrap related operations in a changeset; implement idempotency checks for operations outside changesets`. [src2]

## Anti-Patterns

### Wrong: Polling all records to detect changes in F&O

```python
# BAD -- Fetches all records every time to find what changed
def sync_customers_bad(base_url, token):
    all_customers = fetch_all_fo_records(base_url, "CustomersV3", token)
    for customer in all_customers:
        # Compare each record against local cache...
        pass  # Wastes API calls and hits rate limits quickly
```

### Correct: Use $filter on ModifiedDateTime for incremental sync

```python
# GOOD -- Only fetches records modified since last sync
def sync_customers_good(base_url, token, last_sync_utc):
    filter_expr = f"ModifiedDateTime gt {last_sync_utc}"
    changed = fetch_all_fo_records(
        base_url, "CustomersV3", token, filter_expr=filter_expr
    )
    return changed  # Only changed records, dramatically fewer API calls
```

### Wrong: Using a single service principal for all F&O API traffic

```python
# 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

```python
# GOOD -- Each SP has its own 6,000/5min budget per web server
service_principals = [
    {"client_id": "app-1", "secret": "..."},
    {"client_id": "app-2", "secret": "..."},
    {"client_id": "app-3", "secret": "..."},
]

for i, batch in enumerate(all_batches):
    sp = service_principals[i % len(service_principals)]
    token = get_token(**sp)
    call_api(batch, token=token)  # 3x the throughput
```

### Wrong: Ignoring ETags in BC update operations

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

### Correct: Always include ETag from the most recent GET

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

await axios.patch(`${baseUrl}/customers(${id})`, updateData, {
  headers: {
    'Authorization': `Bearer ${token}`,
    'If-Match': etag
  }
});
```

## Common Pitfalls

- **F&O sandbox vs production limits differ**: Sandbox environments may have fewer web servers, meaning the effective per-user limits are lower (6,000/5min applies per web server, but fewer servers = less total capacity). Fix: `Load-test in a sandbox that matches production web server count; use Throttling Monitoring in LCS to compare`. [src1]
- **F&O entity key complexity**: Many F&O data entities have composite keys with 3-5 fields. Missing any key field in the URL returns a cryptic 400 error. Fix: `Always check $metadata for the entity's key fields before building URLs`. [src2]
- **BC per-user vs per-environment confusion**: BC operational limits documentation shows both per-environment and per-user sections. The per-user limits are the ones that are actively enforced (rolled out late 2023). Fix: `Design for per-user limits; distribute workload across multiple users/service principals`. [src3]
- **F&O `$expand` limitations**: Attempting nested `$expand` (e.g., `$expand=Lines($expand=Dimensions)`) silently returns only the first level. Fix: `Make separate API calls for nested entities; use navigation property URLs for deep traversal`. [src2]
- **BC deprecation of OData page endpoints**: OData web services published from pages/queries at `/ODataV4/` will be deprecated in 2027. Fix: `Migrate to API v2.0 endpoints or custom API pages in AL; these are under /api/ path`. [src7]
- **F&O SaveChangesOptions**: When using the .NET OData client, the default `SaveChanges()` POSTs all fields including unset ones with default values. Fix: `Use SaveChangesOptions.PostOnlySetProperties to send only explicitly set fields`. [src2]

## Diagnostic Commands

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

# F&O: List all available data entities
curl -s -H "Authorization: Bearer {token}" \
  "https://{environment}.operations.dynamics.com/data/" | \
  python3 -c "import sys,json; d=json.load(sys.stdin); print(f'{len(d[\"value\"])} entities available')"

# F&O: Test a simple query (returns first customer)
curl -s -H "Authorization: Bearer {token}" \
  "https://{environment}.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-id}/{environment}/api/v2.0/companies"
# Expected: 200

# BC: List available companies
curl -s -H "Authorization: Bearer {token}" \
  "https://api.businesscentral.dynamics.com/v2.0/{tenant-id}/{environment}/api/v2.0/companies" | \
  python3 -c "import sys,json; [print(c['name'],c['id']) for c in json.load(sys.stdin)['value']]"

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

# Monitor F&O throttling (via LCS or Azure Application Insights)
# Check: FinOps > System Administration > Inquiries > Throttling > API usage monitoring
```

## Version History & Compatibility

| Version / Release | Date | Status | Key Changes | Migration Notes |
|---|---|---|---|---|
| F&O 10.0.36+ | 2024 Wave 1 | Current | User-based limits disabled, removed enable option | Resource-based limits remain mandatory |
| F&O 10.0.33 | 2023 Wave 1 | Previous | User-based limits announced mandatory | Was rolled back; now optional only |
| F&O 10.0.19 | 2021 | Historical | Resource-based service protection introduced | First version with API throttling |
| BC API v2.0 | 2022+ | Current (stable) | 55 standard endpoints, custom API pages | Preferred over v1.0 and OData web services |
| BC API v1.0 | 2020 | Supported (legacy) | Original API version | Migrate to v2.0 for new integrations |
| BC OData page endpoints | 2015+ | Deprecating 2027 | Published pages/queries at /ODataV4/ | Migrate to API v2.0 or custom API pages |

[src1, src3, src7]

### Deprecation Policy

Microsoft follows a continuous release model for F&O (monthly updates to 10.0.x) with a minimum 12-month deprecation notice for breaking changes. Business Central follows a semi-annual major release cycle (Spring and Fall waves) with deprecation notices published in release plans. OData page endpoints in BC received a multi-year deprecation notice (target 2027). [src7]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Real-time CRUD on individual records or small sets (<1K) | Bulk data migration >10K records | Data Management Framework (DMF) / DIXF -- exempt from rate limits |
| Interactive applications needing instant response | Large scheduled ETL jobs | Recurring Integrations API (F&O) or distribute across multiple SPs (BC) |
| Querying with OData filters, sorting, pagination | Cross-system real-time sync needing sub-second latency | Dual Write (F&O to Dataverse) |
| Custom business logic via bound actions (F&O) or unbound actions (BC) | Accessing F&O data from Power Platform / Model-Driven Apps | Virtual Entities |
| Standard entity operations (customers, vendors, orders) | Complex multi-object transactions across many entities | Custom X++ services (F&O) or codeunit APIs (BC) |

## Important Caveats

- F&O service protection limits are **per user, per application ID, per web server** -- if your environment has 3 web servers, your effective capacity is roughly 3x the listed limits, but load balancing is not guaranteed to distribute evenly. [src1]
- User-based service protection limits in F&O are **disabled by default** as of 10.0.36. Resource-based limits are **always on** and cannot be disabled. The resource-based limits may throttle your integration even if you are well within user-based limits, because they measure aggregate server load. [src1]
- BC operational limits are being transitioned from per-environment to **per-user** enforcement (rolled out starting late 2023). The per-user rate of 6,000 requests per 5-minute window is more generous than the old per-environment rate (300-600/min) but is strictly enforced. [src3]
- All rate limits are **subject to change** with each release. Always verify against the latest Microsoft documentation before designing an integration. [src1, src3]
- F&O on-premises deployments do **not** have service protection API limits -- these apply only to cloud-hosted environments. [src1]
- The exemption list for F&O service protection (DMF, Recurring Integrations, Virtual Tables, etc.) may change -- Microsoft has noted that exemptions will eventually be removed with prior notice. [src1]

## Related Units

- [Dynamics 365 Authentication Flows](/business/erp-integration/dynamics-365-authentication/2026)
- [Dynamics 365 Dataverse API Capabilities](/business/erp-integration/dynamics-365-dataverse-api-capabilities/2026)
- [Dynamics 365 DMF Import/Export](/business/erp-integration/dynamics-365-dmf-import-export/2026)
- [Dynamics 365 Virtual Entities](/business/erp-integration/dynamics-365-virtual-entities/2026)
- [Dynamics 365 Dual Write Capabilities](/business/erp-integration/dynamics-365-dual-write-capabilities/2026)
