Eine gültige E-Rechnung (XRechnung oder ZUGFeRD/Factur-X) erzeugst du aus Java am robustesten über eine E-Rechnung API statt durch eigenes XML-Bauen: Du sendest strukturierte Rechnungsdaten per java.net.http.HttpClient, der Dienst rechnet die Summen, vergibt die Nummer und rendert das EN-16931-konforme Dokument. Der Flow sind drei Calls — anlegen, ausstellen, XML oder PDF abholen — die sich mit dem eingebauten HttpClient ohne zusätzliche HTTP-Bibliothek abbilden lassen. Die Beispiele laufen ab Java 17.

XRechnung ist reines XML nach EN 16931, ZUGFeRD/Factur-X ein PDF mit eingebettetem XML. In Java kannst du dieses XML selbst zusammenbauen — und Codelisten, Geschäftsregeln und Profilstände jährlich nachpflegen — oder die Erzeugung an eine E-Rechnung API delegieren. Dieser Beitrag zeigt den API-Weg mit dem eingebauten java.net.http.HttpClient, ganz ohne zusätzliche HTTP-Bibliothek. Die Beispiele brauchen Java 17 oder neuer (sie nutzen Text-Blöcke).

Der Flow: drei Calls

  1. Anlegen — du schickst Kunde und Positionen, der Server rechnet Summen und USt.
  2. Ausstellen — die Rechnungsnummer wird atomar vergeben, das XML gerendert und gegen die EN-16931-Geschäftsregeln validiert.
  3. Abholen — das reine XRechnung-XML (CII) oder das hybride ZUGFeRD/Factur-X-PDF.

Base-URL und Test-Key (sk_test_…) kommen aus der Umgebung, damit sich der Code später ohne Änderung auf produktiv umstellen lässt. Die Base-URL steht offen in der API-Referenz, den Test-Key legst du dir selbst im Kontobereich an.

Ein kleiner Client

Alle Schritte leben in einer Datei, Fortlauf.java. Die Klasse kapselt den Bearer-Header und die Statusprüfung; die Methoden aus den Schritten 1–3 setzt du an der markierten Stelle ein.

Java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
import java.nio.file.Path;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Fortlauf {
    private static final String BASE_URL = System.getenv("FORTLAUF_API_URL");
    private static final String API_KEY = System.getenv("FORTLAUF_API_KEY");
    private final HttpClient http = HttpClient.newHttpClient();

    private <T> HttpResponse<T> send(HttpRequest.Builder req, BodyHandler<T> handler)
            throws Exception {
        HttpRequest built = req.header("Authorization", "Bearer " + API_KEY).build();
        HttpResponse<T> res = http.send(built, handler);
        if (res.statusCode() >= 400) {
            throw new IllegalStateException(
                built.method() + " " + built.uri() + " → HTTP " + res.statusCode());
        }
        return res;
    }

    // Liest die "id" der Rechnung (das erste "id"-Feld der Antwort).
    // In echtem Code nimmst du Jackson: mapper.readTree(json).get("id").asText()
    private static String extractId(String json) {
        Matcher m = Pattern.compile("\"id\"\\s*:\\s*\"([^\"]+)\"").matcher(json);
        if (!m.find()) throw new IllegalStateException("keine id in der Antwort");
        return m.group(1);
    }

    // … hier kommen anlegen(), ausstellen() und abholen() aus den Schritten 1–3 hin …

    public static void main(String[] args) throws Exception {
        Fortlauf api = new Fortlauf();
        String id = api.anlegen();
        api.ausstellen(id);
        api.abholen(id);
        System.out.println("Rechnung " + id + " ausgestellt und abgeholt");
    }
}

1) Rechnung anlegen

Beträge und Mengen gehen als DECIMAL-Strings über die Leitung — nie als double, sonst holt dich die Fließkomma-Rundung ein. Intern rechnest du am besten mit java.math.BigDecimal und serialisierst als String. Der Idempotency-Key macht einen wiederholten Anlege-Call zum Replay statt zum Duplikat.

Java
String anlegen() throws Exception {
    // In der Praxis serialisierst du ein DTO mit Jackson/Gson; hier als Text-Block
    // zur Veranschaulichung — Beträge als Strings, nicht als Zahl-Literale.
    String body = """
        {
          "currency": "EUR",
          "documentTarget": "b2g",
          "buyer": {
            "name": "Zweckverband Wasserversorgung Nordmoor",
            "addressLine1": "Deichweg 15",
            "postalCode": "26847", "city": "Nordmoor", "countryCode": "DE"
          },
          "buyerElectronicAddress": "<LEITWEG-ID>",
          "buyerReference": "<LEITWEG-ID>",
          "invoiceTypeCode": "380",
          "lineItems": [
            { "description": "Betriebsführung Leitstelle (Q3 2026)", "quantity": "3",
              "unitCode": "MON", "unitPriceNet": "2100.00", "taxRatePercent": "19" }
          ]
        }
        """;

    var res = send(
        HttpRequest.newBuilder(URI.create(BASE_URL + "/invoices"))
            .header("Content-Type", "application/json")
            .header("Idempotency-Key", UUID.randomUUID().toString())
            .POST(BodyPublishers.ofString(body)),
        HttpResponse.BodyHandlers.ofString());

    // Status "draft", Summen serverseitig berechnet
    return extractId(res.body());
}

