Validate e-invoices
Check Factur-X, ZUGFeRD and XRechnung invoices you receive with the KoSIT validator and veraPDF, as hybrid PDFs or CII and UBL XML: the verdict, supported formats, errors, limits and billing.
POST /v1/einvoices/validate checks an e-invoice you received, from any sender: a hybrid PDF (Factur-X or ZUGFeRD 2.x, or a PDF carrying an xrechnung.xml) or a standalone XML in UN/CEFACT CII or UBL 2.1. Since 1 January 2025, businesses in Germany must be able to receive e-invoices from other German businesses, so this is the step between your inbox and your accounting system: take the mail attachment, check it, then route it.
The check uses the same validators and rules as the e-invoices Dynamic Document API generates: the KoSIT validator for the XML, and veraPDF for the PDF. Neither validator is modified. Checks are available on every plan, Free included.
What a passed check means
A passed check means the file conforms to the standard: the schema, the profile's rules and, for a PDF, PDF/A-3b. It doesn't mean the invoice is right. Whether its content is correct for VAT, whether the visible pages match the XML and whether the sender is who they claim to be are for you to check.
Check an invoice
Upload the file, then pass the upload's ID as the source:
curl https://api-eu.dynamicdocumentapi.com/v1/uploads \
-H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
-F "file=@R-2026-0042.pdf"
curl https://api-eu.dynamicdocumentapi.com/v1/einvoices/validate \
-H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
-H "Idempotency-Key: inbox-2026-11-0042" \
-H "Content-Type: application/json" \
-d '{ "source": { "upload_id": "upl_01J9ZM1X3F7R8K2C4V6B8N0P2Q" }, "reference": "inbox-2026-11-0042" }'A check reaches one of two verdicts. Both answer 200 with the render object, and the verdict is in result:
{
"id": "rnd_01J9ZM1X3F7R8K2C4V6B8N0P2T",
"object": "render",
"status": "succeeded",
"kind": "einvoice_validation",
"input_type": "einvoice",
"output_format": "xml",
"files": [],
"pages": 1,
"billed_renders": 1,
"reference": "inbox-2026-11-0042",
"result": {
"valid": false,
"document": {
"container": "pdf",
"attachment": "factur-x.xml",
"syntax": "cii",
"profile": "EN16931",
"specification_id": "urn:cen.eu:en16931:2017"
},
"conformance": {
"einvoice": {
"profile": "EN16931",
"syntax": "cii",
"status": "passed",
"validator": "KoSIT Validator 1.6.3",
"scenario": "Factur-X EN 16931 (CII)",
"errors": [],
"warnings": []
},
"pdfa": {
"level": "3b",
"status": "failed",
"validator": "veraPDF 1.30.2",
"profile": "PDF/A-3B validation profile",
"errors": [
{
"rule": "6.2.11.4.1-1",
"message": "The font programs for all fonts used for rendering within a conforming file shall be embedded within that file, as defined in ISO 32000-1:2008, 9.9.",
"location": "root/document[0]/pages[0](3 0 obj PDPage)/contentStream[0]/operators[14]/font[0](Helvetica)",
"count": 1
}
],
"warnings": []
}
}
}
}Here the XML is fine, but the PDF embeds no font program for Helvetica, so it isn't PDF/A-3b and the invoice isn't valid.
| Field | Description |
|---|---|
result.valid | true when the XML passed and, for a PDF, the PDF passed PDF/A-3b too |
result.document.container | pdf or xml: what you sent |
result.document.attachment | The embedded file's name as written in the PDF, such as factur-x.xml; null for XML |
result.document.syntax | cii or ubl |
result.document.profile | The profile the specification identifier names: MINIMUM, BASIC_WL, BASIC, EN16931, EXTENDED or XRECHNUNG |
result.document.specification_id | The specification identifier (BT-24), as written in the XML |
result.conformance.einvoice | The KoSIT validator's report |
result.conformance.pdfa | veraPDF's report, for PDFs only |
The reports have the same fields as the validation report of a generated e-invoice, with the status passed or failed. An invoice the validator accepts with warnings has passed, and the warnings are listed in its report. pages is the PDF's page count, and null for XML.
A check has no file: files is empty, and nothing is delivered. In the dashboard's render log, checks are listed as E-invoice check with their verdict.
Sources
The source takes exactly one of these fields, as for the PDF tools:
| Field | What it is |
|---|---|
upload_id | A file uploaded with POST /v1/uploads. Uploads recognise PDFs by their content, and XML when it starts with <?xml or the file name ends in .xml. |
url | A public https URL, downloaded when the check runs |
render_id | A PDF or XML file of an earlier render in your workspace, such as the output of POST /v1/einvoices. Without file_id it is the render's first PDF or XML file; add file_id (file_…) to pick another. |
data_uri | The file itself, base64-encoded, at most 5 MB: data:application/pdf, data:application/xml, data:text/xml or data:application/octet-stream |
The file must start with %PDF, or with < after an optional byte order mark and whitespace. Anything else fails with 400 validation_error when the API can tell beforehand, as for an upload or a data URI, and with 422 einvoice_not_found otherwise. A data URI also counts towards your plan's request body limit, which is 2 MB on Free, so upload larger files instead. A zero-retention render has no files to read, so its render_id answers 410 file_expired.
The rest of the request is the envelope of POST /v1/renders: mode, timeout_behavior, webhook, reference, metadata, priority, test and the Idempotency-Key header. A check has no filename and no delivery, and sending either fails with 400 validation_error. Your workspace's default storage destinations and email rules don't apply.
Zero-retention workspaces can check invoices too, sync and async, because only the verdict is kept. As for every zero-retention render, reference and metadata are dropped. Values from the document that the validators quote in single quotes, such as 'SECRET-4711' is not a valid value for 'decimal', are replaced with '…' before the verdict is stored.
What is checked
- The file. It is a PDF or an XML. From a PDF, the check reads the first embedded file named
factur-x.xml, elsexrechnung.xml, elsezugferd-invoice.xml, ignoring case. A PDF that needs a password fails with422 pdf_password_required. An encrypted PDF that opens without a password is read, and the PDF/A report lists the encryption. - The XML. It is UTF-8, has no DOCTYPE, is at most 5 MiB, and is an invoice: a CII
CrossIndustryInvoice, or a UBLInvoiceorCreditNote. - The profile. Its specification identifier (BT-24) names one of the supported profiles, exactly as the validator compares it: no whitespace around it and no comment inside it.
- The rules. The KoSIT validator checks the XML against the schema and the profile's Schematron rules. For a PDF, veraPDF checks the file as you received it against PDF/A-3b, which Factur-X requires. A conformant PDF/A-3a or PDF/A-3u passes, while a PDF/A-2 or a PDF that isn't PDF/A fails.
Not checked: the Factur-X XMP metadata (fx:ConformanceLevel, fx:DocumentFileName), the attachment's AFRelationship, whether the visible pages match the XML, signatures, and anything else the validators' rules don't cover, such as whether the invoice is correct for VAT.
Supported formats
| Profile | Specification identifier (BT-24) | Syntaxes |
|---|---|---|
MINIMUM | urn:factur-x.eu:1p0:minimum | CII |
BASIC_WL | urn:factur-x.eu:1p0:basicwl | CII |
BASIC | urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic | CII |
EN16931 | urn:cen.eu:en16931:2017 | CII, UBL |
EXTENDED | urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended | CII |
XRECHNUNG | urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0 | CII, UBL |
These are Factur-X 1.0 and ZUGFeRD 2.1 and later, which share their identifiers, plus XRechnung 3.0 and plain EN 16931. Each XML is checked with the validator's rule set for its profile and syntax; the report's scenario names it.
Not supported yet: ZUGFeRD 1.0, the ZUGFeRD 2.0 identifiers (urn:zugferd.de:2p0:…), XRechnung before 3.0, the XRechnung extension and CVD identifiers, Peppol BIS Billing 3.0 and other specifications built on EN 16931 (CIUS). They fail with 422 einvoice_unsupported, which isn't a verdict that the invoice is invalid, and the message names the identifier that was found.
Errors
An invalid invoice isn't an error: it answers 200 with result.valid: false. These answers mean the check reached no verdict. They cost nothing, and the body includes render_id:
| Status | Code | When |
|---|---|---|
422 | einvoice_not_found | The file is neither a PDF nor an XML, the PDF has no embedded file under one of the three names, or the XML isn't well-formed or isn't a CII or UBL invoice |
422 | einvoice_unsupported | An invoice the check doesn't cover (see Supported formats), no specification identifier, a UBL document with a CII-only profile, a DOCTYPE, an encoding other than UTF-8, an identifier with whitespace around it or a comment inside it, or an XML over 5 MiB. The message names what was found, with values from the document cut to 200 characters. |
422 | einvoice_validation_unavailable | A validator gave no verdict in time. There is no result; retry later. |
422 | pdf_password_required, invalid_pdf_source, url_not_allowed, asset_fetch_failed, payload_too_large | As for the PDF tools |
400 | validation_error | An invalid request, filename or delivery sent, or a source that is neither a PDF nor an XML |
404 | render_not_found, upload_not_found | The source isn't in your workspace, or the upload has expired |
410 | file_expired | The source render has no PDF or XML file any more, or used zero-retention delivery |
413 | payload_too_large | A data URI over 5 MB |
429 | rate_limited, concurrency_limited | See Limits |
Most PDFs in an inbox aren't e-invoices yet. An integration usually treats einvoice_not_found and einvoice_unsupported as an answer ("not an e-invoice this check covers") and routes the file to manual handling, rather than treating them as a failure:
if response.status_code == 422 and response.json()["code"] in ("einvoice_not_found", "einvoice_unsupported"):
route_to_manual_review(file)Limits and test mode
- Running at once: at most 4 checks per workspace, sync and async together, or the plan's limit of concurrent sync renders if that is lower, which is 2 on Free. Beyond it, the API answers
429 concurrency_limitedwithRetry-After: 1. The validators are shared with every e-invoice render, so this limit is lower than the one for renders. A check that never finished stops counting once it is older than the plan's longest async render plus two minutes. - Test mode: a check with a test key, or with
test: true, is free. A verdict can't carry a TEST watermark, so test checks are limited to 25 per workspace and UTC day, on top of the plan's test renders per minute. Beyond either, the API answers429 rate_limited; for the daily limit,Retry-Afterruns to midnight UTC. Live checks don't count towards it. Nothing is stamped on the file, and the source is checked as you received it. - Size: the source limits of the PDF tools apply to the file, and the XML itself can be at most 5 MiB.
Billing
| Check | Billed renders |
|---|---|
| A verdict, valid or invalid | 1 |
No verdict (422, such as einvoice_not_found) | 0 |
| Test checks | 0 |
A check is billed like a PDF tool call, and its billed_renders field shows the number. See Plans and limits.
Related pages
- E-invoicing for creating Factur-X, ZUGFeRD and XRechnung invoices
- PDF tools for uploads and the other sources
- Renders for the render object, webhooks and sync or async mode
- MCP server for the
validate_einvoicetool
E-invoicing
Factur-X and ZUGFeRD PDFs with embedded EN 16931 XML, XRechnung export in CII or UBL, the invoice model, computed totals and validation reports.
Image and screenshot options
Render PNG, JPEG and WebP images from templates, HTML or URLs, with viewport, scale, cropping, transparency and quality settings.