Webhooks with Dynamics 365 Business Central

When integrating external applications with Dynamics 365 Business Central, one of the classical way is to use APIs (standard or custom APIs) and then call the relative endpoints from the external application.

As said in the past, exposing entities by using standard or custom API pages is the recommended way for doing integrations with Dynamics 365 Business Central. By using APIs, the schema of your integration is the following:

Webhooks_Schema_01.jpg

Here, an external application calls a Dynamics 365 Business Central API endpoint by using simple HTTP methods and then it receives a JSON response from the ERP.

One of the problem that you can have when integrating external systems is that sometimes the external application needs to react to changes that occours on ERP entities. Imagine for example to have an external application that needs to handle an external processing (for example shipments labels printing) for every sales order that comes from Dynamics 365 Business Central. Instead of pulling the ERP for retrieving orders and their modifications on some scheduled timing (for example every N minutes), could be useful if the ERP notifies directly the external application every time an entity changes.

This is exactly the concept of Webhooks: Webhooks is the way to get notified if an entity changes in Dynamics 365 Business Central:

Webhooks_Schema_02.jpg

In Dynamics 365 Business Central, every entity exposed as API supports webhooks natively.

To start working with Webhooks, you need to follow a request/response workflow in order to establish the communication:

  1. You need to register a webhook subscription with the entities you want to be subscribed to by providing a notification url (endpoint where Dynamics 365 Business Central will send the notifications for your subscribed entities).
  2. You need to return a validation token to Dynamics 365 Business Central from your notification url (handshake).
  3. When the subscription is established, you can start receiving notifications and you can then renew or remove the subscriptions as needed.

To show how you can work with Webhooks in Dynamics 365 Business Central, imagine that we have an external system that needs to be notified on every changes that occours on a Sales Order record in Dynamics 365 Business Central.

I’ve deployed on my Azure subscription an Azure Function that receives a JSON body (notification from Dynamics 365 Business Central with the details of the modified entity) and then performs some work as needed by our business scenario. When deployed, the Azure Function has a public URL (endpoint) and this is the notification url that we’ll use when establishing the webhook subscription to the Sales Order entity in our ERP.

NOTE 1: in our calls here we’re using Basic authentication, but you can use also OAuth2 and in this case the TENANTID parameter is not needed.

NOTE 2: In the samples provided I’m connected to my main production environment (called Production) but you can change this name accordingly with the name of the Dynamics 365 Business Central environment you want to use.

Checking subscriptions

As a first step, we call the Dynamics 365 Business Central Subscriptions API in order to check if there are active webhooks subscriptions on our tenant. For this, we need to send a GET http request to the following endpoint:

GET https://api.businesscentral.dynamics.com/v2.0/TENANTID/production/api/v1.0/subscriptions

and the response from the tenant is as follows:

D365BCWebhooks_01.png

The response is a JSON array of the active webhook subscriptions and in this case it’s empty (no subscriptions established).

Registering a webhook subscription

To register a webhook subscription, we need to send a POST request to the following endpoint, with Content-Type = application/json:

POST https://api.businesscentral.dynamics.com/v2.0/TENANTID/production/api/v1.0/subscriptions
and by passing the following JSON object in the body:
{

  "notificationUrl": "https://d365bcwebhooks.azurewebsites.net/api/D365BCListener?code=MYFUNCTIONSECRETKEY",

  "resource": "https://api.businesscentral.dynamics.com/v2.0/TENANTID/production/api/v1.0/companies(COMPANYID)/salesOrders",

  "clientState": "SomeSharedSecretForTheNotificationUrl"

}
Here:
  • notificationUrl is the url of my published Azure Function that will receive notifications from Dynamics 365 Business Central. My Azure Function is protected with AuthorizationLevel = Function so the url must provide the access key defined in the Azure Portal for accessing the function.
  • resource is the API address of the Dynamics 365 Business Central resource that we want to subscribe (in this case I want to subscribe the salesOrders API). It’s important to remember that the subscription is per-company, so you need to provide the company id.
  • clientState is optional and can be included as an “opaque key”, enabling the subscriber to verify notifications.

