<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Stefan Meier</title>
    <description>The latest articles on DEV Community by Stefan Meier (@invoicexml).</description>
    <link>https://dev.to/invoicexml</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3899350%2F6c0f7fea-1f60-4852-ad50-24ed56b29091.webp</url>
      <title>DEV Community: Stefan Meier</title>
      <link>https://dev.to/invoicexml</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/invoicexml"/>
    <language>en</language>
    <item>
      <title>Why validating Peppol UBL e-invoices in .NET is harder than it looks</title>
      <dc:creator>Stefan Meier</dc:creator>
      <pubDate>Mon, 27 Apr 2026 07:16:13 +0000</pubDate>
      <link>https://dev.to/invoicexml/why-validating-peppol-ubl-e-invoices-in-net-is-harder-than-it-looks-3m2k</link>
      <guid>https://dev.to/invoicexml/why-validating-peppol-ubl-e-invoices-in-net-is-harder-than-it-looks-3m2k</guid>
      <description>&lt;h1&gt;
  
  
  Why validating Peppol UBL e-invoices in .NET is harder than it looks
&lt;/h1&gt;

&lt;p&gt;When I started building &lt;a href="https://www.invoicexml.com" rel="noopener noreferrer"&gt;InvoiceXML&lt;/a&gt; (a REST API for Peppol and EN 16931 e-invoice compliance), one of the first technical walls I hit was EN 16931 Schematron validation in .NET. I assumed it would be straightforward. It was not.&lt;/p&gt;

&lt;p&gt;This post is about that journey: what Peppol UBL validation actually requires under the hood, why .NET makes it particularly awkward, what the community workarounds look like, and why I eventually decided none of them were good enough for a production compliance service.&lt;/p&gt;

&lt;p&gt;If you are building e-invoicing into a .NET application, whether for Belgium's January 2026 B2B mandate, the broader Peppol network, ZUGFeRD/Factur-X for Germany and France, or XRechnung for German public sector, this problem applies to all of them equally.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Peppol UBL validation actually involves
&lt;/h2&gt;

&lt;p&gt;Most developers, when they first encounter Peppol e-invoicing, assume "validate the invoice" means "check the XML schema." Run the UBL XSD, confirm the document is well-formed, move on.&lt;/p&gt;

&lt;p&gt;That is step one. The real validation is step two: &lt;strong&gt;Schematron&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;EN 16931 defines 200+ business rules that go beyond what XSD can express. Rules like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;BR-CO-14&lt;/code&gt;: Invoice total VAT amount must equal the sum of all VAT category tax amounts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BR-61&lt;/code&gt;: When payment means code is 30 or 58, a payment account identifier (IBAN) must be present&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BR-AE-05&lt;/code&gt;: When VAT category is reverse charge, the VAT rate must be zero&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BR-01&lt;/code&gt;: The invoice must declare a specification identifier&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then on top of EN 16931, Peppol BIS Billing 3.0 adds its own overlay rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PEPPOL-EN16931-R004&lt;/code&gt;: Invoice type code must be 380 for a standard invoice&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PEPPOL-EN16931-R010&lt;/code&gt;: Buyer endpoint identifier must use a valid Peppol scheme&lt;/li&gt;
&lt;li&gt;Additional rules around party identifiers, payment references, and document structure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These rules check semantic correctness across the entire document - arithmetic consistency, cross-field relationships, conditional requirements. XSD cannot express any of this. You need Schematron.&lt;/p&gt;

&lt;p&gt;Schematron is an ISO standard for rule-based XML validation. The EN 16931 and Peppol BIS Schematron artefacts are XSLT files, specifically &lt;strong&gt;XSLT 2.0&lt;/strong&gt;, that you run against the invoice XML to produce an SVRL (Schematron Validation Report Language) output listing every rule violation.&lt;/p&gt;

&lt;p&gt;Here is the problem: &lt;strong&gt;XSLT 2.0 has no official native .NET implementation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The same problem applies to every EN 16931-based format. ZUGFeRD and Factur-X use CII XML inside a PDF/A-3b container, same Schematron requirement. XRechnung adds its own CIUS-REC-DE national extension rules on top. Peppol BIS updates its Schematron quarterly. Every format, same underlying wall.&lt;/p&gt;




