Updating Currency Exchange Rates in Business Central Using an API

Guidelines for Partners

What is an API page 

An API page is a special type of page in Business Central that has no visual interface. You cannot open it from a menu, and it has no buttons. Instead, it is a data endpoint — it speaks OData v4 (a standard REST protocol) and lets other programs read your Business Central data over HTTP 

Think of it like this: 

  • Normal page: a window inside BC that a user opens and interacts with. 
  • API page: a door at the back of BC that other applications knock on to receive data as JSON. 

API pages are perfect when you want Power Automate, Power BI, an external website, or a mobile app to read data from Business Central without a user logging in. 

Common use cases: 

  • Power BI — connect directly to BC data without manual exports 
  • Power Automate / Logic Apps — trigger BC actions from other systems 
  • Excel — live data refresh from BC tables 
  • Mobile apps or web apps — read/write BC data via REST calls 
  • Integration with third-party ERP, CRM, or accounting software 
  • Automated scripts and batch jobs running outside BC 

The Skeleton of an API page 

page 50000 "My First API Page" 

{ 

    PageType = API; 
    APIPublisher = 'mycompany'; 
    APIGroup = 'inventory'; 
    APIVersion = 'v1.0'; 
    EntityName = 'product'; 
    EntitySetName = 'products'; 
    Caption = 'My Product API'; 
    SourceTable = Item; 
    Editable = false; 
    InsertAllowed = false; 
    ModifyAllowed = false; 
    DeleteAllowed = false; 
    DelayedInsert = true; 
    ODataKeyFields = SystemId; 

    layout 
    { 
        area(Content) 
        { 
            repeater(Lines) 
            { 
                field(id; Rec.SystemId) 
                { 
                    Caption = 'Id'; 
                    ApplicationArea = All; 
                } 
                field(no; Rec."No.") 
                { 
                    Caption = 'No.'; 
                    ApplicationArea = All; 
                } 
                field(description; Rec.Description) 
                { 
                    Caption = 'Description'; 
                    ApplicationArea = All; 
                } 
            } 
        } 
    } 
} 

 

Business Central API Properties Explained: 

  • PageType = API This single line makes the page headless. Without it you get a normal list page. With it, the page disappears from all BC menus and becomes reachable only via HTTP. 
  • APIPublisher, APIGroup, APIVersion — your URL address.  
  • APIPublisher: your company name, lowercase, no spaces. Example: contoso 
  • APIGroup: the functional area. Example: sales, inventory, finance. All lowercase. 
  • APIVersion: start with v1.0. Bump to v2.0 only when you make breaking changes. 

These three properties form the URL path of your endpoint: 

https://your-bc-server/api/

{APIPublisher}/{APIGroup}/{APIVersion}/{EntitySetName} 

Example with the values above: 

https://your-bc-server/api/mycompany/inventory/v1.0/products 

Never use the same APIPublisher + APIGroup + APIVersion for two different API pages — they would conflict. 

  • EntitySetName: plural name (collection of items), used in the URL, casesensitive. Example: products 
  • EntityName: singular name (one item), used in metadata, not in the URL.
    Example: product 
  • SourceTable: The Business Central table your API reads from. You can use any standard BC tables (Item, Customer, Vendor…) or your own custom tables. 
  • Editable / InsertAllowed / ModifyAllowed / DeleteAllowed For a safe read-only API set all four to false. Callers can only GET data — they cannot create, change, or delete records. If you do want to allow writes, set the relevant property to true and BC will accept POST, PATCH, and DELETE requests. 
  • DelayedInsert is used for editable API pages. It waits until all fields are filled, then creates the record.  

Set DelayedInsert = true; to insert the record only after all data is provided.  

Not needed if Editable = false. 

  • ODataKeyFields: Tells OData which field(s) uniquely identify one record, used when a caller requests a single entry: 

GET …/products(‘Item0001’)
Usually use SystemID (it never changes). The field must exist on the API page. 

  • Defining Fields — What Data to Expose: The repeater inside the layout section controls which columns appear in the JSON response. Only fields listed here are visible to API callers. 
            repeater (Lines) 
            { 
                field (id; Rec.SystemId) 
                { 
                    Caption = 'Id'; 
                    ApplicationArea = All; 
                } 
             }  

 Adding Business Logic — The ECB Example 

