How to test Business Central webhooks

28 Nov

I don’t think webhooks need an introduction. But for those who are new to the topic, here you can find the official documentation. In short, a webhook is a push notification. The “don’t call us, we’ll call you” scenario of web services. You tell Business Central in what resource you are interested (e.g. Item, Customer, etc.), and they’ll call you in case there is any change. With resource I mean any of the existing APIs, including custom APIs.

Over time, especially during my API courses and also after the session I did at the virtual Directions event about APIs, I’ve got many questions about how to test a Business Central webhook. You want to see the results before you move on with creating an application for it, right? And most people are struggling with the handshake mechanism, how to get that to work when testing the webhooks with a low-code platform or a simple solution?

Ok, here you go. Three different ways to test and work with Business Central webhooks:

Webhook subscriptions flow – Business Central perspective

Before we dive into these options, we need to understand the flow of subscribing to a Business Central Webhook. Let’s say we want to receive notifications in case there is any change in the customers. And we have a service running at a URL https://my.webhooklistener.com to receive those notifications. To tell Business Central to send notifications for the customers resource to this URL, we need to post the following request (leaving out the Authorization header):

POST https://api.businesscentral.dynamics.com/production/api/v2.0/subscriptions
Content-Type: application/json

{
  "notificationUrl": "https://my.webhooklistener.com",
  "resource": "companies(264f8dd2-4a2a-eb11-bb4f-000d3a25f2a9)/customers",
  "clientState": "SuperSecretValue123!"
}

What happens in the background, is that Business Central calls the notification URL passing a validationToken parameter. The service must respond within 5 seconds with status 200 and include the value of the validationToken parameter in the response body. When this handshake mechanism is successfully completed, Business Central will create the webhook subscription. Because a picture is worth a thousand words, here is the flow diagram of this process.

After this, Business Central will start to send notifications to the notification URL when a change occurs in the resource. Until the subscription expires, which happens after 3 days. Then the subscription must be renewed which performs the same handshake verification process.

Webhook subscriptions flow – subscriber perspective

Let’s now look at the subscriber side at the notification URL. The subscriber receives the notifications, checks the clientState value (optionally, but highly recommended), and processes the notifications. However, the very same URL is also called by Business Central during the handshake with the validationToken parameter to verify if the subscriber is actually up and running. And this does not only happen when the subscription is created, it also happens when the subscription is renewed. In other words, the subscriber should first check if the request contains a validationToken parameter and if so, respond with the validation token value in the response body. Let’s look at the flow diagram of the subscriber.

This flow is exactly what we need to implement in order to successfully subscribe to a Business Central webhook. You will recognize this flow in the next examples.

Azure Functions webhook subscriber

With Azure Functions you can easily create a subscriber on Azure and check the results. The simplest way would be to just log the received notifications so you connect to the Azure Functions monitor and watch the messages coming in. Or you go one step further and store the messages on Azure storage, push them to Service Bus queue or store them in a database like Azure SQL or (even better) Cosmos DB. The code below is a simple Azure Function that just logs the received notifications.

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 Kauffmann.TestBCWebhook
{
    public static class BCWebhook
    {
        const string clientState = "SuperSecretValue123!";
        [FunctionName("BCWebhook")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("BCWebhook function received a request.");

            string validationToken = req.Query["validationToken"];
            if (!String.IsNullOrWhiteSpace(validationToken))
            {
                log.LogInformation($"BCWebhook function processed validationToken: {validationToken}");
                return new OkObjectResult(validationToken);
            }

            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();

            dynamic data = JsonConvert.DeserializeObject(requestBody);
            if (data.value[0].clientState != clientState)
            {
                log.LogError("Received notification with incorrect clientState:");
                log.LogError(requestBody);
                return new BadRequestResult();
            }

            log.LogInformation("New notification:");
            log.LogInformation(requestBody);
            
            return new AcceptedResult();
        }
    }
}

