MS Dynamics

Dynamics 365 Business Central OData v4 api/v2.0 and Finance and Supply Chain OData plus DMF, both on Microsoft Entra ID OAuth 2.0, with webhooks and business events.

Dynamics 365 is not one ERP. For a commerce business there are two entirely separate products under the name, and a connector written for one does not touch the other. Business Central is the small and mid-market ERP, descended from Navision, with a clean REST API at api/v2.0 returning JSON, real webhooks, and a single sales order entity. Finance and Supply Chain Management, often still called Finance and Operations or F&O, is the large enterprise product descended from AX, with an OData v4 endpoint over data entities, a file-based Data Management Framework for bulk work, an add-in service for inventory, and a business events framework instead of webhooks. This page covers both and says which product every endpoint belongs to. Establish which one the customer runs before writing a line of code, because almost nothing transfers.

At a glance

Warning

Business Central's item resource exposes a single read-only inventory decimal, which is total inventory across all locations. There is no stock-by-location resource in the v2.0 API. If the customer runs multi-location inventory, per-location stock has to come from a custom API page or an OData v4 web service over an item availability page, which is AL code in their environment. Plan for that conversation early.

What it is

Business Central is Microsoft's ERP for companies with roughly 10 to 500 employees, sold per named user through Microsoft partners, and it is the successor to Dynamics NAV. It is a full financials, inventory, sales and purchasing system, and Microsoft reports it passed 40,000 customers. Its data model is document based: sales quote becomes sales order becomes posted sales shipment and posted sales invoice, and each of those is a distinct record.

Finance and Supply Chain Management is the enterprise tier, descended from Dynamics AX, running as a Tier 2 or higher environment on Azure with a separate Power Platform environment attached. It is what large manufacturers and distributors run. Its data model is far deeper: a sales order line has warehouse, site, product dimension (configuration, size, colour, style, version) and tracking dimension (batch, serial) columns, and inventory is dimensioned along all of them.

API access

Business Central

APIs for Business Central online are enabled by default; no feature flag is needed. What the administrator must do is register an application and grant it access.

  1. Register an application in the customer's Microsoft Entra tenant at entra.microsoft.com. Record the Application (client) ID.
  2. Create a client secret on that application and record it, because it is shown once.
  3. Under API permissions, add Microsoft APIs, Dynamics 365 Business Central, Application permissions, and select API.ReadWrite.All (access to APIs and web services) and Automation.ReadWrite.All (full access to automation) as needed.
  4. Grant admin consent, either in the Azure portal or from the Business Central web client.
  5. In Business Central, open the Microsoft Entra applications page, create a record with that client ID, set State to Enabled, and assign permission sets. Applications cannot be assigned the SUPER permission set; Microsoft's own guidance is least privilege. The built-in D365 AUTOMATION and EXTEN. MGT. - ADMIN sets cover most automation objects.

Endpoint forms, per Microsoft's endpoints page:

Every data call is scoped to a company: .../api/v2.0/companies({companyId})/salesOrders. Get the company id from GET .../api/v2.0/companies first; an environment holds up to 300 companies. The online API update cycle is monthly and new fields appear without a version bump, so tolerate unknown properties. Service-to-service auth reached v2.0 in Business Central 18.3 online, 18.11 and 19.5 on premises.

Neither product is a sales channel: no listings, no marketplace product ids, no channel prices. Dynamics receives orders and is the source of stock and the financial record.

Finance and Supply Chain Management

The OData service root is https://<environment host>/data. Open it in a browser to list every entity the environment exposes. The set of entities is whatever is marked IsPublic in the application object tree, so it varies between customers who have customised.

The administrator registers an Entra application and then adds its client ID under System administration, Setup, Microsoft Entra applications, mapping it to a Finance and Operations user. That mapping is the access control list the client credentials flow is checked against; without it a valid token still gets rejected.

For on premises, <baseurl> must have /namespaces/AXSF appended and AD FS replaces Entra ID.

SDKs

Microsoft publishes sample console applications rather than client libraries: Microsoft/Dynamics-AX-Integration has an ODataConsoleApplication and FileBasedIntegrationSamples/ConsoleAppSamples for the Data Management Framework. For Business Central, microsoft/BCTech carries OAuth flow samples in PowerShell and AL, and microsoft/ALAppExtensions holds the AL source of every v2.0 API page, which is the definitive answer when the reference docs are ambiguous. Authentication on both products is standard MSAL, so use the Microsoft Authentication Library for your language rather than hand-rolling.

Authentication

Both products use Microsoft Entra ID OAuth 2.0. The difference is only the scope.

Business Central, client credentials

  1. Acquire a token from https://login.microsoftonline.com/<tenantId>/oauth2/v2.0/token.
  2. Use grant_type=client_credentials, your client_id and client_secret, and scope https://api.businesscentral.dynamics.com/.default.
  3. Send the result as Authorization: Bearer <access_token>.
  4. Tokens are standard Entra ID access tokens, typically one hour. There is no refresh token in the client credentials flow; mint a new one.
  5. Resolve the company id once per environment and cache it.
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=<client id>
&client_secret=<client secret>
&scope=https://api.businesscentral.dynamics.com/.default
GET https://api.businesscentral.dynamics.com/v2.0/production/api/v2.0/companies HTTP/1.1
Authorization: Bearer eyJ0eXAiOi...
Accept: application/json

Writes need an If-Match header carrying the entity's ETag, or If-Match: * to force. Business Central rejects a PATCH without it.

Microsoft's guidance on the delegated flows is worth repeating: authorization code, implicit and resource owner password flows can all be forced through multifactor authentication by conditional access, which breaks an unattended integration. Use client credentials for anything on a schedule.

Finance and Supply Chain, client credentials

Same token endpoint, different scope. The resource is the environment itself, so the scope is https://<environment host>/.default. The client id must also be present in the environment's Microsoft Entra applications access control list, mapped to a user whose security roles determine what the token can actually reach.

Inventory Visibility add-in, two-step token

The Inventory Visibility service sits outside the Finance and Operations environment and needs a token exchange, not a direct token.

  1. Get an Entra token from https://login.microsoftonline.com/<aadTenantId>/oauth2/v2.0/token with scope 0cdb527f-a8d1-4bf8-9436-b352c68682b2/.default.
  2. POST https://securityservice.operations365.dynamics.com/token with header Api-Version: 1.0 and a body of {"grant_type": "client_credentials", "client_assertion_type": "aad_app", "client_assertion": "<the Entra token>", "scope": "https://inventoryservice.operations365.dynamics.com/.default", "context": "<fno environment id>", "context_type": "finops-env"}.
  3. The response carries access_token, token_type: bearer and expires_in: 3600. Use it as the bearer token on Inventory Visibility calls, together with Api-Version: 1.0.
  4. A 307 response means resend the token request to the URL in the Location header.

The context is the Supply Chain Management environment id from Lifecycle Services, not the Power Platform environment id. This catches people out.

Multi-account

One Entra tenant per customer, one or more environments inside it. A multi-tenant Entra app registration lets one application be consented into many customer tenants, which is the right shape for a connector vendor, but each customer's administrator must still create the Microsoft Entra applications record inside Business Central or Finance and Operations and assign permissions. The credential store keys on (tenant id, product, environment name), and it must also record the company id or legal entity, because both products partition all data by it.

Objects we can read

Business Central paths below are relative to .../api/v2.0/companies({companyId}). Finance and Supply Chain paths are relative to https://<environment host>/data.

Orders

Business Central: GET /salesOrders, GET /salesOrders({id}). Supports POST, PATCH and DELETE. Navigation to customer, currency, paymentTerm, shipmentMethod, salesOrderLines, dimensionSetLines, pdfDocument, attachments.

