With the UAE's adoption of electronic invoicing, ERP systems are expected to communicate with approved Access Point providers that exchange invoices with the Federal Tax Authority (FTA).
In one of my recent Dynamics 365 Finance implementations, I developed a custom X++ service that performs the complete outbound e-Invoice submission process.
The solution
-
Builds the invoice payload from posted invoices
-
Calls the REST API of the Access Point
-
Receives the submission response
-
Stores the returned document identifiers
-
Periodically refreshes invoice status
Let's walk through the implementation.
Solution Architecture
Posted Sales Invoice
¦
?
submitInvoice()
¦
?
buildPayload()
¦
?
sendToFlick()
¦
?
updateResponse()
¦
?
refreshInvoiceStatus()
¦
?
updateStatusResponse()
The implementation follows a clean separation of responsibilities where every method performs one specific task.
Step 1 — Entry Point
The entire submission begins from a single public method.
public static void submitInvoice(CustInvoiceJour _jour)
{
str payload;
str response;
container result;
int statusCode;
payload = EinvoiceOutboundServiceCBS::buildPayload(_jour);
result = EinvoiceOutboundServiceCBS::sendToFlick(payload);
response = conPeek(result,1);
statusCode = conPeek(result,2);
EinvoiceOutboundServiceCBS::updateResponse(
_jour.InvoiceId,
response,
statusCode);
}
What happens here?
Instead of writing everything inside one method, the implementation follows a pipeline approach.
Each responsibility is delegated to another method.
Invoice
↓
Build Payload
↓
Call API
↓
Read Response
↓
Save Response
This makes debugging much easier because every stage can be tested independently.
Step 2 — Reading Data from Dynamics 365
The payload construction starts by collecting invoice information from standard D365 tables.
select firstonly custTable
where custTable.AccountNum == _jour.InvoiceAccount;
Invoice lines are collected using
while select trans
where trans.InvoiceId == _jour.InvoiceId
{
...
}
Totals are calculated using aggregation.
select sum(LineAmount)
from trans
where trans.InvoiceId == _jour.InvoiceId;
Instead of hardcoding totals, the implementation lets SQL Server perform the aggregation, which is considerably more efficient for invoices with many lines.
Step 3 — Constructing JSON using JObject
Rather than manually concatenating JSON strings, the implementation uses Newtonsoft.Json.Linq.
JObject root = new JObject();
JObject document = new JObject();
JObject receivingParty = new JObject();
JObject totals = new JObject();
JArray paymentMeansArr = new JArray();
JArray linesArr = new JArray();
Customer information is added as follows.
receivingParty.add("peppol_id",
"0235:" + custTable.VATNum);
receivingParty.add("vat_number",
custTable.VATNum);
receivingParty.add("legal_name",
custTable.name());
This approach is much cleaner than building JSON manually because:
-
No comma management
-
No quotation issues
-
Easy nesting
-
Easy maintenance
Step 4 — Building Invoice Lines
Each sales line becomes one JSON object.
while select trans
where trans.InvoiceId == _jour.InvoiceId
{
lineObj = new JObject();
lineObj.add("id",
int2str(trans.LineNum));
lineObj.add("quantity",
strFmt("%1",trans.Qty));
lineObj.add("line_extension_amount",
strFmt("%1",trans.LineAmount));
lineObj.add("unit_price",
strFmt("%1",trans.SalesPrice));
linesArr.add(lineObj);
}
The advantage of using a JArray is that the solution automatically supports invoices containing any number of lines without additional coding.
Step 5 — Handling Currency Exchange
One interesting requirement was supporting invoices that were not issued in AED.
if (_jour.CurrencyCode != 'AED')
{
select firstonly exchangeRate
order by ValidFrom desc
where exchangeRate.ValidFrom <= _jour.InvoiceDate
&& exchangeRate.ValidTo >= _jour.InvoiceDate;
rate = exchangeRate.ExchangeRate;
document.add(
"currency_exchange_rate",
num2Str(rate,0,6,1,0));
}
Instead of assuming AED, the implementation dynamically retrieves the applicable exchange rate based on the invoice date.
This makes the solution suitable for multinational organizations issuing invoices in multiple currencies.
Step 6 — Calling the REST API
The submission uses .NET's HttpClient directly from X++.
httpClient = new System.Net.Http.HttpClient();
content = new System.Net.Http.StringContent(
_payload,
System.Text.Encoding::UTF8,
"application/json");
response =
httpClient.PostAsync(
endpoint,
content).Result;
Authentication headers are added before the request.
httpClient.get_DefaultRequestHeaders().Add(
"X-Flick-Auth-Key",
apiKey);
httpClient.get_DefaultRequestHeaders().Add(
"Idempotency-Key",
guid2Str(newGuid()));
Using an Idempotency Key ensures duplicate API requests do not create duplicate invoices if a retry occurs.
Step 7 — Parsing the Response
Once the API returns a response, the JSON is parsed using JObject.
obj = JObject::Parse(_response);
dataToken = obj.GetValue("data");
token = obj.GetValue("status");
token = obj.GetValue("message");
Nested values are extracted as follows.
if (dataObj.GetValue("id"))
documentId =
dataObj.GetValue("id").ToString();
if (dataObj.GetValue("status"))
deliveryStatus =
dataObj.GetValue("status").ToString();
This avoids manual string manipulation and makes the code resilient to changes in JSON formatting.
Step 8 — Updating Dynamics 365
Finally, the response is stored inside a custom tracking table.
ttsBegin;
select firstOnly forUpdate tbl
where tbl.InvoiceNo == _invoiceId;
tbl.PeppolId = documentId;
tbl.TransferStatus = deliveryStatus;
tbl.ResponseMsg = message;
tbl.ResponseCode = _statusCode;
tbl.update();
ttsCommit;
Persisting these values enables users to:
-
Track submitted invoices
-
Troubleshoot failures
-
Store external document identifiers
-
Query reporting status later
Step 9 — Refreshing Invoice Status
Submission doesn't necessarily mean the invoice has been accepted by the FTA.
The solution therefore exposes another service.
response =
EinvoiceOutboundServiceCBS::callStatusApi(
_record.Peppolid);
EinvoiceOutboundServiceCBS::
updateStatusResponse(
_record.InvoiceNo,
response);
The Status API returns:
-
Reporting Reference
-
Reporting Status
-
Exchange Status
Those values are then written back into Dynamics 365 Finance.
Features of the Implementation
-
Clean separation of concerns with dedicated methods for payload creation, API communication, response parsing, and status synchronization.
-
Dynamic JSON generation using Newtonsoft.Json.Linq, eliminating manual string construction.
-
Multi-currency support with exchange rate retrieval based on the invoice date.
-
Secure REST integration using .NET HttpClient from X++.
-
Idempotent request handling to prevent duplicate submissions during retries.
-
Comprehensive response tracking, including HTTP status codes, document identifiers, and API messages.
-
Status synchronization to keep Dynamics 365 Finance aligned with the latest reporting state from the Access Point.
Conclusion
This implementation demonstrates how X++ can be used to build a robust outbound integration for UAE e-Invoicing without relying on external middleware. By leveraging standard Dynamics 365 Finance tables, Newtonsoft.Json for payload generation, and .NET HttpClient for REST communication, the solution delivers a complete end-to-end process—from invoice posting to status tracking.
Beyond meeting compliance requirements, the design emphasizes maintainability through modular methods, making it easier to extend the solution for additional Access Point providers, enhanced error handling, or future regulatory changes.
