Video Series: Web Services in AL

In this series I’m building a Microsoft Dynamics 365 Business Central extension that talks with the US Postal Service for doing address verification.

Episode 1, create the web service call:

Stay tuned for the next episodes where I continue building this extension.

When the extension is done, the source will be available on github.com


In this first part of a video series, Erik walks through the fundamentals of consuming external web services from Business Central AL code. He uses the US Postal Service Address Validation API as a practical example, building out the core HTTP client infrastructure, a setup table, and all the plumbing needed to make an outbound web service call.

Choosing a Web Service: USPS Address Validation

The web service Erik selected for this exercise is the US Postal Service Address Validation API. It’s a straightforward API where you send an address and receive either a confirmation or the corrected address back. The API follows a simple pattern: you make an HTTP GET request to a URL like https://secure.shippingapis.com/ShippingAPI.dll with a query string containing two parameters: API (the name of the API you’re calling) and XML (the XML payload with the address details).

This pattern is consistent across multiple USPS APIs (address validation, zip code lookup, etc.), which means the core calling mechanism can be reused if the extension is expanded later.

Setting Up the Project Structure

Erik starts with a Visual Studio Code workspace already connected to a Business Central instance. The approach is to create a codeunit that serves as the central management point for all web service communication.

The Setup Table and Page

Before writing the web service call, we need somewhere to store configuration — specifically the base URL and a User ID (which you get by registering with the USPS web services). Erik creates a single-record setup table:

table 56102 "US Postal Service Setup"
{
    Caption = 'US Postal Service Setup';
    DataClassification = ToBeClassified;

    fields
    {
        field(1; "Primary Key"; Code[10])
        {
            Caption = 'Primary Key';
            DataClassification = SystemMetadata;
        }
        field(2; URL; Text[250])
        {
            Caption = 'URL';
            DataClassification = SystemMetadata;
        }
        field(3; "User ID"; Text[50])
        {
            Caption = 'User ID';
            DataClassification = SystemMetadata;
        }
    }
    keys
    {
        key(PK; "Primary Key")
        {
            Clustered = true;
        }
    }
}

A card page is created for this setup table. One important difference Erik highlights between NAV and Business Central is the handling of single-record tables. In NAV, there was an implicit insert when opening a card page for these tables. In Business Central, that behavior is gone, so you need to handle it explicitly:

trigger OnOpenPage()
begin
    if Rec.IsEmpty then
        Rec.Insert();
end;

Without this, users would try to type into the fields and nothing would happen — they’d have to manually click the “+” to insert a record first. It’s a small thing, but important for a good user experience.

Building the Web Service Call

The heart of this video is the CallWebService procedure in the management codeunit. Here’s the full function signature and logic Erik builds out:

codeunit 56101 "US Postal Service Management"
{
    procedure CallWebService(API: Text; XMLIn: XmlDocument): XmlDocument
    var
        Setup: Record "US Postal Service Setup";
        Client: HttpClient;
        Request: HttpRequestMessage;
        Response: HttpResponseMessage;
        Headers: HttpHeaders;
        QueryString: Text;
        XMLText: Text;
        TxtOut: Text;
        XMLOut: XmlDocument;
        TypeHelper: Codeunit "Type Helper";
    begin
        if not Setup.Get() then
            Error('US Postal Service setup is needed. Please enter URL and User ID.');

        // Transform XML document to text and URL-encode it
        XMLIn.WriteTo(XMLText);
        TypeHelper.UrlEncode(XMLText);

        // Build the query string
        QueryString := '?API=' + API + '&XML=' + XMLText;

        // Set up the HTTP request
        Request.Method('GET');
        Request.SetRequestUri(Setup.URL + QueryString);

        // Set headers
        Request.GetHeaders(Headers);
        Headers.Add('User-Agent', 'Dynamics 365 Business Central');

        // Make the call
        if not Client.Send(Request, Response) then
            Error('Cannot contact US Postal Service. Network error.');

        // Check HTTP status code
        if Response.HttpStatusCode() <> 200 then
            Error('Web service call failed. Status code: %1', Response.HttpStatusCode());

        // Read the response content
        Response.Content.ReadAs(TxtOut);

        // Parse the response XML
        if XmlDocument.ReadFrom(TxtOut, XMLOut) then
            exit(XMLOut)
        else
            Error('Expected XML format from US Postal Service. Got this instead: %1', TxtOut);
    end;
}

Key Concepts Explained

The HttpClient Pattern

The central piece of this function is the Client.Send(Request, Response) statement. This is where we send an HTTP request to the web service and receive a response. The pattern involves three core types:

  • HttpClient — The client that handles sending the request
  • HttpRequestMessage — The request we’re building and sending
  • HttpResponseMessage — The response we receive back

URL Encoding with Type Helper

Since the XML payload is passed as part of the URL query string, it must be URL-encoded. Special characters like spaces, angle brackets, and ampersands would break the URL if left as-is. The Type Helper codeunit provides a UrlEncode method that converts these characters into their percent-encoded equivalents (e.g., a space becomes %20).

Erik notes an interesting quirk of this function: if you press F12 to inspect it, you’ll see it takes the value by reference and also returns the text. So it encodes the variable in place and returns the result. Be careful not to accidentally double-encode by doing something like XMLText := TypeHelper.UrlEncode(XMLText) — that would encode it twice.

Setting HTTP Headers

While not strictly required, Erik recommends setting a User-Agent header on your requests. This is the header that identifies who is making the call — normally it would be a browser name like “Chrome” or “Firefox.” Setting it to “Dynamics 365 Business Central” is proper etiquette and helps with debugging on the server side when someone reviews access logs.

New AL Data Types and Return Values

Erik highlights several features in AL that weren’t available in the old NAV C/AL environment:

  • XmlDocument as a return type — In NAV, you couldn’t use complex data types as return values from functions. In AL, you can return XmlDocument, making the API much cleaner.
  • Static methods on typesXmlDocument.ReadFrom() is called directly on the type, not on an instance. This is similar to static methods in C#.
  • Fluent dot notation — You can chain calls like Response.Content.ReadAs(TxtOut) without needing intermediate variables.

Error Handling Strategy

The function includes three layers of error handling:

  1. Network errors — If Client.Send() returns false, there’s no network connection or the server couldn’t be reached at all.
  2. HTTP status codes — If the response status code isn’t 200, the call reached the server but something went wrong (404 Not Found, 403 Forbidden, etc.).
  3. XML parsing — Even with a 200 response, the content might not be valid XML. The ReadFrom function returns a boolean indicating whether the parse succeeded.

Erik also mentions that there are additional checks you could perform (such as checking for a 403 Forbidden status, which would indicate you’re not authorized), but the status code check covers the general case well enough.

Summary

In this first video of the series, Erik built the foundational infrastructure for calling external web services from Business Central AL:

  • A setup table and page to store the USPS API URL and User ID
  • A management codeunit with a generic CallWebService function
  • Proper HTTP request construction including method, URI, query string, and headers
  • URL encoding of the XML payload using the Type Helper codeunit
  • Response handling with multiple layers of error checking
  • XML parsing of the response into an XmlDocument for further processing

The function is designed to be reusable — by accepting the API name and XML document as parameters, it can support any of the USPS shipping APIs, not just address validation. In Part 2, Erik will focus on building the actual XML request payload and parsing the response to make this function fully operational.