Business Central

The End of OData in Business Central: How to Rebuild Your ERP Integration Architecture Before It Breaks

Microsoft is deprecating OData endpoints in Business Central v28 (2026 Wave 1). If your ERP integrations still rely on page-based OData or SOAP, they will fail. This is a complete technical guide to migrating to Custom APIs, webhooks, and event-driven architecture — before the deadline.

VTVoyager IT TeamJune 22, 202612 min read

There is a breaking change coming to every Business Central integration that most teams are not prepared for. Microsoft began issuing warnings in BC v26 (2025 Wave 1) when you attempt to expose UI pages as OData endpoints. In v28 (2026 Wave 1), those warnings turn into hard blocks. SOAP endpoints for Microsoft-managed pages are already disabled by default. The clock is running.

If your integrations were built on page-based OData — the approach most partners used for the past decade — they will break without migration. This article is a complete technical walkthrough of what is being removed, what replaces it, and how to architect a modern, resilient integration layer for Business Central in 2026.

This is not a warning about a distant future. BC v28 shipped in April 2026. If you are running cloud tenants, you are already on it. The deprecation warnings in your admin centre are not advisory — they are a countdown.

1. What Is Being Deprecated and Why

Business Central has historically exposed two integration surfaces that are now being retired: page-based OData (exposing UI page objects directly as REST endpoints) and SOAP web services (the legacy XML-based protocol inherited from Dynamics NAV). Both were designed for a pre-cloud era where tight coupling between UI and data was acceptable. In a multi-tenant SaaS architecture, they create serious problems.

  • Page-based OData exposes UI logic to external systems — field visibility rules, FlowFields, and page triggers all execute on every API call, making them slow and fragile.
  • SOAP endpoints are synchronous, stateful, and impose heavy XML parsing overhead that scales poorly under concurrent load.
  • Neither surface has proper versioning semantics — a BC upgrade that changes a page breaks integrations silently.
  • Page endpoints expose more data than intended, creating unintended compliance surface area.

The replacement is a proper, purpose-built API tier: the Standard API v2.0 for common entities and Custom AL APIs for anything beyond that. These are decoupled from UI, versioned explicitly, and designed for machine-to-machine communication.

2. The Three Integration Surfaces in Modern Business Central

Before migrating, it is essential to understand the three surfaces now available and when to use each one.

Standard API v2.0

Microsoft maintains a set of standardised endpoints covering the most common business entities: customers, vendors, items, sales orders, purchase orders, journal entries, and more. These endpoints are stable across upgrades, versioned at the URL level (/api/v2.0/), and tested at scale. For any integration touching standard BC data, this is the correct starting point.

The base URL pattern is: https://{hostname}/api/v2.0/companies({companyId})/{entity}. All responses are JSON. Authentication is OAuth 2.0 via Microsoft Entra ID — client credentials flow for service-to-service, authorisation code flow for user-delegated access.

Custom AL APIs

When you need to expose data that does not exist in the Standard API — custom tables, computed values, or complex business processes — you write an APIPage object in AL. This is the direct replacement for page-based OData. Unlike OData pages, APIPage objects are explicitly designed for API consumption: no UI triggers, no FlowField recalculation overhead, explicit field selection, and version-scoped routing.

An AL API object declares its route using the APIPublisher, APIGroup, APIVersion, and EntityName properties. Once deployed via an AppSource extension or a per-tenant extension, it is available under /api/{publisher}/{group}/{version}/. This gives you full control over versioning — breaking changes go in a new version, old clients keep working.

AL — Custom APIPage (per-tenant extension)
page 50100 "Voyager Item API"
{
    PageType = API;
    APIPublisher = 'voyagerit';
    APIGroup = 'inventory';
    APIVersion = 'v1.0';
    EntityName = 'item';
    EntitySetName = 'items';
    SourceTable = Item;
    DelayedInsert = true;
    ODataKeyFields = SystemId;

    layout
    {
        area(Content)
        {
            field(id; Rec.SystemId)
            {
                Caption = 'Id';
                Editable = false;
            }
            field(number; Rec."No.")
            {
                Caption = 'Number';
            }
            field(displayName; Rec.Description)
            {
                Caption = 'Display Name';
            }
            field(unitPrice; Rec."Unit Price")
            {
                Caption = 'Unit Price';
            }
            field(inventory; Rec.Inventory)
            {
                Caption = 'Inventory';
            }
            field(lastModifiedAt; Rec.SystemModifiedAt)
            {
                Caption = 'Last Modified';
                Editable = false;
            }
        }
    }
}

Once deployed, this endpoint is reachable at: GET /api/voyagerit/inventory/v1.0/companies({companyId})/items. It returns only the six fields you declared — no UI trigger overhead, no FlowField recalculation unless you explicitly add it, and no UI-layer coupling.

Change Notifications (Webhooks)

