AL DevelopmentIntegrations

How to Move SOAP Integrations to APIs in Business Central

Find the integrations that still call Business Central over SOAP, choose the right replacement (standard API, custom API page, or OData unbound action), and rewrite the calls before version 29 breaks them.

11 min read

Version 29.0 (2026 release wave 2) makes Disable SOAP web services on Microsoft UI pages mandatory. Any integration that reads or writes a Microsoft page, such as the Customer Card or Sales Order, through a SOAP endpoint stops working when the environment is updated. SOAP codeunit services still run, but SOAP as a whole is deprecated, so they are next.

This guide walks through finding the SOAP traffic in your environment, picking a replacement for each endpoint, and rewriting the calls. The update window for version 29 closes in February 2027, so plan the work before Microsoft picks the date for you.


Prerequisites

  1. Access to the Business Central environment as an administrator, so you can open the Web Services page.
  2. Application Insights connected to the environment, if you want to find SOAP calls from telemetry. Without it, you rely on the Web Services page and on asking the teams that own each integration.
  3. A sandbox environment copied from production, for testing the new endpoints.
  4. Visual Studio Code with the AL Language extension, if you need a custom API page.
  5. A Microsoft Entra app registration for OAuth 2.0. If an integration still uses a web service access key, fix the authentication at the same time.

Steps

1. Find SOAP calls in telemetry

Two telemetry events help here. RT0053 (Deprecated endpoint called) has been logged since version 26 whenever something calls a Microsoft UI page over SOAP. Its deprecationMessage dimension reads SOAP webservice on UI page by Microsoft publisher. This is the list of endpoints that break in version 29.

// Deprecated SOAP calls to Microsoft UI pages
traces
| where timestamp > ago(60d)
| where customDimensions has "RT0053"
| where customDimensions.eventId == "RT0053"
| project timestamp
, environmentName = customDimensions.environmentName
, category = customDimensions.category
, endpoint = customDimensions.endpoint
, alObjectId = customDimensions.alObjectId
, alObjectName = customDimensions.alObjectName
| summarize calls = count(), lastCall = max(timestamp) by tostring(environmentName), tostring(endpoint), tostring(alObjectName)
| order by calls desc

For the wider picture, query RT0008 (Web Services Call) and filter on category == "SOAP". That shows every SOAP call, including codeunits, which you need to move later.

// All SOAP traffic, pages and codeunits
traces
| where timestamp > ago(60d)
| where customDimensions has "RT0008"
| where customDimensions.eventId == "RT0008"
| where customDimensions.category == "SOAP"
| summarize calls = count() by endpoint = tostring(customDimensions.endpoint), objectType = tostring(customDimensions.alObjectType), objectName = tostring(customDimensions.alObjectName)
| order by calls desc

SOAP calls don’t log HTTP headers, so you can’t see the caller’s user agent. Use the endpoint name and the time pattern of the calls to work out which system is calling.

2. Check the Web Services page

Telemetry only shows endpoints that were called during the retention period. A month-end or year-end integration can be missing from 60 days of data.

  1. Search for Web Services using Alt + Q.
  2. Filter Object Type to Page and look for rows where the SOAP URL column has a value.
  3. Check the Object ID against the base application. Pages in the 1 to 49,999 range and most pages without a custom prefix belong to Microsoft.
  4. Note the Service Name for each row. External systems use it in their endpoint URL, so it is the term to search for in middleware configuration and source code.

Build a simple list as you go:

Service Name Object Caller Replacement
Customer Page 21 Customer Card Webshop sync Standard API customers
SalesOrder Page 42 Sales Order EDI middleware Standard API salesOrders
ItemAvailability Page 5410 (Microsoft) Warehouse app Custom API page
PostShipment Codeunit 50110 (custom) Logistics portal OData unbound action

3. Pick a replacement for each endpoint

There are three realistic targets. Choose based on what the endpoint does.

Current endpoint Replace with Why
Microsoft page for master data or documents Standard API (v2.0) Stable, versioned, maintained by Microsoft, and covers customers, vendors, items, sales and purchase documents, journals, and more
Microsoft page with fields the standard API lacks Custom API page in a per-tenant extension You control the fields and the contract, and Microsoft UI changes don’t affect it
Custom codeunit called over SOAP OData v4 unbound action on the same codeunit No code change in AL, only a new URL and request format on the caller side
Microsoft page exposed over OData v4 Standard API or custom API page It still works, but UI pages change between releases, so the integration can break without warning

Don’t move a SOAP page endpoint to the OData v4 URL of the same page as a permanent fix. It gets you past version 29, but Microsoft’s guidance is the same for both protocols: UI pages are not a stable integration surface.

4. Rewrite page calls against the standard API

The standard API lives under a different path than web services. Compare the URLs:

SOAP (stops working for Microsoft pages in v29)
https://api.businesscentral.dynamics.com/v2.0/{tenantId}/{environment}/WS/{companyName}/Page/Customer

Standard API v2.0
https://api.businesscentral.dynamics.com/v2.0/{tenantId}/{environment}/api/v2.0/companies({companyId})/customers

