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

> Part of the Dynamic Document API documentation. Index of all pages for agents: [llms.txt](https://dynamicdocumentapi.com/llms.txt), every page in one file: [llms-full.txt](https://dynamicdocumentapi.com/llms-full.txt).



`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](https://dynamicdocumentapi.com/docs/e-invoicing): 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 [#check-an-invoice]

Upload the file, then pass the upload's ID as the source:



cURL:

```bash
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" }'
```

Python:

```python
import os

import requests

API = "https://api-eu.dynamicdocumentapi.com/v1"
headers = {"Authorization": f"Bearer {os.environ['DYNAMIC_DOCUMENT_API_KEY']}"}

with open("R-2026-0042.pdf", "rb") as file:
    upload = requests.post(f"{API}/uploads", headers=headers, files={"file": file}, timeout=60).json()

response = requests.post(
    f"{API}/einvoices/validate",
    headers={**headers, "Idempotency-Key": "inbox-2026-11-0042"},
    json={"source": {"upload_id": upload["id"]}, "reference": "inbox-2026-11-0042"},
    timeout=130,
)
if response.status_code == 422:
    problem = response.json()
    print("not checked:", problem["code"], problem["detail"])
else:
    result = response.json()["result"]
    print("valid" if result["valid"] else "invalid", result["document"]["profile"])
```

A check reaches one of two verdicts. Both answer `200` with the render object, and the verdict is in `result`:

```json
{
  "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](https://dynamicdocumentapi.com/docs/e-invoicing#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 [#sources]

The `source` takes exactly one of these fields, as for the [PDF tools](https://dynamicdocumentapi.com/docs/pdf-tools#sources):

| 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`](https://dynamicdocumentapi.com/docs/e-invoicing#export-xrechnung-or-en-16931-xml). 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`](https://dynamicdocumentapi.com/docs/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 [#what-is-checked]

1. **The file.** It is a PDF or an XML. From a PDF, the check reads the first embedded file named `factur-x.xml`, else `xrechnung.xml`, else `zugferd-invoice.xml`, ignoring case. A PDF that needs a password fails with `422 pdf_password_required`. An encrypted PDF that opens without a password is read, and the PDF/A report lists the encryption.
2. **The XML.** It is UTF-8, has no DOCTYPE, is at most 5 MiB, and is an invoice: a CII `CrossIndustryInvoice`, or a UBL `Invoice` or `CreditNote`.
3. **The profile.** Its specification identifier (BT-24) names one of the [supported profiles](#supported-formats), exactly as the validator compares it: no whitespace around it and no comment inside it.
4. **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 [#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 [#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`](https://dynamicdocumentapi.com/docs/errors#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`](https://dynamicdocumentapi.com/docs/errors#einvoice_unsupported)                                                 | An invoice the check doesn't cover (see [Supported formats](#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`](https://dynamicdocumentapi.com/docs/errors#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](https://dynamicdocumentapi.com/docs/pdf-tools#errors)                                                                                                                                                                                                                                                                                                                               |
| `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](#limits-and-test-mode)                                                                                                                                                                                                                                                                                                                                          |

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:

```python
if response.status_code == 422 and response.json()["code"] in ("einvoice_not_found", "einvoice_unsupported"):
    route_to_manual_review(file)
```

## Limits and test mode [#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_limited` with `Retry-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](https://dynamicdocumentapi.com/docs/plans-and-limits#limits-by-plan). Beyond either, the API answers `429 rate_limited`; for the daily limit, `Retry-After` runs 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](https://dynamicdocumentapi.com/docs/pdf-tools#sources) apply to the file, and the XML itself can be at most 5 MiB.

## Billing [#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](https://dynamicdocumentapi.com/docs/pdf-tools#billing) call, and its `billed_renders` field shows the number. See [Plans and limits](https://dynamicdocumentapi.com/docs/plans-and-limits#what-counts-as-a-render).

## Related pages [#related-pages]

* [E-invoicing](https://dynamicdocumentapi.com/docs/e-invoicing) for creating Factur-X, ZUGFeRD and XRechnung invoices
* [PDF tools](https://dynamicdocumentapi.com/docs/pdf-tools) for uploads and the other sources
* [Renders](https://dynamicdocumentapi.com/docs/renders#the-render-object) for the render object, webhooks and sync or async mode
* [MCP server](https://dynamicdocumentapi.com/docs/mcp) for the `validate_einvoice` tool