Sometimes you do not just want to read data — you want the API to also run code when it is called. Our ECB example does exactly that: every time the API receives a request it automatically fetches fresh exchange rates from the internet and saves them first. 

Making an HTTP Request (HttpClient) 

Business Central has a built-in HttpClient object. Here is how to call an external URL: 

procedure FetchRates(var TempRate: Record "Currency Exchange Rate" temporary) 
var 
    Client:       HttpClient;           // makes the HTTP call 
    Response:     HttpResponseMessage;  // holds status code + response body 
    ResponseText: Text;                 // the raw XML/JSON text 
begin 
    // Step 1: send the GET request 
    if not Client.Get(ECBApiUrlLbl, Response) then 
        Error(FetchFailedErr, 'Connection error');   // network problem 
 
    // Step 2: check the server returned success (HTTP 200 OK) 
    if not Response.IsSuccessStatusCode() then 
        Error(FetchFailedErr, Format(Response.HttpStatusCode())); 
 
    // Step 3: read the response body as a text string 
    if not Response.Content.ReadAs(ResponseText) then 
        Error(ResponseReadErr); 

    // Step 4: parse the text and fill our temporary buffer record 
    ParseRatesToBuffer(ResponseText, TempRate); 
end; 
Always check both Client.Get() and IsSuccessStatusCode(). A false from Get() means the network failed. A non-2xx status means the server responded but with an error. 

Parsing XML 

The ECB API returns XML. BC has built-in XML types — no libraries needed. The pattern: read the text into an XmlDocument, then loop over all elements looking for the attributes you need: 

local procedure ParseRatesToBuffer(ResponseText: Text; 
                                   var TempRate: Record "Currency Exchange Rate" temporary) 
var 
    XmlDoc:       XmlDocument; 
    RootElement:  XmlElement; 
    NodeList:     XmlNodeList; 
    Node:         XmlNode; 
    Element:      XmlElement; 
    CurrencyAttr: XmlAttribute;  // will hold currency="USD" 
    RateAttr:     XmlAttribute;  // will hold rate="1.0765" 
    ExchangeRate: Decimal; 

begin 
    // Parse the raw text into an XmlDocument object 
    if not XmlDocument.ReadFrom(ResponseText, XmlDoc) then 
        Error(XmlParseErr); 
 
    // '//*' is XPath for "select every element in the document" 
    XmlDoc.GetRoot(RootElement); 
    RootElement.SelectNodes('//*', NodeList); 
 
    // Loop every element — we only care about ones that look like: 
    //   <Cube currency="USD" rate="1.0765"/> 
    foreach Node in NodeList do begin 
        Element := Node.AsXmlElement(); 
        if Element.Attributes().Get('currency', CurrencyAttr) then 
            if Element.Attributes().Get('rate', RateAttr) then begin 
                // Convert the text "1.0765" to a Decimal — format 9 = invariant (dot separator) 
                Evaluate(ExchangeRate, RateAttr.Value(), 9); 
                // Store in the temporary buffer 
                TempRate.Init(); 
                TempRate."Currency Code"        := CopyStr(CurrencyAttr.Value(), 1, 10); 
                TempRate."Exchange Rate Amount" := ExchangeRate; 
                TempRate.Insert(false); 
            end; 
    end; 

end; 

Evaluate (myDecimal, text, 9) the 9 forces dot-decimal parsing regardless of regional settings. Always use this when parsing numbers from external APIs 

The Complete ECB API Page 

Below is the full working page that combines all the pieces above. You can copy, compile, and deploy this directly: 

