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 type | After version 30 |
|---|---|
| Built-in API v2.0 and automation APIs | Available, the preferred integration surface |
| API pages and API queries in your extensions | Available |
| OData on pages and queries in your own extensions | Available |
| OData unbound actions on codeunits | Available |
| OData on pages from Microsoft apps | Removed |
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 descMetadata 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
| Situation | Target |
|---|---|
| Standard entity that API v2.0 already exposes, such as customers, vendors, items, sales invoices or journals | Built-in API v2.0 |
| You need fields that API v2.0 doesn't expose | Custom API page in your extension |
| You need data joined from several tables, or totals | Custom API query |
| A procedure must run on a record, for example to copy or release a document | Bound action on your API page |
| A client can't be changed before version 30 | Copy 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,EntitySetNameand field names use camelCase and only the characters A-Z, a-z and 0-9.APIVersionfollows the patternvX.Yorbeta. The compiler warns on casing violations and fails on naming violations. - Key. Set
ODataKeyFields = SystemIdand exposeSystemIdasid. 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,ModifyAllowedandDeleteAllowedto switch off what the integration shouldn't do. - Committed data only. The
OnOpenPagetrigger in the example setsReadIsolationtoReadCommitted, 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.
APIVersionaccepts a list, for example'v2.0', 'v1.0', when one page serves more than one version. - Captions.
EntityCaption,EntitySetCaptionand field captions are returned, localized, by theentityDefinitionsendpoint. The Business Central MCP server also usesAboutText, thenEntitySetCaption, thenEntityNameto 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})/customerCreditsTest 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
- Confirm the new endpoints appear on the API Overview page in the sandbox and in production after deployment.
- Compare row counts and totals between the old page endpoint and the new API for the same filters.
- Check telemetry: calls from the migrated client should now log
categoryAPIwith your extension inextensionName. - Unpublish each old page service on the Web Services page once its callers have moved, then watch for
404errors in telemetry that reveal callers you missed. - 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,
SystemIdkeys,DelayedInsertand 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
- Deprecated features in the client, server, database
- API page type
- API query type
- Developing a custom API
- Creating and interacting with an OData V4 bound action
- API endpoint structure
- Publish a web service
- Troubleshooting errors in OData/SOAP web services on pages
- Web service request trace (telemetry)
- Troubleshooting REST API/OData calls
- Working with webhooks
- Operational limits in Business Central online