Custom Services, REST & SOAP
Build custom service endpoints in F&O, consume external REST APIs from X++, work with SOAP services, leverage Electronic Reporting for integration, and use the Batch OData API.
Custom Services, REST & SOAP
OData and DMF handle most integration needs. But sometimes you need a custom endpoint β one that runs specific business logic, validates complex input, or returns a tailored response. And sometimes F&O needs to call out to an external API. This module covers both directions: building endpoints for others to call, and calling external services from X++.
Think of it like a restaurant.
OData is the buffet β standard dishes, self-service, pick what you want. Custom services are the Γ la carte menu β you define exactly what the kitchen prepares and how it is served. You write the βrecipeβ (service class), wrap it in a βmenuβ (service group), and publish it so external systems can order.
Going the other direction β when F&O needs to call an external API (like checking a tax service or sending an SMS) β that is like the restaurant phoning a supplier to get fresh ingredients mid-service.
Building Custom Services
A custom service in F&O consists of three pieces:
- Data contracts β classes that define the request and response structure
- Service class β the business logic (methods the external system calls)
- Service group β the deployment unit that exposes the service as an endpoint
Step 1: Data Contracts
Data contracts use the DataContractAttribute and DataMemberAttribute to define the shape of request and response objects.
[DataContractAttribute]
class InventoryCheckRequest
{
ItemId itemId;
InventLocationId warehouseId;
[DataMemberAttribute('ItemId')]
public ItemId parmItemId(ItemId _itemId = itemId)
{
itemId = _itemId;
return itemId;
}
[DataMemberAttribute('WarehouseId')]
public InventLocationId parmWarehouseId(InventLocationId _warehouseId = warehouseId)
{
warehouseId = _warehouseId;
return warehouseId;
}
}
[DataContractAttribute]
class InventoryCheckResponse
{
ItemId itemId;
real availableQty;
str status;
[DataMemberAttribute('ItemId')]
public ItemId parmItemId(ItemId _itemId = itemId)
{
itemId = _itemId;
return itemId;
}
[DataMemberAttribute('AvailableQuantity')]
public real parmAvailableQty(real _qty = availableQty)
{
availableQty = _qty;
return availableQty;
}
[DataMemberAttribute('Status')]
public str parmStatus(str _status = status)
{
status = _status;
return status;
}
}
Whatβs happening:
DataContractAttributemarks the class as serialisable for service communicationDataMemberAttribute('Name')defines the JSON/XML property name- The
parmmethod pattern is the standard X++ getter/setter β the optional parameter sets the value, the return gets it
Step 2: Service Class
The service class contains the actual business logic. Methods decorated with AifCollectionTypeAttribute (for collections) are exposed as service operations.
class InventoryCheckService
{
[AifCollectionTypeAttribute('return', Types::Class)]
public InventoryCheckResponse checkAvailability(InventoryCheckRequest _request)
{
InventoryCheckResponse response = new InventoryCheckResponse();
InventSum inventSum;
select sum(AvailPhysical) from inventSum
where inventSum.ItemId == _request.parmItemId()
&& inventSum.InventLocationId == _request.parmWarehouseId();
response.parmItemId(_request.parmItemId());
response.parmAvailableQty(inventSum.AvailPhysical);
response.parmStatus(inventSum.AvailPhysical > 0 ? "InStock" : "OutOfStock");
return response;
}
}
ποΈ Vikβs code review note: βAlways validate input in your service class. If someone passes an empty ItemId, donβt let it hit the database β throw a meaningful error. The exam tests whether you know to validate request parameters before processing.β
Step 3: Service Group
A service group is a metadata element that you create in Visual Studio:
- Add new item β Service Group (name:
InventoryCheckServiceGroup) - Add your service to the group β reference the
InventoryCheckServiceclass - Set the service method to
checkAvailability - Set Auto Deploy to
trueon the service group
Once deployed, the endpoint is available at:
POST https://your-environment.operations.dynamics.com/api/services/InventoryCheckServiceGroup/InventoryCheckService/checkAvailability
Request body:
{
"request": {
"ItemId": "D0001",
"WarehouseId": "WH-MAIN"
}
}
Exam tip: Service group auto-deploy
For the exam, remember that setting Auto Deploy = true on a service group means it is automatically available after deployment β no manual activation needed. If Auto Deploy is false, an admin must manually enable it in the Service groups form. The exam may present a scenario where a custom service βisnβt workingβ and the fix is enabling auto-deploy or manual activation.
Consuming External REST APIs from X++
When F&O needs to call an external API β a tax service, an address validation API, a shipping provider β you use .NETβs HttpClient via X++ interop.
Basic GET request
using System.Net.Http;
using System.Net.Http.Headers;
class ExternalApiCaller
{
public static str callExternalGet(str _url, str _bearerToken)
{
HttpClient httpClient = new HttpClient();
HttpResponseMessage response;
str responseBody;
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", _bearerToken);
response = httpClient.GetAsync(_url).Result;
if (response.IsSuccessStatusCode)
{
responseBody = response.Content.ReadAsStringAsync().Result;
}
else
{
throw error(strFmt("API call failed: %1 %2",
response.StatusCode, response.ReasonPhrase));
}
httpClient.Dispose();
return responseBody;
}
}
POST request with JSON body
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
public static str callExternalPost(str _url, str _bearerToken, str _jsonBody)
{
HttpClient httpClient = new HttpClient();
HttpResponseMessage response;
StringContent content;
str responseBody;
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", _bearerToken);
content = new StringContent(_jsonBody, Encoding::UTF8, "application/json");
response = httpClient.PostAsync(_url, content).Result;
if (response.IsSuccessStatusCode)
{
responseBody = response.Content.ReadAsStringAsync().Result;
}
else
{
throw error(strFmt("API call failed: %1", response.StatusCode));
}
httpClient.Dispose();
return responseBody;
}
ποΈ Vikβs caution: βTwo things will bite you with external API calls from X++. First, always dispose HttpClient β F&O doesnβt garbage-collect .NET objects aggressively in batch jobs. Second, these calls are synchronous and block the thread. If the external API is slow, your X++ logic stalls. For batch scenarios, add timeout handling.β
Exam tip: .NET interop in X++
X++ can use .NET classes via the using statement. The exam may test whether you know that System.Net.Http.HttpClient is the correct class for REST calls (not WebClient, which is obsolete). Also know that async/await doesnβt work the same way in X++ β you call .Result to block and get the synchronous result.
SOAP Services
SOAP (Simple Object Access Protocol) uses XML-based messaging with a formal WSDL contract. While REST has largely replaced SOAP for new integrations, F&O still supports SOAP in two ways:
Custom services over SOAP
Every custom service group is exposed as both REST and SOAP. The SOAP endpoint:
https://your-environment.operations.dynamics.com/soap/services/InventoryCheckServiceGroup
Use the WSDL to generate a client proxy in the consuming application.
Legacy document services
Carried forward from AX 2012, document services provide SOAP-based CRUD operations on entities. These are being replaced by OData but still appear in some older integrations.
| Aspect | REST (JSON) | SOAP (XML) |
|---|---|---|
| Format | JSON (lightweight) | XML with SOAP envelope (verbose) |
| Contract | Implicit (URL + HTTP methods) | Explicit WSDL definition |
| Discovery | Browse OData $metadata | Download WSDL document |
| Usage in F&O | Primary / recommended | Legacy, still supported |
| Custom services | Supported | Supported (same service group) |
| Performance | Faster (smaller payload) | Slower (XML parsing overhead) |
| Modern tooling | Excellent (Postman, Power Automate) | Limited (SoapUI, .NET WCF) |
Electronic Reporting (ER)
Electronic Reporting is a configuration-based framework for generating and parsing structured documents β invoices, reports, regulatory filings, data files. While primarily used for document generation, it doubles as a powerful integration tool.
ER as an integration tool
- Outbound: Generate XML, JSON, CSV, or Excel files from F&O data in a specific format required by an external system
- Inbound: Parse incoming files and map their content to F&O entities
- No code required β configurations are built in a visual designer
- Versioning β ER configurations are versioned and can be imported from Microsoftβs Global Repository
π Marcus explaining to Anita: βYour bank requires payment files in a very specific XML format that no standard export gives you. Electronic Reporting lets us configure the exact format they need β tag names, namespace, element order β all without writing a single line of X++.β
Key ER concepts
| Concept | Description |
|---|---|
| Data model | Abstract representation of business data (entity relationships) |
| Model mapping | Connects the data model to actual F&O data sources (tables, entities) |
| Format | The output structure β XML schema, JSON structure, CSV layout |
| Configuration provider | Who owns the configuration (Microsoft, ISV, or custom) |
| Global Repository | Microsoft-hosted library of pre-built ER configurations |
Batch OData API
The Batch OData API allows external systems to create, monitor, and manage batch jobs in F&O programmatically.
What you can do
- Create batch jobs and add tasks via OData
- Monitor batch job status
- Trigger batch job execution
- Query batch job history
Why it matters
Integration scenarios often need to trigger processing that runs as batch jobs (e.g., βprocess all pending invoicesβ). Instead of manually running the batch from the UI, an external orchestrator (like Power Automate or Azure Logic Apps) can call the Batch OData API.
POST https://your-environment.operations.dynamics.com/data/BatchJobs
π Marcusβs use case: βAnitaβs middleware pushes product data via DMF, then needs to trigger a price recalculation batch job. Instead of someone clicking βRunβ in F&O, her middleware calls the Batch OData API to create and execute the job automatically. End-to-end automation.β
Exam tip: Batch OData vs business events
Donβt confuse these. The Batch OData API is for externally triggering batch jobs in F&O (inbound control). Business events (Module 25) are for F&O notifying external systems when something happens (outbound notification). They are complementary β a business event might tell middleware βinvoice postedβ, then middleware calls the Batch OData API to trigger a follow-up job.
Putting It Together: Integration Architecture
A real integration often combines multiple APIs:
External System
β
ββ REST call βββ Custom Service (validate/lookup)
β
ββ DMF enqueue βββ Recurring Import (bulk data)
β
ββ Batch OData βββ Trigger processing job
β
ββ Subscribes to βββ Business Event (notifications)
π Marcusβs architecture whiteboard: βThe best integrations use each API for what it is good at. Custom services for interactive logic, DMF for bulk data, Batch API for triggering jobs, business events for notifications. Trying to do everything through one API is where projects fail.β
Exam Practice
A partner needs to expose a custom inventory validation endpoint that accepts an item ID and returns availability with a custom status code. What should they build?
Vik needs to call an external tax calculation API from X++ during sales order creation. Which .NET class should he use?
A custom service has been deployed but external callers receive a 404 error. The service class and data contracts are correct. What is the most likely cause?
Next up: Dual-Write & Virtual Entities β real-time bidirectional sync between F&O and Dataverse, virtual entities for read-without-copy, and Power Platform integration patterns.