The Embedded Finance Index is live: 6,292 companies and 1,383 providers, mapped across the stack. Explore the Index →

Business Central API Integration Guide for 2026

Business Central API guide for 2026: Entra ID and S2S auth, environments and companies, webhooks, per-user limits and the v29 and v30 web service removals.

GJGJ

GJ · Co-founder, Apideck

14 min readView as .md
Business Central API Integration Guide for 2026

Business Central online allows each user 6,000 OData requests in any five-minute window and processes five of them at a time, according to Microsoft's operational limits page. Service principals get the same allowance as people. That one number decides more about a Business Central API integration than the choice of SDK: how many workers you run and whether you poll or subscribe to webhooks. There is also no "Integrate APIs" setup step to do online: Microsoft's docs say APIs for Business Central online are enabled by default (updated October 2026).

Three ways in, and two that are closing

Business Central supports three types of web services: REST APIs, OData and SOAP. Microsoft calls the REST API stack "the preferred way to integrate with Business Central" in its API overview and tells you to use the highest version available, which today is v2.0. The standard API v2.0 covers the usual accounting records (customers, vendors, sales and purchase invoices, journals, general ledger entries, accounts) and needs no code inside the customer's tenant.

When a field you need isn't in a standard API, you can't add it. "Extending APIs with additional fields isn't currently possible," says the v2.0 reference; you copy the AL code of the standard API page from Microsoft's ALAppExtensions repository and publish a custom API page in an extension. Custom API pages set PageType = API plus a publisher, group and version, and Microsoft's custom API walkthrough recommends ODataKeyFields = SystemId so the key is a single GUID. Their routes follow the pattern api/<APIPublisher>/<APIGroup>/<APIVersion>/. Our guide to building ERP extensions compares the AL extension model with the other ERPs.

The third route, publishing an ordinary UI page as a SOAP or OData web service, is being removed for Microsoft's own pages. Microsoft's platform deprecation list gives the reason plainly: "A UI page isn't an API, so changes to them can happen in a release without being considered a breaking change." In version 29 (2026 release wave 2, shipping from October 2026) a Microsoft page can no longer be exposed as a SOAP endpoint, and from version 30 (2027 release wave 1) it can't be exposed as an OData endpoint either. Major versions ship every April and October and existing environments move over during a five-month update window, per the update rollout timeline, so an integration that calls a Microsoft page over SOAP breaks when the customer's environment moves to version 29, during the update period that ends in early March 2027 (a one-month grace period follows), and one that calls it over OData breaks with version 30.

Timeline of Microsoft's Business Central web service deprecations, from SOAP deprecated in 2021 release wave 1 to Microsoft pages no longer exposable as SOAP endpoints in version 29 and as OData endpoints in version 30

If you find page web services in an existing integration, move those reads to API v2.0 or to a custom API page you own.

Business Central API authentication with Microsoft Entra ID

Business Central online accepts only OAuth 2.0 tokens from Microsoft Entra ID. Web service access keys (basic auth) stopped working for online tenants on October 1, 2022, according to the same deprecation list. You register an app in Entra, single tenant or multitenant, and then pick one of two models, which Microsoft compares on its OAuth page: a user account (delegated, "user impersonation") or an application account (service-to-service, S2S). Delegated calls need a licensed user who signs in and run with that user's permissions. S2S calls need no interactive user and no license, and they run with the permissions you give the application inside Business Central. Our earlier post on authorization in the Business Central API goes deeper into the consent screens.

Delegated access with the authorization code flow

For delegated access, add the Dynamics 365 Business Central API under API permissions and select the delegated permission Financials.ReadWrite.All, as in Microsoft's Connect app walkthrough. The walkthrough uses the authorization code grant against https://login.microsoftonline.com/<your tenant domain>/oauth2/v2.0/authorize with the scope https://api.businesscentral.dynamics.com/.default. Access tokens last one hour by default, and you keep the connection alive with refresh tokens. Microsoft Entra issues those with a 90-day default lifetime outside single-page apps, so a connection nobody uses for three months has to be authorized again.

Client credentials (S2S) for background sync

Microsoft points out on its S2S page that delegated flows can be configured to require MFA, which "prevents unattended integrations." S2S avoids that. The setup has two halves. In Entra, add the application permission API.ReadWrite.All and grant admin consent (or grant it later from Business Central). In Business Central, an administrator opens the Microsoft Entra Applications page, creates a card with your client ID, sets State to Enabled and assigns permission sets. Applications can't be given SUPER; Microsoft names D365 AUTOMATION and EXTEN. MGT. - ADMIN as the sets that cover typical automation objects. Your app then requests a token, adapted from Microsoft's sample:

POST https://login.microsoftonline.com/{{tenantId}}/oauth2/v2.0/token HTTP/1.1
Content-type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id={{clientId}}
&client_secret={{clientSecret}}
&scope=https://api.businesscentral.dynamics.com/.default

There is no refresh token; you request a new access token when the old one expires. The weak point is the credential. Entra caps a client secret at 24 months and Microsoft recommends less than 12, and it recommends a certificate over a secret for production, per its credentials page. An app can hold more than one secret, so add the new one before the old one expires and every customer connection survives the rotation.