page 50001 "ECB Curr. Exch. Rate API" 
{ 
    PageType = API;  APIPublisher = 'defaultpublisher'; 
    APIGroup = 'currencyExchange';  APIVersion = 'v1.0'; 
    EntityName = 'currencyExchangeRate'; 
    EntitySetName = 'currencyExchangeRates'; 
    Caption = 'ECB Currency Exchange Rate API'; 
    SourceTable = "Currency Exchange Rate"; 
    Editable = false;  InsertAllowed = false; 
    ModifyAllowed = false;  DeleteAllowed = false; 
    ODataKeyFields = "Currency Code", "Starting Date"; 
 
    layout { area(Content) { repeater(Lines) { 
        field(currencyCode; Rec."Currency Code") 
            { ApplicationArea=All; Caption='Currency Code'; ToolTip='ISO 4217 code, e.g. USD.'; } 
        field(startingDate; Rec."Starting Date") 
            { ApplicationArea=All; Caption='Starting Date'; ToolTip='Date this rate is valid from.'; } 
        field(exchangeRateAmount; Rec."Exchange Rate Amount") 
            { ApplicationArea=All; Caption='Exchange Rate Amount'; ToolTip='Units of this currency equal to 1 EUR.'; } 
        field(relationalExchRateAmount; Rec."Relational Exch. Rate Amount") 
            { ApplicationArea=All; Caption='Relational Exch. Rate Amount'; ToolTip='Base currency amount (always 1).'; } 
    } } } 
 
    trigger OnOpenPage() 
    begin 
        UpdateExchangeRates(); 
    end; 
 
    var 
        ECBApiUrlLbl:    Label 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml', Locked=true; 
        ApplySuccessMsg: Label 'Updated %1 currency exchange rates.'; 
        FetchFailedErr:  Label 'Could not retrieve ECB rates. Status: %1'; 
        ResponseReadErr: Label 'Could not read the ECB response.'; 
        XmlParseErr:     Label 'Could not parse the ECB XML.'; 
 
    procedure UpdateExchangeRates() 
    var TempRate: Record "Currency Exchange Rate" temporary; 
    begin  FetchRates(TempRate);  ApplyRates(TempRate);  end; 
 
    procedure FetchRates(var TempRate: Record "Currency Exchange Rate" temporary) 
    var Client: HttpClient; Response: HttpResponseMessage; ResponseText: Text; 
    begin 
        if not Client.Get(ECBApiUrlLbl, Response) then Error(FetchFailedErr, 'Connection error'); 
        if not Response.IsSuccessStatusCode() then Error(FetchFailedErr, Format(Response.HttpStatusCode())); 
        if not Response.Content.ReadAs(ResponseText) then Error(ResponseReadErr); 
        ParseRatesToBuffer(ResponseText, TempRate); 
    end; 
 
    procedure ApplyRates(var TempRate: Record "Currency Exchange Rate" temporary) 
    var LiveRate: Record "Currency Exchange Rate"; AppliedCount: Integer; 
    begin 
        if not TempRate.FindSet() then exit; 
        repeat 
            LiveRate.SetLoadFields("Currency Code","Starting Date","Exchange Rate Amount","Relational Exch. Rate Amount"); 
            LiveRate.SetRange("Currency Code", TempRate."Currency Code"); 
            LiveRate.SetRange("Starting Date", TempRate."Starting Date"); 
            if LiveRate.FindFirst() then begin 
                LiveRate."Exchange Rate Amount" := TempRate."Exchange Rate Amount"; 
                LiveRate.Modify(false); 
            end else begin 
                LiveRate.Init(); 
                LiveRate."Currency Code"                := TempRate."Currency Code"; 
                LiveRate."Starting Date"                := TempRate."Starting Date"; 
                LiveRate."Exchange Rate Amount"         := TempRate."Exchange Rate Amount"; 
                LiveRate."Relational Exch. Rate Amount" := 1; 
                LiveRate.Insert(false); 
            end; 
            AppliedCount += 1; 
        until TempRate.Next() = 0; 
        Message(ApplySuccessMsg, AppliedCount); 
    end; 
 
    local procedure ParseRatesToBuffer(ResponseText: Text; var TempRate: Record "Currency Exchange Rate" temporary) 
    var XmlDoc: XmlDocument; RootElement: XmlElement; NodeList: XmlNodeList; 
        Node: XmlNode; Element: XmlElement; 
        CurrencyAttr: XmlAttribute; RateAttr: XmlAttribute; TimeAttr: XmlAttribute; 
        RateDate: Date; ExchangeRate: Decimal; 
    begin 
        if not XmlDocument.ReadFrom(ResponseText, XmlDoc) then Error(XmlParseErr); 
        XmlDoc.GetRoot(RootElement); 
        RootElement.SelectNodes('//*', NodeList); 
        foreach Node in NodeList do begin          // find the date first 
            Element := Node.AsXmlElement(); 
            if Element.Attributes().Get('time', TimeAttr) then begin 
                Evaluate(RateDate, TimeAttr.Value(), 9);  break; 
            end; 
        end; 
        if RateDate = 0D then Error(XmlParseErr); 
        TempRate.Reset();  TempRate.DeleteAll(); 
        foreach Node in NodeList do begin          // then find every rate 
            Element := Node.AsXmlElement(); 
            if Element.Attributes().Get('currency', CurrencyAttr) then 
                if Element.Attributes().Get('rate', RateAttr) then begin 
                    Evaluate(ExchangeRate, RateAttr.Value(), 9); 
                    TempRate.Init(); 
                    TempRate."Currency Code"                := CopyStr(CurrencyAttr.Value(), 1, 10); 
                    TempRate."Starting Date"                := RateDate; 
                    TempRate."Exchange Rate Amount"         := ExchangeRate; 
                    TempRate."Relational Exch. Rate Amount" := 1; 
                    TempRate.Insert(false); 
                end; 
        end; 
    end; 

} 

