Skip to main content

Validate e-invoices

POST /v1/einvoices/validate says which rules an e-invoice passed, by which validator and which release of its rules, and which bytes it judged. It takes two kinds of request:

  • A render of your workspace that made an e-invoice, as JSON with render_id. Every e-invoice Formfeed delivers was validated before it left, and the render keeps that report. The answer is the report, in the shape below. It costs nothing and runs no validator.
  • An e-invoice XML that Formfeed did not make, as multipart/form-data with a file part: one you received, or one another system wrote. From the Starter plan; each upload counts one of the organisation's validations of the day.

The key needs the einvoice:validate scope. Keys made in the app carry it by default, and formfeed login gives it to editors, admins and owners; a key without it answers 403 forbidden.

What the report says, and what it does not​

The report says which rules a file passed, by which validator and release. It does not say whether the document is a proper invoice for VAT purposes, and it does not replace the check a business owes an invoice it receives.

The letter of the German Federal Ministry of Finance on e-invoicing of 15 October 2025 (BStBl I S. 1806) keeps the same things apart:

  • an error of the format makes the document an "other invoice" (sonstige Rechnung, Rn. 6a);
  • an error of the business rules is something else, for example a missing buyer reference (BT-10) in an XRechnung (Rn. 6b);
  • a business may rely on a validator's report, but the report does not replace the recipient's own check (Rn. 35a).

So the report keeps apart what it checked: the file's syntax, its XML schema, and each rule set by name, each with its own result. It states no verdict on the document as an invoice. Where KoSIT's validator recommends accepting or refusing a document, that recommendation is not passed on. This page is not tax advice.

A render of your workspace​

curl https://api-eu.formfeed.dev/v1/einvoices/validate \
-H "Authorization: Bearer $FORMFEED_API_KEY" \
-H "Content-Type: application/json" \
-d '{"render_id": "rnd_0d290c45cbeb4554b946bbad1acee87e"}'

The answer carries source.stored: true and the checks of the report the render keeps: the XML schema of its profile, Mustang's Factur-X rules (which contain the rules of EN 16931), France's rules where they applied, and the PDF/A-3b container with the invoice it carries. pdf_sha256 and display come from that report too. A render that failed keeps its report as well; one that made no e-invoice answers 422 source_invalid.

rules asks for another rule set. en16931 holds the XML of an EN 16931 invoice to CEN's own rules, from the configuration KoSIT publishes for its validator, instead of Mustang's: the render's file is read and validated again, which counts like an upload. Rules that cannot apply to the file's specification identifier, such as xrechnung for a Factur-X file, answer not_checked without counting.

An XML you received​

curl https://api-eu.formfeed.dev/v1/einvoices/validate \
-H "Authorization: Bearer $FORMFEED_API_KEY" \
-F file=@RE-2026-0001.xml \
-F rules=auto
  • What it takes: one XML of up to 4 MB, CII or UBL, in UTF-8. A PDF is not taken (415 file_type_unsupported): send the XML, or for a ZUGFeRD or Factur-X PDF of your own, its render id. A larger request answers 413 file_too_large from its Content-Length, before it is read.
  • What is kept: nothing. The file is validated and forgotten; the answer names its SHA-256 (xml_sha256) and its size.
  • Nothing in a file is followed. A document type (DOCTYPE), XInclude, an encoding other than UTF-8, a document nested deeper than 64 elements, more than 32 attributes on one element, an attribute longer than 4,096 characters or more than 300,000 elements answer not_checked with reason: not_read: no validator read the file. A file that is not well-formed XML answers failed, with its syntax check failed.

How many a day​

PlanValidations of uploads per organisation and UTC day
Freenone
Starter100
Pro500
Business2,000
Enterpriseby contract

A render's stored report never counts. A request the validator never answered (503 conformance_unavailable) gives its validation back; one that ran out of time (500 einvoice_validator_error) counts, because the same file runs out of time again. When the day is used up, the answer is 403 forbidden with feature: einvoice_validation, the plan's limit, what the day has used and resets_at, the start of the next UTC day; never 429. Requests also count against the plan's requests per second, as renders do. The answer's allowance says what the day has used after the request.

Which rules judge a file​

A table in front of the validators chooses the rules, by the root element and an exact match of the specification identifier (BT-24, GuidelineSpecifiedDocumentContextParameter/ID in CII, cbc:CustomizationID in UBL). The file names the rules it claims to follow, and it is held to those.