On a side note, the notification payload contains the clientState value in every single object. The code above only checks it for the first object in the array. It seems overkill to me that it is repeated in every object, it would be sufficient if the clientState value was at the top level, next to value.

The output of the Azure Function looks like this.

If you want to inspect the notification further, then just copy the value and use a JSON viewer. I’m a fan of this online JSON viewer: http://jsonviewer.stack.hu/

Just grab the code and paste it into your Azure Function. Running Azure Functions requires an active Azure Subscription, and to view to the log you need to navigate through the Azure portal. The next option does not require that, it’s completely free and easy to use.

Pipedream

This service can be used to create workflows based on triggers. Workflows are code and you can run them for free (and there are paid plans as well of course). Head over to Pipedream to let them tell what you can do.

I’ve created a workflow that you can just copy and deploy right away. The only thing you need is to create an account, which is free. The workflow consists of a trigger, follows by three steps. Again, you should recognize the steps from the flow diagram for the event subscriber.

How to get your hands on this workflow and make use of it?

Just open this link to get to the shared version of the workflow: https://pipedream.com/@ajkauffmann/business-central-api-webhook-p_3nCDWl

You will get a window that shows the workflow and which has a green button COPY in the right top corner. Click on the COPY button and the workflow will be copied to your Pipedream environment. Then you get a blue DEPLOY button just above the trigger. If you click on it, you will get a unique URL displayed in the trigger that you can use to create a subscriber. You need to be logged in order to deploy a workflow.

Taking the above workflow as an example, you can create a webhook subscription with this call (first time it might run into a timeout!):

POST https://api.businesscentral.dynamics.com/production/api/v2.0/subscriptions
Content-Type: application/json

{
  "notificationUrl": "https://enln9ir1q8i5w5g.m.pipedream.net",
  "resource": "companies(264f8dd2-4a2a-eb11-bb4f-000d3a25f2a9)/customers",
  "clientState": "SuperSecretValue123!"
}

When the workflow is active you will see the events in the left column. These events can be clicked and you can explore the output of every single step in the workflow.

Power Automate Flow

How to receive webhook notifications with Power Automate is a question I see quite often. The handshake mechanism is not supported by the HTTP request trigger, so people are struggling with this getting to work. But by implementing the flow diagram above, it will be no problem. Here is a Flow that handles the validationToken (the handshake) and also checks the clientState.

This Flow is available for download, both for Power Automate and as Logic Apps template. Just download them and import them in your environment. I’ve also submitted it as a template, so hopefully it becomes available soon as a starter template.

Download links:

After the Flow has been activated, you can get the URL from the trigger ‘When a HTTP request is received’. Use this URL to create the subscription. In the Flow history you can view all runs including the data and how it has been processed. If you want to store the notifications, then add it after the step ‘Respond Accepted’ in the ‘If yes’ branch of the step ‘Check clientState’. Pretty simple, isn’t it?

On top of this, it would also be easy to create a custom connector in Power Automate so you can create your own triggers. The only difference with the Flow above would be the trigger, the other steps will be exactly the same. But… you need to keep one thing in mind! Subscriptions expire after three days, you need to renew them before they get expired. That can be done manually of course, which is perfectly fine for testing. For production scenarios you could think of creating a Flow that runs periodically and renews your subscriptions. That should not be very hard to create. However, we are now going beyond the scope of this post which was about testing webhooks.

With this I hope you got some ideas how to easily test your webhooks!

