Architecture

Replace Business Central OData Page Endpoints with Custom API Pages

From version 30, Microsoft pages can't be exposed as OData endpoints. Find the affected integrations and rebuild them as AL API pages and API queries in your own extension.

11 min read
On this page

Starting with Business Central version 30 (2027 release wave 1), you can't expose a Microsoft page, meaning any page from an app published by Microsoft, as an OData endpoint, so integrations that read or write through page-based OData URLs need a new endpoint. The replacement is an AL API page (PageType = API) for create, read, update and delete operations, or an API query (QueryType = API) for read-only joins and totals, both shipped in your own extension and keyed on SystemId. Where the built-in API v2.0 already covers the data, use it and skip the custom code entirely.

Who this is for and what you will have at the end

This guide is for AL developers and integration owners who publish standard pages such as Customer List, Item Card or General Ledger Entries on the Web Services page and consume them through OData V4 from middleware, Power Query, Power BI or custom code.

At the end you will have:

  • A list of OData endpoints that run on Microsoft pages, taken from telemetry.
  • A decision for each one: standard API, custom API page, custom API query or temporary page copy.
  • A working custom API page and API query in an extension, following Microsoft's naming and key conventions.
  • Tested replacement URLs and a plan to unpublish the old page services.

What changes in version 30

The deprecated features list states that from version 30 it's no longer possible to expose a Microsoft page as an OData endpoint. A Microsoft page is a page created in an app with publisher Microsoft, so the change covers Base Application, System Application and every first-party app. The warning was first published for 2025 release wave 1 (version 26), alongside the removal of SOAP on the same pages, which happens a release earlier in version 29.

What stays available:

Endpoint typeAfter version 30
Built-in API v2.0 and automation APIsAvailable, the preferred integration surface
API pages and API queries in your extensionsAvailable
OData on pages and queries in your own extensionsAvailable
OData unbound actions on codeunitsAvailable
OData on pages from Microsoft appsRemoved

Microsoft's stated reason is that a UI page isn't an API. Page fields, parts and layout can change in any release without being treated as a breaking change, so integrations built on them break during normal updates. Microsoft's telemetry guidance adds that SOAP and OData requests to UI pages spend computation resources on UI elements that aren't relevant to the integration, while API pages are optimized for this scenario.

If you also have SOAP integrations, handle them first; see migrating Business Central SOAP page web services to APIs.

Prerequisites

  • Visual Studio Code with the AL Language extension, and a sandbox environment for testing.
  • Telemetry sent to Azure Application Insights, so you can see which OData endpoints are called.
  • An OAuth client for testing. Business Central online accepts only OAuth for web services; the service-to-service setup guide shows how to create one.
  • An object ID range in your extension for the new API pages and queries.

Step 1: Find OData endpoints on Microsoft pages

Every incoming web service call is logged as event RT0008 with a category of API, ODataV4, ODataV3 or SOAP, and the extensionPublisher dimension names the publisher of the app that owns the object behind the endpoint. This query lists OData V4 endpoints on objects published by Microsoft:

traces
| where timestamp > ago(30d)
| where customDimensions has "RT0008"
| where customDimensions.eventId == "RT0008"
| where customDimensions.category == "ODataV4"
| extend endpoint = tostring(customDimensions.endpoint),
         alObjectType = tostring(customDimensions.alObjectType),
         alObjectId = tostring(customDimensions.alObjectId),
         alObjectName = tostring(customDimensions.alObjectName),
         extensionName = tostring(customDimensions.extensionName),
         extensionPublisher = tostring(customDimensions.extensionPublisher)
| where extensionPublisher == "Microsoft"
| summarize calls = count(), lastCall = max(timestamp)
    by endpoint, alObjectType, alObjectId, alObjectName, extensionName
| order by calls desc

Metadata requests and calls to company endpoints don't carry extension data, so they drop out of this filter, which is what you want. To tell callers apart, look at the httpHeaders dimension: if the client sets a User-Agent or client-request-id header, Business Central writes it to telemetry. Any client that reads a page endpoint shows up here, including Power Query in Excel or Power BI, alongside middleware.

Compare the result with the Web Services page, where each page service shows its object ID and service name.

Step 2: Decide where each endpoint goes

SituationTarget
Standard entity that API v2.0 already exposes, such as customers, vendors, items, sales invoices or journalsBuilt-in API v2.0
You need fields that API v2.0 doesn't exposeCustom API page in your extension
You need data joined from several tables, or totalsCustom API query
A procedure must run on a record, for example to copy or release a documentBound action on your API page
A client can't be changed before version 30Copy the page into your extension and publish the copy as OData

Microsoft's guidance for the last case is explicit: copy the page source and host it in an extension or app. It keeps a page-shaped endpoint working, but you now maintain a copy of a UI page with none of the stability of an API. Treat it as a bridge.

Before writing code, open the API Overview page in Business Central. It lists every published API page, API query and API codeunit in the environment, including those added by installed apps, so you can confirm an endpoint doesn't already exist.

Step 3: Build a custom API page

