Skip to content

Ironfang Finance - ZUGFeRD / Factur-X

Validating ZUGFeRD and Factur-X

The profiles, what is checked in a PDF and in its XML, what is not, how to call the API, how to read a result and how to take the XML out of a PDF.

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).

ProfilevariantNote
MINIMUMminimumNot a complete EN 16931 invoice; the result says so.
BASIC WLbasic-wlNot a complete EN 16931 invoice; the result says so.
BASICbasic
EN 16931en16931The profile of the European standard.
EXTENDEDextended
XRECHNUNGxrechnungRecognised 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.

GroupLayerWhat it checks
pdfainputThe request and the PDF within their limits. An encrypted PDF, or one without exactly one embedded invoice XML, is refused as a Problem instead.
pdfapdfaThe PDF against the PDF/A conformance it declares, with veraPDF. A hybrid invoice must declare PDF/A-3, or PDF/A-4f.
attachment_metadatahybrid_bindingFeRD'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_xmlxmlWell-formed XML within the limits, with no DTD, entities beyond the predefined five, or XInclude.
invoice_xmlxsdThe XML schema of the profile.
invoice_xmlzugferd_profileThe 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:

AspectWhen
visual_consistencyEvery PDF. Whether the visible pages show the same invoice as the XML is not compared; the XML is the invoice data.
digital_signatureEvery PDF. Signatures are outside the scope of the check.
pdf_containerXML sent on its own: nothing about a PDF was checked.
en16931_semanticsMINIMUM 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 parameterMeaning
familyzugferd-facturx. Without it the family is read from the document's specification identifier.
variantA profile, from the table above. Without it the profile is read from the document; one that contradicts it is family_mismatch.
document_typeinvoice, credit_note or auto (the default: read from the document).
scopexml 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.
rulesetlatest (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

CodeWhen
no_embedded_invoiceThe PDF carries no invoice XML: it may be an ordinary PDF invoice.
ambiguous_embedded_invoiceThe PDF carries more than one invoice XML.
encrypted_pdfThe PDF is encrypted or password-protected.
processing_limit, payload_too_largeOver a published limit.
family_mismatchThe XML declares another family or profile than the one named; detected says what it declares.
family_undetectedNo profile was named and the XML declares none Ironfang recognises.
unsupported_variantThe XRECHNUNG profile.
unsupported_document_typeNot an invoice or credit note, or a type that contradicts the one named.
scope_unavailableA scope the body cannot be checked at.
validator_unavailable, validation_timeout, internal_errorNo verdict: indeterminate and not charged. Retry.