When the POST request is issued to the subscription API, Dynamics 365 Business Central sends a request to the notificationUrl endpoint by passing a validationToken parameter in the query string.

The subscriber (in my case my Azure Function) retrieves this token from the query string and then it sends back to the caller (Dynamics 365 Business Central) a response with a body that contains the validationToken and by setting http status code = 200 (success).

When Dynamics 365 Business Central receives the response, it checks for the validationToken and if all is ok the subscription is established.

This is the returned response that shows that the webhook subscription is activated (with a proper subscriptionId and expirationDateTime):
D365BCWebhooks_02
If I monitor what happens on my Azure Function, I can see that a request is received (see the spike to the Server requests section of the dashboard):
D365BCWebhooks_03

and if I check the detail of the caller (Azure Application Insights is an extremely useful tool for that), I can see that the caller is an application that comes from North Holland (datacenter location of my Dynamics 365 Business Central tenant):

D365BCWebhooks_04.png

P.S. this is a nice way for knowing the exact location of the datacenter region where your Dynamics 365 Business Central tenant runs.

Receiving notifications

What happens now that my webhook subscription is activated? Dynamics 365 Business Central starts sending notifications to my endpoint every time something happens on the subscribed entities (pushing).

Here we’ve subscribed for receiving notifications about Sales Orders. If I modify a Sales Order in Dynamics 365 Business Central, a notification is sent to my Azure Function (notificationUrl parameter).

This is what you can see from the Azure Portal after modifying something on a Sales Order record:

D365BCWebhooks_06.png

As you can see from the Azure Function monitor (Server requests diagram), a new HTTP request is arrived to my endpoint:

D365BCWebhooks_07.png

and if you inspect the incoming request details, you can see that the caller is always a cloud application that comes from the same region as above mentioned (my Dynamics 365 Business Central tenant).

To show the incoming body request (notification details sent from Dynamics 365 Business Central to my Azure Function) I have logged the incoming request body in the Azure Function log. As you can see in the picture below, it contains a JSON with the modified entity (resource url) and its related state change (created, updated, deleted):

D365BCWebhooks_08.png

Wow… Dynamics 365 Business Central has sent you a notification! 🙂

Please always remember that:

  • a notification sent to the subscriber (notificationUrl endpoint) can contain multiple notifications from different entities (subscriptions)
  • Dynamics 365 Business Central will not send a notification immediately when an entity changes (normally it occours some minutes). This is by design in order to permit to send a single notification even though the entity might have changed several times within a few seconds.

Renewing a webhook subscription

Webhooks subscriptions expires in 3 days (default value). They can be renewed by sending a PATCH request to the subscriptions API (as in the following URL) with Content-Type = application/json:

PATCH https://api.businesscentral.dynamics.com/v2.0/TENANTID/production/api/v1.0/subscriptions('SUBSCRIPTIONID')

This is not well documented by Microsoft actually and it’s a source of problems, but remember that in order to successfully renew a webhook subscription, you need to retrieve the @odata.etag value from the response received when you have established the subscription (see above) and then pass this value as a If-Match block in the PATCH request header.

Without this, you will receive an error that says “Could not validate the client concurrency token required by the service. Please provide a valid token in the client request.”.

Remember also that the @odata.etag value must be passed unescaped (so change \” to “) otherwise you will receive an error saying that “Request data is invalid” (HTTP 400 Bad Request error).

Renewing a subscription also updates the @odata.etag value, so be aware of this (you need to save the new value after the renewal).

NOTE for the on-premise version: the duration for a subscription is 3 days as default also for the on-premise version. This value is specified in the CustomSettings.config file under the ApiSubscriptionExpiration entry. In this file there is also a maximum number of subscriptions admitted specified in the ApiSubscriptionMaxNumberOfSubscriptions entry.

