Microsoft Business Central API v2.0: OData REST Capabilities, Rate Limits, and Integration Patterns
What are the Microsoft Business Central API v2.0 OData-based REST capabilities and limits?
TL;DR
- Bottom line: Business Central API v2.0 is an OData v4 REST API exposing ~55 standard business entities (customers, vendors, items, sales orders, GL entries). Use it for all new integrations; SOAP is deprecated. Rate limits are per-user, not per-environment. [src1]
- Key limit: 6,000 OData requests per user per 5-minute sliding window; max 5 concurrent requests processing at once; 100 simultaneous connections per user. Production gets 600 req/min, sandbox gets 300 req/min. [src1]
- Watch out for: Rate limits are strictly per-user — a single service principal hitting limits throttles only that user. Spread workload across multiple application users to increase throughput. [src1, src2]
- Best for: Real-time CRUD on individual Business Central entities, webhook-driven event notifications, and moderate-volume batch operations via $batch. [src2, src5]
- Authentication: OAuth 2.0 via Microsoft Entra ID. Client credentials (S2S) for unattended integrations with scope
https://api.businesscentral.dynamics.com/.default. Requires Entra ID app withAPI.ReadWrite.Allpermission. [src3]
System Profile
This card covers the standard Microsoft Dynamics 365 Business Central API v2.0 for Business Central Online (SaaS). The API v2.0 provides OData v4 REST endpoints for ~55 standard business entities across financials, sales, purchasing, and inventory. Business Central is Microsoft's cloud ERP for small-to-mid-size businesses. This card does NOT cover: Dynamics 365 Finance & Operations, Dynamics 365 Dataverse/CRM Web API, Business Central on-premises, or custom API pages.
| Property | Value |
|---|---|
| Vendor | Microsoft |
| System | Dynamics 365 Business Central (API v2.0) |
| API Surface | OData v4 (REST/JSON) |
| Current API Version | v2.0 (GA since version 18.3, 2021 wave 2) |
| Editions Covered | Essentials, Premium (SaaS) |
| Deployment | Cloud (Business Central Online) |
| API Docs | Business Central API v2.0 Reference |
| Status | GA |
API Surfaces & Capabilities
| API Surface | Protocol | Best For | Max Records/Request | Rate Limit | Real-time? | Bulk? |
|---|---|---|---|---|---|---|
| Standard API v2.0 | OData v4 / HTTPS/JSON | CRUD on standard entities, queries, webhooks | 20,000 (page size) | 6,000/5min per user | Yes | Via $batch (100 ops) |
| Custom API Pages | OData v4 / HTTPS/JSON | Custom entities via AL extensions | 20,000 (page size) | Shared with standard | Yes | Via $batch |
| Automation API | OData v4 / HTTPS/JSON | Tenant setup, company creation, extension mgmt | 20,000 | Shared with standard | Yes | No |
| SOAP Web Services | SOAP/XML | Legacy integrations (deprecated) | 65,536 KB max message | 6,000/5min per user | Yes | No |
| Webhooks (Subscriptions) | Push/JSON | Event-driven notifications on entity changes | N/A (push) | 200 subscriptions/env | Yes | N/A |
Rate Limits & Quotas
Per-Request Limits
| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Max page size | 20,000 entities | OData v4 query | Use @odata.nextLink for pagination |
| Max request body size | 350 MB | OData v4 | Larger payloads should use file/image column upload |
| Max $batch operations | 100 | OData v4 $batch | Each sub-operation counts toward rate limits |
| Operation timeout | 8 minutes | OData v4 request | Returns 408 Request Timeout |
| Gateway timeout | 10 minutes | Any request | Returns 504 Gateway Timeout |
| Max file upload size | 350 MB | File upload | Upload timeout is 65 seconds |
Rolling / Per-User Limits
| Limit Type | Value | Window | Edition Differences |
|---|---|---|---|
| OData speed (rate) | 6,000 requests | 5-minute sliding window per user | Same for all editions; sandbox: 300/min, production: 600/min (legacy per-env metric) |
| Max concurrent OData requests | 5 per user | Per user (processing) | Excess requests queue for up to 8 min, then 503 |
| Max OData connections | 100 per user | Simultaneous (processed + queued) | Exceeded returns 429 |
| Max OData queue size | 95 per user | Queued requests | Exceeded returns 429 |
| SOAP speed (rate) | 6,000 requests | 5-minute sliding window per user | Same as OData — SOAP is deprecated |
| Max webhook subscriptions | 200 | Per environment | Subscriptions expire after 3 days if not renewed |
Throughput Scaling
Business Central rate limits are per-user. More users = more throughput per environment. If a single service principal is hitting limits, distribute workload across multiple service principals in a round-robin pattern. There are no per-environment daily quotas currently enforced, but Microsoft reserves the right to add entitlement quotas in the future. [src1]
Authentication
All Business Central API v2.0 authentication goes through Microsoft Entra ID (formerly Azure AD) using OAuth 2.0. The scope is https://api.businesscentral.dynamics.com/.default. [src3]
| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| Client Credentials (S2S) | Server-to-server, no user context, unattended integrations | Access: ~60-75 min | No refresh token; request new before expiry | Requires API.ReadWrite.All + Automation.ReadWrite.All permissions. Must create Entra application card in BC. |
| Authorization Code | User-context operations, interactive apps | Access: ~60-75 min; Refresh: up to 90 days | Yes | User must have BC license. Supports MFA. |
| Authorization Code + PKCE | SPAs and mobile apps | Access: ~60-75 min | Yes (limited for SPA) | Required for public clients. |
Authentication Gotchas
- S2S requires TWO setup steps: (1) Register app in Entra ID with API.ReadWrite.All permission AND (2) create a Microsoft Entra application card in Business Central and assign permission sets. Missing step 2 causes 401/403 errors. [src3]
- Application users cannot have SUPER permission set: BC explicitly blocks assigning SUPER to application users. Use D365 AUTOMATION or custom permission sets. [src3]
- Scope must be
https://api.businesscentral.dynamics.com/.default: Using the wrong scope causes authentication failures. The scope is the same for all tenants and environments. [src3] - On-premises requires ValidAudiences configuration: The server must include
https://api.businesscentral.dynamics.comin ValidAudiences. [src3] - Admin consent may be required: For multi-tenant apps, admin consent must be granted in each target tenant. [src3]
Constraints
- Per-user rate limits are strictly enforced: 6,000 req/5 min, 5 concurrent processing slots. A single integration user can bottleneck your entire integration. [src1]
- No bulk/batch API beyond $batch (100 ops max): For high-volume data loads, chunk into $batch requests of 100 operations each, respecting rate limits. [src1, src2]
- Webhook subscriptions expire after 3 days: No automatic renewal. Build a timer to refresh subscriptions every 2 days. [src4]
- API v2.0 cannot be extended with additional fields: Must copy AL code and create a custom API page instead. [src5]
- SOAP is deprecated: Microsoft will reduce SOAP throughput. All new integrations must use OData v4. [src1]
- Webhook collection notifications break Power Automate: If >1,000 records change within 30 seconds, BC sends a collection notification. Power Automate cannot process these. [src4]
Integration Pattern Decision Tree
START — User needs to integrate with Business Central API v2.0
|-- What's the integration pattern?
| |-- Real-time (individual records, <1s)
| | |-- Data volume < 100 records/operation?
| | | |-- YES --> Standard API v2.0: GET/POST/PATCH/DELETE
| | | |-- NO --> $batch requests (chunk into 100 ops per batch)
| | |-- Need notifications on entity changes?
| | |-- YES --> Webhooks (POST /api/v2.0/subscriptions)
| | |-- NO --> REST API polling with $filter=lastModifiedDateTime gt {timestamp}
| |-- Batch/Bulk (scheduled, high volume)
| | |-- Data volume < 6,000 records per 5 min?
| | | |-- YES --> Sequential API calls within rate limit window
| | | |-- NO --> Distribute across multiple service principals (round-robin)
| | |-- Can tolerate overnight processing?
| | |-- YES --> Schedule during off-peak, use multiple users, $batch
| | |-- NO --> Consider iPaaS middleware for managed queuing
| |-- Event-driven (webhooks)
| | |-- Need guaranteed delivery?
| | | |-- YES --> Webhook + dead letter queue (BC retries for 36h)
| | | |-- NO --> Webhook with basic error handling
| | |-- More than 1,000 changes in 30 seconds?
| | |-- YES --> Will receive 'collection' notification — re-query with filter
| | |-- NO --> Individual created/updated/deleted notifications
| |-- File-based (CSV/XML)
| |-- Use RapidStart Services or configuration packages in BC
|-- Which direction?
| |-- Inbound (writing to BC) --> POST/PATCH, use If-Match for concurrency
| |-- Outbound (reading from BC) --> GET with $filter/$select/$expand
| |-- Bidirectional --> Use lastModifiedDateTime for conflict detection
|-- Error tolerance?
|-- Zero-loss required --> Implement idempotency + dead letter queue
|-- Best-effort --> Fire-and-forget with exponential backoff on 429
Quick Reference
| Operation | Method | Endpoint | Payload | Notes |
|---|---|---|---|---|
| List companies | GET | /api/v2.0/companies | N/A | Returns all accessible companies |
| Get customers | GET | /api/v2.0/companies({id})/customers | N/A | Supports $filter, $select, $expand, $orderby |
| Create customer | POST | /api/v2.0/companies({id})/customers | JSON | Returns created entity with system-generated ID |
| Update customer | PATCH | /api/v2.0/companies({id})/customers({id}) | JSON | Requires If-Match header with ETag |
| Delete customer | DELETE | /api/v2.0/companies({id})/customers({id}) | N/A | Requires If-Match header |
| Batch operations | POST | /api/v2.0/$batch | Multipart/JSON | Max 100 operations per batch |
| Create webhook | POST | /api/v2.0/subscriptions | JSON | Requires notificationUrl + resource path |
| Renew webhook | PATCH | /api/v2.0/subscriptions({id}) | JSON | Must renew before 3-day expiry |
| Get metadata | GET | /api/v2.0/$metadata | N/A | Full OData metadata document |
| Deep insert | POST | /api/v2.0/companies({id})/salesQuotes | Nested JSON | Lines can be included in body |
Step-by-Step Integration Guide
1. Register Entra ID application and configure permissions
Register a new application in Microsoft Entra ID. Add Dynamics 365 Business Central API permissions: API.ReadWrite.All (Application type) and optionally Automation.ReadWrite.All. Create a client secret or certificate. Grant admin consent. [src3]
# Using Azure CLI to register app (alternative to portal)
az ad app create --display-name "BC Integration App" --sign-in-audience AzureADMyOrg
# Note the Application (client) ID from output
# Add API permission for Business Central via Azure Portal:
# API permissions > Add > Dynamics 365 Business Central > API.ReadWrite.All
Verify: Navigate to Azure Portal > App registrations > your app > API permissions. Confirm Dynamics 365 Business Central / API.ReadWrite.All shows "Granted".
2. Create Entra application card in Business Central
In BC, search for "Microsoft Entra applications". Create a new card with the client ID from step 1. Set State to Enabled. Assign permission sets (NOT SUPER). [src3]
Business Central steps:
1. Search for "Microsoft Entra applications" in BC
2. Click "New"
3. Enter Client ID from Entra ID app registration
4. Set State = Enabled
5. Assign permission sets (e.g., D365 AUTOMATION)
6. Click "Grant Consent" if needed
Verify: The application card shows State = Enabled and has permission sets assigned.
3. Acquire access token and test connectivity
Acquire an access token using client credentials flow, then call the companies endpoint to verify. [src3]
# Acquire token
TOKEN=$(curl -s -X POST \
"https://login.microsoftonline.com/YOUR_TENANT_ID/oauth2/v2.0/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "scope=https://api.businesscentral.dynamics.com/.default" \
-d "grant_type=client_credentials" \
| jq -r '.access_token')
# List companies
curl -s "https://api.businesscentral.dynamics.com/v2.0/YOUR_TENANT_ID/production/api/v2.0/companies" \
-H "Authorization: Bearer $TOKEN" | jq .
Verify: Response contains value array with company objects containing id, name, displayName.
4. Query entities with OData filters
Use $filter, $select, $expand, and $orderby to query efficiently. [src2, src5]
COMPANY_ID="your-company-id"
curl -s "https://api.businesscentral.dynamics.com/v2.0/YOUR_TENANT_ID/production/api/v2.0/companies(${COMPANY_ID})/customers?\$filter=lastModifiedDateTime gt 2026-01-01T00:00:00Z&\$select=id,displayName,email&\$orderby=displayName" \
-H "Authorization: Bearer $TOKEN" | jq .
Verify: Response contains filtered customer entities. Check @odata.nextLink if more pages exist.
5. Handle pagination with @odata.nextLink
Business Central returns up to 20,000 records per page. Follow @odata.nextLink until no more pages. [src1]
import requests
def get_all_pages(url, headers):
all_records = []
while url:
response = requests.get(url, headers=headers)
response.raise_for_status()
data = response.json()
all_records.extend(data.get("value", []))
url = data.get("@odata.nextLink")
return all_records
Verify: len(records) matches expected count in Business Central.
6. Implement error handling and rate limit retries
Handle 429 (Too Many Requests) with exponential backoff. Also handle 503 and 504. [src2]
import time, requests
def bc_request(method, url, headers, json=None, max_retries=5):
for attempt in range(max_retries):
response = requests.request(method, url, headers=headers, json=json)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 2 ** attempt))
time.sleep(retry_after)
continue
elif response.status_code == 503:
time.sleep(2 ** (attempt + 2))
continue
elif response.status_code == 504:
raise TimeoutError("Request exceeded 10-minute gateway timeout")
response.raise_for_status()
return response
raise Exception(f"Max retries exceeded for {url}")
Verify: Function returns successful response or raises appropriate exception after retries.
Code Examples
Python: CRUD Operations with Rate Limit Handling
# Input: tenant_id, client_id, client_secret, environment_name
# Output: Create, read, update, delete customer in Business Central
import msal, requests
app = msal.ConfidentialClientApplication(
client_id,
authority=f"https://login.microsoftonline.com/{tenant_id}",
client_credential=client_secret
)
result = app.acquire_token_for_client(
scopes=["https://api.businesscentral.dynamics.com/.default"]
)
headers = {
"Authorization": f"Bearer {result['access_token']}",
"Content-Type": "application/json",
"Accept": "application/json"
}
base_url = f"https://api.businesscentral.dynamics.com/v2.0/{tenant_id}/{env}/api/v2.0"
# Get company, then CRUD on customers
companies = requests.get(f"{base_url}/companies", headers=headers).json()
company_url = f"{base_url}/companies({companies['value'][0]['id']})"
# CREATE
new = requests.post(f"{company_url}/customers", headers=headers,
json={"displayName": "Acme Corp"}).json()
# UPDATE (requires If-Match with ETag)
requests.patch(f"{company_url}/customers({new['id']})",
headers={**headers, "If-Match": new["@odata.etag"]},
json={"phoneNumber": "+1-555-0100"})
JavaScript/Node.js: Webhook Subscription Management
// Input: tenantId, clientId, clientSecret, notificationUrl
// Output: Created webhook subscription for customer changes
const { ClientSecretCredential } = require("@azure/identity");
const fetch = require("node-fetch");
const credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
const token = await credential.getToken("https://api.businesscentral.dynamics.com/.default");
const headers = { Authorization: `Bearer ${token.token}`, "Content-Type": "application/json" };
// Create webhook subscription
const sub = await fetch(`${baseUrl}/subscriptions`, {
method: "POST", headers,
body: JSON.stringify({
notificationUrl: "https://your-endpoint.com/bc-notify",
resource: `/api/v2.0/companies(${companyId})/customers`,
clientState: "your-shared-secret"
})
}).then(r => r.json());
// Renew before 3-day expiry with PATCH
cURL: Quick API test
# Acquire token and list companies
TOKEN=$(curl -s -X POST \
"https://login.microsoftonline.com/TENANT/oauth2/v2.0/token" \
-d "client_id=ID&client_secret=SECRET&scope=https://api.businesscentral.dynamics.com/.default&grant_type=client_credentials" \
| jq -r '.access_token')
curl -s "https://api.businesscentral.dynamics.com/v2.0/TENANT/production/api/v2.0/companies" \
-H "Authorization: Bearer $TOKEN" | jq '.value[] | {id, displayName}'
Data Mapping
API URL Structure Reference
| Component | Format | Example |
|---|---|---|
| Base URL (SaaS) | https://api.businesscentral.dynamics.com/v2.0/{tenantId}/{environment}/api/v2.0 | .../v2.0/abc123/production/api/v2.0 |
| Company-scoped entity | {baseUrl}/companies({companyId})/{entity} | .../companies(guid)/customers |
| Automation API | .../api/microsoft/automation/v2.0 | For tenant/company management |
| Custom API | .../api/{publisher}/{group}/{version} | .../api/contoso/app1/v1.0/customEntities |
Data Type Gotchas
- All IDs are GUIDs: Business Central uses GUIDs for entity IDs. You cannot specify your own ID when creating records. [src5]
- ETags are mandatory for updates: Every PATCH and DELETE requires
If-Matchheader with the entity's@odata.etag. Missing it returns BadRequest_InvalidToken. [src6] - DateTime is UTC with Edm.DateTimeOffset: All datetime fields use ISO 8601 with timezone. BC stores in UTC but may display in user timezone. [src5]
- Multi-level $expand is supported: Expand nested navigational properties to reduce API calls, e.g.,
salesOrders?$expand=salesOrderLines($expand=item). [src2]
Error Handling & Failure Points
Common Error Codes
| Code | Meaning | Cause | Resolution |
|---|---|---|---|
| 400 | BadRequest_NotFound | Invalid OData query syntax or entity not found | Check $filter syntax; verify entity name casing |
| 400 | BadRequest_InvalidToken | Missing or wrong If-Match ETag header | Re-fetch entity to get current @odata.etag |
| 401 | Authentication_InvalidCredentials | Invalid/expired token or wrong scope | Verify scope is api.businesscentral.dynamics.com/.default |
| 403 | Authorization | Insufficient permissions for application user | Assign correct permission sets in BC |
| 408 | Request Timeout | Operation exceeded 8-minute timeout | Split into smaller requests; add $filter |
| 409 | Request_EntityChanged | Concurrent modification (ETag mismatch) | Re-read entity, merge changes, retry |
| 429 | Too Many Requests | Rate or connection limit exceeded | Exponential backoff; distribute across users |
| 503 | Service Temporarily Unavailable | Request queue full (>95 queued) | Back off and retry; redistribute load |
| 504 | Gateway Timeout | Request exceeded 10 minutes | Refactor into smaller queries |
Failure Points in Production
- Single service principal bottleneck: All calls through one user hit 5-concurrent/6,000-per-5-min limit. Fix:
Create multiple Entra ID app registrations and BC application users. Distribute requests round-robin.[src1] - Webhook subscriptions silently expire: No notification when subscription expires after 3 days. Fix:
Run scheduled job every 2 days to PATCH all subscriptions. Alert on renewal failure.[src4, src8] - $batch partial failures are silent: $batch returns 200 even if individual operations fail. Fix:
Parse each sub-response and check individual HTTP status codes.[src2] - Webhook validation handshake fails: notificationUrl doesn't return validationToken within timeout. Fix:
Ensure endpoint responds within 5 seconds with 200 OK and validationToken in response body.[src4] - Token not refreshed in long-running batch: Access token expires during multi-hour sync. Fix:
Use MSAL token cache — acquire_token_for_client() handles refresh automatically.[src3]
Anti-Patterns
Wrong: Polling all records to detect changes
# BAD — fetches ALL customers every sync, wastes API calls
all_customers = get_all_pages(f"{company_url}/customers", headers)
for customer in all_customers:
if customer_changed(customer): process(customer)
Correct: Use $filter on lastModifiedDateTime or webhooks
# GOOD — only fetch modified records
url = f"{company_url}/customers?$filter=lastModifiedDateTime gt {last_sync}"
changed = get_all_pages(url, headers)
# EVEN BETTER — use webhooks for push notifications
Wrong: One API call per record in a loop
# BAD — 1,000 items = 1,000 API calls from rate limit
for item in items: requests.post(f"{url}/items", headers=h, json=item)
Correct: Use $batch to group operations
# GOOD — batch up to 100 operations per request
batch = {"requests": [{"method": "POST", "url": f"companies({cid})/items",
"headers": {"Content-Type": "application/json"}, "body": item}
for item in chunk]}
requests.post(f"{url}/$batch", headers=h, json=batch)
Wrong: Ignoring ETag concurrency control
# BAD — no If-Match header, may overwrite concurrent changes
requests.patch(f"{url}/customers({id})", headers=h, json=data)
Correct: Always include If-Match with current ETag
# GOOD — fetch current ETag, include in If-Match
customer = requests.get(f"{url}/customers({id})", headers=h).json()
requests.patch(f"{url}/customers({id})",
headers={**h, "If-Match": customer["@odata.etag"]}, json=data)
Common Pitfalls
- Sandbox vs production rate limits: Sandbox allows 300 req/min vs production's 600 req/min. Fix:
Test with realistic data volumes using production rate limit as baseline.[src1] - Not specifying company in URL: Omitting company scope returns Internal_CompanyNotFound. Fix:
Always include companies({companyId}) in URL. Fetch company ID first via GET /api/v2.0/companies.[src6] - Using $top without $orderby: OData doesn't guarantee ordering without $orderby. Fix:
Always combine $top with $orderby for deterministic pagination.[src2] - Not handling webhook validation handshake: Subscription creation fails cryptically. Fix:
Return validationToken query param as plain text with 200 OK.[src4] - Assuming SUPER works for app users: BC blocks SUPER for application users. Fix:
Use D365 AUTOMATION or custom permission sets.[src3] - Not parsing $batch sub-responses: $batch returns 200 even when individual ops fail. Fix:
Check each sub-response status code individually.[src2]
Diagnostic Commands
# Acquire token for Business Central
TOKEN=$(curl -s -X POST \
"https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/token" \
-d "client_id=CLIENT_ID&client_secret=SECRET&scope=https://api.businesscentral.dynamics.com/.default&grant_type=client_credentials" \
-H "Content-Type: application/x-www-form-urlencoded" | jq -r '.access_token')
# Test authentication - list companies
curl -s "https://api.businesscentral.dynamics.com/v2.0/TENANT/production/api/v2.0/companies" \
-H "Authorization: Bearer $TOKEN" | jq '.value[] | {id, displayName}'
# Check available API endpoints (metadata)
curl -s "https://api.businesscentral.dynamics.com/v2.0/TENANT/production/api/v2.0/\$metadata" \
-H "Authorization: Bearer $TOKEN" | head -100
# List webhook subscriptions
curl -s "https://api.businesscentral.dynamics.com/v2.0/TENANT/production/api/v2.0/subscriptions" \
-H "Authorization: Bearer $TOKEN" | jq '.value[]'
# Verify Entra ID app (Azure CLI)
az ad app show --id CLIENT_ID --query "{name:displayName,appId:appId}" -o table
Version History & Compatibility
| API Version | Availability | Status | Breaking Changes | Migration Notes |
|---|---|---|---|---|
| API v2.0 | BC version 18.3 (2021 wave 2) | Current / GA | Entity changes from v1.0 | Recommended for all new integrations |
| API v1.0 | BC version 14 (2019) | Deprecated | N/A | Transition guide available; v2.0 has different entity names |
| SOAP | BC version 1+ | Deprecated (warning) | Throughput will be reduced | Migrate to OData v4 / API v2.0 immediately |
| Automation API v2.0 | BC version 22 | Current / GA | v1.0 deprecated | Use for tenant/extension management |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Real-time CRUD on standard BC entities | High-volume data migration (>100K records) | iPaaS with managed queuing or RapidStart Services |
| Webhook-driven event sync | Need Dataverse/CRM data | Dynamics 365 Dataverse Web API |
| Moderate-volume scheduled sync | Need custom fields on standard entities | Custom API pages (AL extension) |
| Unattended S2S integrations | Interactive user-facing app with MFA | Authorization Code + PKCE flow |
| Querying financial data | Reporting across large datasets | OData connected services in Excel/Power BI |
Important Caveats
- Rate limits are per-user and strictly enforced since late 2023. Legacy documentation showing per-environment limits is outdated. Current limits are 6,000 OData requests per user per 5-minute sliding window.
- Business Central API v2.0 cannot be extended with additional fields on standard entities. Custom fields require a custom API page in an AL extension.
- Webhook subscriptions expire after 3 days with no automatic renewal and no expiry notification. Build renewal into your integration from day one.
- SOAP endpoints are on a deprecation path. Microsoft has warned that SOAP throughput will be reduced.
- Sandbox environments have lower rate limits (300 req/min) than production (600 req/min). Always validate performance against production limits before go-live.
- Microsoft reserves the right to introduce daily entitlement quotas in the future. Plan for potential changes.