Dokumentation

Das Invoice-Modell

Sie fürchten den amtlichen Papierkram? Müssen Sie nicht. Die EN 16931 definiert eine lange Liste von Feldern, aber Sie brauchen eine Handvoll. Füllen Sie die wenigen Pflichtfelder aus, ergänzen Sie Details nur dort, wo Ihr Fall es verlangt, und die Bibliothek berechnet jede Summe und schreibt darunter das konforme XML. Jedes Feld entspricht einem echten Geschäftsbegriff (sein BT-/BG--Code steht im Typ, falls Sie ihn je brauchen).

Die kleinste Rechnung

Das ist bereits eine vollständige, konforme Rechnung. Alles Weitere auf dieser Seite ist optional.

import { type Invoice } from "@jasy/e-invoice";

const invoice: Invoice = {
  number: "RE-001",
  issueDate: "2026-06-21", // ISO-Datum
  currency: "EUR",
  seller: {
    name: "Northwind GmbH",
    vatId: "DE265013614",
    address: { city: "Berlin", postCode: "10115", country: "DE" },
  },
  buyer: {
    name: "Globex AG",
    address: { city: "München", postCode: "80331", country: "DE" },
  },
  lines: [
    {
      name: "Beratung",
      quantity: 8,
      unit: "HUR",
      netUnitPrice: 120,
      vat: { category: "S", ratePercent: 19 },
    },
  ],
};

number, issueDate, currency, seller, buyer und mindestens eine Position in lines - das ist alles, was verlangt ist. Übergeben Sie das an renderZugferd, und heraus kommen PDF und XML.

Verkäufer und Käufer - seller, buyer

Beide Parteien haben dieselbe Form. name und address (davon ist nur country Pflicht) sind verpflichtend, alles andere ist optional.

seller: {
  name: "Northwind GmbH", // eingetragener Firmenname
  vatId: "DE265013614", // nötig, sobald Sie Umsatzsteuer berechnen
  address: { line1: "Hauptstraße 1", city: "Berlin", postCode: "10115", country: "DE" },
  contact: { name: "A. Schmidt", email: "rechnung@northwind.de", phone: "+49 30 123456" },
  electronicAddress: "rechnung@northwind.de", // Pflicht für XRechnung / Peppol
},

Weitere nützliche Felder: tradingName, taxNumber (Steuernummer), legalRegistrationId (Handelsregisternummer) und additionalLegalInfo (BT-33, der rechtliche Footer - Stammkapital, Geschäftsführung). Der buyer kennt all das ebenso.

Die Adresse nimmt mehr als die vier Felder oben, wenn Sie sie brauchen: line2 und line3 für eine zweite und dritte Adresszeile, und subdivision für eine Region oder ein Bundesland (BT-39 / BT-54). Pflicht ist nur country.

Positionen - lines

Jede Position in lines nennt einen Artikel (name), eine Menge (quantity) in einer Einheit (unit), den Netto-Einzelpreis (netUnitPrice) und ihre Umsatzsteuer (vat). Die Bibliothek multipliziert und summiert - Sie rechnen nie selbst eine Position zusammen.

lines: [
  {
    name: "Beratung",
    description: "Workshop vor Ort", // optional
    quantity: 8,
    unit: "HUR", // UN/ECE Rec 20 - "HUR" Stunden, "C62" Stück, "DAY", "KGM" kg, "MTR" Meter ...
    netUnitPrice: 120,
    vat: { category: "S", ratePercent: 19 },
  },
  {
    name: "Lizenz",
    quantity: 1,
    unit: "C62",
    netUnitPrice: 480,
    vat: { category: "S", ratePercent: 19 },
  },
],

Mehr je Position, wenn Sie es brauchen: sellerItemId / buyerItemId / standardItemId (eine GTIN), priceBaseQuantity (ein Preis „pro 100“), allowancesCharges je Position und eine freie note.

Und für Kunden, die automatisch abgleichen:

