Why Java teams hit ZUGFeRD
The regulatory pressure is domestic and concrete:
The B2B mandate timeline. Receiving e-invoices has been mandatory for all German businesses since January 2025: a buyer can no longer refuse a compliant e-invoice, and a plain PDF no longer counts as one. Issuing becomes mandatory from January 2027 for companies above EUR 800k turnover and from January 2028 for everyone else. ZUGFeRD profiles EN 16931 and above qualify as e-invoices under these rules; the MINIMUM and BASIC WL profiles are booking aids and do not.
ZUGFeRD vs XRechnung. ZUGFeRD is the B2B workhorse: a hybrid PDF the recipient can read and the recipient's software can parse. For B2G, German federal and state portals (ZRE, OZG-RE) require XRechnung instead, a standalone XML sibling built on the same EN 16931 model. If your Java application serves both audiences, the same API surface covers both; the companion guide to XRechnung in Java walks through the B2G side end to end.
GoBD archiving. German bookkeeping rules (GoBD) require invoices to be archived in the form they were received, immutable and machine-evaluable, for the statutory retention period. ZUGFeRD's PDF/A-3 container is convenient here: the archival-grade PDF and the structured XML live in one file, so the document you store is both the human-readable record and the machine-readable one.
The technical work behind "just attach some XML to a PDF" is deceptively deep: a PDF/A-3b container with embedded fonts, ICC color profiles, and XMP metadata declaring the profile; CII XML that satisfies roughly 200 EN 16931 business rules; and Schematron validation to prove it. The technical overview of ZUGFeRD automation walks through each layer in detail.
Mustangproject: an honest comparison
Any serious discussion of ZUGFeRD in Java starts with Mustangproject. It is the dominant open-source ZUGFeRD Java library: Apache-2.0 licensed, actively maintained for over a decade, and the reference many German invoicing products build on. It writes, reads, and validates ZUGFeRD 1.x and 2.x, XRechnung, and Order-X, and ships a useful command-line tool for one-off jobs. Unlike .NET or Node.js, Java even has a native XSLT 2.0 processor in Saxon-HE, so Schematron validation is technically feasible in-process. That capability is exactly why the operating costs deserve an honest look.
The costs show up in operation rather than in the first prototype:
You own the dependency stack. Mustangproject pulls in PDFBox for PDF manipulation, Saxon for Schematron, and veraPDF for PDF/A checks. Each has its own release cadence, CVE stream, and occasional breaking changes, and the combination has to keep working together across your JDK upgrades.
PDF/A-3 conversion of arbitrary input is the hard part. Generating a fresh invoice PDF as PDF/A-3 is manageable. Converting an existing PDF (from JasperReports, a template engine, or an upstream system) into a conformant PDF/A-3 container is not fully solved in open source: fonts that are not embedded, transparency, and color space issues produce files that look fine but fail conformance checks at a receiving platform.
Specification maintenance is on your calendar. ZUGFeRD 2.4 changed mandatory fields, XRechnung releases a new version annually, and every update means new validation artifacts to integrate, test, and ship. The library follows, but the window between a specification's effective date and your production deployment is compliance risk you carry.
Unstructured input is out of scope. Mustangproject reads the structured XML out of existing hybrid PDFs; it cannot extract invoice data from an ordinary PDF, a scan, or a photo. If suppliers send you plain PDFs, that ingestion problem needs a separate solution.
The rest of this guide shows the way out: the compliance layer as a managed service, called with the HTTP client already built into the JDK. Every one of the costs above disappears, because the dependency stack, the PDF/A conversion, the specification updates, and the unstructured-input problem all live server-side, kept current by a team that does e-invoicing compliance full time.
The full comparison and migration guide: Mustangproject alternatives for ZUGFeRD and Factur-X teams →
Setting up the Java client
All examples use java.net.http.HttpClient, part of the JDK since Java 11, written here with Java 17+ text blocks. The only third-party dependency is Jackson for JSON parsing in the validation and extraction examples.
Sign up for a free InvoiceXML account and you will receive 100 free credits on signup, no credit card required. Keep the API key in an environment variable or your secrets manager, not in source:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
String apiKey = System.getenv("INVOICEXML_API_KEY");
String baseUrl = "https://api.invoicexml.com";
The client is immutable and thread-safe; create it once and share it across your application.
Create ZUGFeRD invoices in Java
Send your invoice as JSON. Totals and the VAT breakdown are auto-calculated from the line items, and the API validates the result against the full EN 16931 rule set before returning the PDF.
import java.nio.file.Files;
import java.nio.file.Path;
String payload = """
{
"invoice": {
"invoiceNumber": "RE-2026-001",
"issueDate": "2026-07-25",
"currency": "EUR",
"seller": {
"name": "Mustermann Software GmbH",
"vatIdentifier": "DE123456789",
"postalAddress": {
"line1": "Hauptstrasse 12",
"city": "Berlin",
"postCode": "10115",
"country": "DE"
}
},
"buyer": {
"name": "Beispiel Handel AG",
"postalAddress": {
"line1": "Marienplatz 8",
"city": "Muenchen",
"postCode": "80331",
"country": "DE"
}
},
"paymentDetails": {
"paymentAccountIdentifier": "DE89370400440532013000"
},
"lines": [
{
"quantity": 10,
"item": { "name": "Softwareentwicklung" },
"priceDetails": { "netPrice": 250.00 },
"vatInformation": { "rate": 19 }
}
]
}
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/create/zugferd"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<byte[]> response =
http.send(request, HttpResponse.BodyHandlers.ofByteArray());
if (response.statusCode() == 200) {
Files.write(Path.of("invoice-zugferd.pdf"), response.body());
} else {
System.err.println(new String(response.body()));
}
That is the whole request. The API computes totals and the VAT breakdown, generates the CII XML, embeds it as the factur-x.xml attachment (the name the ZUGFeRD specification prescribes since version 2.2), applies the PDF/A-3 conformance layer with the required XMP metadata, and validates against EN 16931 Schematron before returning the binary PDF.
If validation fails (mismatched totals, a missing mandatory field), the API returns HTTP 400 with the violated rules as structured findings, so a bad invoice never leaves your system.
Full create example on GitHub →
Validate ZUGFeRD invoices in Java
Documents from /v1/create/zugferd have already passed the official profile rules, so this endpoint is for the ones you did not generate: hybrid PDFs arriving from suppliers, output from a legacy tool, files from a subsidiary. It extracts the embedded XML, detects the profile from BT-24, and runs the matching XSD and Schematron rule set.
java.net.http has no built-in multipart support, so file uploads need a small helper that assembles the multipart body. This is the one piece of plumbing in the whole integration, and it is reusable for every upload endpoint:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
static HttpRequest.BodyPublisher multipartBody(
Path file, String boundary, Map<String, String> fields) throws IOException {
List<byte[]> parts = new ArrayList<>();
for (var field : fields.entrySet()) {
parts.add(("--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"" + field.getKey() + "\"\r\n\r\n"
+ field.getValue() + "\r\n").getBytes(StandardCharsets.UTF_8));
}
parts.add(("--" + boundary + "\r\n"
+ "Content-Disposition: form-data; name=\"file\"; filename=\""
+ file.getFileName() + "\"\r\n"
+ "Content-Type: application/octet-stream\r\n\r\n")
.getBytes(StandardCharsets.UTF_8));
parts.add(Files.readAllBytes(file));
parts.add(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
return HttpRequest.BodyPublishers.ofByteArrays(parts);
}
Validation is then a straightforward call, with Jackson mapping the response onto Java records:
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
@JsonIgnoreProperties(ignoreUnknown = true)
public record ValidationResult(
boolean valid,
String detail,
ValidationData data,
List<ValidationFinding> errors,
List<ValidationFinding> warnings) {}
@JsonIgnoreProperties(ignoreUnknown = true)
public record ValidationData(
Boolean schemaValid,
Boolean schematronValid,
String profile) {}
@JsonIgnoreProperties(ignoreUnknown = true)
public record ValidationFinding(
String rule,
Integer line,
String message,
List<String> btCodes,
List<String> fields,
String raw) {}
static ValidationResult validateZugferd(
HttpClient http, String baseUrl, String apiKey, Path pdf)
throws IOException, InterruptedException {
String boundary = "----" + UUID.randomUUID();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/validate/zugferd"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(multipartBody(pdf, boundary, Map.of()))
.build();
HttpResponse<String> response =
http.send(request, HttpResponse.BodyHandlers.ofString());
return new ObjectMapper()
.readValue(response.body(), ValidationResult.class);
}
Usage:
ValidationResult result =
validateZugferd(http, baseUrl, apiKey, Path.of("invoice-zugferd.pdf"));
if (result.valid()) {
System.out.println("Valid ZUGFeRD invoice (" + result.data().profile() + ")");
} else {
System.out.println("Validation failed with " + result.errors().size() + " error(s):");
for (ValidationFinding err : result.errors()) {
System.out.println(" [" + err.rule() + "] " + err.message());
if (err.fields() != null && !err.fields().isEmpty()) {
System.out.println(" Fields: " + String.join(", ", err.fields()));
}
}
}
Both valid and invalid invoices return HTTP 200; branch on result.valid() rather than the status code. Each finding carries a plain-language message, the business term codes, the field paths into the document, and the verbatim validator output in raw.
This slots directly into JUnit: validate every generated document in your test suite, with no Saxon, veraPDF, or Schematron artifacts in your build.
Full validation example on GitHub →
Parse supplier PDFs with AI
The receiving side of the mandate does not stop at hybrid PDFs: plenty of German suppliers still send plain PDFs, scans, or photographed paper invoices, and Mustangproject cannot help there because it requires structured input. POST /v1/parse/json reads such documents visually with OCR and AI field extraction and returns the invoice as the same normalized JSON that the extract endpoint below produces for real ZUGFeRD files. Every response carries a confidence object (overall plus per-area scores for seller, buyer, tax calculation, and line items, each 0.0 to 1.0) so your AP workflow can auto-book the certain reads and put the rest in front of a person:
static JsonNode parseSupplierPdf(
HttpClient http, String baseUrl, String apiKey, Path pdf)
throws IOException, InterruptedException {
String boundary = "----" + UUID.randomUUID();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/parse/json"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(multipartBody(pdf, boundary, Map.of()))
.build();
HttpResponse<String> response =
http.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IllegalStateException(response.body());
}
return new ObjectMapper().readTree(response.body());
}
JsonNode parsed = parseSupplierPdf(http, baseUrl, apiKey, Path.of("supplier-scan.pdf"));
double confidence = parsed.at("/confidence/overall").asDouble();
JsonNode invoice = parsed.get("invoice");
if (confidence >= 0.7) {
bookInvoice(invoice);
} else {
reviewQueue.add(invoice);
}
A file the classifier decides is not an invoice fails with error code 4008 and a multi-invoice PDF with 4009, so the endpoint acts as a filter as much as a parser. When an invoice carries embedded supporting documents (BG-24 delivery notes, timesheets, the original order), /v1/extract/attachments decodes them into a ZIP; see the attachment extraction reference.
For incoming ZUGFeRD invoices that need to land in your ERP, accounting system, or database, the extract endpoint returns a fully parsed JSON representation:
import com.fasterxml.jackson.databind.JsonNode;
static JsonNode extractInvoice(
HttpClient http, String baseUrl, String apiKey, Path pdf)
throws IOException, InterruptedException {
String boundary = "----" + UUID.randomUUID();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/extract/json"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(multipartBody(pdf, boundary, Map.of()))
.build();
HttpResponse<String> response =
http.send(request, HttpResponse.BodyHandlers.ofString());
return new ObjectMapper().readTree(response.body());
}
JsonNode invoice = extractInvoice(http, baseUrl, apiKey, Path.of("incoming-invoice.pdf"));
System.out.println("Invoice " + invoice.get("invoiceNumber").asText());
System.out.println("Total: " + invoice.get("totalAmount").asText());
To get the embedded CII XML instead of parsed JSON, use /v1/extract/xml; the response is raw application/xml you can hand to JAXB or any XML tooling.
Spring Boot integration
In Spring Boot 3.2+, wrap the API in a service built on RestClient and let the container manage configuration and lifecycle:
// application.yml
// invoicexml:
// base-url: https://api.invoicexml.com
// api-key: ${INVOICEXML_API_KEY}
@Configuration
public class InvoiceXmlConfig {
@Bean
RestClient invoiceXmlClient(
@Value("${invoicexml.base-url}") String baseUrl,
@Value("${invoicexml.api-key}") String apiKey) {
return RestClient.builder()
.baseUrl(baseUrl)
.defaultHeader("Authorization", "Bearer " + apiKey)
.build();
}
}
@Service
public class ZugferdService {
private final RestClient client;
public ZugferdService(RestClient invoiceXmlClient) {
this.client = invoiceXmlClient;
}
public byte[] create(Object invoice) {
return client.post()
.uri("/v1/create/zugferd")
.contentType(MediaType.APPLICATION_JSON)
.body(Map.of("invoice", invoice))
.retrieve()
.body(byte[].class);
}
public ValidationResult validate(byte[] pdf) {
MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
form.add("file", new ByteArrayResource(pdf) {
@Override
public String getFilename() { return "invoice.pdf"; }
});
return client.post()
.uri("/v1/validate/zugferd")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(form)
.retrieve()
.body(ValidationResult.class);
}
}
Spring's converters handle the multipart encoding here, so the hand-rolled helper from the plain Java examples is not needed. For resilience, wrap the calls with Resilience4j retry and treat only 5xx and transport errors as retryable; 4xx responses are permanent until the request itself changes.
ZUGFeRD Java library vs REST API
The build-versus-buy summary for Java, without marketing gloss:
Building on Mustangproject means owning the whole stack. The library is free, mature, and proven, and adopting it makes the PDFBox, Saxon, and veraPDF dependency chain yours, along with PDF/A-3 conformance and the update work each specification cycle: watch the FeRD announcement, wait for the library release, bump the version, retest, and redeploy every service that issues invoices. ZUGFeRD moves yearly, so that cycle never ends.
The REST API is a complete compliance service. The official EN 16931 and profile rule sets stay current server-side, new ZUGFeRD versions are live before their effective dates with no change to your code, no monitoring, and no redeploy. Conformant PDF/A-3 output, structured findings your application can act on, XRechnung and Peppol UBL through the same integration, deep e-invoicing expertise with professional support behind it, and missing features integrated on request. Your build keeps zero compliance dependencies.
The deciding question is who owns the moving target. Compliance changes on the regulator's calendar. With a library, that calendar dictates your release schedule indefinitely; with the API, your integration is finished the day it works, because staying compliant is our job.
On data handling: every document is processed statelessly, in memory, and purged on response delivery. Nothing is written to disk, nothing is logged, and no invoice data is used for model training, so VAT numbers, IBANs, and payment data from your Java application never persist outside the single HTTP call.
Complete endpoint reference for Java
| Operation | Endpoint | Input | Output |
| Create ZUGFeRD | POST /v1/create/zugferd | JSON | PDF/A-3 binary |
| Parse PDF with AI | POST /v1/parse/json | Invoice PDF (typed or scanned) | Structured JSON + confidence |
| Extract attachments | POST /v1/extract/attachments | ZUGFeRD PDF or CII/UBL XML | ZIP archive |
| Validate ZUGFeRD | POST /v1/validate/zugferd | ZUGFeRD PDF | Validation JSON |
| Extract as JSON | POST /v1/extract/json | ZUGFeRD PDF | Structured JSON |
| Extract CII XML | POST /v1/extract/xml | ZUGFeRD PDF | CII XML |
| Render as PDF | POST /v1/render/cii/to/pdf | CII XML | PDF binary |
Full OpenAPI 3.1 schema: api.invoicexml.com/v1/openapi | Interactive API explorer: api.invoicexml.com/v1/scalar
Get started
Every Java snippet in this guide is available as a runnable example on GitHub. Clone the repo, set INVOICEXML_API_KEY, and you have a working integration in minutes:
InvoiceXML/facturx-api-examples/java →
Create a free InvoiceXML account → get 100 credits for free, no credit card required.
Related resources:
InvoiceXML is a REST API for European e-invoice compliance covering ZUGFeRD, Factur-X, XRechnung, Peppol UBL, and CII. Stateless processing, GDPR compliant by architecture, and integration from any Java application: Spring Boot, Jakarta EE, batch jobs, or desktop software.