&lt;h2&gt;
  
  
  The .NET XSLT 2.0 problem
&lt;/h2&gt;

&lt;p&gt;.NET has built-in XSLT support via &lt;code&gt;System.Xml.Xsl.XslCompiledTransform&lt;/code&gt;. It supports XSLT 1.0. The EN 16931 Schematron artefacts require XSLT 2.0. &lt;code&gt;XslCompiledTransform&lt;/code&gt; will not run them, it throws immediately on XSLT 2.0 constructs.&lt;/p&gt;

&lt;p&gt;In Java, the standard solution is Saxon-HE, a mature, well-supported XSLT 2.0/3.0 processor from Saxonica. It works, it is what every Java-based e-invoicing tool uses, and it has official support.&lt;/p&gt;

&lt;p&gt;For .NET there is no official Saxon port. What exists is a community effort: &lt;strong&gt;IKVM&lt;/strong&gt;, a tool that cross-compiles Java bytecode to .NET assemblies. Someone took Saxon-HE, ran it through IKVM, and published the result as a NuGet package. This is the &lt;code&gt;SaxonHE12s9apiExtensions&lt;/code&gt; package you will find if you search for "Saxon .NET Core."&lt;/p&gt;

&lt;p&gt;Let me show you what actually using this looks like in practice.&lt;/p&gt;




&lt;h2&gt;
  
  
  The IKVM approach: what it looks like in code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// NuGet packages required:&lt;/span&gt;
&lt;span class="c1"&gt;// SaxonHE12s9apiExtensions&lt;/span&gt;
&lt;span class="c1"&gt;// IKVM.Runtime&lt;/span&gt;
&lt;span class="c1"&gt;// IKVM.Runtime.win-x64 (+ linux-x64, osx-x64, osx-arm64 for cross-platform)&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;net.sf.saxon.s9api&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Processor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;compiler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newXsltCompiler&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Load the Peppol BIS Schematron XSLT (embedded as resource)&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;schematronStream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Assembly&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetExecutingAssembly&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetManifestResourceStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MyApp.Resources.peppolbis-en16931-ubl.xslt"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;executable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;compiler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;javax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StreamSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InputStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Wrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schematronStream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;transformer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load30&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;invoiceSource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;javax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StreamSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StringReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoiceXml&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;saxon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s9api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;XdmDestination&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;transformer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;applyTemplates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoiceSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Now parse the SVRL output XML to find violations&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;svrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getXdmNode&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// ... parse SVRL XML&lt;/span&gt;
&lt;span class="c1"&gt;// ... extract failed-assert elements&lt;/span&gt;
&lt;span class="c1"&gt;// ... read @location, @test, and text() from each&lt;/span&gt;
&lt;span class="c1"&gt;// ... filter fatal errors from warnings&lt;/span&gt;
&lt;span class="c1"&gt;// ... map technical rule codes to human-readable messages&lt;/span&gt;
&lt;span class="c1"&gt;// ... figure out which line item the violation refers to&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you got to this point and it compiled and ran, congratulations. Before you got here you also needed to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Identify the correct combination of IKVM NuGet packages (there are several, they do not all work together on every .NET version)&lt;/li&gt;
&lt;li&gt;Handle platform-specific native libraries, IKVM ships separate packages per runtime identifier and you need all of them for cross-platform deployment&lt;/li&gt;
&lt;li&gt;Embed the correct Schematron XSLT files as &lt;code&gt;EmbeddedResource&lt;/code&gt; in your project&lt;/li&gt;
&lt;li&gt;Build the SVRL parsing layer entirely yourself, the IKVM/Saxon output is raw XML that you then need to traverse to extract rule violations, their locations, and their messages&lt;/li&gt;
&lt;li&gt;Deal with Java type bridging throughout your C# codebase: &lt;code&gt;javax.xml.transform.stream.StreamSource&lt;/code&gt;, &lt;code&gt;java.io.InputStream.Wrapper&lt;/code&gt;, &lt;code&gt;net.sf.saxon.s9api&lt;/code&gt;, Java namespaces sitting in the middle of your C# application&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is worth pausing on. When you see &lt;code&gt;javax.xml.transform&lt;/code&gt; in a C# file, something has gone architecturally sideways. It works (right now, with this combination of package versions, on this runtime). But it is not a foundation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this is not something you want in production
&lt;/h2&gt;