{
  "value": [
    {
      "@odata.etag": "W/\"JzQ0O3BXMHhQTXVQK1J...\"",
      "id": "a1f6f21c-2e2f-ee11-9ab3-6045bd0f1b2c",
      "number": "SO-101452",
      "externalDocumentNumber": "D2C-98217",
      "orderDate": "2026-09-19",
      "customerId": "3a2c0d51-1b6e-ec11-8a63-0022481b5e3f",
      "customerNumber": "C00420",
      "shipToAddressLine1": "123 Main Street",
      "shipToCity": "Mumbai",
      "shipToState": "MH",
      "shipToPostCode": "400001",
      "shipToCountry": "IN",
      "currencyCode": "INR",
      "requestedDeliveryDate": "2026-09-22",
      "discountAmount": 500.0,
      "totalAmountExcludingTax": 10592.0,
      "totalTaxAmount": 1907.0,
      "totalAmountIncludingTax": 12499.0,
      "fullyShipped": false,
      "status": "Open",
      "lastModifiedDateTime": "2026-09-20T07:41:55.29Z",
      "phoneNumber": "+91 22 5550 0123",
      "email": "orders@acmetrading.example"
    }
  ]
}

All shipTo*, billTo*, sellTo*, phoneNumber, email and customerName fields are PII and are not masked.

status is the enum NAV.salesOrderEntityBufferStatus with values Draft, In Review and Open. It does not cover shipped, invoiced or cancelled. Fulfilment progress is fullyShipped on the header and the shipped and invoiced quantities on the lines; a posted order disappears from salesOrders and reappears as a posted shipment and a posted invoice.

Incremental sync filters on lastModifiedDateTime, which is read-only and reliable:

GET /salesOrders?$filter=lastModifiedDateTime gt 2026-09-20T00:00:00Z&$orderby=lastModifiedDateTime&$top=1000

Finance and Supply Chain: GET /data/SalesOrderHeadersV2. Fields include dataAreaId (the legal entity, part of the key), SalesOrderNumber, OrderingCustomerAccountNumber, InvoiceCustomerAccountNumber, OrderCreationDateTime, SalesOrderStatus, CurrencyCode, SalesOrderName, RequestedReceiptDate, plus a long tail of delivery address and terms fields.

Warning

SalesOrderHeadersV2 does not expose a filterable last-modified timestamp. OrderCreationDateTime is a creation timestamp, not a modification timestamp, so it cannot drive an incremental sync. A connector against Finance and Supply Chain has to either do a full crawl with $skip paging and delete tracking, or use change tracking through a Data Management Framework export project, or subscribe to business events. This is the single biggest design constraint on the product.

Cross-company is off by default: OData returns only the user's default legal entity unless you append ?cross-company=true, and you filter a specific one with ?$filter=dataAreaId eq 'usmf'&cross-company=true.

Order items

Business Central: GET /salesOrderLines or GET /salesOrders({id})/salesOrderLines.

{
  "value": [
    {
      "id": "b4e0d21c-2e2f-ee11-9ab3-6045bd0f1b2c",
      "documentId": "a1f6f21c-2e2f-ee11-9ab3-6045bd0f1b2c",
      "sequence": 10000,
      "itemId": "c7a91f88-1b6e-ec11-8a63-0022481b5e3f",
      "lineType": "Item",
      "lineObjectNumber": "FG-OIL-GN-1000",
      "description": "Cold Pressed Groundnut Oil 1L",
      "quantity": 24.0,
      "unitPrice": 441.33,
      "amountExcludingTax": 10592.0,
      "taxCode": "GST18",
      "totalTaxAmount": 1907.0,
      "amountIncludingTax": 12499.0,
      "shipmentDate": "2026-09-22",
      "shippedQuantity": 0.0,
      "invoicedQuantity": 0.0,
      "locationId": "2d4b0a90-1b6e-ec11-8a63-0022481b5e3f"
    }
  ]
}