16 thoughts on “How to test Business Central webhooks

  1. Hi Arend-Jan, great article 😉

    Do you know if events fired by BC (insert, updated, deleted, collection) can be collected in Log Events ? We are facing an issue where some “collection” event seems not fired properly in BC16.

    Unfortunately outgoing telemetry for API is reserved for BC17 and later…
    https://docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/telemetry-webservices-outgoing-trace

    Thanks,
    Xavier.

    • Take a look into codeunit “API Notification Mgt.”. There you will find a number of SendTraceTag commands. But you need to enable detailed logging in this codeunit to get all events. That is possible by subscribing to the event OnGetDetailedLoggingEnabled.

      • Thanks!
        What’s strange is that it seems that we can’t enable “Detailed Log” it in the standard product itself (I didn’t see any subscribed code on event OnGetDetailedLoggingEnabled).
        BTW I will do and let you know.

        • Hi,
          For your information we had to call APIWebhookNotificationMgt.OnDatabaseInsert/Modify/Delete/Rename on our custom table/API to get the proper result…
          Xavier.

  2. Hi Arend, using Pipedream with a docker in NavUserPassword works like a charm but using Windows Authentication it failed creating the subscription ({“code”:”BadRequest”,”message”:”Service specified in the notificationUrl has not responded in time to the validation request.).

    Do you think Docker Authentication type could have an impact ?

    Thanks,
    Xavier.

    • No, I don’t think that NavUserPassword or Windows Authentication makes any difference. I’ve noticed that the the notification URL needs to respond in 2 seconds. And Pipedream seems to have a problem with that sometimes, especially when the service has not been used for a while. Just keep trying…

      • Indeed I resolve this issue by disabling my corporate VPN. But my customer, is a complete different environnement (Azure IaaS/VM) has the same error so it seems to be due to network issues/rules/etc…

  3. Hi AJ,

    I read in the documentation that a patch is needed like this: {{BaseURL}}/{{TenantID}}/{{Environment}}/api/v2.0/subscriptions({{SubscriptionID}}) to renew the subscription. However when I do that in Postman I get an error like this, like there qould be an error in the subscription ID:

    {
    “error”: {
    “code”: “BadRequest_NotFound”,
    “message”: “‘)’ or ‘,’ expected at position 3 in ‘(76a8a5fc1f3740b587321af190b30a6d)’. CorrelationId: 0b715d9a-5e17-4db4-93d4-5028b0918ef1.”
    }
    }

    In the body of the path request I have the same body as what I used to create the subscription. Did you succeed to renew with a patch?

    Second question: are subscription stored in BC, if so, where can I find them?

    • Use single quotes around the subscription ID value in the URL:

      {{BaseURL}}/{{TenantID}}/{{Environment}}/api/v2.0/subscriptions(’76a8a5fc1f3740b587321af190b30a6d’)

      The subscription ID is not a guid, it’s a text value.

      Subscriptions are stored in table 2000000095 “API Webhook Subscription”

  4. Hello AJ,
    Do you know what are the conditions for BC to switch from individual events (inserted, updated, deleted…) to EventType:Collection ? Does we have a way to avoid Collection event and so keep single events ?
    Thanks a lot,
    Xavier.

    • Unfortunately, BC does not support single events. It will always aggregate the events.
      The delay between the attempts can be found in codeunit 6154 “API Webhook Notification Send”, function GetDelayTimeForAttempt

      • As MS told me (after waiting 2 weeks or so…), the number of events that raise a Collection events is 100 by default. This threshold can be updated OnPrem only with the Service parameter ApiSubscriptionMaxNumberOfNotifications.

  5. Hello,

    I’m having a very hard time figuring out how to complete the handshake portion of this with the validationToken. Is there sample code available that does this?

    Thank you,
    Drew

      • Yes, thank you for providing that. We’re using C#. I’ve enlisted some developers from my company to help, and hopefully we can get it working.

  6. Hello AJ,

    I did some test with your pipedream template : subscription is ok but it seems that only the first event is captured by pipedream. This first (and only) event is marked with response.status=400, whatever the type (updated, collection, etc.).

    I tested with the /items API in version v1.0 within a Cronus docker in BC17.4 W1 (and I get the same result with a custom api in BC17.4 or BC16.5).

    Any idea ?
    Kind regards,
    Xavier.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.