Created with AI

Creating ZUGFeRD e-invoices via API: a tutorial with RechnungsAPI

Since 2025-01-01, e-invoicing under § 14 UStG has been the standard for transactions between domestic German businesses: every business must be able to receive e-invoices, while transition periods until the end of 2026 or 2027 apply to issuing them (§ 27(38) UStG). ZUGFeRD is particularly attractive for this because it combines an ordinary PDF invoice with structured XML data – recipients without e-invoicing software simply open the PDF.

This tutorial shows step by step how to create ZUGFeRD invoices with RechnungsAPI, without dealing with XML and PDF/A-3.

What is ZUGFeRD?

ZUGFeRD is the hybrid e-invoice format of the Forum elektronische Rechnung Deutschland (FeRD): a PDF/A-3 file with an embedded XML invoice in the UN/CEFACT CII format. It is technically identical to the French standard Factur-X. The German Federal Ministry of Finance's guidance on mandatory e-invoicing recognizes ZUGFeRD from version 2.0.1 as a permissible format – with the exception of the MINIMUM and BASIC WL profiles. Our article ZUGFeRD for developers explains the technical details.

The problem with native CII/XML

If you implement ZUGFeRD yourself, you have to generate CII XML and embed it into a PDF/A-3 in a standards-compliant way. Even a simple address is deeply nested in CII:

xml
<ram:PostalTradeAddress>
    <ram:LineOne>Musterstraße 55a</ram:LineOne>
    <ram:CityName>Hamburg</ram:CityName>
    <ram:PostcodeCode>12345</ram:PostcodeCode>
    <ram:CountryID>DE</ram:CountryID>
</ram:PostalTradeAddress>

The same address in RechnungsAPI:

json
{
  "address": {
    "line1": "Musterstraße 55a",
    "postalCode": "12345",
    "city": "Hamburg",
    "country": "DE"
  }
}

A single invoice line in CII:

xml
<ram:IncludedSupplyChainTradeLineItem>
    <ram:AssociatedDocumentLineDocument>
        <ram:LineID>1</ram:LineID>
    </ram:AssociatedDocumentLineDocument>
    <ram:SpecifiedTradeProduct>
        <ram:Name>Beratung und Konzeption</ram:Name>
        <ram:Description>Analyse und Erarbeitung eines Konzepts</ram:Description>
    </ram:SpecifiedTradeProduct>
    <ram:SpecifiedLineTradeAgreement>
        <ram:NetPriceProductTradePrice>
            <ram:ChargeAmount>95.00</ram:ChargeAmount>
        </ram:NetPriceProductTradePrice>
    </ram:SpecifiedLineTradeAgreement>
    <ram:SpecifiedLineTradeDelivery>
        <ram:BilledQuantity unitCode="HUR">3</ram:BilledQuantity>
    </ram:SpecifiedLineTradeDelivery>
    <ram:SpecifiedLineTradeSettlement>
        <ram:ApplicableTradeTax>
            <ram:TypeCode>VAT</ram:TypeCode>
            <ram:CategoryCode>S</ram:CategoryCode>
            <ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
        </ram:ApplicableTradeTax>
        <ram:SpecifiedTradeSettlementLineMonetarySummation>
            <ram:LineTotalAmount>285.00</ram:LineTotalAmount>
        </ram:SpecifiedTradeSettlementLineMonetarySummation>
    </ram:SpecifiedLineTradeSettlement>
</ram:IncludedSupplyChainTradeLineItem>

The same information in the RechnungsAPI format:

json
{
  "unitPrice": { "value": "95.00", "currency": "EUR" },
  "quantity": { "value": "3", "unit": "HUR" },
  "item": {
    "name": "Beratung und Konzeption",
    "description": "Analyse und Erarbeitung eines Konzepts",
    "vat": { "code": "S", "rate": "19.00" }
  }
}

On top of that, ZUGFeRD adds the PDF side: PDF/A-3 conformance, embedded fonts and XMP metadata. RechnungsAPI produces both – the CII XML and the compliant PDF – from a single JSON request.

Setup and installation