&lt;p&gt;Here is the specific failure surface you are accepting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A four-way version compatibility matrix.&lt;/strong&gt; Your validation depends on Saxon-HE (Saxonica's release schedule), IKVM (community project, no commercial support, no SLA), the NuGet package publisher maintaining the cross-compilation, and your .NET runtime version. All four need to be compatible simultaneously. When they fall out of sync, and they do, your validation stops working. You find out when a customer reports an unhandled exception.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quarterly Schematron updates.&lt;/strong&gt; OpenPeppol releases new Peppol BIS artefacts roughly every quarter. Each release requires you to download the new XSLT files from the OpenPeppol GitHub repository, embed them in your application, regression-test them against your invoice samples (because occasionally a rule's semantics change, not just its wording), and deploy. Miss a release and your validation accepts invoices that the Peppol network rejects. Your customer's invoice gets bounced and they call your support line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ZUGFeRD, Factur-X, and XRechnung add their own update cycles.&lt;/strong&gt; FeRD releases new ZUGFeRD/Factur-X versions roughly annually. KoSIT releases new XRechnung CIUS versions annually. Each has its own Schematron artefacts you need to track and update independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The SVRL parsing layer is entirely your problem.&lt;/strong&gt; IKVM/Saxon gives you raw SVRL output — an XML document listing violations with XPath locations and technical rule assertions. Turning that into structured error objects with rule codes, severity levels, human-readable messages, and line item references is a non-trivial parsing and mapping exercise that you write, own, and maintain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Java bleeds into your codebase permanently.&lt;/strong&gt; Every developer who joins your team has to understand why there are Java package names in the C# source. Every code review touching the validation layer requires context that is not obvious from reading .NET documentation. It is a maintenance cost that compounds quietly over time.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we built instead
&lt;/h2&gt;

&lt;p&gt;When building InvoiceXML we looked at this honestly and decided that running cross-compiled Java inside a .NET compliance service, where the output determines whether legally significant financial documents are accepted or rejected, was not an acceptable foundation. We built our own validation infrastructure properly.&lt;/p&gt;

&lt;p&gt;The result is a REST endpoint that accepts a UBL invoice, ZUGFeRD or Factur-X PDF, XRechnung XML, or standalone CII document and returns structured validation results in JSON. For UBL it runs the EN 16931 Schematron, detects the &lt;code&gt;CustomizationID&lt;/code&gt; profile automatically, and applies the correct CIUS overlay (Peppol BIS 3.0, NLCIUS, EHF, PINT, or XRechnung) in a single call.&lt;/p&gt;

&lt;p&gt;From a .NET application, the integration is clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultRequestHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authorization&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AuthenticationHeaderValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bearer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MultipartFormDataContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StreamContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"invoice-peppol.xml"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="s"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"invoice-peppol.xml"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PostAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"https://api.invoicexml.com/v1/validate/ubl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;form&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadFromJsonAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ValidationResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Friendly&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] [&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Layer&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// e.g. [BR-CO-14] [en16931] Invoice total VAT does not match...&lt;/span&gt;
        &lt;span class="c1"&gt;// e.g. [PEPPOL-EN16931-R004] [peppol-bis] Invoice type code must be 380...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Validation failed with 2 error(s)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"detectedProfile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Peppol BIS Billing 3.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"documentType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Invoice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"conformanceLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EN16931"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customizationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"xml"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"[BR-CO-14] Invoice total VAT amount must equal the sum of VAT category tax amounts."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"[PEPPOL-EN16931-R004] Invoice type code should be 380 for a standard invoice."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"friendly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"rule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BR-CO-14"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"layer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en16931"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The invoice total VAT amount does not match the sum of VAT breakdown amounts. Check that all tax subtotals are consistent with TaxTotal."&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"rule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PEPPOL-EN16931-R004"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"layer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"peppol-bis"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The InvoiceTypeCode is not 380. Peppol BIS 3.0 requires type code 380 for standard invoices."&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;layer&lt;/code&gt; field is worth highlighting, it tells you whether a violation is a fundamental EN 16931 data problem or a Peppol network-specific configuration issue. That distinction matters operationally: a &lt;code&gt;layer: en16931&lt;/code&gt; error means the invoice data is wrong; a &lt;code&gt;layer: peppol-bis&lt;/code&gt; error means the invoice data might be fine but it will be rejected by the Peppol network specifically. Different root causes, different teams to involve.&lt;/p&gt;