Environments and companies in the URL

Every call names an environment, and every record call names a company. The common endpoint is https://api.businesscentral.dynamics.com/v2.0/<environment name>/api/v2.0, for example .../v2.0/production/api/v2.0, per Microsoft's endpoints page. A direct-tenant form puts the tenant's domain in front of the environment, as in .../v2.0/cronus.com/sandbox/api/v2.0. Customers name their environments themselves, so don't assume "production": GET https://api.businesscentral.dynamics.com/environments/v1.2 lists them with their type (production or sandbox), country and service URL, per the environments API.

Inside an environment, GET <endpoint>/companies returns the companies with their GUIDs, and "the company ID must be provided in the URL for all resource API calls," per Microsoft's API tips. An environment can hold up to 300 companies. Store the tenant, environment and company ID for every customer connection, and let the customer pick the company, because a tenant with a dozen legal entities is normal.

Reading data: filters, expand and server-driven paging

Reads are OData v4 queries. Microsoft's rate-limit guidance recommends filtering on the modification timestamp for incremental sync and using $expand to pull related records in one call. These are its examples:

GET /companies({{companyId}})/customers?$filter=lastModifiedDateTime gt 2019-08-18T20:05:51.603Z

GET /companies({{companyId}})/salesOrders?$filter=orderDate ge 2019-05-01&$expand=paymentTerm,salesOrderLines($expand=account,item($expand=itemCategory))

Always send $select. Without it, Business Central returns every field of the table, "including fields from other extensions," according to the client performance page. The same page says to send Data-Access-Intent: ReadOnly on GET requests so they can run against a read replica, and it is direct about paging: "Do not use $top and $skip query options to implement client-driven paging." Let the server page instead. Online, a page holds at most 20,000 entities and the size can't be raised, but you can lower it per request with Prefer: odata.maxpagesize=300, per the server-driven paging page. Follow the @odata.nextLink URL in each response until it disappears, which is the OData standard's nextLink behavior.

General ledger work goes through generalLedgerEntries, a read-only resource with entryNumber, postingDate, accountNumber, debitAmount, creditAmount and lastModifiedDateTime, documented on its resource page. Financial statements come pre-built. This is Microsoft's income statement request:

GET https://{businesscentralPrefix}/api/v2.0/companies({id})/incomeStatements?$orderby=lineNumber&$filter=dateFilter ge 2019-01-01 and dateFilter le 2020-12-31

The response is a flat list of report lines with display, netChange, lineType and indentation, so you rebuild the hierarchy from the layout fields and skip header and total lines when you sum. Balance sheets follow the same pattern; trial balances return one row per G/L account with debit and credit totals. Our financial statement API comparison shows how other ledgers expose the same reports.

Writing data and posting documents

Creating a record is a POST of JSON to the entity set, and Business Central answers 201 Created with the full record, per the customer create page. Updates are PATCH requests that need an If-Match header; when the ETag you send doesn't match, "the customers won't be updated," says the update page. Microsoft's S2S sample sends If-Match: * to skip the check, which is fine for a test and risky for a sync that might overwrite a user's edit.

Documents and their lines can go in one request with a deep insert. Microsoft's example creates a sales quote with a line:

POST /companies({{companyId}})/salesQuotes
{
    "customerId": "{{customerId}}",
    "salesQuoteLines" :  [
    {"itemId": "{{itemId}}", "quantity": 10} ]
}

A sales invoice created through the API is a draft. Posting it is a separate bound action, POST .../salesInvoices({id})/Microsoft.NAV.post, which returns 204 with no body, per the salesInvoice resource page. Journals work the same way: you add journal lines, then Microsoft.NAV.post on the journal posts the whole batch. Decide early whether your product posts documents or leaves them as drafts for the customer's accountant, because the two produce very different ledgers.

For many writes, an OData $batch request carries up to 100 operations. Add the Isolation: snapshot header and the batch becomes transactional: if one inner request fails, "all changes within a batch will be reverted" unless the AL code behind a request commits, per Microsoft's transactional batch page. Errors come back with codes grouped by prefix, BadRequest_, Authentication_, Authorization_, Internal_ and Application_, listed on the error codes page. Microsoft's advice for Internal_ errors is to attempt the operation again; the other categories need a change to the request or the credentials.

Webhooks and subscription renewal

Business Central has native webhooks, and since delta links were removed in version 24, Microsoft calls them "the preferred approach to change tracking." You subscribe per company and resource with the request from Microsoft's webhooks page:

POST https://{businesscentralPrefix}/api/v2.0/subscriptions
Content-type: application/json
{
  "notificationUrl": "https://{notificationUrl}",
  "resource": "/api/v2.0/companies(f64eba74-dacd-4854-a584-1834f68cfc3a)/customers",
  "clientState": "optionalValueOf2048"
}

Business Central then calls your notificationUrl with a validationToken query parameter, and your endpoint has to return that token in the response body with a 200. The same handshake runs on every renewal. Subscriptions expire after three days unless you renew them with a PATCH, so plan the renewal job from the start. Use clientState as a shared secret to check that a notification came from your subscription.