The examples use the official Node.js SDK. Since RechnungsAPI is a REST API with an OpenAPI specification, the same calls work from any programming language, including generated OpenAPI clients.

bash
npm install @rechnungs-api/client

Client initialization

You receive your API key after registering at rechnungs-api.de. Test keys start with test_, production keys with prod_.

typescript
import { Client } from "@rechnungs-api/client";

export const client = new Client({
  apiKey: process.env.RECHNUNGS_API_KEY || "YOUR_API_KEY",
});

Step by step to a ZUGFeRD invoice

Step 1: Define the sender

typescript
import type { SenderParty } from "@rechnungs-api/client";

const sender: SenderParty = {
  name: "Muster GmbH",
  address: {
    line1: "Musterstraße 55a",
    postalCode: "12345",
    city: "Hamburg",
    country: "DE",
  },
  electronicAddress: {
    scheme: "EM",
    value: "info@example.com",
  },
  contact: {
    name: "Max Mustermann",
    email: "max.mustermann@example.com",
    phone: "+49123456789",
  },
  vatId: "DE123456789", // VAT ID
  taxId: "12/345/67890", // German tax number
  owner: "Max Mustermann", // Owner or managing director
  registration: {
    office: "Amtsgericht Hamburg",
    number: "HRB 123456",
  },
};
  • vatId: The VAT identification number (in Germany "DE" + 9 digits).
  • taxId: Alternatively or additionally, the tax number issued by the tax office.
  • registration / owner: Commercial register entry and authorized representative – they appear on the PDF rendering.

Step 2: Define the recipient

typescript
import type { RecipientParty } from "@rechnungs-api/client";

const recipient: RecipientParty = {
  name: "Beispiel UG (haftungsbeschränkt)",
  address: {
    line1: "Musterweg 3c",
    postalCode: "54321",
    city: "Berlin",
    country: "DE",
  },
  electronicAddress: {
    scheme: "EM",
    value: "buchhaltung@beispiel-ug.de",
  },
  contact: {
    name: "Erika Musterfrau",
    email: "erika.musterfrau@beispiel-ug.de",
    phone: "+49987654321",
  },
  vatId: "DE987654321",
};

The electronicAddress follows the EAS code list; EM (email address) is common in B2B. German authorities are addressed via their Leitweg-ID with the code 0204.

Step 3: Set the payment information

typescript
import type { PaymentInformation } from "@rechnungs-api/client";

const payment: PaymentInformation = {
  means: [{
    code: "58", // SEPA credit transfer
    bankAccount: {
      bankName: "Muster Bank",
      iban: "DE12345678901234567890",
      bic: "MUSTDE12XXX",
    },
  }],
  terms: "Zahlbar innerhalb von 30 Tagen netto",
};

The payment means follows the UNTDID 4461 code list. The most important codes:

  • 58 – SEPA credit transfer
  • 30 – Credit transfer (generic)
  • 59 – SEPA direct debit
  • 48 – Bank card
  • 10 – In cash

Step 4: Create the invoice lines

typescript
import type { DocumentLineCreateRequest } from "@rechnungs-api/client";

const lines: DocumentLineCreateRequest[] = [
  {
    unitPrice: { value: "95.00", currency: "EUR" },
    item: {
      name: "Beratung und Konzeption",
      description: "Analyse und Erarbeitung eines Konzepts für die Digitalisierung",
      vat: { code: "S", rate: "19.00" },
    },
    quantity: { value: "3", unit: "HUR" }, // HUR = hour
  },
  {
    unitPrice: { value: "500.00", currency: "EUR" },
    item: {
      name: "Logo-Design",
      description: "Entwicklung eines Corporate Designs inkl. Logo",
      vat: { code: "S", rate: "19.00" },
    },
    quantity: { value: "1", unit: "H87" }, // H87 = piece
  },
];

Unit codes come from UN/ECE Recommendation N°20 and N°21, for example:

  • H87 = piece, C62 = the unit "one"
  • HUR = hour, DAY = day
  • KGM = kilogram, MTR = metre, MTK = square metre, LTR = litre

