Dynamic Document API documentation for AI agents. The index of all pages is llms.txt, every page in one file is llms-full.txt. Every documentation page is available as Markdown by appending /index.md to its URL or by requesting the page with the header Accept: text/markdown.

DynamicDocumentAPI

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.

View as Markdown

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.

FieldDescription
result.validtrue when the XML passed and, for a PDF, the PDF passed PDF/A-3b too
result.document.containerpdf or xml: what you sent
result.document.attachmentThe embedded file's name as written in the PDF, such as factur-x.xml; null for XML
result.document.syntaxcii or ubl
result.document.profileThe profile the specification identifier names: MINIMUM, BASIC_WL, BASIC, EN16931, EXTENDED or XRECHNUNG
result.document.specification_idThe specification identifier (BT-24), as written in the XML
result.conformance.einvoiceThe KoSIT validator's report
result.conformance.pdfaveraPDF'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:

FieldWhat it is
upload_idA 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.
urlA public https URL, downloaded when the check runs
render_idA 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_uriThe 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

  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, 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

ProfileSpecification identifier (BT-24)Syntaxes
MINIMUMurn:factur-x.eu:1p0:minimumCII
BASIC_WLurn:factur-x.eu:1p0:basicwlCII
BASICurn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basicCII
EN16931urn:cen.eu:en16931:2017CII, UBL
EXTENDEDurn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extendedCII
XRECHNUNGurn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0CII, 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:

StatusCodeWhen
422einvoice_not_foundThe 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
422einvoice_unsupportedAn 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.
422einvoice_validation_unavailableA validator gave no verdict in time. There is no result; retry later.
422pdf_password_required, invalid_pdf_source, url_not_allowed, asset_fetch_failed, payload_too_largeAs for the PDF tools
400validation_errorAn invalid request, filename or delivery sent, or a source that is neither a PDF nor an XML
404render_not_found, upload_not_foundThe source isn't in your workspace, or the upload has expired
410file_expiredThe source render has no PDF or XML file any more, or used zero-retention delivery
413payload_too_largeA data URI over 5 MB
429rate_limited, concurrency_limitedSee 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_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. 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 apply to the file, and the XML itself can be at most 5 MiB.

Billing

CheckBilled renders
A verdict, valid or invalid1
No verdict (422, such as einvoice_not_found)0
Test checks0

A check is billed like a PDF tool call, and its billed_renders field shows the number. See Plans and limits.

  • 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_einvoice tool

On this page