Docs

3 API3.1

3.1

Dry run: POST /validate

Runs the same checks as submit. Stores and sends nothing.

  • In the sandbox

In plain words

A dry run checks an invoice the way a real submission would and sends nothing, so a partner can find every problem before a client's invoice goes anywhere.

POST /validate runs the same checks as a submit and answers with a report. It queues nothing and sends nothing. The call needs a key with the submit scope. The body is JSON, up to 5 MB.

Tip

Start here when you map a new client.

The request

FieldMeaning
invoice_refRequired. The ERP's own document id, up to 100 characters. Echoed in every status event.
routeRequired. DE-XRECHNUNG, PEPPOL, FR-PA, PL-KSEF or RO-EFACTURA.
documentOne invoice in the canonical model. Send this or an export, not both.
export, connectorAn ERP export as the ERP wrote it, with connector set to business-central or sap-b1. Send this or a document, not both. On a route other than Germany the report starts with a mapping layer. mapping_version picks a version other than the live one (see mapping versions).
formatsThe documents to build where the route allows a choice. Germany: any of xrechnung-ubl (the default), xrechnung-cii and zugferd; KoSIT runs once per format, and the PDF also runs Mustang and veraPDF. France: ubl, cii or facturx.
environmentOptional. It must match the environment of your key.
erp_totalsThe totals your ERP computed: payable_amount, currency and optionally tax_amount, as strings. If they differ from the totals the service computes from the lines, the report has a finding EI-TOTALS-MISMATCH on erp_totals.payable_amount.

The full field list is in the reference.

An invoice that passes

The sample request is validate-de-ok.json, an invented German invoice. The report has valid: true, one document with its SHA-256, and four layers that passed.

curl -X POST "https://api-sandbox-eu.eurinvoice.com/validate" \
  -H "Authorization: Bearer <your-api-key>" \
  -H "Content-Type: application/json" \
  --data-binary @validate-de-ok.json
Response200 OK
{
  "valid": true,
  "route": "DE-XRECHNUNG",
  "documents": [
    {
      "sha256": "e21ab9d5097022bea30bfa9f9fe0a4c7ca9afdbd672b0ce2f9150461db773625",
      "kind": "xrechnung-ubl",
      "content_base64": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgi... (7,092 characters, shortened for these docs)"
    }
  ],
  "layers": [
    {
      "findings": [],
      "passed": true,
      "layer": "schema"
    },
    {
      "findings": [],
      "passed": true,
      "layer": "mapping"
    },
    {
      "findings": [],
      "passed": true,
      "layer": "pre-check"
    },
    {
      "findings": [],
      "passed": true,
      "layer": "kosit"
    }
  ]
}
Recorded on 7 Oct 2026. Run in your own folder with the sample file beside it.

An invoice that fails

The same invoice without the seller's name, validate-de-missing-seller-name.json. A dry run still answers 200. valid is false, and each finding names the field and who fixes it. The two highlighted lines are the ones to look at: the catalogue code, and the field in the client's data.

curl -X POST "https://api-sandbox-eu.eurinvoice.com/validate" \
  -H "Authorization: Bearer <your-api-key>" \
  -H "Content-Type: application/json" \
  --data-binary @validate-de-missing-seller-name.json
Response200 OK
{
  "valid": false,
  "route": "DE-XRECHNUNG",
  "layers": [
    {
      "findings": [
        {
          "code": "EI-SCHEMA",
          "field": "seller.name",
          "related": [
            {
              "code": "Art.226(5)",
              "source": "pre-check"
            }
          ],
          "fix_hint": "Read the JSON path in the error and correct the client mapping.",
          "who_fixes": "us",
          "source": "schema",
          "message": "We could not read this invoice from your export. We are correcting our mapping; if a field is missing in the ERP we will tell you which one."
        }
      ],
      "passed": false,
      "layer": "schema"
    },
    {
      "findings": [],
      "passed": true,
      "layer": "mapping"
    },
    {
      "findings": [
        {
          "code": "Art.226(5)",
          "field": "seller.name",
          "related": [
            {
              "code": "EI-SCHEMA",
              "source": "schema"
            }
          ],
          "fix_hint": "Complete the party's name and address in the master data.",
          "who_fixes": "erp",
          "source": "pre-check",
          "message": "A company name or street address is missing. Complete the company or customer record in the ERP."
        }
      ],
      "passed": false,
      "layer": "pre-check"
    }
  ]
}
Recorded on 7 Oct 2026.

Reading the report

  • valid is true only when every layer passed.
  • layers run in order: schema, mapping, pre-check, then the route's own validators, named for what ran (for example KoSIT-XRechnung-3.0.2).
  • Each finding has a catalogue code, the field, who_fixes (us or erp), a fix_hint, a message and the source layer. Errors and the catalogue lists every code.
  • documents lists what was built, with its SHA-256 and, for a file of 2 MiB or less, its bytes in content_base64. It is present when the invoice is valid.

Other answers

StatusMeaning
200A report, valid or not.
400The request cannot be read: not JSON, a field is wrong, or both document and export are present.
401No key, or an unknown key.
403The key does not have the submit scope.
413The body is over 5 MB (payload-too-large).
422A number past the limits, such as one over 40 characters (EI-SCHEMA, naming the field). Every other finding comes in the 200 report.
429Too many requests for the key. Wait for Retry-After seconds.
503Every validator is busy (busy). Retry after Retry-After seconds.
curl -X POST "https://api-sandbox-eu.eurinvoice.com/validate" \
  -H "Authorization: Bearer <your-api-key>" \
  -H "Content-Type: application/json" \
  --data-binary @not-json.txt
Response400 Bad Request
{
  "detail": "The request is not valid JSON.",
  "type": "https://eurinvoice.com/problems/bad-request",
  "title": "The request could not be read",
  "status": 400
}
Recorded on 7 Oct 2026.

On this page