The third surface is event-driven. Business Central can push notifications to an external HTTPS endpoint whenever a subscribed entity changes. A client registers a subscription against any Standard or Custom API endpoint, specifying the notification URL, the resource to watch, and an expiry. BC then delivers a payload to that URL within seconds of a record insert, update, or delete.

This is the correct pattern for synchronisation workloads. The alternative — polling an API endpoint every N seconds — consumes rate limit quota unnecessarily and introduces latency. Webhooks eliminate both problems.

JSON — webhook notification payload (BC → your endpoint)
// BC POSTs this to your notificationUrl on every change
{
  "value": [
    {
      "subscriptionId":     "salesOrders-sync",
      "clientState":        "your-shared-validation-secret",
      "expirationDateTime": "2026-06-25T10:00:00Z",
      "resource":           "companies({companyId})/salesOrders({systemId})",
      "changeType":         "updated",
      "systemId":           "5d115c9c-4499-4e34-97c5-d8c82b5f57ef",
      "systemModifiedAt":   "2026-06-22T09:41:12.453Z"
    }
  ]
}

// Note: the payload is a notification only — not the full record.
// Use systemId to fetch the current record state via a follow-up GET.
// Always verify clientState matches your secret before processing.

3. Rate Limits: The Numbers Every Architect Must Know

Business Central enforces strict rate limits at the API layer. Many integrations that worked fine during development collapse in production because teams did not design for these constraints from the start.

  • 5 concurrent API requests per service-to-service credential at any given moment.
  • 100 queued connections maximum — requests beyond this receive HTTP 429 immediately.
  • 6,000 requests per 5-minute rolling window per credential — sustained throughput of 20 req/s.
  • OData $batch reduces the request count for write operations — up to 100 operations in one HTTP call.
  • $expand reduces request count for reads — fetch related entities in a single call instead of N+1 calls.
  • Data-Access-Intent: ReadOnly header routes read requests to a replica, freeing primary capacity for writes.

The implication for bulk migration jobs is significant. A naive loop that POSTs one record per request will hit the 6,000/5-min ceiling quickly. The correct pattern is $batch for writes, with exponential backoff and jitter when you receive a 429, using the Retry-After header value from BC as the wait duration.

HTTP — $batch request (up to 100 operations per call)
POST https://{tenant}.bc.dynamics.com/api/v2.0/$batch
Authorization: Bearer {token}
Content-Type: application/json

{
  "requests": [
    {
      "id": "1",
      "method": "POST",
      "url": "companies({companyId})/salesOrders",
      "headers": { "Content-Type": "application/json" },
      "body": {
        "customerNumber": "C00010",
        "orderDate": "2026-06-22"
      }
    },
    {
      "id": "2",
      "method": "POST",
      "url": "companies({companyId})/salesOrders",
      "headers": { "Content-Type": "application/json" },
      "body": {
        "customerNumber": "C00011",
        "orderDate": "2026-06-22"
      }
    }
  ]
}

// Response — one entry per request, independent success/failure
{
  "responses": [
    { "id": "1", "status": 201, "body": { "id": "5d115c9c-...", "number": "SO-10042" } },
    { "id": "2", "status": 201, "body": { "id": "8a223f1d-...", "number": "SO-10043" } }
  ]
}

4. Event-Driven Architecture: The Right Pattern for ERP Integration

Polling is the enemy of scalable ERP integration. It burns rate limit quota, introduces latency proportional to poll interval, and creates a thundering herd problem when multiple integrations poll simultaneously. The correct architecture separates BC from downstream systems using an asynchronous message broker.

The Azure Service Bus Pattern

The recommended integration topology for BC in Azure environments is: BC webhook → Azure Function (inbound adapter) → Azure Service Bus queue → downstream consumer. This gives you three critical capabilities that a direct webhook-to-consumer connection cannot provide: durable delivery (messages survive consumer downtime), backpressure handling (the queue absorbs spikes that would otherwise overwhelm the consumer), and dead-lettering (messages that fail repeatedly are moved to a dead-letter queue for inspection rather than silently dropped).

The Azure Function acts as a thin adapter — it validates the webhook signature, extracts the resource URL from the notification, makes a single BC API call to fetch the full record, and enqueues a structured message. It does no business logic. This keeps it testable, independently deployable, and replaceable.

Idempotency: The Non-Negotiable Requirement

BC webhooks deliver at-least-once. A single record change may trigger multiple notifications under high load or after a BC service restart. Every consumer downstream of your queue must be idempotent — processing the same message twice must produce the same result as processing it once. The standard approach is to include the BC record systemId (a stable GUID) and systemModifiedAt timestamp in every message, and check both at the consumer before applying changes.

5. Handling Failures Properly