VAT categories follow the UNTDID 5305 code list:

  • S = standard rate – applies to every rate above 0 %, i.e. both 19 % and the reduced German rate of 7 % (each with the matching rate)
  • Z = tax rate 0 %
  • E = exempt from tax; an exemption reason is required, e.g. for small businesses under § 19 UStG
  • AE = reverse charge (§ 13b UStG)
  • K = intra-community supply, G = export, O = outside the scope of tax

Step 5: ZUGFeRD configuration

typescript
import type { EInvoiceConfiguration } from "@rechnungs-api/client";

const eInvoiceConfig: EInvoiceConfiguration = {
  type: "zugferd",
  profile: "en-16931", // 'minimum' | 'basic-wl' | 'basic' | 'en-16931' | 'extended' | 'xrechnung'
};

The available ZUGFeRD profiles:

  • minimum and basic-wl: only bookkeeping aids – not recognized as e-invoices under § 14 UStG in Germany
  • basic: basic data including invoice lines
  • en-16931: the complete EN 16931 data model – the right choice for most B2B cases
  • extended: extended fields for complex scenarios
  • xrechnung: additionally satisfies the German XRechnung business rules; maximum compatibility, including for public-sector customers

Recommendation: en-16931 for the standard B2B case, or xrechnung for maximum compatibility.

Step 6: Assemble the complete invoice

typescript
import type { DocumentCreateRequest } from "@rechnungs-api/client";

const documentRequest: DocumentCreateRequest = {
  type: "invoice",
  locale: "de-DE",
  number: "RE-2026-001",
  issueDate: "2026-06-15",
  dueDate: "2026-07-15",

  sender,
  recipient,
  payment,
  lines,

  // Enable ZUGFeRD
  eInvoice: eInvoiceConfig,

  // Buyer reference, e.g. an order or project number
  buyerReference: "PROJEKT-2026-ZF",

  // Texts for the PDF rendering
  preTableText: "Sehr geehrte Damen und Herren,\n\nfür die erbrachten Leistungen stellen wir Ihnen folgende Positionen in Rechnung:",
  postTableText: "Vielen Dank für Ihren Auftrag! Bitte überweisen Sie den Betrag bis zum Fälligkeitsdatum auf das angegebene Konto.",

  // Optional: styling of the PDF
  theme: {
    fontFamily: "Open Sans",
  },
};

A note on the xrechnung profile: it requires a buyer reference (buyerReference). If no meaningful reference exists, a placeholder such as "00" can be used in practice.

Step 7: Create and download the ZUGFeRD invoice

The result is a PDF file that invisibly contains the structured XML data:

typescript
import * as fs from "node:fs/promises";

const document = await client.createDocument(documentRequest);
console.log(`ZUGFeRD invoice created! ID: ${document.id}`);

// Download the ZUGFeRD PDF (contains the embedded XML)
const pdf = await client.readDocument(document.id, "pdf");
await fs.writeFile("zugferd-rechnung.pdf", Buffer.from(pdf));

The resulting PDF looks like this:

RechnungsAPI ZUGFeRD PDF

Complete working example

typescript
import * as fs from "node:fs/promises";
import type {
  DocumentCreateRequest,
  RecipientParty,
  SenderParty,
} from "@rechnungs-api/client";
import { Client } from "@rechnungs-api/client";

const client = new Client({
  apiKey: process.env.RECHNUNGS_API_KEY || "YOUR_API_KEY",
});

const sender: SenderParty = {
  name: "Muster GmbH",
  address: {
    line1: "Musterstraße 55a",
    postalCode: "12345",
    city: "Hamburg",
    country: "DE",
  },
  electronicAddress: {
    scheme: "EM",
    value: "info@example.com",
  },
  contact: {
    name: "Max Mustermann",
    email: "max.mustermann@example.com",
    phone: "+49123456789",
  },
  vatId: "DE123456789",
  taxId: "12/345/67890",
  owner: "Max Mustermann",
  registration: {
    office: "Amtsgericht Hamburg",
    number: "HRB 123456",
  },
};