buyerReference trägt die Leitweg-ID (BT-10) — nur im Behördenkontext (B2G) relevant. "documentTarget": "b2g" ist es auch, was die separate XRechnung überhaupt erzeugt. Im inländischen B2B erzeugt Fortlauf stattdessen das hybride ZUGFeRD/Factur-X, in dem BT-10 kein Pflichtfeld ist — dort kannst du das Feld weglassen oder deine eigene Bestellreferenz eintragen.

2) Ausstellen

Java
void ausstellen(String id) throws Exception {
    send(
        HttpRequest.newBuilder(URI.create(BASE_URL + "/invoices/" + id + "/finalize"))
            .method("PATCH", BodyPublishers.noBody()),
        HttpResponse.BodyHandlers.ofString());
    // → Status "issued", Nummer vergeben, XRechnung-XML (CII) gerendert + validiert
}

3) XML oder PDF abholen

Java
void abholen(String id) throws Exception {
    // reines XRechnung-XML (CII)
    send(
        HttpRequest.newBuilder(URI.create(BASE_URL + "/invoices/" + id + "/xml")).GET(),
        HttpResponse.BodyHandlers.ofFile(Path.of("rechnung-" + id + ".xml")));

    // oder das hybride ZUGFeRD/Factur-X-PDF (PDF/A-3 mit eingebettetem XML)
    send(
        HttpRequest.newBuilder(URI.create(BASE_URL + "/invoices/" + id + "/pdf")).GET(),
        HttpResponse.BodyHandlers.ofFile(Path.of("rechnung-" + id + ".pdf")));
}

Starten: FORTLAUF_API_URL=… FORTLAUF_API_KEY=sk_test_… java Fortlauf.java — seit Java 11 startet java eine einzelne Quelldatei direkt, ganz ohne Build-Tool.

Wie das XML technisch ins PDF kommt, zeigt ZUGFeRD-PDF/A-3 erzeugen.

Worauf es in Java ankommt

  • BigDecimal, nie double. Für Geld immer BigDecimal intern und DECIMAL-Strings auf der Leitung ("1250.00", nicht 1250.0). Der Server rechnet in ganzzahligen Minor-Units und rundet einmal; das vermeidet die Summen-Inkonsistenzen, die der Validator sonst anmahnt (KoSIT-Validierung erklärt).
  • Idempotenz im Batch/Scheduler. In einem @Scheduled-Job, mit Spring Retry oder bei Message-Redelivery kann eine Erzeugung doppelt laufen; ein stabiler Idempotency-Key pro Rechnung hält den Nummernkreis lückenlos.
  • Statuscode prüfen. HttpClient wirft bei einem 4xx/5xx nicht von selbst — der kleine Wrapper oben erzwingt, dass ein 422 aus der Validierung deinen Ablauf hart stoppt statt still durchzurutschen.
  • Kein Format selbst pflegen. Codelisten, Geschäftsregeln und der Übergang zu XRechnung 4.0 liegen hinter der API; deine Java-Integration schickt „Kunde, Positionen, Steuersätze“ und bleibt stabil.

Der gezeigte Code ist illustrativ: Die vollständige Dokumentation steht dir in der API-Referenz zur Verfügung — dort steht auch die Base-URL. Den Test-Key legst du dir selbst im Kontobereich an (Schritt für Schritt im Quickstart). Ob du die E-Rechnung selbst baust oder einbindest, ordnet E-Rechnung im eigenen SaaS: bauen oder einbinden? ein. Dieselben drei Calls gibt es auch für PHP, Node.js und Python.

Selbst ausprobieren? Der Zugang ist kostenlos und ohne Karte — Produkt-Einstieg für Entwickler →

Oder den eigenen Abrechnungsfluss konkret durchsprechen: 30 Min, technisch, mit dem Gründer →

Häufige Fragen

Wie erstelle ich eine E-Rechnung mit Java?
Über drei HTTP-Calls gegen eine E-Rechnung API: POST zum Anlegen (der Server rechnet die Summen), PATCH zum Ausstellen (Nummer wird vergeben, XML gerendert und validiert) und GET zum Abholen des XRechnung-XML oder des ZUGFeRD-PDF. In Java genügt dafür der eingebaute java.net.http.HttpClient (die Beispiele laufen ab Java 17) — kein Apache HttpClient und keine eigene Schematron-Validierung nötig.
Warum kein double für Geldbeträge in Java?
Weil double Fließkomma ist und Rundungsfehler erzeugt. Nutze intern java.math.BigDecimal und schicke Beträge und Mengen als DECIMAL-Strings ("1250.00") über die Leitung. Der Server rechnet die Summen und die USt in ganzzahligen Minor-Units und rundet einmal — so entsteht keine Differenz zwischen deinem Client und dem Beleg, die der Validator sonst anmahnt.
Wie mache ich die Erzeugung in Java idempotent?
Setze beim Anlegen einen Idempotency-Key-Header mit einem pro Rechnung stabilen Wert (z. B. UUID.randomUUID() oder deine eigene Beleg-ID). Ein wiederholter POST mit demselben Key ist dann ein Replay statt eines Duplikats — wichtig bei Retries in Spring-@Retryable, in einem @Scheduled-Job oder bei Webhook-Wiederholungen.