13.2 Custom Services & SOAP/REST Endpoints
Key Takeaways
- Custom Services in Dynamics 365 Finance and Operations consist of three core AOT artifacts: Service Data Contracts ([DataContractAttribute]), a Service Class containing business logic operations decorated with [SysEntryPointAttribute(true)], and a Service Group (AxServiceGroup).
- When added to an AOT Service Group with AutoDeploy set to Yes, custom services are automatically provisioned simultaneously as JSON REST endpoints and SOAP XML endpoints without requiring manual IIS or web routing configuration.
- REST custom service operations require HTTP POST requests targeting the URL path /api/services/<ServiceGroup>/<Service>/<Operation>; HTTP GET is not supported for custom service operations.
- The [SysEntryPointAttribute(true)] attribute enforces underlying role-based security authorization, ensuring the runtime verifies that the calling user possesses security privileges granting access to the service operation.
- External daemon applications authenticate using Microsoft Entra ID (Azure AD) OAuth 2.0 Client Credentials grant, and the Application ID must be mapped to a dedicated F&O user account in the 'Microsoft Entra ID applications' form (SysAADClientTable).
13.2 Custom Services & SOAP/REST Endpoints
Quick Answer: Custom Services in Dynamics 365 Finance and Operations allow developers to expose custom X++ business logic as external web services. A complete custom service requires three AOT components: Data Contracts (
[DataContractAttribute]) defining request and response schemas, a Service Class containing public operation methods decorated with[SysEntryPointAttribute(true)], and an AOT Service Group (AxServiceGroup) withAutoDeploy = Yes. The AOS automatically deploys dual endpoints: a JSON REST endpoint (/api/services/<ServiceGroup>/<Service>/<Operation>) and a SOAP endpoint (/soap/services/<ServiceGroup>?wsdl). All REST custom services require HTTP POST. External applications authenticate via Microsoft Entra ID OAuth 2.0 Client Credentials, which must be mapped to an internal User ID and security roles in the Microsoft Entra ID applications (SysAADClientTable) form.
1. Anatomy of an X++ Custom Service
While public Data Entities expose tabular relational data for standard CRUD via OData, Custom Services expose procedural business algorithms (RPC style). When an external application needs to execute business actions—such as calculating tax estimates, validating credit scores, or reserving inventory batches—custom services provide a clean, strongly-typed interface.
Custom Service Component Architecture
┌─────────────────────────────────────────────────────────────┐
│ AOT Service Group │
│ • AxServiceGroup: AutoDeploy = Yes │
│ • Groups one or more services under a shared route │
└──────────────────────────────┬──────────────────────────────┘
│ Deploys
▼
┌─────────────────────────────────────────────────────────────┐
│ Service Class │
│ • Contains public operation methods │
│ • Decorated with [SysEntryPointAttribute(true)] │
│ • Orchestrates X++ business logic and transactions │
└──────────────────────────────┬──────────────────────────────┘
│ Takes / Returns
▼
┌─────────────────────────────────────────────────────────────┐
│ Data Contract Classes │
│ • Request and Response DTOs │
│ • Decorated with [DataContractAttribute] │
│ • Getter/setter parm methods with [DataMemberAttribute] │
└─────────────────────────────────────────────────────────────┘
2. Service Data Contracts: Structure, Attributes & Collections
A Data Contract is an X++ class that defines the data structure (Data Transfer Object / DTO) exchanged between the caller and the service operation. The runtime serialization engine uses data contracts to convert between X++ objects and JSON/XML payloads.
Core Attributes for Data Contracts
[DataContractAttribute]: Applied to the class declaration. Marks the class as a serializable data contract.[DataMemberAttribute('CustomName')]: Applied to accessor methods (parm*). Exposes the method as a property in the serialized payload. Specifying a custom name parameter allows mapping X++ variable names to external camelCase JSON properties.[AifCollectionTypeAttribute]: Mandatory for collection parameters. When a contract property accepts or returns a collection (e.g.,ListorArray), X++ requires this attribute to inform the CLR serializer of the exact underlying item type. Without it, the serialization engine cannot deserialize incoming JSON arrays into concrete contract objects at runtime.
Complete Data Contract Implementation Example
// 1. Request Contract for calculating shipping estimates
[DataContractAttribute]
public final class LogisticsShippingRequestContract
{
private CustAccount customerAccount;
private ItemId itemId;
private Qty quantity;
private List lineItems; // List of LogisticsLineItemContract
[DataMemberAttribute('customerAccount')]
public CustAccount parmCustomerAccount(CustAccount _customerAccount = customerAccount)
{
customerAccount = _customerAccount;
return customerAccount;
}
[DataMemberAttribute('itemId')]
public ItemId parmItemId(ItemId _itemId = itemId)
{
itemId = _itemId;
return itemId;
}
[DataMemberAttribute('quantity')]
public Qty parmQuantity(Qty _quantity = quantity)
{
quantity = _quantity;
return quantity;
}
// CRITICAL: AifCollectionTypeAttribute specifies the exact class inside the List
[DataMemberAttribute('lineItems'),
AifCollectionTypeAttribute('_lineItems', Types::Class, classStr(LogisticsLineItemContract)),
AifCollectionTypeAttribute('return', Types::Class, classStr(LogisticsLineItemContract))]
public List parmLineItems(List _lineItems = lineItems)
{
lineItems = _lineItems;
return lineItems;
}
}
// 2. Response Contract returned to the external client
[DataContractAttribute]
public final class LogisticsShippingResponseContract
{
private boolean isAvailable;
private Amount estimatedCost;
private str carrierService;
[DataMemberAttribute('isAvailable')]
public boolean parmIsAvailable(boolean _isAvailable = isAvailable)
{
isAvailable = _isAvailable;
return isAvailable;
}
[DataMemberAttribute('estimatedCost')]
public Amount parmEstimatedCost(Amount _estimatedCost = estimatedCost)
{
estimatedCost = _estimatedCost;
return estimatedCost;
}
[DataMemberAttribute('carrierService')]
public str parmCarrierService(str _carrierService = carrierService)
{
carrierService = _carrierService;
return carrierService;
}
}
3. The Service Class & SysEntryPointAttribute
The Service Class contains the operational methods that execute business logic. Every public method meant to be exposed as a service endpoint must be decorated with the SysEntryPointAttribute.
Understanding [SysEntryPointAttribute(true)]
The boolean parameter on SysEntryPointAttribute dictates how the AOS handles authorization:
[SysEntryPointAttribute(true)](Best Practice / Required for Production): The framework performs a strict runtime security authorization check. It verifies that the authenticated calling user has been granted a Security Privilege or Duty that contains access to this specific service operation menu item. If the user lacks permissions, execution is denied with HTTP 403 Forbidden.[SysEntryPointAttribute(false)](Security Risk): Bypasses entry point authorization checks. The method executes under the caller's identity without verifying service-level privileges. This should never be used for sensitive or transactional business logic.
Complete Service Class Implementation Example
public final class LogisticsShippingService
{
/// <summary>
/// Calculates shipping availability and rate estimate.
/// </summary>
/// <param name = "_request">The shipping request data contract.</param>
/// <returns>A populated shipping response data contract.</returns>
[SysEntryPointAttribute(true)]
public LogisticsShippingResponseContract calculateShippingRate(LogisticsShippingRequestContract _request)
{
LogisticsShippingResponseContract response = new LogisticsShippingResponseContract();
if (!_request || !_request.parmCustomerAccount())
{
throw error("@SYS312345"); // Customer account is mandatory
}
// Invoke internal X++ business logic (e.g., inventory lookup & carrier rating)
CustTable custTable = CustTable::find(_request.parmCustomerAccount());
if (!custTable)
{
response.parmIsAvailable(false);
return response;
}
// Calculate pricing logic
Amount estimatedShipping = this.computeRateInternal(_request.parmItemId(), _request.parmQuantity());
response.parmIsAvailable(true);
response.parmEstimatedCost(estimatedShipping);
response.parmCarrierService("Standard Ground");
return response;
}
private Amount computeRateInternal(ItemId _item, Qty _qty)
{
// Internal calculation logic
return _qty * 4.50;
}
}
4. Service Groups & Automatic Endpoint Routing
In Dynamics 365 Finance and Operations, developers do not configure IIS web handlers, routing tables, or .svc files. Deployment is entirely metadata-driven through AOT Service Groups.
Service Group Configuration
- In Visual Studio Solution Explorer, right-click your model and select Add > New Item > Services > Service Group (e.g.,
LogisticsServiceGroup). - Set the property
AutoDeploy = Yes. - Right-click the Service Group node, select New Service, and reference your Service Class (e.g.,
LogisticsShippingService). - Save, build the solution, and synchronize database/metadata.
Endpoint URL Structure
Once deployed, the AOS engine automatically creates two distinct endpoints accessible over HTTPS:
1. JSON REST Endpoint
- URL Syntax:
https://<environment>.operations.dynamics.com/api/services/<ServiceGroup>/<ServiceName>/<OperationName> - Example:
https://contoso.operations.dynamics.com/api/services/LogisticsServiceGroup/LogisticsShippingService/calculateShippingRate - HTTP Method Constraint: Must always be HTTP
POST. Even if the operation only queries or reads data without altering state, the REST custom service engine rejects HTTPGETrequests with405 Method Not Allowed. The request parameters must be passed in the JSON body:{ "_request": { "customerAccount": "US-001", "itemId": "1000", "quantity": 25 } }
2. SOAP Endpoint
- URL Syntax:
https://<environment>.operations.dynamics.com/soap/services/<ServiceGroup>?wsdl - Example:
https://contoso.operations.dynamics.com/soap/services/LogisticsServiceGroup?wsdl - Emits full WSDL XML metadata for consumption by legacy SOAP clients, enterprise service buses (ESB), or .NET WCF proxies.
5. Authentication & Authorization Pipeline (Entra ID & F&O Setup)
Custom services do not support Basic Authentication or anonymous access. All communication requires token-based authentication via Microsoft Entra ID (formerly Azure Active Directory).
1. Microsoft Entra ID App Registration
To enable an external daemon application or integration middleware to authenticate:
- In the Azure Portal, navigate to Microsoft Entra ID > App registrations and click New registration.
- Note the Application (Client) ID and Directory (Tenant) ID.
- Under Certificates & secrets, generate a new Client Secret (or upload a public certificate).
- No redirect URI is required for daemon service-to-service flows.
2. OAuth 2.0 Client Credentials Grant
The external application issues an HTTP POST to the Microsoft Entra ID token endpoint:
- URL:
https://login.microsoftonline.com/<TenantId>/oauth2/v2.0/token - Body (
application/x-www-form-urlencoded):grant_type:client_credentialsclient_id:<Azure_App_Client_Id>client_secret:<Azure_App_Client_Secret>scope:https://<environment>.operations.dynamics.com/.default
- The token service returns an
access_token(JWT Bearer token).
3. F&O Registration: SysAADClientTable
An Entra ID token alone is insufficient; D365 F&O must map that token to an internal user identity.
- Navigate to System administration > Setup > Microsoft Entra ID applications (table
SysAADClientTable). - Click New and configure:
- Client ID: The exact Application (Client) ID registered in Entra ID.
- Name: Friendly descriptive name (e.g.,
ECommerce Logistics Middleware). - User ID: An active F&O service user account (e.g.,
SVC_Logistics).
[!IMPORTANT] Why Mapping to an F&O User ID is Mandatory: F&O relies on the assigned internal User ID to evaluate security roles, determine table-level permissions, enforce Extensible Data Security (XDS) policies, identify the default company (
DataAreaId), and populate database audit fields (CreatedBy,ModifiedBy). If the Client ID is not registered inSysAADClientTable, the AOS returns401 Unauthorized.
4. Role-Based Security Privileges
To satisfy [SysEntryPointAttribute(true)], developers must grant access to the service operation:
- Create a Security Privilege in Visual Studio.
- Under the privilege's Permissions > Service Operations node, add the service class operation (e.g.,
LogisticsShippingService.calculateShippingRate) and setAccessLevel = Correct. - Add the privilege to a Security Duty, and assign the duty to a Security Role.
- Assign the security role to the service account user mapped in
SysAADClientTable.
A developer writes an X++ custom service class method to post general journal batches submitted by an external payroll application. The developer decorates the operation method with [SysEntryPointAttribute(true)]. What is the operational effect of passing true to this attribute?
An external integration developer is attempting to call a newly deployed custom service endpoint using the following HTTP request: GET https://contoso.operations.dynamics.com/api/services/InventoryServiceGroup/InventoryService/getOnHandBalance. The request fails with HTTP status 405 Method Not Allowed. How should the developer resolve this issue?
An enterprise integration team has created an Azure App Registration with a client secret for a daemon middleware system. The middleware successfully acquires a valid JWT Bearer token from Microsoft Entra ID, but when it calls a D365 F&O custom service REST endpoint, the server returns HTTP 401 Unauthorized. What configuration step was omitted inside Dynamics 365 Finance and Operations?
A developer is authoring an X++ Data Contract class that contains a parameter method returning a List of child contract objects (SalesLineContract). When calling the custom service, the JSON array fails to deserialize into the X++ list. Which attribute is required on the parameter method to enable collection deserialization?