You have built a Factur-X invoice and every container check passes. The catalog carries an /AF array, the EmbeddedFiles name tree resolves to the right file specification, the embedded factur-x.xml has the correct /AFRelationship of Alternative, and the built-in ValidateFacturXInvoice returns 1. Then you run the same file through veraPDF, the reference checker that tax portals use, and it rules the whole document is not a valid PDF/A-3. The structure is right. The metadata is the problem, and the failure is one of the easiest in the entire e-invoice workflow to miss
The reason is worth understanding in full, because it explains a class of PDF/A defect that has nothing to do with the visible page or the attachment and everything to do with how XMP describes itself. This is the trap that hides behind a green container check
The four properties that fail the file
A Factur-X invoice writes four custom properties into its XMP packet so that downstream software can read the invoice profile without parsing the embedded XML. They live in the Factur-X namespace under the fx prefix: fx:DocumentFileName, fx:DocumentType, fx:Version, and fx:ConformanceLevel. They are exactly the metadata a reader needs to know that this PDF carries an EN 16931 invoice named factur-x.xml at version 1.0
None of those four properties is part of any XMP schema that PDF/A predefines. The Dublin Core, XMP Basic, PDF, and PDF/A identification schemas are known to a conforming reader, but fx: is not. When veraPDF walks the XMP and reaches a property whose namespace it does not recognise, it looks for a declaration that would tell it what the property means. If that declaration is absent, it reports a failure against ISO 19005-3 clause 6.6.2.3.1, which requires that every property not drawn from a predefined schema be described in a PDF/A extension schema. Four undeclared properties, four ways for the file to be rejected, and not one of them is visible to a container check
Why PDF/A refuses a bare custom property
The rule looks pedantic until you remember what PDF/A is for. The format exists so that a file can be opened and understood decades from now, by software that was never told about the conventions of 2026. A conforming reader is expected to make sense of the document from the document alone, with no external registry to consult
Custom metadata breaks that promise unless the file carries its own description. Given a bare fx:ConformanceLevel property, a future reader cannot know the namespace URI the fx prefix binds to, whether the value is text or a date or an integer, or whether the property describes the document itself or some external resource. The PDF/A extension schema mechanism closes that gap. It lets the file declare, in a fixed XMP structure, the namespace, prefix, and for each property a value type and a category of internal or external. Once that declaration is present the property is self-describing, and clause 6.6.2.3.1 is satisfied. Without it, the validator has no choice but to treat the property as unintelligible and fail the file. The category distinction matters here: invoice properties such as these describe data that comes from outside the PDF processor, so they are declared external rather than internal
What the extension schema declaration contains
The declaration is an rdf:Description in the XMP packet that uses the three AIIM-defined namespaces pdfaExtension, pdfaSchema, and pdfaProperty. Inside a pdfaExtension:schemas bag sits one schema entry that names the Factur-X schema, gives its pdfaSchema:namespaceURI and pdfaSchema:prefix, and then lists the four properties in a pdfaSchema:property sequence. Each property carries a name, a pdfaProperty:valueType of Text, and a pdfaProperty:category of external. The illustrative markup below shows the shape of that block
<rdf:Description rdf:about=""
xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/"
xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#"
xmlns:pdfaProperty="http://www.aiim.org/pdfa/ns/property#">
<pdfaExtension:schemas>
<rdf:Bag>
<rdf:li rdf:parseType="Resource">
<pdfaSchema:schema>Factur-X PDFA Extension Schema</pdfaSchema:schema>
<pdfaSchema:namespaceURI>urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#</pdfaSchema:namespaceURI>
<pdfaSchema:prefix>fx</pdfaSchema:prefix>
<pdfaSchema:property>
<rdf:Seq>
<rdf:li rdf:parseType="Resource">
<pdfaProperty:name>DocumentFileName</pdfaProperty:name>
<pdfaProperty:valueType>Text</pdfaProperty:valueType>
<pdfaProperty:category>external</pdfaProperty:category>
<pdfaProperty:description>name of the embedded XML invoice file</pdfaProperty:description>
</rdf:li>
<!-- DocumentType, Version, ConformanceLevel declared the same way -->
</rdf:Seq>
</pdfaSchema:property>
</rdf:li>
</rdf:Bag>
</pdfaExtension:schemas>
</rdf:Description>
The namespace URI and prefix are not fixed strings. They follow the profile. A Factur-X document uses urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# with the fx prefix, while a ZUGFeRD 2.0 file selected through zugferd-invoice.xml resolves to a different URI under its own schema name. The extension schema has to declare the same namespace URI that the property block actually uses, or the validator still cannot connect the two. PDFlibPas derives both values from the file name and version you pass, so the declaration and the property block always agree
How the helper writes both halves together
In PDFlibPas you do not assemble that XML by hand. You put the document into a PDF/A-3 mode and call one method. The first thing to settle is the conformance flag, because Factur-X requires PDF/A-3. Calling SetPDFAMode(7) selects the PDF/A-3u level, which sets pdfaid:part to 3 and pdfaid:conformance to U in the identification schema. The XMP packet now carries the right part and conformance before any invoice metadata is added
var
FileID: Integer;
begin
PDF.SetPDFAMode(7); // PDF/A-3u: pdfaid:part=3, conformance=U
PDF.NewDocument;
// draw the human-readable invoice page here
FileID := PDF.AddFacturXAssociatedFileFromString(
InvoiceXML, // raw UTF-8 XML bytes
'EN16931', // ConformanceLevel
'factur-x.xml', // embedded file name
'Factur-X invoice XML', // /Desc text
'Alternative', // /AFRelationship
'1.0', // profile version
''); // optional country code
if FileID = 0 then
Exit; // not PDF/A-3, or XML/profile mismatch
PDF.SaveToFile('factur-x.pdf');
end;
A single call to AddFacturXAssociatedFileFromString does the work that the failing file was missing. It embeds the XML as a PDF/A-3 associated file with the relationship you named, and it records the four fx properties along with the schema name, namespace URI, and prefix for the chosen profile. When the document is saved, an internal step named ApplyFacturXMetadata injects both the property block and the matching pdfaExtension:schemas declaration into the XMP packet, so the custom properties arrive already described. The method returns 0 if the document is not in a PDF/A-3 mode or if the XML does not match the declared profile, which is the same guard that stops a malformed invoice from reaching the file in the first place
The blind spot the container check cannot see
This is the part to name plainly, because it is the reason the bug hides. ValidateFacturXInvoice checks the container. It confirms the catalog has an /AF entry, the EmbeddedFiles name tree is present, the invoice XML exists, the embedded file name matches the profile, the guideline ID in the XML agrees with the conformance level, and the /AFRelationship is one PDF/A-3 allows. Those are real checks and they catch real defects. GetFacturXValidationIssues reports them by name, with identifiers such as MissingCatalogAF, NotPDFA3, ConformanceGuidelineMismatch, InvalidAFRelationship, and InvalidFileNameProfile
What it does not check is whether the XMP extension schema is present and correct. A file whose container is flawless but whose fx properties are undeclared passes every issue check and returns 1, because nothing in that list inspects the pdfaExtension:schemas block. That is precisely why a hand-built invoice, or one produced by a pipeline that wrote the property block without the declaration, can sail through the built-in validator and still fail veraPDF on clause 6.6.2.3.1. The container validator and the PDF/A metadata validator answer different questions, and only the full PDF/A checker answers the second one
Reading issues so you know which layer broke
Because the two layers fail independently, the right diagnostic habit is to read the container issues first and treat a clean result as a statement about the container only, never about PDF/A metadata. Run the built-in validation, collect the issue list, and act on it before you reach for an external tool
var
Issues: WideString;
begin
if PDF.ValidateFacturXInvoice = 0 then
begin
Issues := PDF.GetFacturXValidationIssues('|');
// container-level identifiers, for example:
// MissingCatalogAF, NotPDFA3, MissingEmbeddedFilesNameTree,
// ConformanceGuidelineMismatch, InvalidAFRelationship
WriteLn('Container issues: ', Issues);
end
else
WriteLn('Container OK; verify XMP extension schema with a PDF/A checker.');
end;
When that call returns an issue name, the fault is in the container and the message tells you which part. When it returns clean and veraPDF still rejects the file, the fault is almost always the XMP extension schema, and the fix is to let AddFacturXAssociatedFileFromString write the metadata rather than constructing the property block yourself. Keeping the two questions separate in your own mind is what turns a baffling rejection into a one-line diagnosis: container problems surface through the issue list, schema-declaration problems surface only through a PDF/A validator, and confusing the two is what lets the bug hide
The broader PDF/A and PDF/UA conformance picture, including how to run a preflight pass before a file leaves your build, is covered in the PDF/A and PDF/UA preflight walkthrough. If your invoice also has to be accessible, the structure tree that PDF/A-3a and tagged PDF depend on is the subject of the tagged-PDF accessibility article. The extension schema handling described here ships as part of the PDFlibPas Delphi PDF Library alongside the Factur-X, ZUGFeRD, and XRechnung profile support documented across this blog