Spread the love

So a couple of colleagues and I got the task to create an integration between a webshop and Business Central OnPrem, and we jumped on the opportunity to use webhooks to achieve this, however, this was not the smooth ride that we had hoped for, so in this post, I will share with you what we learned so you do not have to make the same mistakes that we made.

What is a webhook?

A webhook is part of a pattern that goes by the principle “Don’t call us, we will call you” What this means is that instead of an external service calling Business Central to check if there are any changes made to a record, Business Central will call out to an external endpoint every time that a record has been changed, so the idea is that instead of a pull request, Business Central will create push request, however unlike a regular push where we write some code inside Business Central that will send a request to an endpoint, a webhook is based on a subscription, meaning that if we wanted to push changes done on a customer record to different systems, using a regular push pattern we would have to write one HTTP request to each endpoint, while when using webhook we do not need to write any code in Business Central because anyone that subscribes to our webhook will be notified when a record is modified on the endpoint that they have set up when subscribing. Any page of type API can be subscribed to without having to do anything extra.

Setting up Business Central for Webhooks

For Business Central Cloud you do not need to do anything to set up webhooks, however on-prem you will need to enable Odata, API, and job queue and this is where we learned our first lesson because it has always been best practice to create a separate service tier for our web services, so that this service tier can be placed in a DMZ. Therefore we would not enable Odata on the service tier to which our users connect, however, the way that webhooks work in Business Central subscriptions is fired by the user session. They are only fired if the service tier that triggers the events has API enabled, so you will have to enable API on the service tier that the users access, the next issue we faced is much like having to enable API on your service tier, all subscriptions are fired as a background job queue so you will also have to enable the job queue on your main service tier. As I said, this is not a problem in the cloud because here all of this is already set up by default, but it can give you some problems on-prem since it goes against the best practices we have always used about splitting responsibility.

How Webhooks Work in Business Central

Behind the scenes then webhooks are actually quite simple, the first thing that you must do is you must have an API page, once your API page is published anyone can subscribe to that page, this is done by creating a subscribe request against Business Central where you tell Business Central what you wish to subscribe to and with a link to a URL which should be notified when data has been changed on a record that has been subscribed to when subscribing to an API page, it will create a record in the table 2000000095 “API Webhook Subscription” records in this table will always be checked on global Insert, Modify and Delete Triggers, and if a global trigger is called for a record that has a subscription, a record will be added to 2000000096 “API Webhook Notification” and a background Task Scheduler will be run Codeuint 6154 “API Webhook Notification Send” which will notify all active subscribers by sending an ID to their endpoints which the subscriber can then use to call your API page and get the complete record. On a side note then you should know that all subscriptions are only valid for three days, this can, however, be changed on-premise, after a subscription has expired you must renew it.

Code

With all the documentation out of the way, let us look at some code; the first thing we need to do is create a simple API page, now you can of cause use any of the standard pages if you so desire, but I will create a simple API page for Items, which looks as follows.

page 50102 Item
{
    APIGroup = 'apiGroup';
    APIPublisher = 'publisherName';
    APIVersion = 'v1.0';
    ApplicationArea = All;
    Caption = 'item';
    DelayedInsert = true;
    EntityName = 'item';
    EntitySetName = 'items';
    PageType = API;
    SourceTable = Item;
    ODataKeyFields = SystemId;
    DataAccessIntent = ReadOnly;
    Editable = false;

    layout
    {
        area(content)
        {
            repeater(General)
            {
                field(no; Rec."No.")
                {
                    Caption = 'No.';
                }
                field(description; Rec.Description)
                {
                    Caption = 'Description';
                }
                field(systemId; Rec.SystemId)
                {
                    Caption = 'SystemId';
                }

            }
        }
    }
}

You can use this URL to check what subscriptions are created in your environment:

GET: https://api.businesscentral.dynamics.com/v2.0/{{TeantID}}/{{enviromentName}}/api/v1.0/subscriptions

If this returns a blank value that means that you do not have an active subscription.

You can use this URL to get a list of all API’s that we can subscribe to.

GET https://api.businesscentral.dynamics.com/v2.0/{{TeantID}}/{{enviromentName}}/api/microsoft/runtime/beta/companies({{CompanyID}})/webhookSupportedResources

This will give you a log list including standard API’s but the one that we are looking for is this one.

next we have to register a subscription this is done by making a POST request against the same URL:

POST: https://api.businesscentral.dynamics.com/v2.0/{{TeantID}}/{{enviromentName}}/api/v1.0/subscriptions

With a JSON payload:

{

  "notificationUrl": "URL to service to be notified",

  "resource": "URL to your API",

  "clientState": "token to be pased (this is optional)"

}

The notification URL can be an Azure Function, Logic App, PowerAutomate, or any other endpoint that can handle a POST request, for simplicity I have just created a simple Azure Function that can handle the notification my Azure function looks like this:

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace WebHooks
{
    public static class Function1
    {
        [FunctionName("MyItemNotification")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
    string validationToken = req.Query["validationToken"];
    if (!String.IsNullOrWhiteSpace(validationToken))
    {
        log.LogInformation($"ValidationToken: {validationToken}");
        return new OkObjectResult(validationToken);
    }

    string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    dynamic data = JsonConvert.DeserializeObject(requestBody);

    log.LogInformation("New notification:");
    log.LogInformation(requestBody);

    return new OkObjectResult(requestBody);
}
    }
}


What this Azure function does is that it checks if validationToken is parsed and if so it will return it to the caller, and if the validationToken is not passed that means that the Azure Function is being triggered by a webhook, and in my example, I am just logging the event, but in a real scenario, your function should check if your secret token is valid and if it is then making an HTTP request against the API endpoint that you have subscribed to, using the ID that is passed to your Azure function, to get the record that has been changed.

So using my Azure function as my notification URL my JSON payload will look like this to register my subscription.

{

  "notificationUrl": "https://webhooks20230425143351.azurewebsites.net/api/MyItemNotification?code=XXX",
  "resource": "https://api.businesscentral.dynamics.com/v2.0/05a908b7-abf4-4be4-b685-8d437022beec/dev/api/publisherName/apiGroup/v1.0/companies(5501977c-539e-ed11-9889-000d3a39ac50)/items",
  "clientState": "MySecretToken"

}

If you now call

GET: https://api.businesscentral.dynamics.com/v2.0/{{TeantID}}/{{enviromentName}}/api/v1.0/subscriptions

You should now see your subscription.

{
    "@odata.context": "https://api.businesscentral.dynamics.com/v2.0/05a908b7-abf4-4be4-b685-8d437022beec/dev/api/v1.0/$metadata#subscriptions",
    "value": [
        {
            "@odata.etag": "W/\"JzIwOzEwMDY3NzYwNjEzNzUzMTgzMjUwMTswMDsn\"",
            "subscriptionId": "acd1ac95bdb642ea9bb4361b332edd13",
            "notificationUrl": "https://webhooks20230425143351.azurewebsites.net/api/MyItemNotification?code=xxxx",
            "resource": "https://api.businesscentral.dynamics.com/v2.0/05a908b7-abf4-4be4-b685-8d437022beec/dev/api/publisherName/apiGroup/v1.0/companies(5501977c-539e-ed11-9889-000d3a39ac50)/items",
            "timestamp": 158191,
            "userId": "2f871da8-a7c8-45fd-aea4-7cf28f596871",
            "lastModifiedDateTime": "2023-04-25T13:08:56Z",
            "clientState": "MySecretToken",
            "expirationDateTime": "2023-04-28T13:08:56Z",
            "systemCreatedAt": "2023-04-25T13:08:57.267Z",
            "systemCreatedBy": "2f871da8-a7c8-45fd-aea4-7cf28f596871",
            "systemModifiedAt": "2023-04-25T13:08:57.267Z",
            "systemModifiedBy": "2f871da8-a7c8-45fd-aea4-7cf28f596871"
        }
    ]
}

And you are now set up, so whenever an Item record is updated in my Business Central my Azure Function will be notified and call my Azure Function with a payload like this:

{ "value": [ { "subscriptionId": "acd1ac95bdb642ea9bb4361b332edd13", "clientState": "MySecretToken", "expirationDateTime": "2023-04-28T13:08:56Z", "resource": "https://api.businesscentral.dynamics.com/v2.0/05a908b7-abf4-4be4-b685-8d437022beec/dev/api/publisherName/apiGroup/v1.0/companies(5501977c-539e-ed11-9889-000d3a39ac50)/items(6bdffba5-539e-ed11-9889-000d3a39ac50)", "changeType": "updated", "lastModifiedDateTime": "2023-04-27T06:02:33.757Z" } ] }

So all I would have to do is call the resource that has been passed: https://api.businesscentral.dynamics.com/v2.0/05a908b7-abf4-4be4-b685-8d437022beec/dev/api/publisherName/apiGroup/v1.0/companies(5501977c-539e-ed11-9889-000d3a39ac50)/items(6bdffba5-539e-ed11-9889-000d3a39ac50) and I will get the Item that has been changed.

Your subscription will expire after 3 days, so you will have to renew it and this is done by doing a PATCH call against the following endpoint:

PATCH https://api.businesscentral.dynamics.com/v2.0/{{TeantID}}/{{enviromentName}}/api/v1.0/subscriptions('{{SubscriptionsID}}')

With a header

If-Match : {{odata.etag}} //mine would be W/"JzE4OzY4NjU3MzQxNDEyMTQxMzY4ODE7MDA7Jw==" 

you can also use * instead of odatatag, and you need to send a blank Json payload:

{}

this will give you a result where you can see that your expirationDateTime has been updated to 3 days in the future:

So a good idea would be to set up a timer that would refresh your subscription every 3 days, to make sure that it does not expire.

Conclusion

So as a conclusion on webhooks in Business Central, I would like to say that this is a powerful tool that you should at least consider using when making integrations, however, there is one side effect to webhooks and that is, you might end up with your notification URL to get a lot of notifications about changes on fields in the record that you might not care about because the webhooks will always be trigger no matter if the changes are relevant for you, a simple way around this is to implement a sort of log table which you will only update when it is relevant, and the subscriber will then subscribe to your log table, and thereby only being notified when the log table has been updated, this will of cause require you to write some code in AL to fill out your log table when relevant fields are updated.

And that is all for this blog post, I hope that it inspires you to adopt webhooks into your workflows, and until next time stay safe.

Leave a Reply