Every business that runs Microsoft Dynamics 365 Business Central eventually faces the same question: how do we get our ERP data out - and into every other system that needs it? Whether that is your company intranet, a supplier portal, a Power BI dashboard, a mobile warehouse app, or a custom internal web application, the answer starts with the same two technologies: OData and the Business Central API.
This guide is written for developers, IT managers, and solution architects who need to connect Business Central to something else. We cover the complete picture - from understanding the protocols, to writing your first authenticated API call, to designing integration patterns that hold up in production.
What is OData and Why Does Business Central Use It?
OData (Open Data Protocol) is an OASIS-ratified open standard that defines a universal grammar for building and consuming RESTful APIs. Instead of every API inventing its own way to express filtering, sorting, or field selection, OData defines it once and every compliant client and server understands it natively.
Business Central implements OData v4, the current generation. When BC exposes a page or query as a web service, the following query capabilities become available automatically without any server-side coding:
$filter- server-side filtering with rich comparison and logical operators$select- field projection, return only the fields your app needs$orderby- sort results by one or more fields$topand$skip- paginate large result sets$expand- embed related entity data inline in a single request$count- return the total number of matching records
Because OData is a published standard, dozens of client libraries already support it: Power BI, Excel Power Query, .NET, JavaScript, Python, Java, and many more. You rarely need to write low-level HTTP plumbing - pick a library, point it at the endpoint, and start querying.
Two Interfaces: REST API vs OData Web Services
Business Central exposes data through two related but distinct interfaces. Understanding the difference is the single most important decision every integrator makes before writing a line of code.
| Dimension | BC REST API (v2.0) | OData Web Services |
|---|---|---|
| URL prefix | /api/v2.0/ |
/ODataV4/ |
| Schema | Fixed, Microsoft-defined | Dynamic - mirrors the published page or query |
| Versioning | Strict (v1.0, v2.0, custom API) | None - schema reflects the live page |
| Custom tables | Via custom API pages in AL | Publish any page or query directly |
| Upgrade stability | High - versioned contracts | Lower - page changes can affect consumers |
| Best for | Standard entities, stable contracts, marketplace apps | Custom data, internal tools, Power BI, intranets |
Rule of thumb: Use the REST API when Microsoft already defines the entity (customers, items, sales orders, purchase invoices). Use OData web services when you need custom tables, extended fields, or joined queries that do not exist in the standard API.
Authentication: OAuth 2.0 with Microsoft Entra ID
Basic authentication for Business Central SaaS has been deprecated. All production integrations must use OAuth 2.0 via Microsoft Entra ID (formerly Azure Active Directory). There are two flows you need to know:
Client Credentials Flow (Service-to-Service)
Used for background jobs, scheduled syncs, and machine-to-machine integrations. There is no human in the loop. The application authenticates with a client ID and secret and acts as itself. This is the workhorse of intranet and web app integrations.
Authorization Code Flow (Delegated)
Used when the integration acts on behalf of a signed-in user - for example, an intranet page that shows the current user's pending BC approvals.
Registering Your App in Entra ID
- In the Azure portal go to Microsoft Entra ID → App registrations → New registration.
- Give the app a meaningful name, for example Intranet-BC-Connector.
- Under Certificates & secrets create a client secret and copy it immediately - it will not be shown again.
- Under API permissions add
Dynamics 365 Business Centralwith theAPI.ReadWrite.Allapplication permission and click Grant admin consent. - In Business Central open the Microsoft Entra Applications page, create a new record with the same Application (Client) ID, and assign an appropriate permission set.
Acquiring a Token in Node.js
const { ConfidentialClientApplication } = require("@azure/msal-node");
const cca = new ConfidentialClientApplication({
auth: {
clientId: process.env.BC_CLIENT_ID,
clientSecret: process.env.BC_CLIENT_SECRET,
authority: `https://login.microsoftonline.com/${process.env.BC_TENANT_ID}`
}
});
async function getBCToken() {
const result = await cca.acquireTokenByClientCredential({
scopes: ["https://api.businesscentral.dynamics.com/.default"]
});
return result.accessToken; // Valid ~60 minutes - cache this
}
That token goes into the Authorization: Bearer <token> header of every API request. Cache tokens aggressively and refresh on receiving a 401 response.
Security note: Never embed client secrets in front-end JavaScript or commit them to source control. Token acquisition must always happen on a trusted backend server. Store secrets in Azure Key Vault or your platform's secrets manager.
Endpoint Anatomy
Business Central API URLs follow a consistent structure. Once you understand each segment you can build any URL from memory.
Standard REST API
https://api.businesscentral.dynamics.com/v2.0/{tenantId}/{environment}/api/v2.0/companies({companyId})/customers
{tenantId}- your Microsoft Entra Directory (tenant) ID, a GUID{environment}- typically Production or Sandboxapi/v2.0- the standard API versioncompanies({companyId})- BC supports multiple companies per tenant; scope to one by its GUIDcustomers- the entity set (also:items,salesInvoices,vendors,purchaseOrders, etc.)
OData Web Service
https://api.businesscentral.dynamics.com/v2.0/{tenantId}/{environment}/ODataV4/Company('CRONUS')/CustomerCard
Notice: ODataV4 replaces api/v2.0, the company is referenced by its display name (URL-encoded), and CustomerCard is the service name you published in BC - not a Microsoft-defined entity name.
Discovering Available Endpoints
GET /ODataV4/$metadata
Authorization: Bearer eyJ0eXAiOiJKV1Qi...
This returns the full OData metadata document - an XML description of every entity, property, and relationship available on the tenant. Invaluable when exploring an unfamiliar environment or generating client code.
OData Query Options in Practice
These five query options cover ninety percent of real-world integration scenarios:
$filter - Server-side filtering
GET /customers?$filter=country eq 'GB' and balance gt 5000
Supports comparison operators (eq, ne, gt, ge, lt, le), logical operators (and, or, not), and string functions (contains, startswith, tolower).
$select - Field projection
GET /customers?$select=number,displayName,email,balance,country
This is the single biggest performance win available. Most BC entities expose 50+ fields; your UI probably needs 5–10. Always use $select in production integrations.
$orderby - Sorting
GET /salesInvoices?$orderby=invoiceDate desc,totalAmountIncludingTax desc
$top and $skip - Pagination
GET /items?$top=50&$skip=100
BC also returns an @odata.nextLink property in large responses. Always follow that link for the next page rather than incrementing $skip manually - it accounts for server-side cursor management correctly.
$expand - Embed related entities
GET /salesInvoices?$expand=salesInvoiceLines&$top=10
Returns parent invoices with all child lines embedded in a single request. Use sparingly - expanding deep hierarchies produces large payloads.
Writing Data: POST, PATCH, DELETE
Creating and updating records works as expected with one BC-specific requirement: updates via PATCH must include the If-Match header containing the record's current @odata.etag to prevent lost-update conflicts.
PATCH /customers(7e3d4e00-1a2b-3c4d-5e6f-7a8b9c0d1e2f)
Authorization: Bearer ...
If-Match: W/"JzQ0O0xMSkdTZ0E5..."
Content-Type: application/json
{
"displayName": "Acme Corporation Ltd.",
"email": "accounts@acme.example"
}
Publishing Custom OData Web Services from Business Central
The real power of OData surfaces when you publish your own endpoints. Any page or query in BC can be exposed as a web service in seconds - with no additional server infrastructure required.
Step 1 - Create a Page in AL
In Visual Studio Code with the AL Language extension, create a list page targeting your table:
page 50100 "Purchase Requisition API"
{
PageType = List;
SourceTable = "Purchase Requisition";
DelayedInsert = true;
Editable = true;
layout
{
area(Content)
{
repeater(Group)
{
field(requisitionId; Rec."Requisition ID") { Caption = 'requisitionId'; }
field(requestedBy; Rec."Requested By") { Caption = 'requestedBy'; }
field(itemNo; Rec."Item No.") { Caption = 'itemNo'; }
field(quantity; Rec.Quantity) { Caption = 'quantity'; }
field(status; Rec.Status) { Caption = 'status'; }
field(dueDate; Rec."Due Date") { Caption = 'dueDate'; }
}
}
}
}
Step 2 - Publish via the Web Services Page
In Business Central:
- Search for Web Services and open the list.
- Click New and set Object Type to Page.
- Enter your page ID (
50100) and a service name (purchaseRequisitions). - Check the Published toggle - the endpoint is immediately live.
Your custom endpoint is now available at:
GET /ODataV4/Company('Contoso')/purchaseRequisitions
?$filter=status eq 'Open'
&$select=requisitionId,requestedBy,itemNo,quantity,dueDate
&$orderby=dueDate asc
Step 3 - Consume It from Your Web App
const token = await getBCToken();
const url = new URL(
`https://api.businesscentral.dynamics.com/v2.0/${TENANT_ID}/Production` +
`/ODataV4/Company('Contoso')/purchaseRequisitions`
);
url.searchParams.set("$filter", "status eq 'Open'");
url.searchParams.set("$select", "requisitionId,requestedBy,itemNo,quantity,dueDate");
url.searchParams.set("$orderby", "dueDate asc");
url.searchParams.set("$top", "100");
const response = await fetch(url.toString(), {
headers: { "Authorization": `Bearer ${token}` }
});
const { value: requisitions } = await response.json();
renderDashboard(requisitions);
Real-World Integration Patterns
Pattern 1: Read-Through Intranet Dashboard
Your intranet homepage displays live BC data - open purchase orders, inventory alerts, top customers, unpaid invoices. The implementation pattern:
- A lightweight backend (Azure Functions, Node.js API, ASP.NET minimal API) acquires an OAuth token on a server-to-server basis.
- It calls BC with aggressive
$selectand$filterto fetch only what is needed. - Responses are cached in Redis or in-memory for 1–5 minutes depending on how frequently the data changes.
- The intranet front end calls your backend API - never BC directly.
Users never authenticate against BC, credentials never touch the browser, and the experience is fast because of caching.
Pattern 2: Write-Back Employee Portal
Employees submit expense claims, purchase requisitions, or time entries through a polished web form. The integration flow:
- User submits the form to your backend.
- Backend validates all fields (types, ranges, foreign key lookups against BC reference data).
- Backend transforms the payload into BC's expected schema and POSTs to the API.
- BC returns the newly created entity with its system-assigned ID.
- Backend returns the BC entity ID to the user as a reference number for tracking.
Pattern 3: Event-Driven Bidirectional Sync
For near-real-time sync with a CRM, e-commerce platform, or HR system, use BC's built-in webhook subscriptions rather than polling.
// Register a webhook subscription via the BC API
POST /api/v2.0/subscriptions
Content-Type: application/json
Authorization: Bearer ...
{
"notificationUrl": "https://yourapp.example.com/webhooks/bc",
"resource": "/api/v2.0/companies({companyId})/customers",
"clientState": "your-secret-validation-token"
}
BC sends a POST to your notificationUrl whenever a customer is created or modified. Subscriptions expire after a few days and must be renewed - build renewal into your integration from day one.
Pattern 4: Bulk Analytics Extract
For BI tools and data warehouse feeds, schedule a nightly job that pulls only records modified since the last run:
GET /api/v2.0/companies({id})/customers
?$filter=lastModifiedDateTime gt 2026-05-24T00:00:00Z
&$select=number,displayName,balance,country,lastModifiedDateTime
&$top=1000
Follow @odata.nextLink for subsequent pages until no link is returned. For very high volumes (hundreds of thousands of records), consider Microsoft Fabric Link for Dynamics 365 - it replicates BC tables into OneLake without any API call overhead.
Handling API Responses and Pagination
All BC API responses return JSON. A successful collection response looks like this:
{
"@odata.context": "https://api.businesscentral.dynamics.com/v2.0/.../customers",
"@odata.count": 1247,
"value": [
{
"id": "7e3d4e00-1a2b-3c4d-5e6f-7a8b9c0d1e2f",
"number": "C00010",
"displayName": "Acme Corporation",
"balance": 12450.00,
"@odata.etag": "W/\"JzQ0O0xMSkdTZ0E5...\""
}
// ... more records
],
"@odata.nextLink": "https://api.businesscentral.dynamics.com/v2.0/.../customers?$skiptoken=..."
}
Always store and follow @odata.nextLink when present. Also store the @odata.etag for any record you intend to update - you will need it for the If-Match header on PATCH requests.
Performance and Throttling
Business Central enforces API rate limits to protect shared tenant infrastructure. When you exceed them BC returns HTTP 429 Too Many Requests with a Retry-After header specifying how many seconds to wait.
Production performance checklist:
- Always use
$select- most BC entities expose 50+ fields; requesting all of them multiplies payload size and processing time unnecessarily. - Filter server-side - use
$filterto push filtering into BC rather than downloading all records and filtering in your application. - Cache reference data aggressively - items, customers, chart of accounts, and unit-of-measure codes change infrequently. Cache them for 15–60 minutes.
- Implement exponential backoff with jitter on 429 responses - never tight-loop retry.
- Use the
$batchendpoint for bulk write operations to reduce round trips and token overhead. - Run bulk extracts off-peak - schedule large data pulls outside business hours to avoid competing with interactive users.
- Check
Rec.IsTemporaryin AL event subscribers - skip business logic for temporary scratch records.
Security and Governance
An ERP integration is a high-value target. Apply these controls without exception:
- Principle of least privilege - create a custom BC permission set exposing only the specific tables and operations your integration requires. Do not assign SUPER.
- Rotate secrets on a schedule - client secrets should expire and be rotated. Prefer certificate credentials over secrets wherever your platform supports them.
- Never expose BC credentials in front-end code - all OAuth token acquisition must happen on a trusted server-side component.
- Log every API call - at minimum record the timestamp, calling application, endpoint path, HTTP method, and response status. Enable BC's Change Log for data-level audit trails.
- Validate all inbound data rigorously - before pushing form input to BC, validate types, lengths, enumeration values, and foreign key existence.
- Apply Conditional Access policies - in Entra ID restrict the integration app's sign-ins to known IP ranges or require managed device compliance.
- Handle PII responsibly - customer and employee data is regulated under GDPR and similar regimes. Do not cache personal data longer than necessary; propagate deletion requests.
Troubleshooting Common Issues
401 Unauthorized - Either the token has expired (refresh on 401) or the Entra application is not registered inside BC with permission sets assigned. Both registrations are required - the Entra app registration alone is not sufficient.
403 Forbidden on a specific entity - The permission set assigned to the integration app in BC does not include access to that table or operation. Adjust the permission set - not the Entra configuration.
404 Not Found on a known entity - Almost always a company-scope problem. Verify that the company ID or name in your URL exactly matches a company that exists in the target environment.
422 Unprocessable Entity - BC validation rejected your payload. The response body always contains a human-readable error message - log it, read it, and fix the payload. Common causes: missing required fields, a foreign key that does not exist, or a violated business rule.
429 Too Many Requests - You are being throttled. Read the Retry-After header value in seconds and pause before retrying. Implement exponential backoff with jitter for sustained high-volume workloads.
412 Precondition Failed on PATCH - Your If-Match ETag is stale - another process updated the record between your GET and PATCH. Re-fetch the record to get the current ETag, then retry your update with the fresh value.
Getting Started: Your First Integration in 15 Minutes
Follow these five steps to go from zero to a working authenticated API call:
- Register your app in Microsoft Entra ID and note the Application (Client) ID, Client Secret, and Tenant ID.
- Grant API permissions - add
Dynamics 365 Business Central → API.ReadWrite.Alland click Grant admin consent. - Register inside BC - open the Microsoft Entra Applications page, add a record with your Client ID, and assign a permission set.
- Acquire a token using the client credentials flow (see code example in the Authentication section above).
- Verify authentication by calling
GET /api/v2.0/companies- a 200 response with your company list confirms everything is wired correctly.
From that foundation you can add endpoints incrementally - one entity, one integration pattern at a time.
Closing: Build the Bridge, Then Iterate
Business Central's combination of OData v4, the versioned REST API, and the ability to publish any custom page as a live endpoint makes it one of the most integration-friendly ERP platforms available today. You do not need expensive middleware, complex ETL pipelines, or a dedicated integration team to connect BC to your intranet or web applications.
What you do need is a clear understanding of which interface fits each use case, a properly configured OAuth application, and a backend service that handles tokens, caching, and error recovery. The patterns in this guide cover every real-world scenario from a simple read-through dashboard to a bidirectional event-driven sync.
Start with the simplest pattern that solves your immediate problem. Ship it, monitor it, and expand from there. The API will be there every step of the way.
