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:
Create a service object named
SalesOrderByCarrierCodeAdd the
getSalesOrderDatamethod fromSalesOrderBusinessServiceAssign the service to a service group (e.g.
SalesOrder)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.
