A Factur-X or ZUGFeRD invoice is two documents wearing one filename. The outer document is a PDF/A-3 container that an archival reader has to accept for the next ten years. The inner document is an XML invoice that a buyer's accounting system has to parse against EN 16931. The mistake that ships broken invoices into production is believing that getting the first one right gets the second one for free. It does not. A file can be a flawless PDF/A-3 and still carry XML that no tax authority will accept, and it can carry textbook EN 16931 XML inside a container that fails archival validation. The two layers are validated by two different tools that know nothing about each other, and a real pipeline has to satisfy both
Two validators, two different questions
veraPDF is the reference implementation for PDF/A. Point it at an invoice and it answers one question: is this a conformant PDF/A-3 file. It checks the things ISO 19005-3 cares about. Is every font embedded. Is there an OutputIntent. Does the XMP metadata declare the right part and conformance level. For an e-invoice it also checks the associated-file plumbing that PDF/A-3 requires, because the XML rides along as an embedded file with an /AFRelationship and an entry in the document catalog's /AF array. veraPDF says nothing about whether the invoice total adds up, because that is not in its remit
Mustang is the open-source validator from the Mustangproject. It asks the orthogonal question: is the embedded XML a valid invoice. It runs the XML against the schema for the declared profile and then applies the EN 16931 business rules and the country-specific rule sets layered on top, XRechnung's CIUS among them. It checks that a seller VAT identifier is present when the totals demand one, that allowance and charge amounts reconcile against the document total, that the profile URN in the XML matches what the file claims to be. Mustang does not care whether the surrounding PDF embeds its fonts, because that is veraPDF's job
Neither tool is a superset of the other. veraPDF passes a structurally perfect container around nonsense XML. Mustang passes perfect XML wrapped in a container with a missing OutputIntent. Each catches exactly the class of defect the other is blind to, which is the entire reason a serious validation harness runs both and treats a file as shippable only when both agree
The validation matrix
To prove the library produces files that survive both gates, the harness builds a matrix. Six invoice profiles cover the range a European pipeline meets in practice: Factur-X EN 16931, Factur-X BASIC, the Factur-X EXTENDED France B2B variant, XRechnung 3.0, ZUGFeRD 1.0 COMFORT, and ZUGFeRD 2.0 BASIC. Each profile is generated against two PDF/A sub-conformance levels, 3b and 3u, because the level B and level U requirements diverge on Unicode mapping and a file that passes one can fail the other. Six profiles times two levels is twelve files, every one of them built headless by the same code path the GUI sample ships, so the artifacts under test are not hand-tuned for the test
The generator writes all twelve and a script feeds each one to both validators. On the first full run veraPDF passed all twelve. The container plumbing was correct across the board: associated files registered, XMP conformance declared, output intents in place. Mustang passed eight. Four invoices were structurally valid PDF/A-3 files carrying XML that the business-rule validator rejected, which is precisely the split the two-tool approach exists to surface. Had the harness trusted veraPDF alone, those four would have looked finished
The two fixes that closed the gap
The four Mustang failures came from two distinct causes, and the fix for each is a detail worth knowing before you generate these profiles yourself
The first was the Factur-X EXTENDED France B2B profile. The original generator passed an internal label as the conformance level and an internal URN as the guideline, and Mustang rejected the file with an invalid-conformance-value error followed by an unsupported-profile-type error. The reason is that the XMP fx:ConformanceLevel field is not a free-text slot for your own profile naming. Factur-X defines exactly five standard values for it: MINIMUM, BASIC WL, BASIC, EN 16931, and EXTENDED. A France-specific B2B invoice is still an EXTENDED-profile document as far as the XMP metadata is concerned. The French character of the invoice is not expressed by inventing a sixth conformance value. It is expressed by the country code, FR, and by the guideline identifier inside the XML, which has to carry the urn:cen.eu:en16931:2017#conformant# prefix that marks a CIUS conformant to EN 16931. Passing the standard EXTENDED value with FR as the country code and the correct guideline URN made the file conformant
In the library API that is a call to AddFacturXAssociatedFileFromString with the conformance, country, and guideline aligned. The conformance level argument carries the standard token, the country code argument carries FR, and the guideline URN lives in the XML bytes you pass in
var
FileID: Integer;
begin
PDF.SetPDFAMode(5); // PDF/A-3b
PDF.NewDocument;
// ... draw the human-readable invoice page ...
// ExtendedXML carries an EN 16931 guideline URN of the form
// urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended
FileID := PDF.AddFacturXAssociatedFileFromString(
ExtendedXML,
'EXTENDED', // standard fx:ConformanceLevel, not an internal label
'factur-x.xml',
'Factur-X EXTENDED invoice',
'Alternative', // /AFRelationship
'1.0',
'FR'); // France B2B marked by country code, not by conformance
if FileID = 0 then
raise Exception.Create('Factur-X attachment rejected');
PDF.SaveToFile('02_Factur-X-EXTENDED-FR_PDFA-3b.pdf');
end;
The second cause was the ZUGFeRD 1.0 COMFORT profile, and it had nothing to do with metadata. ZUGFeRD 1.0 is validated against the :1p0 XSD, which is stricter about cardinality than the prose summaries suggest. The XSD requires that the header settlement summation, ram:SpecifiedTradeSettlementMonetarySummation, contain ram:ChargeTotalAmount and ram:AllowanceTotalAmount each exactly once. The generated XML omitted both, so Mustang reported that the elements must occur exactly one time. These are not optional when the schema says minOccurs is one. Emitting both in XSD sequence order, immediately after ram:LineTotalAmount, with a value of 0.00 when there are no charges or allowances, satisfied the schema. A zero is a present element; an absent element is a schema violation. With those two fixes in place the matrix went to twelve of twelve on Mustang while staying twelve of twelve on veraPDF
The XRechnung fields that flip invalid to valid
XRechnung deserves its own note because its German CIUS adds business rules that are absent from the base EN 16931 set, and they fail in ways that look like nothing is wrong with the document at a glance. Two of them concern electronic addresses. BT-34 is the seller's electronic address and BT-49 is the buyer's electronic address, the routing endpoints a German public-sector portal uses to deliver and acknowledge the invoice. The base EN 16931 model treats them as optional. XRechnung does not. Omit either and the invoice is well-formed, schema-valid, and rejected
The third is rule BR-DE-6, which requires the seller's contact telephone number to be present. It is the kind of field a developer drops because it feels like presentation rather than data, and its absence produces a validation failure that points at the seller contact group rather than at anything obviously missing. Supplying BT-34, BT-49, and the seller phone number is what moves an XRechnung file from invalid to valid under Mustang, and none of it changes anything veraPDF sees, because all three live in the XML
Wiring library output to a validator
The architectural point behind the harness generalizes to any business system. The PDF library writes a conformant container and embeds the XML. It does not, and should not, attempt to be the EN 16931 business-rule authority. ValidateFacturXInvoice in the library checks container consistency, that the catalog /AF array, the embedded-files name tree, the XMP DocumentFileName, the profile, the guideline, and the /AFRelationship all agree, but it does not validate tax codes or reconcile amounts. The right division of labor is for the business system to extract the XML and hand it to a dedicated invoice validator, exactly as the harness hands it to Mustang
Reading the file back tells you what was actually written. DetectFacturXInvoice reports whether an invoice was recognized, and GetFacturXInvoiceInfo reads the metadata fields by tag: tag 1 is the embedded file name, tag 2 the XMP DocumentFileName, tag 5 the conformance level, tag 6 the guideline identifier, and tag 7 the /AFRelationship. Confirming that the conformance level you read back is the standard token and not an internal label is the cheapest way to catch the EXTENDED mistake before a file leaves your build
function ExtractAndInspect(const PdfPath: string): AnsiString;
var
Profile, Guideline: WideString;
begin
Result := '';
PDF.LoadFromFile(PdfPath);
if PDF.DetectFacturXInvoice = 1 then
begin
Profile := PDF.GetFacturXInvoiceInfo(5); // fx:ConformanceLevel
Guideline := PDF.GetFacturXInvoiceInfo(6); // XML guideline ID
Writeln('Profile: ', Profile);
Writeln('Guideline: ', Guideline);
// Hand the raw XML to a dedicated EN 16931 / Mustang validator.
Result := PDF.ExtractFacturXXMLToString;
end;
end;
ExtractFacturXXMLToString returns the raw XML bytes as an AnsiString, ready to write to a file or stream into a validator process. In the test harness that target is Mustang, invoked through its command-line jar, with veraPDF run in the same pass over the same file. The wiring is small: a console generator, EInvoiceValidation.dpr, writes the twelve files using the shared invoice model from the sample, and a script, run-validation.ps1, drives both validators over the output directory and prints a pass and fail table. The same two-step shape, generate with the library and verify with external validators, is what a continuous-integration job should run on every change to invoice generation, because the only way to know a file satisfies both layers is to ask both tools
If your pipeline also has to certify the container before signing, the preflight side of this work is covered in our walkthrough of PDF/A and PDF/UA preflight in Delphi, and the broader certify-then-sign flow is described in the compliance and signing workbench. Both build on the same generation path that ships as part of the Delphi PDF Library for Delphi and C++Builder, alongside the PDF/A, associated-file, and metadata APIs used here