<?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: facturata</title>
    <description>The latest articles on DEV Community by facturata (@facturata).</description>
    <link>https://dev.to/facturata</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4013869%2Fa75c8204-8a20-4ca1-931f-9b5ae208f8ee.png</url>
      <title>DEV Community: facturata</title>
      <link>https://dev.to/facturata</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/facturata"/>
    <language>en</language>
    <item>
      <title>Your Factur-X passes XSD and still gets rejected. Here is why.</title>
      <dc:creator>facturata</dc:creator>
      <pubDate>Tue, 28 Jul 2026 14:58:06 +0000</pubDate>
      <link>https://dev.to/facturata/your-factur-x-passes-xsd-and-still-gets-rejected-here-is-why-341g</link>
      <guid>https://dev.to/facturata/your-factur-x-passes-xsd-and-still-gets-rejected-here-is-why-341g</guid>
      <description>&lt;p&gt;If you are adding Factur-X or ZUGFeRD support to an app before the French 2026 e-invoicing deadline, you will meet this bug that is not a bug: a file that is perfectly well formed, opens fine in a PDF reader, passes the XSD, and still gets refused by the receiving platform.&lt;/p&gt;

&lt;p&gt;The format is the easy part. The EN 16931 business rules are what actually reject invoices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Format validation is not conformance
&lt;/h2&gt;

&lt;p&gt;A Factur-X file is a PDF/A-3 with a CII XML (the EN 16931 semantic model) embedded inside it. ZUGFeRD 2.x in Germany is the same file. Two layers decide whether it is accepted:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Structure: the XSD. It checks that elements exist and are well typed. Most tools get this right.&lt;/li&gt;
&lt;li&gt;Business rules: a Schematron layer of roughly 200 rules (BR-&lt;em&gt;, BR-CO-&lt;/em&gt;, BR-S-*), plus national rules on top. This is where valid-looking files die.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The XSD says "this is a syntactically correct CII document." It says nothing about whether your totals add up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules that bite most often
&lt;/h2&gt;

&lt;p&gt;These are the ones I see fail again and again, with the reason:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BR-CO-15: the grand total with VAT must equal the total without VAT plus the total VAT. The classic cause is rounding line by line and then summing in the wrong order.&lt;/li&gt;
&lt;li&gt;BR-CO-10 and BR-CO-13: the sum of line net amounts must equal the document line total. One rounded line, one mismatch.&lt;/li&gt;
&lt;li&gt;BR-S-08: for each standard-rated VAT breakdown, the taxable base must equal the sum of the line nets at that rate. Multi-rate invoices break this constantly.&lt;/li&gt;
&lt;li&gt;BR-CO-17: for a VAT breakdown, the tax amount must equal the taxable amount times the rate divided by 100, rounded to two decimals. A half-cent drift is enough.&lt;/li&gt;
&lt;li&gt;BR-S-05 and its siblings: use a 0 percent or exempt rate and forget the exemption reason, and the file is rejected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A concrete rounding trap. Take three lines at 19.99 with 20 percent VAT:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;round each line first:  net 19.99, VAT 4.00  -&amp;gt;  3 lines: net 59.97, VAT 12.00
sum then tax:           net 59.97 * 0.20 = 11.994  -&amp;gt;  11.99
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Round in the wrong place and BR-CO-17 or BR-CO-15 fails, even though every number looks reasonable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate before you send
&lt;/h2&gt;

&lt;p&gt;Do not let the receiver be your validator. Options, open-source first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The KoSIT validator, the reference validation used in Germany, runs the official Schematron and returns the exact failing rules.&lt;/li&gt;
&lt;li&gt;Mustangproject (Java, Apache-2.0) can generate and validate Factur-X or ZUGFeRD.&lt;/li&gt;
&lt;li&gt;Whatever validator you use, pin the rule-set version. The EN 16931 artefacts revise roughly every six months. Valid last quarter is not valid today, and the exchange carries no version handshake, so the receiver's rule set quietly sets your real deadline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;During the transition, validate outgoing files against both the current and the previous rule set, and treat "valid at sender, rejected at receiver" as a metric to watch, not a one-off surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal check in code
&lt;/h2&gt;

&lt;p&gt;If you already produce a Factur-X PDF, extract the embedded XML and run it through a Schematron validator. A generate-and-validate API call, JSON in and a validated Factur-X out, looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://api.facturata.com/v1/invoices &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; @invoice.json | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.facturx_pdf_base64'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; invoice.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.facturata.com/v1/invoices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# your invoice as JSON
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;facturx_pdf_base64&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="c1"&gt;# totals and VAT are recomputed and validated server-side before you get the file
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The endpoint is not the point. The point is that totals and VAT should be recomputed and passed through the XSD and the full Schematron before a file ever leaves your system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the ugly cases early
&lt;/h2&gt;

&lt;p&gt;One clean invoice passes almost anywhere. The rejections live in the cases teams test last:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;credit notes and corrections&lt;/li&gt;
&lt;li&gt;prepayments and deposits, then the final invoice that nets them out&lt;/li&gt;
&lt;li&gt;multi-rate VAT on one document&lt;/li&gt;
&lt;li&gt;reverse charge and self-billing&lt;/li&gt;
&lt;li&gt;exemptions with the correct reason code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full disclosure: I build Facturata, a free EN 16931 validator and a generate API, so these rules are what I work on all day. Whatever stack you pick, hosted or open-source, test those five cases before September, not after the first rejection.&lt;/p&gt;

</description>
      <category>facturx</category>
      <category>einvoicing</category>
      <category>api</category>
      <category>python</category>
    </item>
    <item>
      <title>I built an API that turns JSON into legally-compliant EU e-invoices (Factur-X), because 2026 is coming</title>
      <dc:creator>facturata</dc:creator>
      <pubDate>Sat, 04 Jul 2026 19:46:15 +0000</pubDate>
      <link>https://dev.to/facturata/i-built-an-api-that-turns-json-into-legally-compliant-eu-e-invoices-factur-x-because-2026-is-329l</link>
      <guid>https://dev.to/facturata/i-built-an-api-that-turns-json-into-legally-compliant-eu-e-invoices-factur-x-because-2026-is-329l</guid>
      <description>&lt;p&gt;If you build software that touches invoices in Europe, the next 24 months are going to be busy. France requires all businesses to receive e-invoices from September 2026. Germany already requires reception since 2025, with issuing phased in 2027-2028. Belgium goes B2B via Peppol in 2026.&lt;/p&gt;

&lt;p&gt;The dominant format in France/Germany is Factur-X (identical to ZUGFeRD 2.x): a normal PDF invoice with machine-readable XML (EN 16931 standard) embedded inside. Nice idea. Implementing it correctly is not nice: the XSD is the easy part, then come ~200 schematron business rules (BR-&lt;em&gt;, BR-CO-&lt;/em&gt;, BR-S-*...), VAT category logic, and country-specific rulesets on top.&lt;/p&gt;

&lt;p&gt;So I built Facturata: one POST with plain invoice JSON, and you get back a compliant Factur-X PDF (or raw CII XML), validated against the official XSD + full schematron before it leaves the server. There is also /validate (check any Factur-X/ZUGFeRD file, including the French 2026 CTC ruleset) and /extract (pull the XML out of a PDF).&lt;/p&gt;

&lt;p&gt;Design choices worth mentioning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deterministic engine, no LLM at runtime. Compliance tooling should not hallucinate.&lt;/li&gt;
&lt;li&gt;
&lt;ul&gt;
&lt;li&gt;Stateless: documents processed in memory, nothing stored. Makes GDPR conversations short.&lt;/li&gt;
&lt;/ul&gt;

&lt;ul&gt;
&lt;li&gt;- Totals and VAT breakdowns computed server-side, so garbage-in cannot produce arithmetically broken invoices (the rules catch that anyway, but better to never emit it).&lt;/li&gt;
&lt;li&gt;- Built on the excellent open-source Akretion factur-x library + official validation artifacts.

&lt;ul&gt;
&lt;li&gt;There is a free web validator (no signup) at &lt;a href="https://facturata.com" rel="noopener noreferrer"&gt;https://facturata.com&lt;/a&gt; if you just want to check files, and a free API tier (15 docs/month) on RapidAPI if you want to integrate. Feedback very welcome, especially from anyone wrestling with XRechnung or Peppol quirks.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>fintech</category>
      <category>europe</category>
    </item>
  </channel>
</rss>