lineType is the enum NAV.invoiceLineAggLineType with values Comment, Account, Item, Resource, Fixed Asset and Charge. Only Item lines map to order_items; comment lines carry no quantity and must be skipped or the totals will not reconcile.

Finance and Supply Chain: GET /data/SalesOrderLines, keyed by dataAreaId, SalesOrderNumber and LineNumber. Fields include ItemNumber, OrderedInventoryQuantity, SalesPrice, SalesUnit, LineAmount, ShippingWarehouseId, ShippingSiteId, RequestedReceiptDate, RequestedShipDate, CustomerLineNumber, SalesStatus, and the product dimensions ProductConfigurationId, ProductSizeId, ProductColorId, ProductStyleId, ProductVersionId. Those five dimensions are what make a Finance and Supply Chain SKU: item number alone is not unique at the sellable level.

Products and listings

Business Central: GET /items. Properties are id, number, displayName, displayName2, type (Inventory, Service, Non-Inventory), itemCategoryId, itemCategoryCode, blocked, gtin, inventory (read-only, total across all locations), unitPrice, priceIncludesTax, unitCost, taxGroupId, taxGroupCode, baseUnitOfMeasureId, baseUnitOfMeasureCode, generalProductPostingGroupCode, inventoryPostingGroupCode and lastModifiedDateTime. Navigation to itemCategory, unitOfMeasure, picture, itemVariants, defaultDimensions and documentAttachments.

itemVariants is where colour and size live in Business Central, and a variant has its own code but shares the parent item's price unless a specific price rule exists.

Finance and Supply Chain: GET /data/ReleasedProductsV2, keyed by dataAreaId and ItemNumber, with ProductName, SearchName, ProductNumber, ProductGroupId, ItemModelGroupId and a wide set of unit, tax and dimension group fields. A product must be released to a legal entity before it appears here; the shared product master is a different entity.

Neither product has listings. channel_listing_id and url have no source.

Inventory

Business Central: the only published figure is item.inventory, a read-only decimal of total inventory. There is no per-location resource in the v2.0 API. Options, in order of preference:

  1. Have the customer publish a custom API page over Item Availability by Location, or expose the Item Ledger Entry table as an OData v4 web service. Both are AL work in their environment, and once published the page is reachable at .../api/<publisher>/<group>/<version>/companies({id})/<entity> and is webhook-eligible.
  2. Reconstruct from posted document lines, which is expensive and drifts.
  3. Accept total-only stock, which is workable for a single-warehouse business and useless otherwise.

Finance and Supply Chain: the supported answer is the Inventory Visibility add-in, a separate service with its own REST API and its own token. On-hand is queried by POST, not GET, because the filter is a document.

POST https://inventoryservice.operations365.dynamics.com/api/environment/{environmentId}/onhand/indexquery HTTP/1.1
Api-Version: 1.0
Authorization: Bearer <access token>
Content-Type: application/json

{
  "filters": {
    "organizationId": ["usmf"],
    "productId": ["M0030"],
    "siteId": ["1"],
    "locationId": ["11", "12", "13"]
  },
  "groupByValues": ["colorId", "sizeId"],
  "returnNegative": true
}
[
  {
    "productId": "M0030",
    "dimensions": {
      "ColorId": "White",
      "siteid": "1",
      "locationid": "13"
    },
    "quantities": {

      "iv": {
        "availabletoreserve": 20,
        "totalavailable": 20,
        "totalordered": 20,
        "totalonorder": 5
      },
      "pos": { "inbound": 0, "outbound": 0 }
    }
  }
]

The quantities object is keyed by data source: fno is what Finance and Operations itself holds, iv is the add-in's own aggregate, and any other key (such as pos) is a source the customer has configured. iv.totalavailable is the figure to publish to a channel. fno.physicalinvent is physical on hand.