{
  name: "Zähler Typ M-200",
  quantity: 2,
  unit: "C62",
  netUnitPrice: 480,
  vat: { category: "S", ratePercent: 19 },
  orderLineRef: "12",            // welche Zeile ihrer Bestellung damit beglichen wird (BT-132)
  objectRef: "METER-99213",      // der abgerechnete Gegenstand (BT-128)
  buyerAccountingRef: "CC-4712", // ihre Kostenstelle, für diese Position (BT-133)
  originCountry: "CH",           // Ursprungsland (BT-159)
}

Umsatzsteuer - vat

Die Umsatzsteuer steht an der Position (und an jedem Eintrag in allowancesCharges): vat mit einer Kategorie (category) und ihrem Satz (ratePercent).

categoryBedeutungratePercent
SRegelsteuersatzder Prozentsatz, z. B. 19
ZNullsatz0
ESteuerbefreitweglassen
AESteuerschuldnerschaft des Leistungsempfängers (Reverse Charge)weglassen
KInnergemeinschaftliche Lieferung (EU)weglassen
GAusfuhr außerhalb der EUweglassen
ONicht steuerbarweglassen

Für die Kategorien außer dem Regelsatz verlangt die EN 16931 einen Grund. Geben Sie ihn einmal je Kategorie in vatExemptionReasons an:

const invoice: Invoice = {
  // ... jede Position verwendet { category: "AE" } ...
  vatExemptionReasons: {
    AE: {
      text: "Reverse charge - Steuerschuldnerschaft des Leistungsempfängers (§13b UStG)",
      code: "VATEX-EU-AE",
    },
  },
};

Die optionalen Extras

Greifen Sie danach, wenn Ihr Fall es verlangt. Nichts davon ist Pflicht.

Daten und Referenzen

dueDate: "2026-07-05",
buyerReference: "04011000-12345-34", // die Leitweg-ID - PFLICHT für XRechnung (B2G)
purchaseOrderRef: "PO-7782",   // ihre Bestellnummer (BT-13)
salesOrderRef: "SO-1190",      // Ihre Auftragsnummer (BT-14)
contractRef: "C-2026-09",
projectRef: "Neue Halle 4",    // das Projekt, zu dem die Rechnung gehört (BT-11)
tenderRef: "VgV-2026-0031",    // Vergabe oder Los, für öffentliche Aufträge (BT-17)
objectRef: "METER-99213",      // was abgerechnet wird, wenn es ein Gegenstand ist (BT-18)
buyerAccountingRef: "CC-4711", // ihre Kostenstelle (BT-19)
notes: ["Vielen Dank für Ihren Auftrag."],
noteSubjectCode: "AAI",        // worum es in den Hinweisen geht, als Code (BT-21)

Große Kunden bestehen oft auf buyerAccountingRef: Damit kann ihre Buchhaltung die Rechnung zuordnen, ohne dass ein Mensch sie liest. Beide Parteien können außerdem eine Verzeichniskennung tragen - eine GLN oder Ähnliches - als seller.identifier (BT-29) und buyer.identifier (BT-46).

Zahlung (payment) - damit der Käufer weiß, wohin und wie er zahlt:

payment: {
  meansCode: "58", // SEPA-Überweisung (UNCL 4461); "30" ist eine einfache Überweisung
  iban: "DE89370400440532013000",
  bic: "COBADEFFXXX",
  accountName: "Northwind GmbH",
  terms: "Zahlbar innerhalb von 14 Tagen ohne Abzug.",
  reference: "RE-2026-0184", // BT-83, was der Zahler angeben soll; ohne Angabe die Rechnungsnummer
  meansText: "SEPA-Lastschrift", // BT-82, wenn ein Code allein es nicht sagt
},

reference ist der Verwendungszweck - der Text, den der Zahler auf die Überweisung schreibt. Lassen Sie ihn weg, wird die Rechnungsnummer verwendet, und das ist fast immer das, was Sie wollen.

Skonto - cashDiscounts

