The reform reshapes how domestic B2B invoices travel, not just what they look like:
Deadlines. September 1, 2026: reception becomes mandatory for all French businesses, and issuance becomes mandatory for grandes entreprises and ETIs. September 1, 2027: issuance extends to PME and micro-entreprises. Alongside e-invoicing, an e-reporting obligation covers B2C and cross-border transactions on the same calendar.
Delivery model. Invoices do not travel by email under the reform. They are exchanged through registered Plateformes de Dématérialisation Partenaires (PDPs), which validate documents, route them to the recipient's platform via the central directory, and report tax data to the administration. Since the state scaled the public portal back to the directory and data-concentrator role in 2024, every business needs a PDP connection; your invoices must survive a PDP's format checks, which is why validation belongs in your pipeline before submission.
B2G stays on Chorus Pro. Invoicing the French public sector has run through Chorus Pro since the 2017-2020 phase-in, and Factur-X is an accepted upload format there. One generation pipeline can therefore serve both your B2B and public-sector customers.
Accepted formats. The reform's core set ("socle") is Factur-X, UBL, and CII. Factur-X is the common choice for issuers whose customers still expect a PDF they can open, which in practice is most of them.
Factur-X profiles and versions
Factur-X is maintained by FNFE-MPE (Forum National de la Facture Electronique et des Marchés Publics Electroniques) together with the German FeRD, and the two publish in lockstep: Factur-X 1.0.7 corresponds to ZUGFeRD 2.3. The version numbers differ, the artifacts are shared.
The standard defines a ladder of five profiles, each declaring how much structured data the XML carries:
| Profile | Structured content | French reform status |
| MINIMUM | Header amounts only | Booking aid, not a full e-invoice |
| BASIC WL | Header level, no lines | Booking aid, not a full e-invoice |
| BASIC | Full invoice with simple lines | Qualifies as a structured e-invoice |
| EN 16931 (Comfort) | Complete EN 16931 semantic model | Qualifies; the recommended default |
| EXTENDED | EN 16931 plus industry extensions | Qualifies, for complex scenarios |
For French B2B invoicing, generate EN 16931 unless a trading partner explicitly requires something else. That is what /v1/create/facturx produces, and the profile a document declares in its BT-24 identifier is exactly the rule set it gets validated against.
The Factur-X Java library landscape
Search for a Factur-X Java library and you will land on Mustangproject, the Apache-2.0 project that has co-evolved with the standard for years and serves as its de-facto reference implementation on the JVM. It can build a hybrid PDF from its invoice object model, read the XML back out, and run validation, and since Saxon-HE is a native Java library, Schematron works in-process on the JVM in a way it simply does not in most other ecosystems. It is the strongest local option in any ecosystem, which is exactly why its operating costs are worth spelling out.
What the first prototype will not show you is the operating cost. The library sits on PDFBox, Saxon, and veraPDF, and those dependencies' upgrade and CVE cycles become yours. When FNFE-MPE ships a new Factur-X release, picking up the new schemas and rule sets is your migration project. Converting an invoice PDF that already exists (from a reporting engine or an upstream system) into genuinely conformant PDF/A-3 remains the sharpest edge, since font embedding and color-space defects surface only when a PDP rejects the file. And reading data out of an ordinary, non-hybrid supplier PDF is out of scope entirely; the library needs structured input.
The sections below take the other route: the compliance machinery runs server-side behind a REST API, and your Java code is reduced to HTTP calls with the client the JDK already ships.
The full comparison and migration guide: Mustangproject alternatives for ZUGFeRD and Factur-X teams →
Client setup
Two constants and one client instance are the whole setup. Jackson appears later for JSON parsing; nothing else is required.
Create a free InvoiceXML account to get an API key with 100 free credits, no credit card needed. Load the key from the environment or a secrets manager rather than hard-coding it:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
static final String BASE_URL = "https://api.invoicexml.com";
static final String API_KEY = System.getenv("INVOICEXML_API_KEY");
static final HttpClient HTTP = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
Create Factur-X invoices in Java
A Java 17 text block holds the request body. Note the French specifics: the intra-community VAT number (N° TVA, FR + 2-digit key + SIREN), the SIREN carried in legalRegistration with the SIRENE scheme code 0002, and mixed VAT rates (20% standard, 5.5% reduced) across the lines. Totals and the per-rate VAT breakdown are computed for you.
import java.nio.file.Files;
import java.nio.file.Path;
String invoiceJson = """
{
"invoice": {
"invoiceNumber": "FA-2026-0042",
"issueDate": "2026-09-01",
"currency": "EUR",
"seller": {
"name": "Atelier Numerique SARL",
"vatIdentifier": "FR32123456789",
"legalRegistration": { "identifier": "123456789", "schemeId": "0002" },
"postalAddress": {
"line1": "18 Rue de la Republique",
"city": "Lyon",
"postCode": "69002",
"country": "FR"
}
},
"buyer": {
"name": "Librairie Grand Siecle SAS",
"postalAddress": {
"line1": "5 Boulevard Saint-Germain",
"city": "Paris",
"postCode": "75005",
"country": "FR"
}
},
"paymentDetails": {
"paymentAccountIdentifier": "FR7630006000011234567890189"
},
"lines": [
{
"quantity": 12,
"item": { "name": "Maintenance applicative" },
"priceDetails": { "netPrice": 180.00 },
"vatInformation": { "rate": 20 }
},
{
"quantity": 200,
"item": { "name": "Guide utilisateur imprime" },
"priceDetails": { "netPrice": 4.50 },
"vatInformation": { "rate": 5.5 }
}
]
}
}
""";
HttpRequest create = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/v1/create/facturx"))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(invoiceJson))
.build();
HttpResponse<byte[]> created = HTTP.send(create, HttpResponse.BodyHandlers.ofByteArray());
if (created.statusCode() != 200) {
throw new IllegalStateException(new String(created.body()));
}
Files.write(Path.of("facture-facturx.pdf"), created.body());
The response is a finished Factur-X EN 16931 document: PDF/A-3 container, embedded factur-x.xml, XMP metadata declaring the profile, already checked against the EN 16931 Schematron. A request whose data would produce a non-compliant invoice (say, a VAT breakdown that does not reconcile) is refused with HTTP 400 and the violated rules listed as findings, which means nothing invalid ever reaches a customer or a PDP.
Runnable create example on GitHub →
Validate Factur-X invoices in Java
Invoices from /v1/create/facturx clear the official rules before the API returns them, so they meet a PDP's acceptance checks as they are. Validation matters for everything else in a French pipeline: supplier documents arriving that claim to be Factur-X, and files produced by systems you did not build. The endpoint reads the embedded XML, detects the declared profile, and runs the matching official rules, including the French BR-FR constraints.
File uploads need multipart encoding, which java.net.http does not provide out of the box. A compact helper covers every upload endpoint in this guide:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Map;
import java.util.UUID;
final class MultipartUpload {
static HttpRequest.BodyPublisher of(Path file, String boundary,
Map<String, String> formFields) throws IOException {
var chunks = new ArrayList<byte[]>();
formFields.forEach((name, value) -> chunks.add(
("--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + name
+ "\"\r\n\r\n" + value + "\r\n").getBytes(StandardCharsets.UTF_8)));
chunks.add(("--" + boundary
+ "\r\nContent-Disposition: form-data; name=\"file\"; filename=\""
+ file.getFileName() + "\"\r\nContent-Type: application/octet-stream\r\n\r\n")
.getBytes(StandardCharsets.UTF_8));
chunks.add(Files.readAllBytes(file));
chunks.add(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
return HttpRequest.BodyPublishers.ofByteArrays(chunks);
}
}
Map the JSON verdict onto records with Jackson and the call becomes:
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
record FacturxReport(boolean valid, String detail, ReportData data,
List<Finding> errors, List<Finding> warnings) {}
@JsonIgnoreProperties(ignoreUnknown = true)
record ReportData(Boolean schemaValid, Boolean schematronValid, String profile) {}
@JsonIgnoreProperties(ignoreUnknown = true)
record Finding(String rule, String message, List<String> fields) {}
static FacturxReport validateFacturx(Path pdf) throws IOException, InterruptedException {
String boundary = "----" + UUID.randomUUID();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/v1/validate/facturx"))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(MultipartUpload.of(pdf, boundary, Map.of()))
.build();
HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
return new ObjectMapper().readValue(response.body(), FacturxReport.class);
}
FacturxReport report = validateFacturx(Path.of("facture-facturx.pdf"));
if (report.valid()) {
System.out.println("Compliant Factur-X, profile " + report.data().profile());
} else {
report.errors().forEach(f ->
System.out.printf("[%s] %s (fields: %s)%n", f.rule(), f.message(), f.fields()));
}
A finished validation always answers HTTP 200; the pass or fail verdict lives in valid, with non-2xx statuses reserved for transport problems such as a bad API key or an unreadable upload. Because the findings carry field paths, an AP workflow can point a supplier at the exact value that failed. Wire this into your CI as well: a JUnit test that validates a freshly generated invoice catches regressions in your invoice data mapping long before a PDP does.
Runnable validation example on GitHub →
Parse supplier PDFs with AI
The reform obliges you to receive e-invoices, but nothing obliges every supplier to send one on day one, so ordinary PDFs will keep arriving. POST /v1/parse/json turns those into data: the AI pipeline runs OCR and semantic field extraction over typed, scanned, and photographed invoices and answers with the same normalized JSON the extract endpoint below produces for genuine Factur-X, plus a confidence object (overall and four area scores between 0.0 and 1.0) that Jackson maps onto a record for threshold-based routing:
@JsonIgnoreProperties(ignoreUnknown = true)
record ParseConfidence(double overall, double sellerIdentification,
double buyerIdentification, double taxCalculation, double lineItems) {}
@JsonIgnoreProperties(ignoreUnknown = true)
record ParsedInvoice(JsonNode invoice, ParseConfidence confidence) {}
static ParsedInvoice parseSupplierPdf(Path pdf) throws IOException, InterruptedException {
String boundary = "----" + UUID.randomUUID();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/v1/parse/json"))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(MultipartUpload.of(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().readValue(response.body(), ParsedInvoice.class);
}
ParsedInvoice parsed = parseSupplierPdf(Path.of("fournisseur-scan.pdf"));
if (parsed.confidence().overall() >= 0.7) {
bookInvoice(parsed.invoice());
} else {
reviewQueue.add(parsed);
}
Non-invoices are rejected with error code 4008 and multi-invoice files with 4009, so the endpoint filters junk before your booking logic ever runs. The envelope mirrors the /v1/create request body, and for e-invoices carrying embedded supporting documents (the EN 16931 BG-24 group), /v1/extract/attachments returns the original files as a ZIP; see the attachment extraction reference.
On the receiving side, incoming Factur-X files carry their own structured data, and the extract endpoint hands it to you parsed and normalized, ready for your ERP or accounting import:
import com.fasterxml.jackson.databind.JsonNode;
static JsonNode readIncoming(Path pdf) throws IOException, InterruptedException {
String boundary = "----" + UUID.randomUUID();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/v1/extract/json"))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(MultipartUpload.of(pdf, boundary, Map.of()))
.build();
HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
return new ObjectMapper().readTree(response.body());
}
JsonNode facture = readIncoming(Path.of("fournisseur-facturx.pdf"));
System.out.println("Facture " + facture.get("invoiceNumber").asText()
+ " de " + facture.at("/seller/name").asText());
Prefer the raw embedded XML? /v1/extract/xml returns the CII document as application/xml for JAXB or StAX processing.
Spring Boot integration
In a Spring Boot 3.2+ service, bind the connection settings with @ConfigurationProperties and expose the operations through a bean built on RestClient:
@ConfigurationProperties(prefix = "invoicexml")
public record InvoiceXmlProperties(String baseUrl, String apiKey) {}
@Service
public class FacturxService {
private final RestClient rest;
public FacturxService(InvoiceXmlProperties props) {
this.rest = RestClient.builder()
.baseUrl(props.baseUrl())
.defaultHeader("Authorization", "Bearer " + props.apiKey())
.build();
}
public byte[] createFacturx(Object invoiceData) {
return rest.post()
.uri("/v1/create/facturx")
.contentType(MediaType.APPLICATION_JSON)
.body(Map.of("invoice", invoiceData))
.retrieve()
.body(byte[].class);
}
public FacturxReport validateFacturx(byte[] pdf) {
MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
form.add("file", new ByteArrayResource(pdf) {
@Override
public String getFilename() { return "facture.pdf"; }
});
return rest.post()
.uri("/v1/validate/facturx")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(form)
.retrieve()
.body(FacturxReport.class);
}
}
Spring supplies the multipart converter, so the hand-rolled body builder stays in the plain-Java examples. Add Resilience4j retry around the calls for production, retrying 5xx and I/O failures only; a 400 carries validation findings you want to read, not retry.
Factur-X Java library vs REST API
A fair comparison for French projects:
The library route is a standing engineering commitment. Mustangproject is free and proven, and adopting it means curating the PDFBox, Saxon, and veraPDF dependency stack, owning PDF/A-3 conformance, and treating every Factur-X release as your own migration project: watch the FNFE-MPE announcement, wait for the library to catch up, bump the version, retest, and redeploy every service that issues invoices. With the September 2026 reform live and evolving, that cycle repeats on the government's schedule, not yours.
The API is a complete compliance service. Current FNFE-MPE artifacts applied server-side the day they take effect, with nothing to monitor, no artifact updates, and no redeploys on your side. PDF/A-3 output that holds up under PDP acceptance checks, BR-FR findings your support team can forward to a customer verbatim, deep e-invoicing expertise and professional support behind the integration, missing features integrated on request, and the same integration extending to ZUGFeRD, XRechnung, and Peppol UBL when your market does. Your codebase keeps one HTTP client and zero compliance dependencies.
The deciding question is who owns the moving target. Compliance changes on the regulator's calendar. Build on a library and that calendar drives your releases indefinitely; build on the API and your integration is finished the day it works, because staying compliant is our job.
Data handling is strict throughout: requests are processed in memory and discarded when the response ships. No storage, no logging of document content, no training on your data. SIREN numbers, IBANs, and customer relationships exist server-side only for the duration of one HTTP exchange.
Endpoint reference
| Operation | Endpoint | Input | Output |
| Create Factur-X | POST /v1/create/facturx | 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 | Factur-X PDF or CII/UBL XML | ZIP archive |
| Validate Factur-X | POST /v1/validate/facturx | Factur-X PDF | Validation JSON |
| Extract as JSON | POST /v1/extract/json | Factur-X PDF | Structured JSON |
| Extract CII XML | POST /v1/extract/xml | Factur-X PDF | CII XML |
| Render CII 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
The runnable versions of these snippets live on GitHub; export INVOICEXML_API_KEY and they work as-is:
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 Factur-X, ZUGFeRD, XRechnung, Peppol UBL, and CII. Stateless processing, GDPR compliant by architecture, and callable from any Java application: Spring Boot, Jakarta EE, Quarkus, or a plain JVM service.