Limits: a query returns up to 5,000 items by product id, bulk write APIs take at most 512 records per request, and by-location partitioning caps sites multiplied by locations at 10,000 in version 1.0. Version 2.0 of indexquery adds pageNumber and pageSize query parameters and a nextLink in the response, and allows several organization ids in one call.

Shipments and tracking

Business Central: posted sales shipments are not in the v2.0 API surface documented here. The bound action Microsoft.NAV.shipAndInvoice on a sales order posts both a shipment and an invoice, and the resulting invoice is readable through salesInvoices with its orderId pointing back. To read the shipment itself, the customer's environment must expose Posted Sales Shipments as a custom API page or a web service. Carrier and tracking number are on that posted shipment header in the base application, not on the sales order.

Finance and Supply Chain: packing slips and shipments are separate entities in the /data service; which ones are public varies by environment. Read the service root to find out rather than assuming.

Sample response not published for either shipment object, because neither is in the documented standard API surface.

Returns and cancellations

Business Central: GET /salesCreditMemos and salesCreditMemoLines are the financial return. They are webhook-supported. A sales invoice can be reversed with the bound action Microsoft.NAV.cancel, or a corrective credit memo created with Microsoft.NAV.makeCorrectiveCreditMemo. A sales order that has not been posted is cancelled with DELETE /salesOrders({id}); once posted it cannot be deleted and must be credited.

Finance and Supply Chain: return orders are a distinct document type with their own entity; confirm the exact entity name against the customer's service root.

Payments and settlements

Business Central: GET /salesInvoices and GET /salesInvoiceLines.

Header properties include id, number, externalDocumentNumber, invoiceDate, postingDate, dueDate, customerPurchaseOrderReference, customerId, customerNumber, customerName, the three address blocks, currencyCode, orderId and orderNumber (read-only, the join back to the sales order), paymentTermsId, shipmentMethodId, remainingAmount, discountAmount, totalAmountExcludingTax, totalTaxAmount, totalAmountIncludingTax, status, lastModifiedDateTime, phoneNumber and email.

status is the enum NAV.invoiceEntityAggregateStatus with values blank, Draft, In Review, Open, Paid, Canceled and Corrective. remainingAmount reaching zero and status becoming Paid is the settlement signal.

Note

Microsoft documents a real trap here: the id of a salesInvoice can differ from the systemId of the underlying posted record, because the systemId of an unposted invoice is carried into the posted invoice in the API but not in the record. To map a posted sales invoice record's systemId to the API resource, read https://{businesscentralPrefix}/microsoft/automate/v1.0/companies({id})/postedSalesInvoices({systemId}) and take the apiId property.

Neither product has a marketplace settlement object. Channel payouts must be reconciled against invoices on externalDocumentNumber.

Customers

Business Central: GET /customers. Webhook supported. Carries the customer number, display name, addresses, contact details, payment terms, tax fields and blocked status, all of it PII where personal.

Finance and Supply Chain: GET /data/CustomersV3, keyed by dataAreaId and CustomerAccount.

Locations

Business Central: GET /locations lists the warehouses and is the source for locationId on order lines.

Finance and Supply Chain: GET /data/Warehouses with dataAreaId, WarehouseId, WarehouseName and OperationalSiteId. Note the two-level model: a site contains warehouses, and both appear on every inventory and order line record.

Writing back: listings, price and stock

Sales orders in

Business Central: POST /salesOrders creates the header, then POST /salesOrderLines per line, or post the header with a nested salesOrderLines array in the same request body.

POST https://api.businesscentral.dynamics.com/v2.0/production/api/v2.0/companies(<companyId>)/salesOrders HTTP/1.1
Authorization: Bearer <token>
Content-Type: application/json

