Many German and Austrian companies still run Microsoft Dynamics NAV. All of them must now send and receive structured e-invoices. Since January 2025, German companies must accept e-invoices. From 2027, they must send them. The plain PDF from the NAV report does not count. The law wants a hybrid file: a PDF the customer can open, with XML inside that their software can read. This file is ZUGFeRD.
This guide adds ZUGFeRD to Dynamics NAV 2018. You need three C/AL codeunits and one REST API. The code only uses the HttpClient and JsonObject types that come with NAV 2018. The same code also creates XRechnung for public buyers and Factur-X for French customers. The import section shows how to read supplier invoices into NAV. The DotNet section covers NAV 2017 and older. If you already use Business Central, read the Business Central guide instead. It shows the same integration in AL.
Why NAV needs ZUGFeRD now
The law asks for a specific format. In Germany, companies must accept e-invoices since January 2025. Large companies must send them from 2027, all companies from 2028. A ZUGFeRD file in the EN 16931 profile or higher is valid. A normal PDF is not. The Germany mandate page lists the dates and formats. German public buyers need XRechnung since 2020. France needs structured e-invoices from September 2026.
NAV stops at the printed PDF. Posting creates the Sales Invoice Header and Lines. The report creates a PDF. The document sending profile emails it. Nothing in NAV creates a PDF/A-3 file with the embedded factur-x.xml. No update for NAV will add this.
The Business Central migration is too late. Many NAV companies plan to move to Business Central. That often takes years. The deadline is closer. The integration in this guide takes about one day. It carries over to Business Central without changes. See the migration section.
The rules are complex. EN 16931 has about 200 business rules. The VAT amounts must match to the cent. The PDF must follow PDF/A-3 with exact metadata. Germany and France add their own rules. All of these change over time. The technical overview of ZUGFeRD explains each layer.
The API handles all of this on the server. NAV sends the invoice data it already has. The API returns a finished, validated file.
What NAV 2018 includes
NAV 2018 is the first version with HTTP and JSON types in C/AL: HttpClient, HttpContent, HttpHeaders, HttpResponseMessage, JsonObject, JsonArray and JsonToken. They run on the service tier. You need no DotNet add-in and no client component. Business Central uses the same types with the same method names. This is why the code in this guide looks almost the same as the Business Central version.
NAV 2018 does not have two things that Business Central has. There is no Isolated Storage for secrets, so the API key goes into a setup table with a masked field. There is no Temp Blob codeunit, so binary data uses the TempBlob record with its Blob field. Everything else is there, including Document Attachment and Incoming Document.
NAV 2018 also runs AL extensions. If your partner works in Visual Studio Code instead of C/SIDE, use the AL code from the Business Central guide. It compiles for NAV 2018 with the two changes above.
How the integration works
| Customer | Format | Endpoint | Result |
|---|---|---|---|
| German business (B2B) | ZUGFeRD EN 16931 | /v1/create/zugferd | Hybrid PDF/A-3 |
| German public buyer (B2G) | XRechnung | /v1/create/xrechnung | XML with Leitweg-ID |
| French business | Factur-X EN 16931 | /v1/create/facturx | Hybrid PDF/A-3 |
| Peppol recipient | Peppol BIS | /v1/create/ubl | UBL XML |
All four endpoints take the same JSON body. The codeunit picks the endpoint by the customer's Country/Region Code and a public-sector flag. One request per posted invoice. One validated file back. The file is saved where NAV users expect it.
You create four new objects. They are not standard NAV objects. You create them from the listings in this guide, in your own object range. The numbers below are examples. Use any free numbers between 50000 and 99999.
- Table 50100 "InvoiceXML Setup": the API key and the import settings.
- Codeunit 50100 "InvoiceXML Setup Mgt.": reads the setup table.
- Codeunit 50101 "InvoiceXML Create": builds the JSON, calls the API and attaches the PDF (steps 2 to 4).
- Codeunit 50102 "InvoiceXML Import": reads supplier invoices and creates purchase invoices.
Everything else is standard NAV 2018: Sales Invoice Header and Line, Company Information, General Ledger Setup, Document Attachment, Vendor, Purchase Header and Line, and the TempBlob record.
Step 1: Store the API key
Create a free InvoiceXML account. You get an API key and 100 free credits. No credit card is needed. In NAV 2018, store the key in a setup table. Set ExtendedDatatype = Masked on the field so the page never shows it. Limit access to the table with a permission set.
// Table 50100 "InvoiceXML Setup"
// Fields:
// 1 Primary Key Code[10]
// 2 API Key Text[250] ExtendedDatatype = Masked
// 3 Base URL Text[250] InitValue = https://api.invoicexml.com
// 4 Purchase G/L Account Code[20] TableRelation = "G/L Account"
// 5 Review Threshold Decimal confidence below this waits for review
// Codeunit 50100 "InvoiceXML Setup Mgt."
// Locals: Setup : Record "InvoiceXML Setup"
PROCEDURE GetApiKey() : Text
BEGIN
Setup.GET;
Setup.TESTFIELD("API Key");
EXIT(Setup."API Key");
END;
PROCEDURE BaseUrl() : Text
BEGIN
Setup.GET;
IF Setup."Base URL" = '' THEN
EXIT('https://api.invoicexml.com');
EXIT(Setup."Base URL");
END;
PROCEDURE PurchaseGLAccount() : Code[20]
BEGIN
Setup.GET;
Setup.TESTFIELD("Purchase G/L Account");
EXIT(Setup."Purchase G/L Account");
END;
The codeunit and the table have different names on purpose. C/AL allows the same name for both, but two names are easier to read in the Locals window. All other functions in this guide use the codeunit, never the table.
C/AL declares variables in the Locals window of each function, not in the code. Each listing in this guide lists its variables in a comment at the top. Create them with the given types, then paste the code into C/SIDE.
Step 2: Map NAV fields to JSON
The API takes an EN 16931 invoice as JSON. All the data is already on the posted invoice and in Company Information. This is the mapping:
| Dynamics NAV field | JSON path | Note |
|---|---|---|
Sales Invoice Header."No." | invoice.invoiceNumber | BT-1 |
"Posting Date" | invoice.issueDate | Format as yyyy-MM-dd |
"Currency Code" | invoice.currency | Blank means LCY. Use the LCY code from General Ledger Setup |
| Company Information (Name, VAT Registration No., address) | invoice.seller | The seller VAT id is required |
"Sell-to Customer Name" and the sell-to address fields | invoice.buyer | Country as ISO code (DE, AT, FR) |
| Company Information IBAN | invoice.paymentDetails.paymentAccountIdentifier | Recommended for B2B |
Sales Invoice Line.Description | lines[].item.name | Per line |
Quantity / "Unit Price" | lines[].quantity / lines[].priceDetails.netPrice | Net prices. The API calculates the totals |
"VAT %" | lines[].vatInformation.rate | The API calculates the VAT per rate |
"Your Reference" or a custom field | invoice.buyerReference | Holds the Leitweg-ID for XRechnung |
You do not send totals, rounding or VAT amounts. The API calculates them from the lines. If the numbers do not add up, the API returns an error instead of a file. This catches rounding and discount problems before the customer sees them. If your invoices use prices including VAT (Prices Including VAT on the header), send the net unit price: the line Amount divided by Quantity, not the Unit Price.
In C/AL, the mapping is a function that builds a JsonObject and returns it as text:
// Codeunit 50101 "InvoiceXML Create"
// Locals of BuildInvoiceJson:
// SalesInvLine : Record "Sales Invoice Line"
// CompanyInfo : Record "Company Information"
// GLSetup : Record "General Ledger Setup"
// Body, Invoice, Seller, SellerAddress, Buyer, BuyerAddress,
// Payment, LineObj, Item, Price, Vat : JsonObject
// Lines : JsonArray
// CurrencyCode : Code[10]
LOCAL PROCEDURE BuildInvoiceJson(SalesInvHeader : Record "Sales Invoice Header") BodyText : Text
BEGIN
CompanyInfo.GET;
GLSetup.GET;
Invoice.Add('invoiceNumber', SalesInvHeader."No.");
Invoice.Add('issueDate', FORMAT(SalesInvHeader."Posting Date", 0, '<Year4>-<Month,2>-<Day,2>'));
CurrencyCode := SalesInvHeader."Currency Code";
IF CurrencyCode = '' THEN
CurrencyCode := GLSetup."LCY Code";
Invoice.Add('currency', CurrencyCode);
Seller.Add('name', CompanyInfo.Name);
Seller.Add('vatIdentifier', CompanyInfo."VAT Registration No.");
SellerAddress.Add('line1', CompanyInfo.Address);
SellerAddress.Add('city', CompanyInfo.City);
SellerAddress.Add('postCode', CompanyInfo."Post Code");
SellerAddress.Add('country', CompanyInfo."Country/Region Code");
Seller.Add('postalAddress', SellerAddress);
Invoice.Add('seller', Seller);
Buyer.Add('name', SalesInvHeader."Sell-to Customer Name");
BuyerAddress.Add('line1', SalesInvHeader."Sell-to Address");
BuyerAddress.Add('city', SalesInvHeader."Sell-to City");
BuyerAddress.Add('postCode', SalesInvHeader."Sell-to Post Code");
BuyerAddress.Add('country', SalesInvHeader."Sell-to Country/Region Code");
Buyer.Add('postalAddress', BuyerAddress);
Invoice.Add('buyer', Buyer);
IF CompanyInfo.IBAN <> '' THEN BEGIN
Payment.Add('paymentAccountIdentifier', CompanyInfo.IBAN);
Invoice.Add('paymentDetails', Payment);
END;
SalesInvLine.SETRANGE("Document No.", SalesInvHeader."No.");
SalesInvLine.SETFILTER(Type, '<>%1', SalesInvLine.Type::" ");
SalesInvLine.SETFILTER(Quantity, '<>0');
IF SalesInvLine.FINDSET THEN
REPEAT
CLEAR(LineObj);
CLEAR(Item);
CLEAR(Price);
CLEAR(Vat);
LineObj.Add('quantity', SalesInvLine.Quantity);
Item.Add('name', SalesInvLine.Description);
LineObj.Add('item', Item);
Price.Add('netPrice', SalesInvLine."Unit Price");
LineObj.Add('priceDetails', Price);
Vat.Add('rate', SalesInvLine."VAT %");
LineObj.Add('vatInformation', Vat);
Lines.Add(LineObj);
UNTIL SalesInvLine.NEXT = 0;
Invoice.Add('lines', Lines);
Body.Add('invoice', Invoice);
Body.WriteTo(BodyText);
END;
This is already a valid EN 16931 invoice. You can add line discounts, item numbers and units in the same way. See the priceDetails, item and unitCode fields in the create reference. The filter on Type::" " skips comment lines. NAV stores them as lines with a blank type and no quantity.
Step 3: Create the ZUGFeRD invoice
This function sends the JSON to the API and receives the PDF. The PDF goes into a TempBlob record. The caller can then attach it, save it or send it.
// Codeunit 50101 "InvoiceXML Create"
// Locals of CreateHybridInvoice:
// Setup : Codeunit "InvoiceXML Setup Mgt."
// Client : HttpClient
// Content : HttpContent
// ContentHeaders : HttpHeaders
// Response : HttpResponseMessage
// BodyText : Text
// ErrorText : Text
// Endpoint : Text
// PdfOutStream : OutStream
// ResponseInStream : InStream
PROCEDURE CreateHybridInvoice(SalesInvHeader : Record "Sales Invoice Header";VAR TempBlob : Record TempBlob)
BEGIN
BodyText := BuildInvoiceJson(SalesInvHeader);
// Same format, two names: pick the endpoint by the buyer's country
IF SalesInvHeader."Sell-to Country/Region Code" = 'FR' THEN
Endpoint := '/v1/create/facturx'
ELSE
Endpoint := '/v1/create/zugferd';
Content.WriteFrom(BodyText);
Content.GetHeaders(ContentHeaders);
ContentHeaders.Remove('Content-Type');
ContentHeaders.Add('Content-Type', 'application/json');
Client.DefaultRequestHeaders.Add('Authorization', 'Bearer ' + Setup.GetApiKey);
IF NOT Client.Post(Setup.BaseUrl + Endpoint, Content, Response) THEN
ERROR('The InvoiceXML API could not be reached.');
IF NOT Response.IsSuccessStatusCode THEN BEGIN
Response.Content.ReadAs(ErrorText);
ERROR('E-invoice generation failed:\%1', ErrorText);
END;
Response.Content.ReadAs(ResponseInStream);
TempBlob.Blob.CREATEOUTSTREAM(PdfOutStream);
COPYSTREAM(PdfOutStream, ResponseInStream);
END;
The API returns a finished file: a PDF/A-3 container with the embedded factur-x.xml and the XMP metadata for the EN 16931 profile. The file is validated on the server before it is sent. If the data is not valid, the API returns HTTP 400 with a list of the broken rules. The error path reads this response and shows it. The message names the field the user must fix. NAV's ERROR rolls back the transaction, so nothing incomplete is saved.
The service tier needs outbound HTTPS access to api.invoicexml.com. If your server is behind a proxy, set the proxy for the NAV Server service account, as for any other outbound call.
Step 4: Attach the PDF to the posted invoice
Save the PDF as a document attachment on the posted invoice. It then shows in the Attachments fact box on the Posted Sales Invoice page. Your existing document sending can pick it up from there.
// Codeunit 50101 "InvoiceXML Create"
// Locals of AttachToPostedInvoice:
// DocumentAttachment : Record "Document Attachment"
// RecRef : RecordRef
PROCEDURE AttachToPostedInvoice(SalesInvHeader : Record "Sales Invoice Header";VAR TempBlob : Record TempBlob)
BEGIN
RecRef.GETTABLE(SalesInvHeader);
DocumentAttachment.SaveAttachment(RecRef, SalesInvHeader."No." + '-zugferd.pdf', TempBlob);
END;
Call the two functions from an action on the Posted Sales Invoice page for a manual button. To create the file automatically on posting, subscribe to the OnAfterPostSalesDoc event in codeunit 80. Every created file is validated on the server, so you need no extra check before sending. If you receive hybrid PDFs from other systems, send them to POST /v1/validate/zugferd as a multipart upload. It returns the same structured result. The online validator runs the same check in the browser.
NAV's document sending profiles attach the report PDF to the email. Change your email code to attach the hybrid PDF from the attachment table instead. The hybrid PDF already contains the readable invoice. The customer gets one PDF that opens normally and that their software can also read.
Import incoming supplier invoices
Since January 2025, German companies must accept e-invoices. More and more supplier invoices will arrive as ZUGFeRD PDFs. Some suppliers will still send plain PDFs. Two endpoints turn both into data for a purchase invoice. NAV's Incoming Documents feature is the right place to keep the original files.
ZUGFeRD and Factur-X files: send them to POST /v1/extract/json. The API reads the embedded XML (or a standalone CII or UBL file) and returns it as InvoiceDocument JSON. No AI is involved. You book exactly what the supplier declared.
Plain PDFs: send them to POST /v1/parse/json. The API reads typed, scanned and photographed invoices with AI. It returns the same JSON plus a confidence object with scores between 0.0 and 1.0, in total and per area. Your code can import invoices with a high score and send low scores to a review page. Files that are not invoices are rejected with error code 4008.
// Codeunit 50102 "InvoiceXML Import"
// Locals of ExtractIncomingInvoice:
// Setup : Codeunit "InvoiceXML Setup Mgt."
// Client : HttpClient
// Content : HttpContent
// ContentHeaders : HttpHeaders
// Response : HttpResponseMessage
// TempBody : Record TempBlob (Temporary = Yes)
// FileInStream : InStream
// BodyInStream : InStream
// BodyOutStream : OutStream
// Boundary : Text
// ResponseText : Text
PROCEDURE ExtractIncomingInvoice(VAR TempBlob : Record TempBlob;FileName : Text;VAR Extracted : JsonObject)
BEGIN
// The endpoint takes a multipart form with one field named "file"
Boundary := 'InvoiceXML' + DELCHR(FORMAT(CREATEGUID), '=', '{}-');
TempBody.Blob.CREATEOUTSTREAM(BodyOutStream);
BodyOutStream.WRITETEXT('--' + Boundary);
BodyOutStream.WRITETEXT();
BodyOutStream.WRITETEXT('Content-Disposition: form-data; name="file"; filename="' + FileName + '"');
BodyOutStream.WRITETEXT();
BodyOutStream.WRITETEXT('Content-Type: application/octet-stream');
BodyOutStream.WRITETEXT();
BodyOutStream.WRITETEXT();
TempBlob.Blob.CREATEINSTREAM(FileInStream);
COPYSTREAM(BodyOutStream, FileInStream);
BodyOutStream.WRITETEXT();
BodyOutStream.WRITETEXT('--' + Boundary + '--');
TempBody.Blob.CREATEINSTREAM(BodyInStream);
Content.WriteFrom(BodyInStream);
Content.GetHeaders(ContentHeaders);
ContentHeaders.Remove('Content-Type');
ContentHeaders.Add('Content-Type', 'multipart/form-data; boundary=' + Boundary);
Client.DefaultRequestHeaders.Add('Authorization', 'Bearer ' + Setup.GetApiKey);
IF NOT Client.Post(Setup.BaseUrl + '/v1/extract/json', Content, Response) THEN
ERROR('The InvoiceXML API could not be reached.');
Response.Content.ReadAs(ResponseText);
IF NOT Response.IsSuccessStatusCode THEN
ERROR('Extraction failed:\%1', ResponseText);
Extracted.ReadFrom(ResponseText);
END;
Some e-invoices contain attached files, such as delivery notes or timesheets (EN 16931 group BG-24). This is common on German public sector invoices. POST /v1/extract/attachments returns these files as one ZIP. See the attachment extraction reference.
Create a purchase invoice from the JSON
Both endpoints return the same InvoiceDocument JSON. One import function works for both. It finds the vendor by the VAT id from the invoice, creates the purchase header, and adds one purchase line per invoice line. JsonObject.SelectToken reads a value by its path, which keeps the code short.
// Codeunit 50102 "InvoiceXML Import"
// Locals of CreatePurchaseInvoice:
// Setup : Codeunit "InvoiceXML Setup Mgt."
// GLSetup : Record "General Ledger Setup"
// Vendor : Record Vendor
// PurchHeader : Record "Purchase Header"
// PurchLine : Record "Purchase Line"
// Token : JsonToken
// LineToken : JsonToken
// Lines : JsonArray
// Line : JsonObject
// i : Integer
// LineNo : Integer
PROCEDURE CreatePurchaseInvoice(Extracted : JsonObject) : Code[20]
BEGIN
// Find the vendor by the VAT id the API read from the invoice
Extracted.SelectToken('invoice.seller.vatIdentifier', Token);
Vendor.SETRANGE("VAT Registration No.", Token.AsValue.AsText);
IF NOT Vendor.FINDFIRST THEN
ERROR('No vendor with VAT registration no. %1.', Token.AsValue.AsText);
PurchHeader.INIT;
PurchHeader."Document Type" := PurchHeader."Document Type"::Invoice;
PurchHeader.INSERT(TRUE);
PurchHeader.VALIDATE("Buy-from Vendor No.", Vendor."No.");
Extracted.SelectToken('invoice.invoiceNumber', Token);
PurchHeader.VALIDATE("Vendor Invoice No.", Token.AsValue.AsText);
Extracted.SelectToken('invoice.issueDate', Token);
PurchHeader.VALIDATE("Document Date", Token.AsValue.AsDate);
IF Extracted.SelectToken('invoice.dueDate', Token) THEN
PurchHeader.VALIDATE("Due Date", Token.AsValue.AsDate);
GLSetup.GET;
IF Extracted.SelectToken('invoice.currency', Token) THEN
IF Token.AsValue.AsText <> GLSetup."LCY Code" THEN
PurchHeader.VALIDATE("Currency Code", Token.AsValue.AsText);
PurchHeader.MODIFY(TRUE);
Extracted.SelectToken('invoice.lines', Token);
Lines := Token.AsArray;
FOR i := 0 TO Lines.Count - 1 DO BEGIN
Lines.Get(i, LineToken);
Line := LineToken.AsObject;
LineNo += 10000;
PurchLine.INIT;
PurchLine."Document Type" := PurchHeader."Document Type";
PurchLine."Document No." := PurchHeader."No.";
PurchLine."Line No." := LineNo;
PurchLine.INSERT(TRUE);
PurchLine.VALIDATE(Type, PurchLine.Type::"G/L Account");
PurchLine.VALIDATE("No.", Setup.PurchaseGLAccount);
Line.SelectToken('item.name', Token);
PurchLine.VALIDATE(Description, COPYSTR(Token.AsValue.AsText, 1, MAXSTRLEN(PurchLine.Description)));
Line.SelectToken('quantity', Token);
PurchLine.VALIDATE(Quantity, Token.AsValue.AsDecimal);
Line.SelectToken('priceDetails.netPrice', Token);
PurchLine.VALIDATE("Direct Unit Cost", Token.AsValue.AsDecimal);
Line.SelectToken('vatInformation.rate', Token);
IF PurchLine."VAT %" <> Token.AsValue.AsDecimal THEN
ERROR('Line %1: the invoice states %2% VAT, the posting setup gives %3%.',
LineNo, Token.AsValue.AsDecimal, PurchLine."VAT %");
PurchLine.MODIFY(TRUE);
END;
EXIT(PurchHeader."No.");
END;
Three things come from your setup, not from the API:
- The G/L account. Add a
Purchase G/L Accountfield to the setup table. If you keep supplier article numbers on your item cards, mapitem.sellerAssignedIdto an item number instead. - The VAT rate. NAV takes it from the posting groups. The code compares it with the rate on the invoice. A mismatch is an error, so a wrong rate never posts without anyone noticing.
- The totals. The code does not write them. After posting, compare
totals.grandTotalAmountfrom the JSON with the amount including VAT on the purchase header.
For files from /v1/parse/json, check confidence.overall before you call this function. Below the threshold, put the file in a review queue. Keep the threshold in the setup table, so finance can change it without a code change.
Keep the original file with the document. Link the supplier's PDF as an Incoming Document on the purchase header before posting. The original then stays attached to the posted invoice, as GoBD requires.
XRechnung for German public buyers
Invoices to German federal, state and municipal buyers must be XRechnung, not ZUGFeRD. XRechnung is pure XML with no PDF. Portals such as ZRE and OZG-RE validate it against the KoSIT rules. The same codeunit creates it. You need a third endpoint and two more fields:
The Leitweg-ID goes in buyerReference. The public buyer gives you this routing id when you start working with them. Rule BR-DE-15 rejects invoices without it. Store it on the customer card in its own field, and map it into the request.
Seller contact details are required. XRechnung needs the seller contact (name, phone, email) and electronic addresses for both parties. EN 16931 makes these optional, XRechnung does not. Company Information holds all of them.
Call POST /v1/create/xrechnung with the extended body. The API returns XRechnung XML, validated against EN 16931 and the current KoSIT rules. Pure XML is hard to read for the accounting team. POST /v1/render/xrechnung/to/pdf creates a readable PDF preview. Attach it next to the XML. Only the XML goes to the portal. The XRechnung API toolkit explains the full B2G process, including validation and error handling.
Factur-X for French customers
ZUGFeRD and Factur-X are the same standard. The German FeRD and the French FNFE-MPE publish it together. The XML is the same, the PDF/A-3 container is the same, and the embedded file name is the same. Only the name differs. This is why the create function only checks the customer's country. The JSON body stays the same. Only the endpoint changes to /v1/create/facturx. The result is ready for the French e-invoicing platforms. If you invoice French companies from NAV, one codeunit covers both countries. The Factur-X API toolkit covers the French side in detail.
NAV 2017 and older: use DotNet
Versions before NAV 2018 have no HTTP or JSON types. But every version since NAV 2013 R2 can use DotNet interop on the service tier. The .NET Framework there includes System.Net.Http.HttpClient. The request, the endpoints and the returned files are the same. Only the code around them changes. Declare the DotNet variables with RunOnClient = No so the call runs on the server.
// Codeunit 50101 "InvoiceXML Create": CreateHybridInvoice for NAV 2013 R2 to NAV 2017
// Locals:
// Setup : Codeunit "InvoiceXML Setup Mgt."
// HttpClient : DotNet System.Net.Http.HttpClient
// Content : DotNet System.Net.Http.StringContent
// Response : DotNet System.Net.Http.HttpResponseMessage
// Encoding : DotNet System.Text.Encoding
// MemStream : DotNet System.IO.MemoryStream
// ResultBytes : DotNet System.Array
// BodyText : Text
// PdfOutStream : OutStream
PROCEDURE CreateHybridInvoice(SalesInvHeader : Record "Sales Invoice Header";VAR TempBlob : Record TempBlob)
BEGIN
BodyText := BuildInvoiceJson(SalesInvHeader);
HttpClient := HttpClient.HttpClient;
HttpClient.DefaultRequestHeaders.Add('Authorization', 'Bearer ' + Setup.GetApiKey);
Content := Content.StringContent(BodyText, Encoding.UTF8, 'application/json');
Response := HttpClient.PostAsync(Setup.BaseUrl + '/v1/create/zugferd', Content).Result;
IF NOT Response.IsSuccessStatusCode THEN
ERROR('E-invoice generation failed:\%1', Response.Content.ReadAsStringAsync.Result);
ResultBytes := Response.Content.ReadAsByteArrayAsync.Result;
TempBlob.Blob.CREATEOUTSTREAM(PdfOutStream);
MemStream := MemStream.MemoryStream(ResultBytes);
MemStream.CopyTo(PdfOutStream);
END;
To build the JSON on these versions, use a System.Text.StringBuilder and escape the text fields. Or use the Newtonsoft JObject types that NAV 2016 and later ship in the Add-ins folder. The field mapping is the same as in the table above. Older servers may need TLS 1.2 enabled in the .NET Framework. A registry setting on the NAV Server machine does this. api.invoicexml.com accepts TLS 1.2 and later only.
Moving to Business Central later
Only the language is specific to NAV. The JSON body, the four endpoints, the error handling and the country check are the contract. They are the same in the Business Central guide, which shows the same functions in AL. To move the codeunit:
- HttpClient and JsonObject keep their names and methods. C/AL keywords in capitals become AL keywords in lower case. The Locals window becomes a
varblock in the code. - The setup table is replaced by Isolated Storage. The key is then never stored in a table.
- The TempBlob record is replaced by the Temp Blob codeunit with
CreateOutStreamandCreateInStream.
The field mapping, the Leitweg-ID field on the customer card, the country check and the import flow stay the same. The current ZUGFeRD, Factur-X and XRechnung rules are always live on the server, before and after the migration. Doing the integration in NAV now does not add work later. It removes one item from the migration project.
The API stores nothing. Documents are processed in memory and deleted when the response is sent. Nothing is logged, and no invoice data is used to train any model. Your VAT numbers, IBANs and customer data never leave your database for longer than one request. Behind the API is a team that works on European e-invoicing full time, with professional support. Missing features are added on request.
Endpoint reference
| Operation | Endpoint | Input | Output |
|---|---|---|---|
| Create ZUGFeRD | POST /v1/create/zugferd | JSON | PDF/A-3 binary |
| Create XRechnung | POST /v1/create/xrechnung | JSON | XRechnung XML |
| Create Factur-X | POST /v1/create/facturx | JSON | PDF/A-3 binary |
| Validate ZUGFeRD / Factur-X | POST /v1/validate/zugferd, /v1/validate/facturx | Hybrid PDF | Validation JSON |
| Validate XRechnung | POST /v1/validate/xrechnung | XRechnung XML | Validation JSON |
| Extract as JSON | POST /v1/extract/json | Hybrid PDF or CII/UBL XML | Structured JSON |
| Parse PDF with AI | POST /v1/parse/json | Any invoice PDF (typed, scanned, photo) | Structured JSON + confidence |
| Extract attachments | POST /v1/extract/attachments | Hybrid PDF or CII/UBL XML | ZIP archive |
| Render XRechnung as PDF | POST /v1/render/xrechnung/to/pdf | XRechnung XML | PDF binary |
Full OpenAPI 3.1 schema: api.invoicexml.com/v1/openapi | Interactive API explorer: api.invoicexml.com/v1/scalar
Get started
The whole integration is the three codeunits in this guide: store the key, build the JSON, call the endpoint, attach the result. No DotNet add-ins on NAV 2018. No format logic in your database. Nothing to change when the formats change or when you move to Business Central. This guide is part of the integrations hub, which also covers Business Central, Power Automate, Zapier and other platforms.
Create a free InvoiceXML account → get 100 credits for free, no credit card required.
Related resources:
- E-invoicing in Dynamics 365 Business Central: ZUGFeRD and Factur-X with one API call
- E-invoicing in Power Automate: ZUGFeRD, XRechnung, Factur-X and UBL from Dynamics 365
- ZUGFeRD API: the complete toolkit for German B2B
- XRechnung API: the complete toolkit for German B2G
- Factur-X API: the complete toolkit
- ZUGFeRD automation: the technical overview
- E-invoicing mandate deadlines by country
- Full API documentation
InvoiceXML is a REST API for European e-invoice compliance. It covers ZUGFeRD, Factur-X, XRechnung, Peppol UBL and CII. It stores no data, is GDPR compliant by design, and works from any Microsoft stack: Dynamics NAV C/AL, Business Central AL extensions, Power Automate flows, Azure Functions or ASP.NET Core services.