Dynamics 365 Business Central: using OData V4 Bound Actions

I’ve promised this post to some attendees of my last Dynamics 365 Business Central development workshop in Microsoft Italy (c/o Microsoft House) last week.

Question was: How can I call Dynamics 365 Business Central logic from an external application? Simple answer given by all: you can publish a codeunit as web service and use the SOAP endpoint.

But if I want to use OData? You cannot publish a codeunit as an OData endpoint. Answer: you can call custom Dynamics 365 Business Central functions via ODataV4 by using Bound Actions. Bound Actions are actions related to an entity that you can call via an HTTP call and they can invoke your business logic (functions).

Unfortunately, documentation about how to use OData V4 bound actions with Dynamics 365 Business Central is quite poor and with this post I would like to help clearing this topic a bit more. There are two main scenarios that I want to cover here:

  • Calling a D365BC codeunit that performs business logic, like creating new entities
  • Calling a procedure in a D365BC codeunit by passing parameters and reading a return value

For this sample I’ve decided to create an extension with a codeunit that contains the business logic that I want to call via OData. My codeunit has two business functions:

  • CloneCustomer: it creates a new customer based on an existing customer record
  • GetSalesAmount: it gives the total sales amount for a given customer

The codeunit code is defined as follows:


To use OData V4 bound actions you need to declare a function in a page and this function must have the [ServiceEnabled] attribute.

For this demo project, I would like to publish the Customer Card (page 21) as OData web service, so the natural thing to do is to create a pageextension object of the Customer Card to add our [ServiceEnabled] procedure and then publishing the Customer Card as web service. If you try to do this, it will never work!

If you declare a [ServiceEnabled] function in a pageextension object and you try to reach the metadata of the OData endpoint (baseurl/ODataV4/$metadata), you will not see the action published.

To publish your action attached to the Customer entity, you need to create a new page like the following and then publishing it as web service:


Here, ODataKeyFields property specify what is the field to use as key when calling the OData endpoint (I want the “No.” field of the Customer record).

Inside this page, I declare two procedures to call the two methods defined above in our AL codeunit:



  • CloneCustomer is a procedure called without parameters. It takes the context of the call and calls the CloneCustomer method defined in our codeunit.
  • GetSalesAmount is a procedure that takes a Code parameter, calls the GetSalesAmount procedure defined in our codeunit and returns the result as response.

What happens with the following definitions when we publish the MyCustomerCard page as web service (here called MyCustomerCardWS)?

If we reach the OData V4 metadata, we can see that now we have the actions published:


Now we can try to call our bound actions via OData. As a first step, we want to call the CloneCustomer function. For this, we need to send a POST request to the following endpoint:


I’m using the REST Client extension to send HTTP requests to the above endpoint. This is the request sent:


and this is the result of this call:


What happens on Dynamics 365 Business Central? The code in our codeunit is called and we have a Customer record created (cloned by the customer with “No.” = 10000 as the input):


Our second function to call (GetSalesAmount) wants a Code[20] parameter as input (not needed but it’s only to show hot to pass parameters to a bound action). We need to send a POST request to the following endpoint:


by passing a JSON body with the parameters (name and value).

This is the request sent:


and this is the response:


We have the value of the total sales amount for the given customer (retrieved by calling our codeunit method).

Here there’s a point to remember, because it was for me a source of hours spent on debugging: parameter’s name to pass on the JSON object must match with the OData metadata, not with your function’s parameters.

For example, if you declare the bound action as follows:


where CustomerNo parameter has capital letters, the OData metadata is as follows:


so the JSON must be passed accordingly (parameters names must match).

