Standard APIs of Business Central have been designed for simplicity and easy consumption. For SaaS providers who integrate with a variety of SaaS ERPs, APIs will look very familiar. The APIs are enabled by default in the SaaS version, and can be enabled for on-premises installations as well.

Every client that can make HTTP calls can consume REST APIs. Using the GET, POST, PATCH, and DELETE verbs of the HTTP protocol, entities can be created, read, updated, and deleted (CRUD).  

Business Central APIs leverage OData (Open Data Protocol), which is a set of best practices for consuming and building RESTful APIs. OData provides flexibility and enables consumers to pass queries using $filter, $expand to fetch child entities in one request, and $select for projection.

Beyond CRUD operations, it's useful to be able to subscribe to state changes in a service. Often, changes to the data/state in one system will cause another system to react to those changes. Business Central now supports two ways of achieving that, which are described in the following sections.

Poll-based change tracking

OData can use delta tokens to track changes, which provides the consumer a way to retrieve a list of entity changes in a given interval. Delta tokens have been supported by the SaaS version of Business Central for a long time.

Delta tokens imply polling behavior, which is good for some scenarios, where the client asks for changes periodically and then reacts to changes.  

Event-driven service integrations

Using events changes the behavior from polling to pushing. Instead of polling for changes, the client must now subscribe to the events that are pushed to it. This means that a client subscribing to a webhook notification will be notified by Business Central if an entity changes its state.

Let's walk through setting up a client/subscriber that listens to webhook notifications from Business Central. The client will be implemented using Azure Functions. In the Azure portal, create an Azure Function, choose the HTTP Trigger template, and paste the following code:

#r "Newtonsoft.Json"
using System.Collections.Concurrent;
using System.Net;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
private static string startLine = "==========================================\n";
private static string endLine = "\n==========================================";
private static string delimiter = "\n------------------------------------------\n";
private static int maxCount = 30;
private static ConcurrentDictionary<string,List<string>> cache = new ConcurrentDictionary<string,List<string>>();

public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
    if (req.Method != "GET" && req.Method != "POST")
        return new BadRequestObjectResult("Unexpected " + req.Method + " request");
    string validationToken = req.Query["validationToken"];
    if (validationToken != null)
        dynamic data = JsonConvert.SerializeObject(validationToken);
        return new ContentResult { Content = data, ContentType = "application/json; charset=utf-8", StatusCode = 200 };

    List<string> history;

    string testerId = req.Query["testerId"];
    if (testerId == null)
        testerId = string.Empty;

    bool found = cache.TryGetValue(testerId, out history);
    if (!found || history == null)
        history = new List<string>();

    if (req.Method == "GET")
        string responseBody = startLine + string.Join(delimiter, history.ToArray()) + endLine;
        return new OkObjectResult(responseBody);

    string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    string status = req.Query["status"];
    int statusCode;
    if (string.IsNullOrEmpty(status) || !Int32.TryParse(status, out statusCode))
        statusCode = 200;

    if (!found)

    string historyItem = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff zzz") + ", status=" + statusCode + delimiter + requestBody + '\n';
    history.Insert(0, historyItem);
    var count = history.Count;
    if (count >= maxCount)
        history.RemoveRange(maxCount, count - maxCount);

    return new StatusCodeResult(statusCode);


After you save the function, use “Get Function URL” to retrieve the URL. In the next step, this URL will be used to creating the subscription.

The subscriber must register a subscription with Business Central that specifies which resource to receive notifications for, and a notificationUrl (URL to the Azure Function) that specifies the endpoint that Business Central will call when changes occur to the resource - webhook is triggered.

When subscribing, a handshake is performed between Business Central and the subscriber. The subscriber must return the validation token sent by Business Central in the initial subscription request. The handshake is performed in the Azure Function by taking the validationToken from the request and returning it in the body within a short time limit. The following POST request registers the webhook subscription if the handshake is successful.

Content-type: application/json
Authorization: basic or bearer token
  "notificationUrl": "https://{notificationUrl}",
  "resource": "/api/beta/companies({ID})/customers"

When the handshake is complete, Business Central will send notifications to the notificationUrl. The notificationURL for the Azure Function looks like this:

The testerId parameter is just for convenience. It enables more subscribers/testers/clients on the same Azure Function.

Triggering the webhook

You’re all set. Now go modify, delete, or create one or more customers in the Business Central web client or through the APIs. Changes to the entities will be tracked, and a message (event) containing the changes will be sent to the subscriber.

The following example is a message from two subscriptions for which the same notificationUrl is specified. In addition to updated and created, changeType can be deleted or collection. If changeType is collection, a lot of entities have been changed. To handle that, the resource URI will contain an OData filter, enabling the client to fetch changes. 

  "value": [
      "subscriptionId": "webhookItemsId",
      "clientState": "someClientState",
      "expirationDateTime": "2018-10-29T07:52:31Z",
      "resource": "api/beta/companies(b18aed47-c385-49d2-b954-dbdf8ad71780)/items(26814998-936a-401c-81c1-0e848a64971d)",
      "changeType": "updated",
      "lastModifiedDateTime": "2018-10-26T12:54:20.467Z"
      "subscriptionId": "webhookCustomersId",
      "clientState": "someClientState",
      "expirationDateTime": "2018-10-29T12:50:30Z",
      "resource": "api/beta/companies(b18aed47-c385-49d2-b954-dbdf8ad71780)/customers(130bbd17-dbb9-4790-9b12-2b0e9c9d22c3)",
      "changeType": "created",
      "lastModifiedDateTime": "2018-10-26T12:54:26.057Z"

The payload never contains data, just the URL to the resource that is changed. The subscriber must call Business Central to retrieve the resource.

Remember to stay in touch

A subscription will expire after three days. To maintain the subscription and keep receiving notifications, the subscription must be renewed. This is done by issuing a PATCH request on the subscription. PATCH also requires a handshake.

Things to explore

Webhooks are made at the platform level, and V2 extensions can expose webhooks as well. Pages of PageType = API will expose webhooks with a few limitations. For APIs to have webhooks support, APIs cannot be leveraging the following:

  • Be exposed as Query objects of the type API.
  • Pages cannot have composite keys (multi value keys).
  • Pages cannot use temporary or system tables as a source.

These limitations also apply to standard APIs.


Webhooks are now available in Business Central to give a client the opportunity to be notified of changes to entities in Business Central. For more details, see the documentation on webhooks.

Get a list of all posts in the Holiday count down series here: