<?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: Patrick Jerominek</title>
    <description>The latest articles on DEV Community by Patrick Jerominek (@pje_xhub_io).</description>
    <link>https://dev.to/pje_xhub_io</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%2F4062624%2F38f1b388-e707-4828-afc7-ea360860fe0b.png</url>
      <title>DEV Community: Patrick Jerominek</title>
      <link>https://dev.to/pje_xhub_io</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pje_xhub_io"/>
    <language>en</language>
    <item>
      <title>German e-invoicing in a few lines of code (XRechnung, ZUGFeRD, EN 16931)</title>
      <dc:creator>Patrick Jerominek</dc:creator>
      <pubDate>Wed, 05 Aug 2026 07:04:00 +0000</pubDate>
      <link>https://dev.to/pje_xhub_io/german-e-invoicing-in-a-few-lines-of-code-xrechnung-zugferd-en-16931-390j</link>
      <guid>https://dev.to/pje_xhub_io/german-e-invoicing-in-a-few-lines-of-code-xrechnung-zugferd-en-16931-390j</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cross-posting checklist (nicht mitveröffentlichen):&lt;/strong&gt; dev.to zuerst, 24h später Medium, Hashnode, Indie Hackers.&lt;br&gt;
Auf &lt;strong&gt;allen&lt;/strong&gt; Kopien &lt;code&gt;canonical_url&lt;/code&gt; auf dieselbe URL setzen. Zwei Optionen — vorher entscheiden:&lt;br&gt;
&lt;strong&gt;(a)&lt;/strong&gt; Canonical auf die eigene Domain (empfohlen, wenn der Artikel auch im Blog auf invoice-api.xhub.io steht) — dann zählt die SEO-Kraft dort.&lt;br&gt;
&lt;strong&gt;(b)&lt;/strong&gt; Canonical auf die dev.to-Fassung, wenn es keine eigene Blog-Version gibt.&lt;br&gt;
Aktuell im Frontmatter: Quickstart-Doku. Das ist nur richtig, wenn der Artikel als Ergänzung dieser Seite gelten soll — sonst auf (a) oder (b) ändern.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  German e-invoicing in a few lines of code
&lt;/h1&gt;

&lt;p&gt;If you build billing software, an ERP integration, or a marketplace that touches German customers, you have a deadline: &lt;strong&gt;31 December 2026&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's when the first transition period ends. Until then you may still send paper or a plain PDF in B2B — with the recipient's consent. From &lt;strong&gt;1 January 2027&lt;/strong&gt; companies above €800,000 in prior-year revenue must &lt;em&gt;issue&lt;/em&gt; structured e-invoices. Everyone below that threshold gets one more year: from &lt;strong&gt;1 January 2028&lt;/strong&gt; it applies without exception.&lt;/p&gt;

&lt;p&gt;(Receiving is a separate matter and already settled — every German company has had to be able to &lt;em&gt;accept&lt;/em&gt; e-invoices since January 2025.)&lt;/p&gt;

&lt;p&gt;Most teams estimate a quarter for this. Here's why they're wrong — and what the work actually looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the standard actually asks for
&lt;/h2&gt;

&lt;p&gt;The relevant norm is &lt;strong&gt;EN 16931&lt;/strong&gt;, the European semantic standard for electronic invoicing. In Germany it shows up in two flavours:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XRechnung&lt;/strong&gt; is pure XML. No visual layout — it's built for machines. It's the standard in public-sector business (B2G), routed via a &lt;code&gt;Leitweg-ID&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ZUGFeRD&lt;/strong&gt; is a hybrid: a normal PDF/A-3 with the same structured data embedded as XML. Humans read the PDF, machines parse the XML. From version 2.x its profiles are aligned with France's Factur-X — the two are close cousins rather than unrelated standards, which matters once you cross the border.&lt;/p&gt;

&lt;p&gt;Both are valid e-invoices. Both satisfy EN 16931. The difference is whether your recipient wants something to look at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why teams over-estimate this
&lt;/h2&gt;