{
  "customerNumber": "C00420",
  "externalDocumentNumber": "D2C-98217",
  "orderDate": "2026-09-19",
  "requestedDeliveryDate": "2026-09-22",
  "currencyCode": "INR",
  "salesOrderLines": [
    {
      "lineType": "Item",
      "lineObjectNumber": "FG-OIL-GN-1000",
      "quantity": 24,
      "unitPrice": 441.33
    }
  ]
}

Updates are PATCH /salesOrders({id}) with If-Match set to the ETag from the previous read, or If-Match: *. externalDocumentNumber is the place to put the channel order id; it is not enforced unique, so the connector must check before creating or it will duplicate on retry.

Posting is a bound action, not a status field: POST .../salesOrders({id})/Microsoft.NAV.shipAndInvoice returns 204 with no body and creates the posted shipment and posted invoice.

Finance and Supply Chain: POST /data/SalesOrderHeadersV2 then POST /data/SalesOrderLines. OData create runs the entity's validateField, defaultRow and validateWrite methods in order, so a create that the UI would reject is rejected here too, with an X++ validation message. All key fields must be supplied on every call, including dataAreaId.

For volume, use the Data Management Framework rather than per-record OData. A batch of many thousand orders goes in as a data package:

  1. POST /data/DataManagementDefinitionGroups/Microsoft.Dynamics.DataEntities.GetAzureWriteUrl with {"uniqueFileName": "<guid>.zip"}, which returns a BlobId and a BlobUrl carrying a shared access signature.
  2. Upload the package zip to that blob URL.
  3. POST .../Microsoft.Dynamics.DataEntities.ImportFromPackage with {"packageUrl": "...", "definitionGroupId": "<data project name>", "executionId": "", "execute": true, "overwrite": true, "legalEntityId": "usmf"}, which returns an executionId.
  4. Poll POST .../Microsoft.Dynamics.DataEntities.GetExecutionSummaryStatus with {"executionId": "..."} until the value is one of Succeeded, PartiallySucceeded, Failed or Canceled.
  5. On failure, GenerateImportTargetErrorKeysFile then GetImportTargetErrorKeysFileUrl gives the keys that failed, and GetExecutionErrors gives the messages.

The data project must exist in the environment before the call; the API will not create it. ImportFromPackage runs as a batch, and Microsoft warns explicitly against calling it on parallel threads.

Invoices in

Business Central: POST /salesInvoices creates a draft, POST .../salesInvoices({id})/Microsoft.NAV.post posts it, Microsoft.NAV.postAndSend posts and emails it, Microsoft.NAV.cancel reverses it. All return 204.

Finance and Supply Chain: invoicing is driven by the sales order's confirm, pick, pack and invoice sequence, not by creating an invoice entity. ExportToPackage on an invoice data project is the read path.

Stock adjustments in

Business Central: not exposed on the v2.0 API. Item journals, which are how stock actually moves, are not in the documented resource set. Either expose an item journal as a custom API page, or push the adjustment as a document (a purchase invoice for receipts, a sales invoice for issues), or accept that Business Central owns stock and the connector only reads it. Say this to the customer early.

Finance and Supply Chain: three routes.

  • POST /api/environment/{environmentId}/onhand posts a delta, with id for idempotency, organizationId, productId, dimensions and a quantities object nested under a data source name. onhand/bulk takes up to 512 at once.
  • POST /api/environment/{environmentId}/setonhand/{inventorySystem}/bulk overrides an absolute quantity, which is the counting correction case, up to 512 records per request.
  • POST /api/environment/{environmentId}/transaction/adjustment/bulk syncs external inventory changes back into Finance and Operations itself, changing the ledger rather than only the visibility layer.
{
  "id": "cyclecount-2026-09-21-0001",
  "organizationId": "usmf",
  "productId": "FG-OIL-GN-1000",
  "dimensions": {
    "siteId": "1",
    "locationId": "11",
    "colorId": "red"
  },
  "quantities": {
    "pos": { "inbound": 1 }
  }
}

