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 seit Java 11 eingebauten HttpClient ohne zusätzliche HTTP-Bibliothek abbilden lassen.
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 seit Java 11 eingebauten java.net.http.HttpClient, ganz ohne zusätzliche
HTTP-Bibliothek.
Der Flow: drei Calls
- Anlegen — du schickst Kunde und Positionen, der Server rechnet Summen und USt.
- Ausstellen — die Rechnungsnummer wird atomar vergeben, das XML gerendert und gegen die EN-16931-Geschäftsregeln validiert.
- 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 — beides bekommst du mit dem Sandbox-Zugang.
Ein kleiner Client
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
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 {
HttpResponse<T> res = http.send(
req.header("Authorization", "Bearer " + API_KEY).build(), handler);
if (res.statusCode() >= 400) {
throw new IllegalStateException(
req.build().method() + " → HTTP " + res.statusCode());
}
return res;
}
}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.
import java.net.http.HttpRequest.BodyPublishers;
import java.util.UUID;
// 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());
// draft: Status "draft", Summen serverseitig berechnet; id aus dem JSON lesen
String id = extractId(res.body()); // z. B. per Jackson: mapper.readTree(...).get("id")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
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 + validiert3) XML oder PDF abholen
import java.nio.file.Path;
// 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")));Wie das XML technisch ins PDF kommt, zeigt ZUGFeRD-PDF/A-3 erzeugen.
Worauf es in Java ankommt
BigDecimal, niedouble. Für Geld immerBigDecimalintern und DECIMAL-Strings auf der Leitung ("1250.00", nicht1250.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 stabilerIdempotency-Keypro Rechnung hält den Nummernkreis lückenlos. - Statuscode prüfen.
HttpClientwirft bei einem 4xx/5xx nicht von selbst — der kleine Wrapper oben erzwingt, dass ein422aus 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. Base-URL und Test-Key bekommst du mit dem Sandbox-Zugang im Entwickler-Bereich. 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 →