„2 % innerhalb von 14 Tagen“ ist kein Nachlass im Sinne der EN 16931, und das ist die eine Stelle, an der die naheliegende Abkürzung eine falsche Rechnung erzeugt. Ein Abzug, der nur gilt, wenn der Käufer früh zahlt, darf die Rechnungssumme nicht mindern, denn beim Ausstellen weiß niemand, ob er gezogen wird. Er gehört deshalb in die Zahlungsbedingungen, als payment.cashDiscounts:

payment: {
  iban: "DE89370400440532013000",
  terms: "Zahlbar innerhalb von 30 Tagen.",
  cashDiscounts: [
    { days: 14, percent: 2 },  // 2 % bei Zahlung innerhalb von 14 Tagen
    { days: 30, percent: 1 },
  ],
},

Die Basis ist die Rechnungssumme einschließlich Umsatzsteuer, auf die in Deutschland üblicherweise Skonto berechnet wird; je Stufe lässt sie sich mit baseAmount überschreiben. Die Summen bewegen sich nicht - das Skonto wird auf die Seite gedruckt und in BT-20 in der maschinenlesbaren Form geschrieben, die die XRechnung vorschreibt:

#SKONTO#TAGE=14#PROZENT=2.00#
Bilden Sie Skonto nicht als Eintrag in allowancesCharges ab. Der Betrag würde sofort abgezogen, obwohl er nur bei früher Zahlung gilt - eine Datei, die jeden Validator besteht und die falsche Summe nennt. xrechnungProblems() sucht genau diesen Fehler und benennt ihn.

SEPA-Lastschrift - directDebit

Wenn Sie einziehen, statt bezahlt zu werden, muss der Zahler die Lastschrift auf seinem Kontoauszug wiedererkennen:

payment: {
  meansCode: "59", // SEPA-Lastschrift (UNCL 4461); "58" ist eine SEPA-Überweisung
  directDebit: {
    mandateReference: "MND-2024-00871", // das Mandat, das er unterschrieben hat (BT-89)
    creditorId: "DE98ZZZ09999999999",   // Ihre Gläubiger-ID (BT-90)
    debitedIban: "DE02120300000000202051", // das belastete Konto (BT-91)
  },
},

Alle drei stehen auf der Seite und im XML. Ein Einzug ohne Mandatsreferenz wird von der Vorabprüfung gemeldet, weil der Zahler ihn mit nichts abgleichen kann.

Nachlässe oder Zuschläge auf den Beleg (allowancesCharges, isCharge unterscheidet sie) - nach der Summe der Positionen angewendet, jeweils mit eigener Umsatzsteuer:

allowancesCharges: [
  { isCharge: false, amount: 50, reason: "Treuerabatt", vat: { category: "S", ratePercent: 19 } },
  { isCharge: true, amount: 12, reason: "Versand", vat: { category: "S", ratePercent: 19 } },
],

Jeder nimmt außerdem einen reasonCode - das codierte Gegenstück zu reason (UNCL 5189 für einen Nachlass, UNCL 7161 für einen Zuschlag). Manche Empfänger gleichen über den Code ab statt über den Text.

Sie können statt eines festen Betrags (amount) einen Prozentsatz (percent) angeben, und dann bleiben „10 %“ im XML und auf der Seite erhalten, statt still in Euro umgerechnet zu werden:

allowancesCharges: [
  {
    isCharge: false,
    baseAmount: 3980, // worauf sich der Prozentsatz bezieht (BT-93)
    percent: 10,      // der Satz (BT-94)
    reason: "Rahmenvertrag",
    vat: { category: "S", ratePercent: 19 },
  },
],

Der Betrag wird abgeleitet. An einer Position funktioniert es genauso (BT-137/138 und BT-142/143). Geben Sie einen ausdrücklichen amount und einen Prozentsatz an, gewinnt der Betrag - Sie zu übergehen würde einen Tippfehler verstecken -, aber die Vorabprüfung meldet die Abweichung mit beiden Zahlen.

Auf der Seite steht dann Rahmenvertrag (10 % von 3.980,00 €) -398,00 € statt eines unerklärten -398,00 €.

Gutschriften und Korrekturen - precedingInvoices

Eine Gutschrift (type: 381), die nicht über precedingInvoices sagt, was sie korrigiert, lässt sich ihrem Original nicht zuordnen - und nach dem Original sucht ein Prüfer:

const creditNote: Invoice = {
  number: "GS-2026-0011",
  type: 381, // Gutschrift
  precedingInvoices: [{ number: "RE-2026-0187", issueDate: "2026-08-14" }],
  // ... der Rest wie üblich
};

Mehrere sind erlaubt - eine Korrektur kann mehr als eine Rechnung betreffen. Das Datum ist optional, löst aber eine Nummer auf, die sich über die Jahre wiederholt.

Anhänge - supportingDocuments

Ein Stundenzettel, ein Leistungsnachweis, ein Lieferschein. Wer nach Aufwand abrechnet, hat so etwas, und es getrennt per E-Mail zu schicken verschenkt genau den Vorteil, für den es die E-Rechnung gibt:

supportingDocuments: [
  {
    reference: "TIMESHEET-09",
    description: "Stundenzettel September",
    file: {
      content: readFileSync("stunden.csv"), // Uint8Array
      mimeType: "text/csv",
      filename: "stunden.csv",
    },
  },
  // oder ein Link statt einer Datei:
  { reference: "AB-2026-14", description: "Auftragsbestätigung", url: "https://…/ab.pdf" },
],

Eine file reist zweimal mit: Base64-codiert im XML, wo eine Maschine sie findet, und als eingebettete Datei im PDF/A-3, wo ein Mensch sie per Doppelklick öffnet. Nur das Rechnungs-XML ist als Rechnungsdaten markiert, Anhänge als Ergänzungen. Die Seite nennt sie, damit niemand erst auf die Idee kommen muss, in der Anhangsleiste nachzusehen.

Und noch ein paar: delivery (date, recipientName, eine Lieferadresse address und locationId für eine Standortnummer statt einer Adresse), payeeName mit payeeIdentifier und payeeLegalRegistrationId (wenn die Zahlung an jemand anderen als den Verkäufer geht - etwa einen Factor), paidAmount (bereits gezahlt, wird abgezogen und ergibt den Zahlbetrag) und type (380 eine Handelsrechnung, 381 eine Gutschrift).

Auf einen glatten Zahlbetrag runden - roundingAmount

Manche Häuser ziehen einen runden Betrag ein. roundingAmount ist eine Zahl mit Vorzeichen, die zum Zahlbetrag addiert wird; die Rechnungssumme selbst bewegt sich nicht:

roundingAmount: -0.4, // 107,40 Summe -> 107,00 zu zahlen

Die Seite druckt die Rundungszeile mit Vorzeichen, denn ein Zahlbetrag, der nicht der Summe abzüglich des bereits Gezahlten entspricht, sieht sonst wie ein Rechenfehler aus.

Rechnungen in fremder Währung - taxCurrency

Ist die Rechnung nicht in Euro, schuldet ein deutscher Verkäufer dem Finanzamt die Umsatzsteuer trotzdem in Euro. Geben Sie sie mit taxCurrency und taxTotalInTaxCurrency an, sonst ist die Rechnung rechtlich mangelhaft:

currency: "USD",
taxCurrency: "EUR",          // die Währung, in der die Umsatzsteuer verbucht wird (BT-6)
taxTotalInTaxCurrency: 172.4, // die Steuersumme darin (BT-111)
Die Umrechnung ist Ihre Sache, nicht unsere. Welcher Kurs gilt - am Tag der Leistung, am Rechnungsdatum, der Monatsdurchschnitt - ist eine steuerliche Frage, und eine Zahl auf eine Rechnung zu setzen, die niemand gewählt hat, wäre schlimmer, als danach zu fragen. xrechnungProblems() warnt, sobald die Währung nicht EUR ist und keine Buchungswährung angegeben wurde.

Das ist das ganze Modell. Beginnen Sie mit der kleinsten Rechnung und ergänzen Sie nur, was Sie brauchen - Summen und XML bleiben darunter jedes Mal korrekt.