API pages produce versioned, OData V4 REST endpoints that support webhooks. They can't be shown in the UI, and they can't be extended with a page extension, so if a standard API is missing a field you must create a new API page.

The following page exposes customer credit data that a credit-management system needs:

page 50140 "Customer Credit API"
{
    PageType = API;
    APIPublisher = 'contoso';
    APIGroup = 'finance';
    APIVersion = 'v1.0';
    EntityCaption = 'Customer Credit';
    EntitySetCaption = 'Customer Credits';
    EntityName = 'customerCredit';
    EntitySetName = 'customerCredits';
    SourceTable = Customer;
    ODataKeyFields = SystemId;
    DelayedInsert = true;
    Extensible = false;
    InsertAllowed = false;
    DeleteAllowed = false;
 
    layout
    {
        area(Content)
        {
            repeater(Group)
            {
                field(id; Rec.SystemId)
                {
                    Caption = 'Id';
                    Editable = false;
                }
                field(number; Rec."No.")
                {
                    Caption = 'Number';
                    Editable = false;
                }
                field(name; Rec.Name)
                {
                    Caption = 'Name';
                    Editable = false;
                }
                field(creditLimit; Rec."Credit Limit (LCY)")
                {
                    Caption = 'Credit Limit (LCY)';
                }
                field(blocked; Rec.Blocked)
                {
                    Caption = 'Blocked';
                }
            }
        }
    }
 
    trigger OnOpenPage()
    begin
        Rec.ReadIsolation := IsolationLevel::ReadCommitted;
    end;
}

Rules that matter

  • Naming. APIPublisher, APIGroup, EntityName, EntitySetName and field names use camelCase and only the characters A-Z, a-z and 0-9. APIVersion follows the pattern vX.Y or beta. The compiler warns on casing violations and fails on naming violations.
  • Key. Set ODataKeyFields = SystemId and expose SystemId as id. Microsoft recommends a single GUID key because some integrations, such as Power Automate and Power Apps, don't work otherwise, and webhooks aren't supported for composite keys.
  • Permitted operations. API pages support create, read, update and delete by default. Use InsertAllowed, ModifyAllowed and DeleteAllowed to switch off what the integration shouldn't do.
  • Committed data only. The OnOpenPage trigger in the example sets ReadIsolation to ReadCommitted, the documented way to make an API expose only committed data.
  • Enums over options. Option fields appear in metadata as Edm.String; enum fields get their own type listing every member, which clients can validate against.
  • Avoid ODataEDMType. Complex types built with this property are deprecated and slow, because they're calculated at runtime. Use first-level fields or navigation properties instead.
  • Several versions. APIVersion accepts a list, for example 'v2.0', 'v1.0', when one page serves more than one version.
  • Captions. EntityCaption, EntitySetCaption and field captions are returned, localized, by the entityDefinitions endpoint. The Business Central MCP server also uses AboutText, then EntitySetCaption, then EntityName to describe the API as a tool for AI agents, so meaningful captions pay off twice.

Parent and child entities

For header-and-line data, add an API page part and link it on SystemId. The platform then generates a navigation property with a referential constraint, which lets clients use $expand and deep inserts. The following part points at a hypothetical child API page whose source table has a Customer Id GUID field:

part(creditNotes; "Customer Credit Note API")
{
    Caption = 'Credit Notes';
    EntityName = 'creditNote';
    EntitySetName = 'creditNotes';
    SubPageLink = "Customer Id" = field(SystemId);
}

Parts are one-to-many by default. Add Multiplicity = ZeroOrOne; for a one-to-one relationship.

Step 4: Build an API query for joins and totals

If the old OData endpoint was a page with FlowFields or calculated totals, an API query is usually a better fit. It joins data items, can aggregate with Method = Sum, and is read-only. Microsoft's documented example returns sales per customer from the customer ledger:

query 50141 "Customer Sales API"
{
    QueryType = API;
    APIPublisher = 'contoso';
    APIGroup = 'finance';
    APIVersion = 'v1.0';
    EntityName = 'customerSale';
    EntitySetName = 'customerSales';
 
    elements
    {
        dataitem(Customer; Customer)
        {
            column(customerNumber; "No.") { }
            column(name; Name) { }
            dataitem(CustLedgerEntry; "Cust. Ledger Entry")
            {
                DataItemLink = "Customer No." = Customer."No.";
                SqlJoinType = LeftOuterJoin;
                DataItemTableFilter = "Document Type" = filter(Invoice | "Credit Memo");
                column(totalSalesAmount; "Sales (LCY)")
                {
                    Method = Sum;
                }
                filter(dateFilter; "Posting Date") { }
            }
        }
    }
}

API queries aren't webhook-enabled and reject writes, so keep them for reporting and lookups.

Step 5: Add bound actions for operations

Where a SOAP or OData integration used to trigger business logic on a record, add a procedure with the [ServiceEnabled] attribute and a WebServiceActionContext parameter, and set the result with SetObjectType, SetObjectId, AddEntityKey and SetResultCode. Clients call it with a POST to the record URL followed by Microsoft.NAV.{procedureName}, the same pattern the standard API uses for actions such as salesInvoices({id})/Microsoft.NAV.post. Bound actions can't be added by extending an existing page exposed as a web service; declare them on your own page.