The id is an idempotency key: Microsoft states it exists so that a resubmission after a service failure is not counted twice. Use it.

Soft reservations are separate: onhand/reserve takes a quantity, a modifier and ifCheckAvailForReserv, and returns a reservationId that onhand/unreserve needs along with an OffsetQty.

Price

Business Central: PATCH /items({id}) with unitPrice. Customer-specific and quantity-break pricing lives in price lists, which are not in the standard v2.0 surface and need a custom API.

Finance and Supply Chain: sales price trade agreements are data entities under /data; the exact names vary by version, so read the service root.

Batching

Both products support OData $batch. Business Central caps a batch at 100 operations. Finance and Supply Chain uses OData changesets, where each changeset is a single atomic transaction, and the .NET client exposes this as SaveChangesOptions.BatchWithSingleChangeset.

Webhooks and notifications

Business Central: real webhooks

POST .../api/v2.0/subscriptions with a body of notificationUrl, resource and an optional clientState up to 2048 characters.

{
  "notificationUrl": "https://connector.example.com/hooks/bc",
  "resource": "/api/v2.0/companies(f64eba74-dacd-4854-a584-1834f68cfc3a)/salesOrders",
  "clientState": "shared-secret-value"
}

Business Central then calls your notificationUrl with a validationToken query string parameter. You must echo that token in the response body with status 200 or the subscription is not created. The same handshake is required on renewal.

Subscriptions expire after three days. Renew with PATCH .../subscriptions({id}), which also requires the handshake. The current expiry is in expirationDateTime on the subscription. Maximum 200 webhook subscriptions per environment.

Notifications are batched and may contain events from several of your subscriptions:

{
  "value": [
    {
      "subscriptionId": "webhookItemsId",
      "clientState": "shared-secret-value",
      "expirationDateTime": "2026-09-24T07:52:31Z",
      "resource": "api/v2.0/companies(b18aed47-c385-49d2-b954-dbdf8ad71780)/items(26814998-936a-401c-81c1-0e848a64971d)",
      "changeType": "updated",
      "lastModifiedDateTime": "2026-09-21T12:54:20.467Z"
    }
  ]
}

changeType is created, updated, deleted, or collection. Business Central waits 30 seconds after the first change before sending, so that an entity edited several times produces one notification. If more than 1,000 records change inside that window, it sends a single collection notification carrying a filtered resource URL instead of one notification per record, and you re-query with that filter. A connector must handle collection or it will silently miss bulk edits.

Verification is by clientState, which Microsoft describes as an opaque token and a shared secret. There is no HMAC signature header. Treat clientState as the secret and compare it constant-time.

Retries continue for 36 hours, but only if you respond 408, 429 or 5xx. Any other status code stops retries and deletes the subscription. Returning a 400 on a body you could not parse will silently unsubscribe you.

Webhook-supported entities include salesOrders, salesInvoices, salesCreditMemos, salesQuotes, items, itemCategories, customers, vendors, purchaseInvoices, accounts, currencies, dimensions, generalLedgerEntries, journals, paymentMethods, paymentTerms, shipmentMethods, unitsOfMeasure, employees, countriesRegions, companyInformation and customerPaymentJournals. Query the live list with GET .../api/microsoft/runtime/beta/companies({companyId})/webhookSupportedResources?$filter=resource eq 'v2.0*'.

For document APIs a change to a line raises a notification on the header, which is what you want.

Webhooks are not supported where the source table is temporary or a system table, where the API page has a composite key, or where the API is a query rather than a page.

Finance and Supply Chain: business events

There is no subscription API you call. The customer activates business events from the catalogue at System administration, Set up, Business events and assigns endpoints. Supported endpoint types are Azure Service Bus, Azure Event Grid, Power Automate and a few others. Your connector receives events by owning the Service Bus queue or Event Grid topic, which means a paid Azure subscription on someone's side.