The API identifies companies by ID, not by name. Get the ID once:

GET https://api.businesscentral.dynamics.com/v2.0/{tenantId}/{environment}/api/v2.0/companies
Authorization: Bearer {token}

Then read and write records with standard REST verbs. A filtered read:

GET .../api/v2.0/companies({companyId})/customers?$filter=number eq '10000'&$select=number,displayName,balanceDue
Authorization: Bearer {token}

An update needs the record’s @odata.etag value in the If-Match header:

PATCH .../api/v2.0/companies({companyId})/customers({customerId})
Authorization: Bearer {token}
Content-Type: application/json
If-Match: W/"JzE5OzEyMzQ1Njc4OTAxMjM0NTY3ODkwMTsn"

{
  "phoneNumber": "+47 22 00 00 00"
}

Field names differ from the page. The API uses camelCase names like displayName and balanceDue instead of captions like Name and Balance (LCY). Map each field the integration uses before you rewrite the client.

5. Build a custom API page when the standard API falls short

If the integration needs fields that the standard API doesn’t expose, create an API page in a per-tenant extension.

page 50100 "Customer Credit API"
{
    PageType = API;
    APIPublisher = 'contoso';
    APIGroup = 'finance';
    APIVersion = 'v1.0';
    EntityName = 'customerCredit';
    EntitySetName = 'customerCredits';
    SourceTable = Customer;
    ODataKeyFields = SystemId;
    Editable = false;

    layout
    {
        area(Content)
        {
            repeater(Records)
            {
                field(id; Rec.SystemId) { }
                field(number; Rec."No.") { }
                field(displayName; Rec.Name) { }
                field(creditLimitLcy; Rec."Credit Limit (LCY)") { }
                field(balanceLcy; Rec."Balance (LCY)") { }
                field(blocked; Rec.Blocked) { }
            }
        }
    }
}

After you publish the extension, the endpoint is available at:

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

Custom API pages don’t need a row on the Web Services page. Treat the APIVersion as a contract: when you need a breaking change, add a v2.0 page next to the old one instead of changing v1.0 in place.

6. Move SOAP codeunits to OData unbound actions

A codeunit that is published as a web service is also reachable as an OData v4 unbound action. The AL code stays the same. Only the caller changes.

codeunit 50110 "Shipment Service"
{
    procedure PostShipment(orderNo: Code[20]; shipDate: Date): Text
    var
        SalesHeader: Record "Sales Header";
    begin
        SalesHeader.Get(SalesHeader."Document Type"::Order, orderNo);
        SalesHeader.Validate("Posting Date", shipDate);
        SalesHeader.Ship := true;
        SalesHeader.Invoice := false;
        Codeunit.Run(Codeunit::"Sales-Post", SalesHeader);
        exit('Shipped');
    end;
}

With the codeunit published on the Web Services page under the service name ShipmentService, the caller sends a POST to {ServiceName}_{ProcedureName}:

POST https://api.businesscentral.dynamics.com/v2.0/{tenantId}/{environment}/ODataV4/ShipmentService_PostShipment?company={companyId}
Authorization: Bearer {token}
Content-Type: application/json

{
  "orderNo": "101005",
  "shipDate": "2026-10-12"
}

The response wraps the return value:

{
  "@odata.context": "https://api.businesscentral.dynamics.com/v2.0/.../ODataV4/$metadata#Edm.String",
  "value": "Shipped"
}

Parameter names in the JSON body must match the AL parameter names. Keep them short and without spaces.

7. Test in a sandbox and switch over

  1. Create a sandbox from a copy of production in the Business Central admin center.
  2. Update the sandbox to version 29 if it isn’t already on it.
  3. Point a test instance of each integration at the sandbox and run its normal cycle, including any month-end jobs.
  4. Check the RT0008 telemetry for the sandbox. The calls should now show category as API or ODataV4, not SOAP.
  5. Switch production integrations to the new endpoints.
  6. On the Web Services page in production, clear Published on the old SOAP rows once nothing calls them.

Common Mistakes

  • Moving to the OData URL of the same Microsoft page and stopping there. It works today, but the next UI change on that page can break the integration.
  • Keeping company names in URLs. The API uses company IDs. Hard-coded names with special characters such as æ, ø, or & also cause encoding problems.
  • Forgetting the ETag on updates. PATCH and DELETE requests without If-Match fail. Read the record first, or use If-Match: * only when you accept overwriting concurrent changes.
  • Leaving basic authentication in place. Web service access keys are a legacy option. Move to OAuth 2.0 while the integration is open for changes anyway.
  • Only checking two weeks of telemetry. Quarterly and yearly jobs hide. Use the longest retention you have and confirm with the integration owners.

Next Steps

Once the SOAP endpoints are gone, set up telemetry alerts on RT0008 with category == "SOAP" so that new SOAP integrations get noticed early. Then plan the move for the remaining SOAP codeunits, since those are next in line for removal.


For background on the standard API endpoints, authentication, and filtering, see How to Use APIs in Business Central.