const recipient: RecipientParty = {
  name: "Beispiel UG (haftungsbeschränkt)",
  address: {
    line1: "Musterweg 3c",
    postalCode: "54321",
    city: "Berlin",
    country: "DE",
  },
  electronicAddress: {
    scheme: "EM",
    value: "buchhaltung@beispiel-ug.de",
  },
  contact: {
    name: "Erika Musterfrau",
    email: "erika.musterfrau@beispiel-ug.de",
    phone: "+49987654321",
  },
  vatId: "DE987654321",
};

const documentRequest: DocumentCreateRequest = {
  type: "invoice",
  locale: "de-DE",
  number: "RE-2026-001",
  issueDate: "2026-06-15",
  dueDate: "2026-07-15",

  sender,
  recipient,
  buyerReference: "PROJEKT-2026-ZF",

  preTableText: "Sehr geehrte Damen und Herren,\n\nfür die erbrachten Leistungen stellen wir Ihnen folgende Positionen in Rechnung:",
  postTableText: "Vielen Dank für Ihren Auftrag! Bitte überweisen Sie den Betrag bis zum Fälligkeitsdatum auf das angegebene Konto.",

  lines: [
    {
      unitPrice: { value: "95.00", currency: "EUR" },
      item: {
        name: "Beratung und Konzeption",
        description: "Analyse und Erarbeitung eines Konzepts für die Digitalisierung",
        vat: { code: "S", rate: "19.00" },
      },
      quantity: { value: "3", unit: "HUR" },
    },
    {
      unitPrice: { value: "500.00", currency: "EUR" },
      item: {
        name: "Logo-Design",
        description: "Entwicklung eines Corporate Designs inkl. Logo",
        vat: { code: "S", rate: "19.00" },
      },
      quantity: { value: "1", unit: "H87" },
    },
  ],

  payment: {
    means: [{
      code: "58",
      bankAccount: {
        bankName: "Muster Bank",
        iban: "DE12345678901234567890",
        bic: "MUSTDE12XXX",
      },
    }],
    terms: "Zahlbar innerhalb von 30 Tagen netto",
  },

  eInvoice: {
    type: "zugferd",
    profile: "en-16931",
  },

  theme: {
    fontFamily: "Open Sans",
  },
};

async function main() {
  const document = await client.createDocument(documentRequest);
  console.log(`ZUGFeRD invoice created! ID: ${document.id}`);

  const pdf = await client.readDocument(document.id, "pdf");
  await fs.writeFile("zugferd-rechnung.pdf", Buffer.from(pdf));

  console.log(`Number: ${document.number}`);
  console.log(`Net amount: ${document.netAmount.value} ${document.netAmount.currency}`);
  console.log(`Gross amount: ${document.grossAmount.value} ${document.grossAmount.currency}`);
}

main();

Advanced features

Delivery address and delivery period

typescript
const documentRequest: DocumentCreateRequest = {
  // ... other fields
  delivery: {
    address: {
      line1: "Lieferstraße 10",
      postalCode: "98765",
      city: "München",
      country: "DE",
    },
  },
  deliveryPeriod: {
    startDate: "2026-06-01",
    endDate: "2026-06-30",
    vatDate: "35",
  },
};

The optional vatDate field states when the VAT becomes chargeable. The subset of the UNTDID 2005 code list prescribed by EN 16931 is permitted: 3 (invoice date), 35 (delivery date) or 432 (payment).

typescript
import * as fs from "node:fs/promises";

const logoBuffer = await fs.readFile("./company-logo.png");
const logoBase64 = logoBuffer.toString("base64");

const documentRequest: DocumentCreateRequest = {
  // ... other fields
  theme: {
    logo: `data:image/png;base64,${logoBase64}`,
    fontFamily: "Open Sans",
  },
};

Error handling

typescript
import { ApiError } from "@rechnungs-api/client";

try {
  const document = await client.createDocument(documentRequest);
  // ...
} catch (error) {
  if (error instanceof ApiError) {
    console.error(`API error [${error.status}]:`, error.body);
  }
  throw error;
}

Conclusion

With RechnungsAPI you create ZUGFeRD invoices through a JSON API – the service takes care of the CII XML, the PDF/A-3 embedding and the validation. You provide the invoice data, the API delivers a finished hybrid e-invoice.

You can find all the details and further features in the documentation, where you can also try the API directly.

Sources