Not so easy, but very powerful when you understand the logic 🙂




  1. Hi Stefano,

    I Hope you can help me.
    I’m struggling with calls to my OData custom procedures. I succeed in publish my procedure as a Bound Action with [ServiceEnabled] annotation, but when I try to call it, it does nothing (return a 200 HTTP Status code regardless I exit my procedure with actionContext.SetResultCode(WebServiceActionResultCode::Updated)) when I declare a void procedure like
    procedure CloneCustomer()
    or replies with an error like:
    “error”: {
    “code”: “Unknown”,
    “message”: ” Object reference not set to an instance of an object. CorrelationId: cd0b2024-b486-4b19-bb44-35f0dde76f4d.”
    If I declare a procedure with a parameter like :
    procedure CloneCustomer(actionContext: WebServiceActionContext)

    Any suggestions? Thanks!


  2. Hellow again,

    I follow your post, and you are right, it works as expected. So, I check every single step and I finally find what was going on with error “Object reference not set to an instance of an object”, and it is because I wrote my Bound Action without the var keyword before parameter WebServiceActionContext. So I’ve changed from:
    procedure CloneCustomer(actionContext: WebServiceActionContext)
    procedure CloneCustomer(var actionContext: WebServiceActionContext)

    And error was solved! My bad.

    So, let me ask you another question, how can I debug or log to see why my code is not working as expected?
    Thanks again Stefano.!


  3. I’m struggeling with authentication issues. I always get 401 Unauthorized returned.
    How do you need to build the http header if you have username = myUser and password = Test1234 ??
    Using a browser, I always can authenticate using username & password. NTLM is enabled on BC Service.


    1. If you’re using D365BC onpremise with Windows Authentication you need to use username (Domain\Username) and the domani password (not web service access key)


      1. If you’re using Windows Authentication in D365BC and you’re using ODATA with basic authentication it works (I’ve different applications that uses this type of authentication). With C# is new NetworkCredential(user,pwd).


      2. I’m trying to create a HTTP Request in Postman.

        Authorization: Basic myUser Test1234

        will return 401 Unauthorized,


    1. Hi Renno,

      I’m developing in c# and this code works for me, hope you can use it:

      WebRequest webRequest = WebRequest.Create(uri);
      webRequest.ContentType = “application/json”;
      webRequest.Method = “GET”;

      NetworkCredential currentNetworkCredential = new NetworkCredential()
      Domain = DOMAIN,
      UserName = USERNAME,
      Password = PASSWORD

      CredentialCache credentialsCache = new CredentialCache
      {uri, “NTLM”, currentNetworkCredential}

      webRequest.Credentials = credentialsCache;


  4. Hi Stefano !

    Thanks for useful tips !

    Tried to use your brilliant tip for an API instead of a page odata web service, but cannot find the exact syntax for the URL …

    I have an API function for the Customer Discount Group table, and added a simple function to this table , named getSomething having in parameter text ( called it customerNo) and returns a text …
    Just to test – the idea is a function to call by parameter, and return an answer ..

    I have added the function like you described for your function GetSalesAmount with ServiceEnabled,
    And I find the service in the Edmx service description file …

    When I call the API (function custDiscountGroups) using POSTMAN then it works :


    “@odata.context”: “https://wsh1-bc163.westeurope.cloudapp.azure.com:7048/bc/api/pilaro/webshop/v1.0/$metadata#companies(7937ed82-5fb7-ea11-bb3d-001dd8b76844)/custDiscountGroups”,
    “value”: [
    “@odata.etag”: “W/\”JzQ0O1JUY0I4WkVpdVhQYXFUd29sRDM0VXJ4aFgwSS9qYlFtRXpwM1NvYzRHSmM9MTswMDsn\””,
    “custDiscountGroup”: “DETALJ”,
    “description”: “Detalj”
    “@odata.etag”: “W/\”JzQ0O2Myb3pKdXhKL2d4RU1NcnVrOVRMbXJOS0JQdHE5cFFNTG9ZSnloVTY0OE09MTswMDsn\””,
    “custDiscountGroup”: “STORKUNDE”,
    “description”: “Storkunder”

    But how do I call the getSomething function – have tried a lot of different syntax :







    “error”: {
    “code”: “BadRequest_NotFound”,
    “message”: “The request URI is not valid. Since the segment ‘custDiscountGroups’ refers to a collection,
    this must be the last segment in the request URI or it must be followed by an function or action that can be bound to it otherwise all intermediate segments must refer to a single resource.
    CorrelationId: 6f3a6627-2fd1-4bdd-b09e-635fdbf71107.”

    Content from Edmx refers to namespace Microsoft.NAV , and the getSomething function is defined in there ..



      1. edmx:DataServices
        Schema Namespace=”Microsoft.NAV” xmlns=”http://docs.oasis-open.org/odata/ns/edm”
        EntityType Name=”custDiscountGroup”
        PropertyRef Name=”custDiscountGroup”
        Property Name=”custDiscountGroup” Type=”Edm.String” Nullable=”false” MaxLength=”20″
        Property Name=”description” Type=”Edm.String” MaxLength=”100″
        Action Name=”getSomething” IsBound=”true”
        Parameter Name=”bindingParameter” Type=”Microsoft.NAV.custDiscountGroup”
        Parameter Name=”customerNo” Type=”Edm.String”
        ReturnType Type=”Edm.String”


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

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