Step 6: Call and test the new endpoint

Custom APIs share the standard base URL. Replace v2.0 in the API route with your publisher, group and version. The Microsoft Entra tenant ID segment is optional; including it makes the target tenant explicit:

https://api.businesscentral.dynamics.com/v2.0/{tenantId}/{environmentName}/api/contoso/finance/v1.0/companies({companyId})/customerCredits

Test the main operations against a sandbox:

BASE="https://api.businesscentral.dynamics.com/v2.0/$TENANT_ID/sandbox/api/contoso/finance/v1.0/companies($COMPANY_ID)"
 
curl -H "Authorization: Bearer $TOKEN" "$BASE/customerCredits?\$filter=creditLimit gt 0"
 
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "If-Match: $ETAG" -d '{"creditLimit": 25000}' \
  "$BASE/customerCredits($CUSTOMER_ID)"
 
curl -H "Authorization: Bearer $TOKEN" "$BASE/customerSales"

The $metadata document at the API route root describes every entity and property; generate typed clients from it rather than hand-writing models. Since version 24, custom APIs default to $schemaversion=2.0, the same OData feature level as the standard APIs, so you don't need to pass it.

Verify the cutover

  1. Confirm the new endpoints appear on the API Overview page in the sandbox and in production after deployment.
  2. Compare row counts and totals between the old page endpoint and the new API for the same filters.
  3. Check telemetry: calls from the migrated client should now log category API with your extension in extensionName.
  4. Unpublish each old page service on the Web Services page once its callers have moved, then watch for 404 errors in telemetry that reveal callers you missed.
  5. Re-run the Step 1 query monthly until it returns nothing for Microsoft-published objects.

Troubleshooting

The compiler rejects EntityName or APIGroup. The value contains characters outside A-Z, a-z and 0-9. Remove spaces, hyphens and underscores.

405 Method Not Allowed on POST to the query endpoint. API queries are read-only. Use an API page for writes.

BadRequest_MethodNotImplemented. The entity doesn't support bound actions, or an $orderby on a query didn't match the default sort fields of the underlying query object.

404 Not Found on the custom route. Check publisher, group and version in the URL against the AL properties, and that the extension is installed in the environment you're calling.

413 Request Entity Too Large. The request would return more than 20,000 entities, the OData page-size limit in Business Central online. Filter the request or follow the server-driven paging links.

Webhook subscriptions to the custom API fail. The page uses a temporary source table, a system table, the Job Queue Entry table or a composite key, or the endpoint is an API query. Fix the key and source, then subscribe as described in Business Central API webhooks.

Checklist

  • ODataV4 calls on Microsoft-published objects inventoried from RT0008 telemetry.
  • Standard API v2.0 used wherever it covers the data.
  • Custom API pages use camelCase names, SystemId keys, DelayedInsert and only the operations the client needs.
  • Joins and totals served by API queries, operations by bound actions.
  • New endpoints tested in a sandbox and visible on the API Overview page.
  • Old page services unpublished, with telemetry watched for stragglers.
  • All work finished before the environment updates to version 30.

References

Questions people ask

When does Business Central stop exposing Microsoft pages as OData endpoints?

In version 30, 2027 release wave 1. From then on, a page from any app with publisher Microsoft, such as Base Application or System Application, can't be exposed as an OData endpoint. Pages, queries and codeunits in your own extensions are not part of this removal.

Can I extend a standard Business Central API page with extra fields?

No. API pages and API queries can't be extended with page extensions. Microsoft's guidance is to copy the AL code of the standard API into your own extension and create a custom API based on it.

What is the difference between an API page and an API query?

An API page supports create, read, update and delete operations and can raise webhook notifications. An API query joins data from several tables and can aggregate it, but it's read-only and isn't webhook-enabled.

Why should ODataKeyFields be set to SystemId?

SystemId is an immutable GUID on every record. Microsoft recommends a single GUID key because some integrations, including Power Automate and Power Apps, don't work with other keys, and webhooks aren't supported for API pages with composite keys.

Business CentralALOData v4API Pages
  1. Business Central API Webhooks: Create, Validate and Renew Subscriptions

    Receive Business Central change notifications instead of polling: build a validating receiver in Azure Functions, create a subscription, process notifications and renew before the three-day expiry.

    Architecture10 min read
  2. Business Central SOAP Retirement in v29: Migrate Page Web Services to APIs

    Version 29 removes SOAP endpoints on Microsoft pages in Business Central. Find the integrations that still call them with telemetry and move each one to API v2.0, a custom API or OData.

    Architecture12 min read
  3. Connect Business Central to Outlook, Teams and SharePoint in Microsoft 365

    Set up Business Central email, the Outlook add-in, the Teams app, read-only access with Microsoft 365 licenses, OneDrive, and SharePoint storage for document attachments.

    Architecture13 min read