&lt;p&gt;The naive plan looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Research the EN 16931 semantic model&lt;/li&gt;
&lt;li&gt;Map your domain objects onto it&lt;/li&gt;
&lt;li&gt;Generate valid XML&lt;/li&gt;
&lt;li&gt;Build a validation pipeline against KoSIT schemas&lt;/li&gt;
&lt;li&gt;Handle edge cases — credit notes, reverse charge, intra-community supply, rounding&lt;/li&gt;
&lt;li&gt;Maintain all of it as the specification moves&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 1 and 4 are where the quarter goes. The semantic model has hundreds of business terms with conditional cardinalities. KoSIT validation isn't a schema check — it's Schematron rules on top of the XSD, and a document can be schema-valid but rule-invalid.&lt;/p&gt;

&lt;p&gt;None of that is interesting work. It's also not differentiating work: your competitors' XRechnung is byte-for-byte as compliant as yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The version that isn't a project
&lt;/h2&gt;

&lt;p&gt;Post JSON, get back a validated document:&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;-X&lt;/span&gt; POST https://service.invoice-api.xhub.io/api/v1/invoice/de/xrechnung/generate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer sk_test_abc123..."&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;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "invoice": {
      "invoiceNumber": "RE-2026-001",
      "type": "invoice",
      "issueDate": "2026-04-10",
      "dueDate": "2026-05-10",
      "currency": "EUR",
      "seller": {
        "name": "Meine Firma GmbH", "vatId": "DE123456789",
        "street": "Musterstraße 1", "city": "Berlin",
        "postalCode": "10115", "countryCode": "DE"
      },
      "buyer": {
        "name": "Kunde AG", "vatId": "DE987654321",
        "street": "Kundenweg 42", "city": "München",
        "postalCode": "80331", "countryCode": "DE"
      },
      "countrySpecific": { "buyerReference": "BUYER-REF-001" },
      "items": [{
        "position": 1, "description": "Beratungsleistung",
        "quantity": 10, "unit": "HUR", "unitPrice": 150.00,
        "taxRate": 19, "netAmount": 1500.00,
        "taxAmount": 285.00, "grossAmount": 1785.00
      }],
      "subtotal": 1500.00, "total": 1785.00,
      "taxSummary": [{ "taxRate": 19, "netAmount": 1500.00, "taxAmount": 285.00 }],
      "paymentTerms": { "dueDays": 30 }
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same call in TypeScript — &lt;code&gt;invoice&lt;/code&gt; is the object from the payload above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://service.invoice-api.xhub.io/api/v1/invoice/de/xrechnung/generate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer sk_test_abc123...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;invoice&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`API returned &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Invalid invoice: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Decode base64 → UTF-8. Don't use atob() here: it returns a Latin-1&lt;/span&gt;
&lt;span class="c1"&gt;// binary string and will mangle "Musterstraße" / "München".&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;xml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// In Node:&lt;/span&gt;
&lt;span class="c1"&gt;// const xml = Buffer.from(result.data, 'base64').toString('utf8');&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in Python:&lt;br&gt;
&lt;/p&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;response&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://service.invoice-api.xhub.io/api/v1/invoice/de/xrechnung/generate&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;Authorization&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;Bearer sk_test_abc123...&lt;/span&gt;&lt;span class="sh"&gt;'&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;invoice&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&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;# requests sets Content-Type automatically
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&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="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;errors&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid invoice: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;errors&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;xml&lt;/span&gt; &lt;span class="o"&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;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Reading the response
&lt;/h2&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;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xrechnung"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filename"&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-2026-001.xml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mimeType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application/xml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:abc123..."&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="s2"&gt;"PD94bWwgdmVyc2lvbj0iMS4wIj8+..."&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;"warnings"&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;Three fields matter in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;data&lt;/code&gt;&lt;/strong&gt; — the document itself, base64-encoded. XML for XRechnung, PDF bytes for ZUGFeRD.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hash&lt;/code&gt;&lt;/strong&gt; — SHA-256 of the document. Store it; it's what makes your archive defensible later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;errors&lt;/code&gt; / &lt;code&gt;warnings&lt;/code&gt;&lt;/strong&gt; — validation results in the same response. You know at request time whether the invoice holds up, not when your customer's accounts-payable system rejects it three weeks later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat a non-empty &lt;code&gt;errors&lt;/code&gt; array as a hard failure in your pipeline. Warnings are usually about optional business terms and are safe to log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Switching format is switching endpoint
&lt;/h2&gt;

&lt;p&gt;The same payload produces ZUGFeRD instead of XRechnung by changing the path segment. Today that covers XRechnung, ZUGFeRD and plain PDF.&lt;/p&gt;

&lt;p&gt;The reason this matters is what comes next. Sell across the EU and each market wants its own thing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;France: &lt;strong&gt;Factur-X&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Italy: &lt;strong&gt;FatturaPA&lt;/strong&gt;, submitted through the SdI system&lt;/li&gt;
&lt;li&gt;Spain: &lt;strong&gt;Facturae&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Belgium, the Netherlands, Bulgaria, Romania: &lt;strong&gt;UBL&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Austria: &lt;strong&gt;ebInterface&lt;/strong&gt; · Czechia: &lt;strong&gt;ISDOC&lt;/strong&gt; · Hungary: &lt;strong&gt;NAV&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those seven are on the roadmap rather than live today — check &lt;a href="https://invoice-api.xhub.io/en/docs/api/formats" rel="noopener noreferrer"&gt;the formats endpoint&lt;/a&gt; for the current state before you plan around one.&lt;/p&gt;

&lt;p&gt;The architectural point stands regardless of vendor: if your invoice model is coupled to one output format, every new market is a new project. If it isn't, it's a config change. Design for that even if you only ship Germany today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things worth getting right regardless of tooling
&lt;/h2&gt;

&lt;p&gt;Whatever you build on, these bite eventually:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Invoice numbering.&lt;/strong&gt; German §14 UStG requires a sequential number that is assigned only once. If two orders complete simultaneously and you derive the number from a &lt;code&gt;COUNT(*)&lt;/code&gt;, you will eventually issue a duplicate — which is the part that actually breaks. Use a dedicated table with a transactional counter, not your primary key and not a timestamp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Credit notes.&lt;/strong&gt; Every refund needs its own document with its own number from the same sequence. Retrofitting this is unpleasant — model it from the start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Archiving.&lt;/strong&gt; Germany's GoBD rules expect the invoice to remain readable and unaltered for years. Store the exact bytes you sent plus the hash, not a re-rendered version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't block checkout.&lt;/strong&gt; Generating an invoice means an external call. Push it to a queue or worker. Nobody should wait on your compliance layer to complete a purchase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it without signing up
&lt;/h2&gt;

&lt;p&gt;There's a &lt;a href="https://invoice-api.xhub.io/en/playground" rel="noopener noreferrer"&gt;playground&lt;/a&gt; that runs without an account, and test keys (&lt;code&gt;sk_test_...&lt;/code&gt;) are free — test-mode calls don't cost anything, so you can wire up your integration and run it in CI before deciding anything.&lt;/p&gt;

&lt;p&gt;If you're on WooCommerce, there's also a &lt;a href="https://wordpress.org/plugins/invoice-api-xhub-for-woocommerce/" rel="noopener noreferrer"&gt;GPLv2 plugin on wordpress.org&lt;/a&gt; that does this at the order level, no code required.&lt;/p&gt;




&lt;p&gt;The deadline is real, and it's closer than most roadmaps assume. The implementation doesn't have to eat a quarter.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://beeback.de" rel="noopener noreferrer"&gt;BeeBack UG&lt;/a&gt; in Frankfurt — &lt;a href="https://invoice-api.xhub.io" rel="noopener noreferrer"&gt;invoice-api.xhub.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>germany</category>
      <category>compliance</category>
    </item>
  </channel>
</rss>
