How to Build Custom REST APIs in Dynamics 365 F&O

How to Build Custom REST APIs in Dynamics 365 F&O

In modern enterprise landscapes, Dynamics 365 F&O rarely operates in isolation. Secure and scalable integrations are essential, and custom REST APIs in D365 SCM provide a powerful way to expose business data in a controlled and extensible manner.

In this post, I walk through a step-by-step approach to building a custom REST API in D365 SCM, using a real-world example that retrieves sales order data by carrier code. This pattern can be reused for multiple integration scenarios with external systems.

Why Build Custom APIs in D365 SCM?

Custom APIs are often required when standard OData or Data Management capabilities are not sufficient. They offer:

  • Scalable integration patterns for enterprise-grade solutions

  • Secure access using OAuth authentication via Microsoft Entra ID

  • Flexible data contracts tailored to business requirements

  • Better performance control compared to generic endpoints

In this example, we will build a SalesOrderByCarrierCode API that returns sales orders and related sales lines for a given carrier and company.

1. Defining Data Contracts

Data contracts define the structure of the request and response payloads. They ensure correct serialization and a clean API surface.

Request Data Contract:

The request contract accepts a carrier code and company (dataAreaId), allowing consumers to filter data precisely.

[DataContract]
public class SalesOrderDataRequest
{
    TMSCarrierCode carrierCode;
    DataAreaId dataAreaId;

    [DataMember("CarrierCode")]
    public TMSCarrierCode parmCarrierCode(TMSCarrierCode _carrierCode = carrierCode)
    {
        carrierCode = _carrierCode;
        return carrierCode;
    }

    [DataMember("DataAreaId")]
    public DataAreaId parmDataAreaId(DataAreaId _dataAreaId = dataAreaId)
    {
        dataAreaId = _dataAreaId;
        return dataAreaId;
    }
}

Response Data Contract:

The response contract represents the sales order header and its related sales lines.

[DataContract]
public class SalesOrderDataResponse
{
    SalesId salesId;
    AccountNum accountNum;
    List salesLines;

    [DataMember("SalesId")]
    public SalesId parmSalesId(SalesId _salesId = salesId)
    {
        salesId = _salesId;
        return salesId;
    }

    [DataMember("AccountNum")]
    public AccountNum parmAccountNum(AccountNum _accountNum = accountNum)
    {
        accountNum = _accountNum;
        return accountNum;
    }

    [DataMember("SalesLines"),
     DataCollection(Types::Class, classStr(SalesLinesDataResponse))]
    public List parmLine(List _salesLines = salesLines)
    {
        salesLines = _salesLines;
        return salesLines;
    }
}

Sales Line Data Contract:

Each sales line is represented by a nested data contract.

[DataContract]
public class SalesLinesDataResponse
{
    LineNum lineNum;
    ItemId itemId;
    Qty qty;

    [DataMember("LineNum")]
    public LineNum parmLineNum(LineNum _lineNum = lineNum)
    {
        lineNum = _lineNum;
        return lineNum;
    }

    [DataMember("ItemId")]
    public ItemId parmItemId(ItemId _itemId = itemId)
    {
        itemId = _itemId;
        return itemId;
    }

    [DataMember("Qty")]
    public Qty parmQty(Qty _qty = qty)
    {
        qty = _qty;
        return qty;
    }
}

2. Implementing the Business Logic Service

The service class contains the core logic that retrieves data from D365 SCM. In this example, data is efficiently fetched by joining SalesTable and TMSSalesTable, followed by retrieving related sales lines.

public class SalesOrderBusinessService
{
    public List getSalesOrderData(SalesOrderDataRequest _request)
    {
        List salesOrdersList = new List(Types::Class);
        SalesTable salesTable;
        TMSSalesTable tmsSalesTable;
        SalesLine salesLine;

        while select tmsSalesTable
            join salesTable
            where tmsSalesTable.CarrierCode == _request.parmCarrierCode()
               && tmsSalesTable.DataAreaId == _request.parmDataAreaId()
               && salesTable.SalesId == tmsSalesTable.SalesId
        {
            List salesLinesList = new List(Types::Class);

            while select salesLine
                where salesLine.SalesId == tmsSalesTable.SalesId
            {
                SalesLinesDataResponse lineResponse = new SalesLinesDataResponse();
                lineResponse.parmItemId(salesLine.ItemId);
                lineResponse.parmLineNum(salesLine.LineNum);
                lineResponse.parmQty(salesLine.QtyOrdered);
                salesLinesList.addEnd(lineResponse);
            }

            SalesOrderDataResponse orderResponse = new SalesOrderDataResponse();
            orderResponse.parmSalesId(tmsSalesTable.SalesId);
            orderResponse.parmAccountNum(salesTable.CustAccount);
            orderResponse.parmLine(salesLinesList);

            salesOrdersList.addEnd(orderResponse);
        }

        return salesOrdersList;
    }
}

3. Publishing the Service

To expose the business logic as a REST API:

  1. Create a service object named SalesOrderByCarrierCode

  2. Add the getSalesOrderData method from SalesOrderBusinessService

  3. Assign the service to a service group (e.g. SalesOrder)

  4. Set Auto Deploy = Yes to ensure deployment during builds

Once deployed, the service becomes accessible via a REST endpoint.

4. Authentication and App Registration

Secure access is handled through Microsoft Entra ID (Azure AD).

Register the Application

  • Create a new app registration

  • Assign required D365FO API permissions

  • Grant admin consent where needed

Link the App in D365 SCM

Navigate to: System administration > Setup > Azure Active Directory applications

Add the application using its Client ID to authorize API access.

5. Testing the API with Postman

Testing the API with Postman

Obtain an Access Token

POST https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token

Body (form-data):

  • client_id

  • client_secret

  • grant_type = client_credentials

  • scope = https://{d365-url}/.default

 

Call the Custom API

POST https://{d365-url}/api/services/SalesOrder/SalesOrderByCarrierCode/getSalesOrderData
 

Headers:

Authorization: Bearer {access_token}
 
 

Body:

{
"CarrierCode": "yourCarrierCode",
"DataAreaId": "yourDataAreaId"
}
 

Conclusion

Custom REST APIs in Dynamics 365 Supply Chain Management provide a robust and secure way to expose business data for integration scenarios. By combining structured data contracts, service-based architecture, and Azure-based authentication, you can deliver scalable and maintainable integration solutions.

The example shown here can easily be extended to support additional entities or more complex business logic, making it a solid foundation for enterprise integrations.

Leave a Comment

Your email address will not be published. Required fields are marked *