RootSpecification identifier (BT-24)Rule set (rule_set)Validator
CIIFactur-X and ZUGFeRD 2.x: MINIMUM, BASIC WL, BASIC, EN 16931 (urn:cen.eu:en16931:2017), EXTENDEDfactur-xMustang, with the rules of Factur-X 1.09 / ZUGFeRD 2.5 for the profile
CII, UBL Invoice, UBL CreditNoteXRechnung 3.0 (urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0), its Extension, CVDxrechnungKoSIT's validator with KoSIT's configuration for XRechnung
UBL Invoice, UBL CreditNoteurn:cen.eu:en16931:2017en16931KoSIT's validator with CEN's rules from the same configuration, without XRechnung's
anyPeppol BIS Billing 3.0none yetnot checked
any other root, or any other identifiernonenot checked, naming what it found
  • The XRechnung Extension in a UBL credit note has no rules in KoSIT's configuration, so such a file is not checked.
  • In CII, urn:cen.eu:en16931:2017 is the EN 16931 profile of Factur-X and ZUGFeRD and goes to Mustang; rules: en16931 holds it to CEN's own rules instead.
  • Peppol BIS Billing 3.0 is not checked yet.
  • rules other than auto judges only a file whose identifier that rule set is for; any other file answers not_checked with reason: rules_not_applicable.

The releases of the validators and rules are pinned, and every report names those that judged the file (validator, rules, scenario, versions). The coverage page lists the current pins.

The answer​

{
"id": "6f2c7e9a-1d4b-4c3e-8a5f-2b7d9e0c1a34",
"result": "failed",
"reason": null,
"explanation": null,
"source": {
"type": "upload",
"filename": "RE-2026-0001.xml",
"bytes": 21304
},
"route": {
"syntax": "cii",
"document": "invoice",
"root": "CrossIndustryInvoice",
"customization_id": "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0",
"rule_set": "xrechnung"
},
"validator": "KoSIT validator 1.6.3",
"rules": "XRechnung 3.0.2, configuration of 2026-08-31",
"scenario": "EN16931 XRechnung (CII)",
"profile": null,
"checks": [
{
"kind": "syntax",
"name": "XML well-formedness, UTF-8",
"result": "passed",
"messages": []
},
{
"kind": "schema",
"name": "XML Schema for UN/CEFACT XML (SCRDM - CII uncoupled)",
"result": "passed",
"messages": []
},
{
"kind": "rules",
"name": "Schematron rules for EN16931 (CII)",
"result": "passed",
"messages": []
},
{
"kind": "rules",
"name": "Schematron rules for CIUS XRechnung (CII)",
"result": "failed",
"messages": [
{
"part": "xml",
"severity": "error",
"rule": "BR-DE-15",
"message": "Das Element \"Buyer reference\" (BT-10) muss übermittelt werden.",
"location": "/rsm:CrossIndustryInvoice"
}
]
}
],
"truncated": false,
"versions": {
"kosit": "1.6.3",
"xrechnung_configuration": "2026-08-31",
"xrechnung_schematron": "2.6.0",
"cen": "1.3.16",
"saxon": "12.10"
},
"xml_sha256": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b",
"pdf_sha256": null,
"display": null,
"validated_at": "2026-11-06T10:15:02.118Z",
"allowance": { "limit": 100, "used": 7, "resets_at": "2026-11-07T00:00:00Z" }
}
FieldWhat it says
resultpassed: a validator ran its rules to the end and every check passed. failed: a check found an error. not_checked: no rule set here is for the file, or it was not read, or the validator did not finish
reason, explanationWhy it was not checked, as a code and a sentence that names what was found: not_read, unknown_root, no_customization_id, unknown_customization_id, not_offered, no_scenario, rules_not_applicable, validator_stopped, not_validated, no_verdict
routeWhat the table read off the file before any validator did: syntax, document, root, the identifier as the file names it, and the rule set it chose
validator, rules, scenario, profile, versionsWhich validator, which rules and which release judged the file
checksEvery check with its own result (passed, failed, or not_run where it did not run to its end or never ran), and its findings: errors first, at most 100 a check (truncated says when there were more)
xml_sha256SHA-256 of the XML that was judged, as it arrived
allowanceWhat the day's validations have used after this request; null where nothing was counted

The answer is 200 whatever the result: a file that fails is a report, not an error. New values of result, reason and a check's kind may be added; treat one you do not know as not passed.

In the SDKs, the CLI and agents​

const report = await client.einvoices.validate({
file: { data: xml, name: 'RE-2026-0001.xml' },
});
const stored = await client.einvoices.validate(
'rnd_0d290c45cbeb4554b946bbad1acee87e',
);
report = client.einvoices.validate(file=xml, file_name="RE-2026-0001.xml")
stored = client.einvoices.validate("rnd_0d290c45cbeb4554b946bbad1acee87e")
  • The CLI's formfeed einvoice <slug> --validate writes the XML of a data set offline and sends that file here.
  • The MCP server has the tool validate_einvoice.