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. If you want a local library, Java is the language where that decision is most defensible.
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 REST alternative: the compliance layer as a managed service, called with the HTTP client already built into the JDK. The two approaches also combine well, and a common production pattern is local generation with Mustangproject plus API validation of every outgoing document.
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
Before sending an invoice to a customer, or when receiving hybrid PDFs from suppliers, validate that the document passes the official profile rules. The endpoint 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 →
Convert PDF invoices to ZUGFeRD
If your Java application already produces plain PDF invoices (JasperReports, iText, a template engine) or you ingest supplier PDFs, the transform endpoint converts them to compliant ZUGFeRD without touching your rendering pipeline. The AI extraction handles scanned and photographed documents as well.
static byte[] pdfToZugferd(
HttpClient http, String baseUrl, String apiKey, Path pdf)
throws IOException, InterruptedException {
String boundary = "----" + UUID.randomUUID();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/v1/transform/to/zugferd"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(multipartBody(pdf, boundary, Map.of("profile", "en16931")))
.build();
HttpResponse<byte[]> response =
http.send(request, HttpResponse.BodyHandlers.ofByteArray());
if (response.statusCode() != 200) {
throw new IllegalStateException(new String(response.body()));
}
return response.body();
}
byte[] zugferd = pdfToZugferd(http, baseUrl, apiKey, Path.of("supplier-invoice.pdf"));
Files.write(Path.of("supplier-zugferd.pdf"), zugferd);
The profile form field accepts minimum, basicwl, basic, en16931, and extended. Besides PDF, the endpoint accepts JPEG, PNG, TIFF, WEBP, HEIC, DOCX, and XLSX inputs. This is the capability with no open-source equivalent: Mustangproject and its peers require structured data and cannot read an ordinary PDF.
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:
Choose Mustangproject when your invoices are generated from structured data you control, air-gapped or fully in-process operation is a hard requirement, and you have the capacity to own the PDFBox, Saxon, and veraPDF stack plus the update work each specification cycle. It is free, mature, and proven.
Choose the REST API when you want the compliance layer maintained for you. The API keeps the official EN 16931 and profile rule sets current server-side, produces conformant PDF/A-3 output from either structured JSON or ordinary PDFs, covers XRechnung and Peppol UBL through the same integration, and returns structured findings your application can act on. New ZUGFeRD versions are deployed before their effective dates with no change to your code.
Combine both when you already generate locally: keep Mustangproject for generation and add API validation of every outgoing document. The validate endpoint accepts files generated by any library.
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 |
| PDF to ZUGFeRD (AI) | POST /v1/transform/to/zugferd | PDF, image, DOCX, XLSX | PDF/A-3 binary |
| 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.