Notifications are batched. Business Central waits 30 seconds after the first change, and if more than 1,000 records change in that window you get one collection notification with a filter to fetch them instead of individual events. A notification tells you which record changed, not what changed, so you still read the record. If your endpoint is down, Business Central retries for 36 hours, but only when you answer 408, 429 or a 5xx; any other error code deletes the subscription.

The supported v2.0 entities include customers, vendors, items, accounts, salesInvoices, purchaseInvoices, salesOrders, journals and generalLedgerEntries, and the full list is on the webhooks page. Two edge cases cause missed events. Changes by a delegated admin don't trigger a notification until a licensed user makes a change, and changes by users who can't schedule job queues wait for a user who can. An environment can hold 200 subscriptions in total, which matters when you subscribe to several resources for each of a customer's 30 companies. Run a periodic lastModifiedDateTime sweep alongside webhooks to catch what they miss.

Operational limits and 429 handling

The per-user limits arrived from late December 2023 and are, in Microsoft's words, "strictly enforced," where the older per-environment numbers of 300 (sandbox) and 600 (production) requests per minute were not.

Business Central online OData limits: 6,000 requests per user per five-minute window, 5 concurrent requests per user, 100 connections and 95 queued requests, an 8-minute request timeout, 20,000 entities per page, 100 operations per batch and 200 webhook subscriptions per environment

A request over the rate, connection or queue limit gets HTTP 429. Microsoft's rate-limit page asks for retry logic with a cool-off period and lists interval, incremental, exponential back-off and randomized strategies. Requests beyond the five in flight wait in a queue, and after 8 minutes there they fail with 503. Long requests have their own ceiling, and here Microsoft's pages disagree: the operational limits page cancels a request after 8 minutes with 408, while the rate-limit page says 10 minutes and 504. Design for 8 and split large reads with filters.

Throughput scales with users. Microsoft's FAQ on the limits page says you can "distribute your requests in a round-robin fashion or rotation through a list of users or service principals." For a single integration app, the practical answer is fewer, better requests: $select, $expand, deep inserts, $batch and webhooks instead of tight polling loops.

Sandboxes and testing

Customers on the Essentials or Premium plan get one production environment and three sandbox environments at no extra charge, per Microsoft's environment types page. For your own development, sign up for a Business Central trial tenant, or buy the Partner Sandbox license (minimum five users, Microsoft partner ID required). Sandboxes run on a different performance tier from production, so don't load-test in one.

A sandbox copied from production is not a clean clone for integrations. Microsoft's list of precautions for a copy includes clearing base application integration settings and disabling all webhooks, so re-subscribe after every copy and test your setup steps on the copy as if it were a new customer.

Gotchas to plan for

  • A client secret that expires stops every customer connection on that app at once. Track the expiry date or use a certificate.
  • Permissions differ per customer. An S2S app reads only what its permission sets allow, and a delegated connection loses access when the user who authorized it loses theirs.
  • Standard APIs can't be extended with extra fields, so a customer's custom fields mean a custom API page you deploy to their environment.
  • Posting groups decide where a document lands in the ledger. Apideck's connector notes say vendor posting group and G/L account posting groups can't be set through the API, so they have to be configured in Business Central before you create suppliers or bills.
  • Purchase orders are written header-only through Apideck, per its connector docs, so line items don't reach Business Central.
  • Version 29 removes SOAP on Microsoft pages during the current update window. Check customer integrations that predate API v2.0.

Building on Apideck instead

A direct Business Central API integration means an Entra app with secret rotation, per-customer environment and company selection, webhook renewal every three days and retry logic sized to per-user limits. Apideck's Microsoft Dynamics 365 Business Central API integration puts Business Central behind the same Accounting API as the other ledgers it supports. According to the connector page, it covers 25 resources and runs the OAuth flow and token refresh. In Vault, your customer authorizes with Microsoft and then picks the environment and the company. There are limits to know about. The unified API maps the trial balance but not Business Central's income statement or balance sheet, the connector has no webhooks so changes are polled, and only Business Central online is supported. A list call looks like this:

curl "https://unify.apideck.com/accounting/invoices?limit=20" \
  -H "Authorization: Bearer <APIDECK_API_KEY>" \
  -H "x-apideck-app-id: <APP_ID>" \
  -H "x-apideck-consumer-id: <CONSUMER_ID>" \
  -H "x-apideck-service-id: microsoft-dynamics-365-business-central"

If Business Central is your only ERP and you need custom API pages or near-real-time webhooks, build directly on API v2.0 with S2S authentication and a certificate. If it is one of several ledgers your customers run, start with Apideck and keep direct code for the records the unified model doesn't cover.

Ready to get started?

Scale your integration strategy and deliver the integrations your customers need in record time.

Ready to get started?
Talk to an expert

Frequently asked questions

Trusted by fast-moving product & engineering teams

Bonsai by Zoom
JobNimbus
Blue Zinc
Exact
Drata