Delete a webhook subscription

To delete a webhook subscription, you can send a DELETE http request to the following endpoint:

DELETE https://api.businesscentral.dynamics.com/v2.0/TENANTID/production/api/v1.0/subscriptions('SUBSCRIPTIONID')

Also this is actually not well documented by Microsoft, but to successfully delete a webhook subscription you need to do exactly like the renewing process previously described (If-Match header with the @odata.etag value).

List of webhooks supported entities

To have a list of all the webhooks supported entities, you can send a GET request to the webhookSupportedResources endpoint (please be aware of the endpoint url):

GET https://api.businesscentral.dynamics.com/v2.0/TENANTID/production/api/microsoft/runtime/beta/companies(COMPANYID)/webhookSupportedResources
This is the response for my tenant:

D365BCWebhooks_09.png

Here you can see all your standard and custom entities exposed as APIs.

As you can see, Webhooks are extremely powerful and permits you to have a “push-based solution”, where is the ERP that calls you when something happens.

From the external application, in order to have a complete workflow always running by code (no human interaction) you need to:

  • Establish the subscription to an entity and save the @odata.etag, subscriptionId and expirationDateTime values.
  • Handle the notifications accordingly to your business needs. On a notification you have the changeType parameter (what happened on your subscribed entity) and the resource endpoint (that you can use to retrieve the entity details).
  • When you’re near to the expirationDateTime value, you need to renew the subscription (by passing the subscriptionId) and save the new @odata.etag and expirationDateTime values.

 

 