Relevant settings, with Microsoft's defaults: retry count 3, wait between retries 1,000 milliseconds, endpoints allowed per event 10, processing threads maximum 4, HTTP timeout 10 seconds.

Events carry a control number for idempotency, and Microsoft states explicitly that delivery order is not guaranteed. Filterable fields on Service Bus and Event Grid are Category, Business event ID (the implementation class name) and Legal entity.

Microsoft is blunt that business events are not a data export mechanism: they are lightweight notifications and do not carry large bodies. Treat an event as a signal to go and read the entity.

Polling fallback

Business Central polls well on lastModifiedDateTime. Finance and Supply Chain does not, because SalesOrderHeadersV2 has no modification timestamp. For that product, either run a Data Management Framework export project with change tracking enabled, which exports only the delta, or subscribe to business events, or do a paged full crawl on a schedule the environment can absorb.

Rate limits and pagination

Business Central publishes precise numbers. These are from Microsoft's operational limits page, last revised 2025-04-08.

Per environment, OData v4:

Per user (rolled out from late December 2023), OData v4: max concurrent requests 5, max connections 100, max request queue size 95, and a rate of 6,000 requests in a 5-minute sliding window, then HTTP 429. SOAP has the same per-user rate and Microsoft notes SOAP will be deprecated.

A queued request that waits more than 8 minutes returns HTTP 503. Microsoft's explicit advice is that throughput scales with the number of users or service principals you spread the load across, and they recommend round-robin rotation through several service principals for a heavy integration. That is an unusual design instruction but it is the documented one.

Finance and Supply Chain OData supports $filter, $count, $orderby, $skip, $top and $expand (first level only) and $select. Server-driven paging has a maximum page size of 10,000. The has and in operators are not supported. contains is implemented as a wildcard, written $filter=StringField eq '*retail*'. Enums are qualified, for example $filter=PersonGender eq Microsoft.Dynamics.DataEntities.Gender'Unknown'. Array fields are not supported at all. The first OData call after an AOS restart is slow because the metadata cache is cold.

Data Management Framework exported packages sit in blob storage for seven days and are then deleted.

Practical cadence. Business Central: webhooks for orders, invoices, credit memos, items and customers, with a nightly reconciliation pass on lastModifiedDateTime to catch anything the 36-hour retry window dropped. Finance and Supply Chain: Inventory Visibility every 5 minutes for stock, business events for order state, and a Data Management Framework export with change tracking every 15 to 30 minutes for order and invoice detail.

Mapping to the unified model

orders

order_items

products

listings

Not mappable. sku from number or ItemNumber, price from unitPrice, stock from inventory or Inventory Visibility, status from blocked, updated_at from lastModifiedDateTime. listing_id, channel_listing_id and url have no source in either product.

inventory

shipments

returns, settlements, customers, locations

Gaps and open questions

  • Business Central has no per-location stock in the v2.0 API and no item journal write. Both need AL work in the customer's environment. This is the decisive question for any commerce connector and should be asked in the first call.
  • Business Central posted sales shipments, and therefore carrier and tracking number, are outside the documented v2.0 resource set. The exact custom API page needed was not verified here.
  • SalesOrderHeadersV2 in Finance and Supply Chain has no filterable modified timestamp. Whether a given customer's environment has change tracking enabled on the relevant data entities is unknown until asked.
  • Finance and Supply Chain entity names vary by version and by customisation. Every name used here beyond SalesOrderHeadersV2, SalesOrderLines, ReleasedProductsV2, CustomersV3 and Warehouses must be checked against the customer's own /data service root.
  • Inventory Visibility is an add-in that must be installed and configured, including a dimension and index configuration that determines what a query can filter on. An environment without it has no supported per-location stock API.
  • The per-user rate limits were rolled out gradually from late December 2023 and Microsoft's page says to check the current figure. The numbers quoted here are as published on 2026-09-21.

Sources