Skip to content

Factur-X: XRECHNUNG conformance level embeds factur-x.xml instead of the mandated xrechnung.xml #1701

Closed

Description

@nhh

Summary

Since 8.34.0 (#1552), Gotenberg can turn a PDF into a Factur-X / ZUGFeRD invoice (facturxXml, facturxConformanceLevel, facturxVersion), and XRECHNUNG is an accepted value for facturxConformanceLevel. Regardless of the level, the XML is always embedded as factur-x.xml and the XMP says <fx:DocumentFileName>factur-x.xml</fx:DocumentFileName>. The docs say so too: "Embedded as factur-x.xml regardless of the uploaded filename."

For the XRECHNUNG reference profile, the Factur-X / ZUGFeRD specification requires the embedded file and fx:DocumentFileName to be xrechnung.xml, and explicitly forbids factur-x.xml in that profile. So the output for facturxConformanceLevel=XRECHNUNG does not follow the spec. All other levels are fine.

Fix idea: choose the file name from the conformance level: xrechnung.xml for XRECHNUNG, factur-x.xml for everything else.

Steps to reproduce

Gotenberg 8.38.1. invoice.xml is any CII invoice following the XRechnung CIUS.

docker run --rm -p 3000:3000 gotenberg/gotenberg:8.38.1

curl --request POST http://localhost:3000/forms/pdfengines/factur-x \
  --form [email protected] \
  --form [email protected] \
  --form 'facturxConformanceLevel=XRECHNUNG' \
  -o out.pdf

pdfdetach -list out.pdf
pdfinfo -meta out.pdf | grep -E 'fx:(DocumentFileName|ConformanceLevel)'

The same code path is used by /forms/pdfengines/embed, merge, split, /forms/libreoffice/convert and the Chromium convert routes.

Expected

1 embedded files
1: xrechnung.xml
<fx:DocumentFileName>xrechnung.xml</fx:DocumentFileName>
<fx:ConformanceLevel>XRECHNUNG</fx:ConformanceLevel>

Actual (derived from the code paths linked below)

1 embedded files
1: factur-x.xml
<fx:DocumentFileName>factur-x.xml</fx:DocumentFileName>
<fx:ConformanceLevel>XRECHNUNG</fx:ConformanceLevel>

There is no clean workaround inside Gotenberg:

  • The generic embeds field keeps the file name but writes no fx XMP. Combined with the Factur-X fields it produces two invoice XMLs, and §7.7 forbids a factur-x.xml in that profile.
  • injectFacturXIntoXMP does nothing when the fx namespace is already present, so a second pass cannot correct the XMP.
  • /forms/pdfengines/metadata/write (ExifTool) usually breaks PDF/A (per the docs), and ExifTool does not know the fx namespace without a custom config.

Specification references

Factur-X 1.07.2 / ZUGFeRD 2.3.2 (FNFE-MPE & FeRD, 15 Nov 2024), German edition (package: https://www.ferd-net.de/standards/paket-downloads/index.html):

  • §6.2 "Die Einbettung der XML-Datei", p. 27:

    Die XML-Datei wird immer unter dem Namen "factur-x.xml" eingebettet. Die einzige Ausnahme bilden Referenzprofile wie das Profil XRECHNUNG; hier muss der Name „xrechnung.xml" lauten.

    (The XML file is always embedded as factur-x.xml. The only exception are reference profiles such as XRECHNUNG; there the name must be xrechnung.xml.)

  • §7.7 "Das Referenzprofil XRECHNUNG", p. 68:

    Der Name der xml-Komponente von Factur-X / ZUGFeRD muss immer xrechnung.xml lauten, sie darf nicht factur-x.xml sein. Konsequenterweise darf es in einem XRECHNUNG-Profil Factur-X (ZUGFeRD) auch keine Einbettung einer factur-x.xml-Datei geben.

    (The name must always be xrechnung.xml and must not be factur-x.xml. Consequently an XRECHNUNG-profile invoice must not embed a factur-x.xml at all.)

  • §7.7 XMP table, p. 70: fx:DocumentFileName "… muss mit dem Wert des Eintrags /F im Dateispezifikationswörterbuch identisch sein. Dies ist ein fester Wert im Profil XRECHNUNG: xrechnung.xml"

  • Per the spec changelog, §7.7 was added in 1.0.06 (2022-03-01, = ZUGFeRD 2.2).

  • Factur-X 1.09.2 / ZUGFeRD 2.5.2, as summarised in phax/kaltblut docs/2.5.2.md: "factur-x.xml; xrechnung.xml for the XRECHNUNG reference profile", with BR-HYBRID-13 (fatal: embedded file name SHALL be from the HybridDocumentFilename list) and BR-HYBRID-14 (warning: embedded file name SHOULD match fx:DocumentFileName).

  • ZUGFeRD FAQ (https://zugferd.org/e-invoicing/1.0.0/faq.en.html): for ZF2 the embedded file is named "zugferd-invoice.xml, factur-x.xml or xrechnung.xml".

Other implementations:

Note: Mustang's PDFValidator accepts factur-x.xml for any level, so the current output may pass Mustang. The problem shows up with receivers that look for xrechnung.xml when the level is XRECHNUNG, and with validators implementing the 1.09 BR-HYBRID rules.

Where it happens (v8.38.1, e4ee0f2)

Proposed fix

Derive the name from the level in one place and use facturX.DocumentFileName everywhere downstream.

--- a/pkg/gotenberg/pdfengine.go
+++ b/pkg/gotenberg/pdfengine.go
@@
 	FacturXDocumentFileName string = "factur-x.xml"
+
+	// XRechnungDocumentFileName is the name mandated for the XRECHNUNG
+	// reference profile (Factur-X 1.07.2 / ZUGFeRD 2.3.2, §6.2 and §7.7).
+	XRechnungDocumentFileName string = "xrechnung.xml"
 )
+
+// FacturXDocumentFileNameFor returns the name of the embedded XML invoice for
+// the given conformance level.
+func FacturXDocumentFileNameFor(conformanceLevel string) string {
+	if conformanceLevel == FacturXConformanceXRechnung {
+		return XRechnungDocumentFileName
+	}
+	return FacturXDocumentFileName
+}
--- a/pkg/modules/pdfengines/routes.go
+++ b/pkg/modules/pdfengines/routes.go
@@ func FormDataPdfFacturX(form *api.FormData) (gotenberg.FacturX, string) {
-		DocumentFileName: gotenberg.FacturXDocumentFileName,
+		DocumentFileName: gotenberg.FacturXDocumentFileNameFor(conformanceLevel),
@@
-	err := embedFacturXXml(ctx, engine, facturxXmlPath, inputPaths)
+	err := embedFacturXXml(ctx, engine, facturX.DocumentFileName, facturxXmlPath, inputPaths)
@@
-func embedFacturXXml(ctx *api.Context, engine gotenberg.PdfEngine, facturxXmlPath string, inputPaths []string) error {
+func embedFacturXXml(ctx *api.Context, engine gotenberg.PdfEngine, fileName, facturxXmlPath string, inputPaths []string) error {
@@
-	canonicalPath := fmt.Sprintf("%s/%s", embedDir, gotenberg.FacturXDocumentFileName)
+	canonicalPath := fmt.Sprintf("%s/%s", embedDir, fileName)

Tests: a table test for FacturXDocumentFileNameFor (all six levels), an injectFacturXIntoXMP case asserting <fx:DocumentFileName>xrechnung.xml</fx:DocumentFileName> for XRECHNUNG, and an integration scenario on /forms/pdfengines/factur-x with facturxConformanceLevel=XRECHNUNG checking PDF/A-3b validity and that xrechnung.xml is embedded with relationship Alternative.

Docs: "Embedded as factur-x.xml (xrechnung.xml for XRECHNUNG), regardless of the uploaded filename." on the Factur-X and Attachments pages.

Side note

facturxVersion defaults to 1.0. The §7.7 table in 1.07.2 shows fx:Version example 2p1 for XRECHNUNG ("always use the currently valid version number"), while the 1.09.2 summary says 1.0 for all profiles. Callers can already set facturxVersion, so maybe a docs sentence is enough.

Environment

  • Gotenberg 8.38.1 (commit e4ee0f29), unchanged on main at 764c7f2c
  • PDF engine: QPDF (default)

Related: #1552

Activity

  1. gulien commented on Oct 26, 2026

    @gulien
    Member

    Fixed in 03963a9: the XRECHNUNG level now embeds xrechnung.xml and writes it to fx:DocumentFileName. Thanks for the thorough report!

  2. nhh commented on Oct 26, 2026

    @nhh
    ContributorAuthor

    @gulien Thanks for the immediate implementation!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions