Automation MCP Server Features Blog Pricing Contact
Integration ZUGFeRD Format

Create and Validate ZUGFeRD Invoices in Java: Complete Guide

A practical guide to the full ZUGFeRD lifecycle in Java: creating hybrid invoices from structured data, validating against EN 16931 with the official Schematron, converting ordinary PDFs with AI extraction, and importing incoming invoices as JSON. Plain java.net.http, no SDK, with an honest look at where Mustangproject fits.

Germany's B2B e-invoicing mandate has made ZUGFeRD a concrete requirement for Java teams. Since January 2025 every German business must be able to receive structured e-invoices, and issuance becomes mandatory in phases from 2027. For developers running Spring Boot services, Jakarta EE applications, or plain Java batch jobs that bill German customers, the question is how to produce and verify compliant hybrid invoices without turning the team into PDF/A-3 and Schematron specialists.

One scoping note up front: ZUGFeRD 2.x and Factur-X are technically the same standard, identical UN/CEFACT CII XML inside a PDF/A-3 container, with ZUGFeRD as the German branding and Factur-X as the French one. This guide covers the German side. If you invoice French customers under the September 2026 reform, the companion guide to Factur-X in Java walks the same lifecycle with the French requirements and sample data.

Java is also the one ecosystem with a genuinely strong open-source option. Mustangproject is mature, actively maintained, and the reference implementation many German products build on. This guide is honest about where it is the right choice, and where a managed REST API is the better fit.

What follows covers the full lifecycle using the ZUGFeRD API from plain Java: creating invoices from structured data, validating against EN 16931 with the official Schematron, converting ordinary PDFs to ZUGFeRD with AI extraction, and extracting incoming invoices as JSON. All examples use java.net.http.HttpClient on Java 17 or later, with no SDK to install, and are available in the InvoiceXML Java examples repository.

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.


Extract structured data from ZUGFeRD

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

OperationEndpointInputOutput
Create ZUGFeRDPOST /v1/create/zugferdJSONPDF/A-3 binary
PDF to ZUGFeRD (AI)POST /v1/transform/to/zugferdPDF, image, DOCX, XLSXPDF/A-3 binary
Validate ZUGFeRDPOST /v1/validate/zugferdZUGFeRD PDFValidation JSON
Extract as JSONPOST /v1/extract/jsonZUGFeRD PDFStructured JSON
Extract CII XMLPOST /v1/extract/xmlZUGFeRD PDFCII XML
Render as PDFPOST /v1/render/cii/to/pdfCII XMLPDF 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.

Start free today

Ready to automate your invoices?

Validate, convert and embed compliant e-invoices through one API. Start your 30-day free trial. No credit card required.

GDPR Compliant No credit card required Setup in minutes
Peppol UBL
Factur-X
EN 16931
142 / 142 passed
Compliant
PDF/A-3 embedded