After publishing the extension, any external tool can call your API via HTTP. 

Get All Records 

GET 
https://{tenant}.api.businesscentral.dynamics.com/v2.0/{tenant}/api/defaultpublisher/currencyExchange/v1.0/ currencyExchangeRates 

Response (JSON): 

  "value": [ 
    { "currencyCode": "USD", "startingDate": "2026-05-25", 
      "exchangeRateAmount": 1.0765, "relationalExchRateAmount": 1 }, 
    { "currencyCode": "GBP", "startingDate": "2026-05-25", 
      "exchangeRateAmount": 0.8573, "relationalExchRateAmount": 1 } 
  ] 

Filter with OData $filter — No AL Code Needed 

GET .../currencyExchangeRates?$filter=startingDate ge 2026-01-01 

GET .../currencyExchangeRates?$filter=currencyCode eq 'USD' 

OData $filter, $top, $skip, and $orderby work automatically on every field in your repeater. You do not need to write any filter code in AL 

Common Mistakes to Avoid: 

  • Duplicate APIPublisher + APIGroup + APIVersion: each combination must be unique across all API pages in your app. 
  • Spaces in EntityName / EntitySetName: use camelCase only. ‘salesOrder’ is correct; ‘Sales Order’ will fail. 
  • Hardcoded strings in Error/Message: always use Label variables. AppSource validation will reject hardcoded strings. 
  • Forgetting ODataKeyFields: without this, callers cannot request a single record. 
  • No error handling on HttpClient.Get(): networks fail — always check the return value and handle errors gracefully. 

API page vs List page — comparison 

  API Page (PageType = API)  List Page (PageType = List) 
Who uses it  External systems, scripts, apps  BC users in the web client 
Access method  HTTP GET/POST/PATCH/DELETE  Browser — BC web client 
Output format  JSON (OData v4)  HTML table in BC 
Has visual UI  No  Yes 
Can have action buttons  No — actions are never rendered  Yes — visible in ribbon 
Port  7047 (OData port)  8080 (Web Client port) 
URL pattern  /api/publisher/group/version/entity  /BC/WebClient/?page=XXXXX 

 

Why Use a Business Central API

API pages in Business Central let you share your data with other systems without showing anything on the screen. There’s no UI, no buttons — just a clean endpoint that other tools can call to get data as JSON. 

When you create a page with PageType = API, you basically open a door for apps like Power BI, Power Automate, Excel, or external systems to access your data directly. 

But API pages are not only for reading data. They can also run logic and automate tasks. For example, you can fetch data from another system, process it, and store it in Business Central — all through one API call. 

In simple terms, an API page is not just a way to expose data — it’s a way to connect Business Central with the rest of the world and make it work smarter.