Ironfang Finance validates ZUGFeRD and Factur-X invoices and credit notes either as a hybrid PDF, the PDF with its invoice XML embedded, or as the XML on its own. ZUGFeRD 2.5.2 and Factur-X 1.09.2 are the same standard, and a document declaring either is checked the same way. Try it without an account in the ZUGFeRD / Factur-X validator, also in German.
Profiles and versions
The profile is read from the specification identifier in the XML (the guideline ID, ram:GuidelineSpecifiedDocumentContextParameter), and each profile has its own release: its schema and the official ZUGFeRD 2.5.2 Schematron for that profile, from FeRD's package. The result names it in ruleset.variant, ruleset.official_release (2.5.2) and ruleset.technical_release (1.09.2).
| Profile | variant | Note |
|---|---|---|
| MINIMUM | minimum | Not a complete EN 16931 invoice; the result says so. |
| BASIC WL | basic-wl | Not a complete EN 16931 invoice; the result says so. |
| BASIC | basic | |
| EN 16931 | en16931 | The profile of the European standard. |
| EXTENDED | extended | |
| XRECHNUNG | xrechnung | Recognised and refused with unsupported_variant. |
Every profile is UN/CEFACT CII D22B, for invoices and credit notes. An XRechnung in UBL or CII is validated as the xrechnung family; see the XRechnung guide.
What is checked
A PDF is checked in three groups, reported in groups, each holding its layers. A layer that fails stops the ones after it, and the result says each later layer was not reached rather than passed. XML sent on its own runs the invoice XML group only.
| Group | Layer | What it checks |
|---|---|---|
pdfa | input | The request and the PDF within their limits. An encrypted PDF, or one without exactly one embedded invoice XML, is refused as a Problem instead. |
pdfa | pdfa | The PDF against the PDF/A conformance it declares, with veraPDF. A hybrid invoice must declare PDF/A-3, or PDF/A-4f. |
attachment_metadata | hybrid_binding | FeRD's rules for the container, as Ironfang's checks FW-HYB-001 to FW-HYB-013: the invoice is an associated file with a permitted relationship, name and media type, and the Factur-X XMP metadata declares the XML's own profile, the embedded file's name, the document type INVOICE and a Factur-X version. |
invoice_xml | xml | Well-formed XML within the limits, with no DTD, entities beyond the predefined five, or XInclude. |
invoice_xml | xsd | The XML schema of the profile. |
invoice_xml | zugferd_profile | The official ZUGFeRD 2.5.2 Schematron for the profile, including the EN 16931 business rules the profile applies. |
Only fatal findings fail a document; warning and information never do. The binding checks carry the level FeRD sets for each requirement.
What is not checked
Every result lists what it cannot establish in coverage.not_checked:
| Aspect | When |
|---|---|
visual_consistency | Every PDF. Whether the visible pages show the same invoice as the XML is not compared; the XML is the invoice data. |
digital_signature | Every PDF. Signatures are outside the scope of the check. |
pdf_container | XML sent on its own: nothing about a PDF was checked. |
en16931_semantics | MINIMUM and BASIC WL, which do not carry a complete EN 16931 invoice. |
Validate with the API
POST /finance/v2/einvoices/validate takes the PDF as application/pdf or the XML as application/xml. Name the family, as this script does, so that a document declaring another family is refused with what it declares. The script keeps the result in result.json and derives the Idempotency-Key from the file, so a retry returns the first result and is not charged again.
#!/usr/bin/env bash
# Validate one ZUGFeRD / Factur-X invoice, PDF or XML, with the Ironfang
# Finance API and keep the result.
#
# IRONFANG_API_KEY=... ./validate-zugferd.sh invoice.pdf
#
# A .pdf is checked as a hybrid invoice (PDF/A, attachment and metadata,
# then its embedded XML); anything else is sent as the invoice XML alone.
# The result is written to result.json. A retry with the same file sends
# the same Idempotency-Key, so it returns the first result and is not
# charged again.
set -euo pipefail
file="$1"
base="${IRONFANG_FINANCE_API:-https://api.ironfang.uk/finance}"
key="zf-$(sha256sum "$file" | cut -c1-40)"
case "$file" in
*.pdf|*.PDF) type="application/pdf" ;;
*) type="application/xml" ;;
esac
curl --fail-with-body --silent --show-error \
"$base/v2/einvoices/validate?family=zugferd-facturx" \
-H "Authorization: Bearer $IRONFANG_API_KEY" \
-H "Content-Type: $type" \
-H "Idempotency-Key: $key" \
--data-binary "@$file" \
--output result.json
grep -o '"outcome":"[a-z]*"' result.json | head -n 1
| Query parameter | Meaning |
|---|---|
family | zugferd-facturx. Without it the family is read from the document's specification identifier. |
variant | A profile, from the table above. Without it the profile is read from the document; one that contradicts it is family_mismatch. |
document_type | invoice, credit_note or auto (the default: read from the document). |
scope | xml or hybrid_pdf. It follows the body: a PDF is checked at hybrid_pdf and XML at xml; asking for the other is scope_unavailable. |
ruleset | latest (the default) or an exact release id from GET /finance/v2/einvoices/rulesets. |
A PDF may be up to 20 MiB, with at most 200 pages and 20 embedded files, 25 MiB of decoded attachments and 256 MiB of decoded content; its invoice XML, like XML sent on its own, up to 5 MiB. A PDF has 30 seconds to reach a verdict. Without an API key the same request is anonymous: nothing is stored and tighter rate limits apply. With a key, a completed validation is one operation from your Finance allowance and the result is kept for 30 days at GET /finance/v2/einvoices/results/{id}. The full contract is the V2 OpenAPI document, and how V2 differs from V1 is in the V1 and V2 guide.
Reading a result
outcome is valid or invalid for a completed validation; anything that stopped a verdict, such as a timeout, is a Problem with outcome: indeterminate and nothing charged. For a PDF, input describes the PDF and input.embedded_xml the XML that was checked (its size, SHA-256 and file name); declared.pdf is what the PDF's metadata says about itself, copied: the Factur-X conformance level, file name, document type and version, and the PDF/A part and conformance.
Findings on the input, pdfa and hybrid_binding layers carry a pdf location, the PDF object path; findings on the XML carry an XPath. veraPDF's rules are reported as ISO19005-<part>:<clause>-<test>, once per failed rule. engines names both engines, the XML engine and the PDF engine, each by image digest. next_actions includes extract_xml for a PDF.
Taking the XML out of a PDF
POST /tools/v2/invoice/extract takes a PDF (application/pdf, up to 20 MiB) and answers its invoice XML byte for byte, as application/xml. The invoice is identified exactly as validation identifies it, so the XML is the one a validation of the same PDF checks. Nothing is judged and nothing is stored. A PDF without an embedded invoice, with more than one, encrypted, or over a limit is refused with the matching code. To read the XML as a document, open it in the XRechnung viewer, which reads the CII XML of ZUGFeRD and Factur-X.
Problems
| Code | When |
|---|---|
no_embedded_invoice | The PDF carries no invoice XML: it may be an ordinary PDF invoice. |
ambiguous_embedded_invoice | The PDF carries more than one invoice XML. |
encrypted_pdf | The PDF is encrypted or password-protected. |
processing_limit, payload_too_large | Over a published limit. |
family_mismatch | The XML declares another family or profile than the one named; detected says what it declares. |
family_undetected | No profile was named and the XML declares none Ironfang recognises. |
unsupported_variant | The XRECHNUNG profile. |
unsupported_document_type | Not an invoice or credit note, or a type that contradicts the one named. |
scope_unavailable | A scope the body cannot be checked at. |
validator_unavailable, validation_timeout, internal_error | No verdict: indeterminate and not charged. Retry. |