41 Comments

  1. i don’t suppose you have a github where you stored this test azure function, do you?! loved this article, still trying to wrap my head around how the azure function portion works though. is it…multiple functions within that azure function, or one function with multiple parameters or…?? i’ve never used azure functions so when i created a function and then it makes me create my first function i got lost on what the proper procedure is from there.

    Like

  2. Really good post, I’m interested in practical use of this – but I can’t figure out what happens if either BC or 3rd party System is down for a few milliseconds when this communication happens – BC is not 100 % online, nor is the 3rd Party System in a cloud world. So what kind of error handling is needed compared to a traditional “Queue it and Process it” approach. If one wanted to use this, can it only be used for “less” important notifications – or could it be used for critical areas too?

    Like

    1. You can secure the webhooks flow as per your needs. Common way for having 99.99% reliability is to use queues like Azure Queue or Azure Service Bus for storing the message that arrives from D365BC and process it later.

      Like

      1. Best way for this (and more reliable) is to receive webhooks response in an Azure Function and then call an HTTP triggered Flow by passing the JSON body you need.

        Like

  3. Hi Stefano,

    Very useful indeed. It seems the only way for BC online to notify another service if e.g. a record is created and this record is not exposed by default. Currently i m trying to get this done in order to expose changes in cust ledger entries, but i cant fully get how to build the azure function. I ld appreciate if you can share an example/ guide of the function if possible. Thanks!

    Like

  4. Hi Stefano,
    I’m trying to test subscrition to webhook on my test environment, but even passing the validationToken back in the response I always get
    “Service has not provided a valid validation token. CorrelationId: 1216ee7f-5e5a-443c-9338-b6e4180ed06a.”

    My Request is as follow:
    POST [MYSERVER]:7048/bc/api/v1.0/subscriptions
    {
    “notificationUrl”: “[MYNOTIFICATIONSERVER]/subscribe”,
    “resource”: “[]MYSERVER]/bc/api/v1.0/companies(1aaef56e-78af-4eb4-b7e7-a479774a679f)/customers”
    }

    My notification server respond with this body:

    {
    validationToken:213f871d-e87f-4562-b505-97cccfc8f22d
    }

    Can you please help me

    Like

      1. Hi René,
        I tried to use webhook.site to track my webhooks issued from BC16 but I still got a badrequest with error “Service has not provided a valid validation token”
        How did you format the JSon / clientState to hve authentication to webhook.site …?
        Thanks a lot.

        Like

    1. BC sends a token to Azure Function, you need to return that token back to BC.
      Once you returned that token back to BC, Postman will return this below instead of that other message about ‘Service has not provided a valid valiation token’

      Like

  5. Thanks for the blog. It’s really helpful. I’m trying to use webhook with custom API. I can see my custom API in the list of supported subscription but it always failed when I try to register webhook the error message is “No HTTP resource was found that matches the request URI” .
    Any idea ? Thanks.

    Like

  6. Hi Stefano, extremely interesting and useful article for another time. Thanks for the blog. Have you ever experienced the webhook for multiple consecutive runs (4) to have an output of collection with the same resource? (i.e. exactly the same lastModifiedDateTime filter on the table)

    Like

      1. yes, actually four times. Our case is accessing a custom api for customer ledger entries, so we have add a lastmodifieddate field in the cus. ledger entries table and in the api, in order the collection filter to work in the webhook. We are posting 100 invoices. We are using the webhook in an Azure LogicApp to integrate data to D365 CRM.
        For some reason in 4 runs the same collection is returned as output from the webhook:
        “resource”: “xxxiLimited/xxxxyyyy/v1.0/companies(74c2176-5232-4740-assdf-27e59875c1)/CustomerLedgerEntries?$filter=lastModifiedDateTime%20gt%202020-10-20T14:55:13.5Z”,
        “changeType”: “collection”,

        And as hard as i have tried to find an explanation (i.e. when or how is fired the webhook from the BC) in the documents, i cannot find something. Are you aware for any technical docs about it?
        Best regards

        Like

      2. Hello Alexandros,

        I’m facong the same issue.
        Did you find a solution ?

        Like

    1. Best way for this (and more reliable) is to receive webhooks response in an Azure Function and then call an HTTP triggered Flow by passing the JSON body you need.

      Like

  7. Many thanks Demiliani,
    Let me add a little note, When we need to renew subscription we need to send json body again (same json body as when we subscribed), surly we can use old notificationUrl or modify it.
    Thanks again 💙

    Like

  8. Hi Demiliani,
    Great post, this really helped a lot. I have this working thanks to you. Question: is it possible to not send a notification on every change. I would like to only send a notification from BC if a field ‘Status’ in my table has a certain value. I tried to filter on the OnOpenPage of the API page, but this does not work. The notification is still being send. Retreiving the resource does not work, the filter does its work then.

    OnOpenPage()
    SETFILTER(Status, ‘P20’);

    Regards,

    Niels

    Like

    1. A webhook notification is automatically sent by the platform on every change of the entity exposed via API, so you cannot change this behavior. You can try with SourceTableView on the API page but not sure that it works.

      Like

      1. hi everyone, really exciting post and also the comments. Stefano can you please ellaborate about the usage of SourceTableView to amend the behavior? Thanks again

        Like

      2. A webhook is triggered when the subscribed entity exposed by the API is changed. If you want to expose not the entire table but only some filtered records, you can use the page’s SourceTableView property.

        Like

      3. In my case adding a WHERE condition in SourceTableView hasn’t changed the behavior. The webhook is still fired when the record doesn’t meet the criteria.

        Like

  9. Hi!
    Great post! I have custom api for Item that has subpages for Item Unit of Measure and Prices. I want to trigger the Item API webhook when new UOM or Price is added for the Item. Is there a way to pragmatically trigger the webhook without modifying the Item record itself?

    Like

  10. Hi. Great post! Could you please show an example on the code used for renewing og deleting a subscription? I cannot get any luck with when trying to use the if-match header, and how the @odata-etag should be sent. Getting the 400 bad request. Running BC13 on-premise. Thanks and Regards Martin

    Like

  11. Hello, is it possible to use Webhooks in case of communication between two BC environments? E.g. I create Item in BC Environment 1 and I would like to send the notification to BC Environment 2 that new Item has been created and in order to synchronize both BC Environments that Item will be created in BC Enviroment 2 as well. Is it possible somehow via using notificationUrl pointed to BC Environment 2 directly (no Azure function), please? Or in another words what’s the best practice for synchronizing entities between BC Environments?

    Like

    1. A webhook is a “signal” that is sent to an http endpoint and this endpoint needs to be able to handle that signal when received. Yes you can use a web service or other things like Power Automate or Logic Apps or every HTTP Trigger you want.
      Item synch between tenants can also be done by using Power Automate directly or by using a Logic App or also in a scheduled way (timer triggered) by using APIs for example (with a filter on the SystemModifiedDate).

      Like

  12. I would just like to add something here in case someone else is having the same issue as me.

    If you are a delegated admin and are trying to get this to work for a customer but you never get any pushes to your endpoint even if subscription exists, keep reading.

    The issue is that as a delegated admin you cannot start a job queue and the webhook notifications are actually pushed out with a jobqueue transactions. So all actions you as a delegated admin do in the system gets recorded in table “API Webhook Notification” however they cannot be sent. When a user with a normal license change something this will trigger the job queue transaction and send all previous notifications as well.

    I hope this can save someone else a few hours of head scratching.

    Liked by 1 person

    1. Hi!

      Thanks a million. Have spent the whole day trying to understand this issue until i found your answer!
      Rgds from Mattias

      Like

  13. Logic App request-response trigger example with query validationtoken response to add subscription

    {
    “definition”: {
    “$schema”: “https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#”,
    “actions”: {
    “Response”: {
    “inputs”: {
    “body”: “@triggerOutputs()[‘queries’][‘validationToken’]”,
    “statusCode”: 200
    },
    “kind”: “Http”,
    “runAfter”: {},
    “type”: “Response”
    }
    },
    “contentVersion”: “1.0.0.0”,
    “outputs”: {},
    “parameters”: {},
    “triggers”: {
    “manual”: {
    “inputs”: {},
    “kind”: “Http”,
    “type”: “Request”
    }
    }
    },
    “parameters”: {}
    }

    Like

  14. Hi Stefano, could you please explain how webhooks works in case we have rollback on the business central side? for example we created customer record, sent webhook, sent changes but then error and rollback of created record occurred. Does rollback could also send webhook? Do you know how MS handle this? i tried to understand this from w1 logic but didn’t get it.

    Like

      1. Sorry, i would try to be more clear. As i understood from w1 webhook call placed in OnDatabaseModify(Insert, Delete, Modify) triggers. And im trying to understand how would be next case correctly processed.

        Transaction Started(Codeunit which creates 1 million customers)

        Customer 1 Record Created(OnDatabaseInsert tigger sends webhook)
        Customer 2 Record Created(OnDatabaseInsert tigger sends webhook)
        ……..
        Transaction still continue to run for example 1 hour
        During the transaction running External Service receives webhook, calls to BC api and creates Customer 1(2,3,…) on its own side.
        ……..
        On creating last one customer in BC error occurred and all transaction roll backed and all customers on BC side deleted. During rollback as i understand no BC triggers works so we would couldn’t notify external service that at least something changed in the customer list and as a result we would not have customers in BC but would have them on the external db. Hope this example is clear. Trying to understand how MS handle such cases.

        Like

  15. Hi Stefano, is there a way to protect the API that receives the BC notification, using Basic Auth or similar? For example, can BC add a predefined authorization header to the notifications? Or how do you prevent your listening API from missues and harmful traffic?

    Like

    1. In the provided example I’m using an Azure Function and the subscription is done by using the function key. Only by passing that key you can call with the AF. Custom headers parameters from the webhook are not supported.

      Like

  16. Great blog post, but I was really hoping it would be the other way around. Of all the APIs we integrate with, none of them seem to support oauth webhooks, and BC appears to have no way to consume them. It’s a real shame that we have to create a shunt with azure to make it happen.

    Like

Leave a comment

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