The most common failure pattern we see in BC integrations is silent data loss: a webhook notification fires, the consumer throws an uncaught exception, and nobody notices until a user reports a discrepancy three days later. A production-grade integration architecture must make failures visible and recoverable.

  • Dead-letter queues: configure Azure Service Bus to move messages to the dead-letter queue after N delivery attempts. Alert on dead-letter queue depth > 0.
  • Structured logging: log every message with its BC systemId, processing timestamp, and outcome. This makes replay and investigation tractable.
  • Circuit breaker: if BC is returning 503s during a maintenance window, back off entirely rather than pounding the rate limit. Re-enable after a successful probe.
  • Subscription renewal: BC webhook subscriptions expire (maximum 3 days for cloud tenants). Your integration layer must monitor expiry and renew subscriptions proactively — not reactively after they lapse.
  • Replay capability: store the raw BC notification payload before processing. If the consumer has a bug, you can replay from stored events after the fix is deployed.

What We See Break in Production

After migrating a dozen BC integration layers over the past 18 months, the same failures appear repeatedly. Webhook subscriptions expire silently — BC cloud tenants have a 3-day maximum subscription lifetime and the system does not alert you when one lapses; the integration just stops receiving events and nobody notices until a user reports stale data. Token caching is the second recurring gap: teams that request a fresh Entra ID token on every API call eventually hit Microsoft's identity endpoint throttle, which sits entirely outside BC's own rate limits and returns a completely different 429. The third is fan-out conflicts — two downstream systems subscribing to the same BC entity, both receiving the same change notification, and both writing back to BC simultaneously, creating a last-write-wins race on the same record. All three are preventable with proper subscription health monitoring, a shared token cache with pre-emptive refresh, and write-back routing logic that identifies the system of record per entity type.

6. Authentication Architecture for Service-to-Service Integration

All modern BC API access uses OAuth 2.0 via Microsoft Entra ID. For service-to-service integrations (no human in the loop), the correct flow is Client Credentials — register an application in Entra ID, assign it API permissions for Dynamics 365 Business Central, and use a client secret or managed identity certificate to obtain tokens.

Tokens have a 1-hour TTL. A production integration must implement token caching and pre-emptive refresh — request a new token when the current one has less than 5 minutes remaining, not after it has expired. Hitting the Entra ID token endpoint on every API call adds 100–200ms latency and risks throttling on the identity layer, separate from BC rate limits.

Use Managed Identity wherever possible. It eliminates credential rotation entirely — Azure handles key rotation automatically, and there is no client secret to leak, rotate, or expire at 3am before a go-live. If your integration host is an Azure Function, ACI container, or App Service, Managed Identity is available with zero configuration cost.

7. The Migration Path From OData to Custom APIs

If you have existing page-based OData integrations, here is the migration sequence we follow with clients.

  • Audit: run Get-NAVAppInfo and review your OData endpoints in the BC admin centre. Identify every page published as a web service and map it to the consuming system.
  • Match to Standard API v2.0: for each OData endpoint, check whether the Standard API v2.0 already covers the data. Customers, items, sales orders, and vendors are all covered. If v2.0 covers it, migrate directly — no AL work required.
  • Write Custom APIPages for gaps: for anything not in Standard v2.0, write a minimal APIPage in AL. Expose only the fields the integration actually consumes — not the full table. This is also the opportunity to add computed fields that previously required FlowField recalculation in the OData layer.
  • Replace polling with webhooks: for every integration that polls an OData endpoint on a schedule, replace it with a webhook subscription and an async consumer.
  • Validate rate limit headroom: run your new integration in a sandbox under realistic load. Use the BC telemetry in Azure Application Insights to confirm you are operating well inside the 6,000/5-min window.
  • Deprecate the old web service registrations: once consumers are migrated and stable, remove the page web service registrations in BC. This eliminates the deprecation warnings from the admin centre.

8. What This Means for Your ERP Strategy in 2026

The OData deprecation is not just a technical housekeeping exercise. It is a forcing function toward an integration architecture that is more resilient, more observable, and more scalable than what most BC deployments have today. Teams that complete this migration will have a proper event-driven data layer — one that can feed analytics pipelines, trigger workflow automation, and support AI use cases like real-time anomaly detection and demand forecasting.

Teams that do not complete this migration will find their integrations breaking silently as BC upgrades progress. Cloud tenants have no opt-out — Microsoft applies upgrades automatically. On-premises and private cloud deployments on older versions will eventually need to upgrade too, and the longer the migration is deferred, the more technical debt accumulates.

At Voyager IT, we have been migrating clients from legacy OData integrations to Custom API and webhook-based architectures since early 2025. The process is tractable — most integrations can be migrated in two to four weeks with a small team — but it requires deliberate planning, not a last-minute scramble.

If you are unsure which of your Business Central integrations are affected, start with the Web Services page in BC (Settings → Web Services). Every entry with Type = Page and a published OData endpoint is at risk. That list is your migration backlog.

Business CentralERP IntegrationODataCustom APIWebhooksAzure Service BusAPI Architecture