&lt;p&gt;Both valid and invalid invoices return HTTP 200. Branch on the &lt;code&gt;valid&lt;/code&gt; boolean, not on HTTP status code. This keeps the integration clean, no exception handling for the expected invalid case.&lt;/p&gt;

&lt;p&gt;No XSLT processor in your project. No IKVM. No Java type interop. No Schematron files to embed and update quarterly. No SVRL parsing layer to write and maintain. No &lt;code&gt;javax.xml&lt;/code&gt; in your C# codebase.&lt;/p&gt;

&lt;p&gt;The same endpoint handles ZUGFeRD and Factur-X PDFs (CII is extracted from the PDF/A-3b container automatically), XRechnung (CIUS-REC-DE rules applied on top of EN 16931), and CII XML, all from the same HTTP call pattern, all returning the same response structure.&lt;/p&gt;




&lt;h2&gt;
  
  
  The broader point
&lt;/h2&gt;

&lt;p&gt;The IKVM/Saxon path is the only community answer to a real gap in the .NET ecosystem. For a proof of concept, a local testing tool, or a one-off conversion script, it is functional. For a production application where invoice validation determines whether legally significant financial documents are accepted or rejected, the maintenance overhead, four-way version compatibility, quarterly Schematron updates across multiple formats, SVRL parsing, Java type leakage, is a recurring cost that is very easy to underestimate at the "install the NuGet package" stage.&lt;/p&gt;

&lt;p&gt;If you are adding e-invoicing validation to a .NET application and the compliance surface is growing, Belgium's mandate is live, France follows September 2026, Germany's issuing obligation hits January 2027, it is worth thinking honestly about whether owning that maintenance is the right use of your team's time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.invoicexml.com/validate-ubl" rel="noopener noreferrer"&gt;InvoiceXML UBL validation (try free, no account required)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.invoicexml.com/validate-facturx" rel="noopener noreferrer"&gt;InvoiceXML ZUGFErD/Factur-X validation (no code, no account required)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.invoicexml.com/validate-xrechnung" rel="noopener noreferrer"&gt;InvoiceXML XRechnung validation (no code, no account)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.invoicexml.com/docs" rel="noopener noreferrer"&gt;InvoiceXML API docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ConnectingEurope/eInvoicing-EN16931" rel="noopener noreferrer"&gt;EN 16931 Schematron artefacts - ConnectingEurope GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/OpenPEPPOL/peppol-bis-invoice-3" rel="noopener noreferrer"&gt;Peppol BIS artefacts - OpenPeppol GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/itplr-kosit/xrechnung-schematron" rel="noopener noreferrer"&gt;XRechnung Schematron - KoSIT GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.invoicexml.com/e-invoicing-mandates" rel="noopener noreferrer"&gt;E-invoicing mandate deadlines by country&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I'm Stefan, founder of &lt;a href="https://www.invoicexml.com" rel="noopener noreferrer"&gt;InvoiceXML&lt;/a&gt; - a REST API for Peppol and EN 16931 e-invoice compliance covering Peppol UBL, ZUGFeRD, Factur-X, XRechnung, and CII. Happy to answer questions in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>peppol</category>
      <category>einvoicing</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
