For software vendors
invowerk checks e-invoices over a REST API, in the build of your software or over MCP. You get the same result as in the free web tool, as JSON with a validation report.
What is checked · API · Check in the build · MCP · More endpoints
What invowerk checks
-
XRechnung 3.0.2 and EN 16931 (UBL and CII) under the KoSIT validation configuration of Aug 31, 2026. The result of the KoSIT validator 1.6.3, the validation program of the Coordination Office for IT Standards (KoSIT), applies. When it cannot be reached, invowerk checks with the same rules and says so in the
verdict_sourcefield. - ZUGFeRD 2.5.2 and Factur-X under the FeRD validation rules per profile, from MINIMUM to EXTENDED.
- Peppol BIS Billing 3.0.21.
- The PDF: PDF/A with veraPDF 1.30.2, plus the file name, the XMP metadata and how the XML file is embedded.
-
Hints (
IW-*): extra checks such as check digits, totals and the comparison of the PDF with the invoice data. They are in thehintsfield and do not change the result.
Rule sets, changes and the test files are on the test bench. Differences between e-invoice validators explains why validators disagree. Every rule with its explanation is under error codes.
API
Send the invoice (XML or PDF) as the request body to POST https://api.invowerk.dev/v1/validate, with the API key in the X-API-Key header. Without a key, a lower limit applies. The result always comes with HTTP 200. Any other status means the request itself failed (for example 402, 413 or 429). With ?explain=true, messages for common rules include their explanation.
curl -sS -H "X-API-Key: $INVOWERK_KEY" \
--data-binary @invoice.xml https://api.invowerk.dev/v1/validateimport os
import requests
with open("invoice.xml", "rb") as fh:
r = requests.post(
"https://api.invowerk.dev/v1/validate",
headers={"X-API-Key": os.environ["INVOWERK_KEY"]},
data=fh,
timeout=60,
)
r.raise_for_status()
report = r.json()
print(report["valid"], report["verdict_source"])
for f in report["findings"]:
if f["severity"] == "fatal":
print(f["rule_id"], f["message"])The main fields of the response
valid | The result (true or false). |
verdict_source | official: the result of the KoSIT validator. local: the result of invowerk's own check. |
findings[] | All messages with rule_id and severity (fatal = error, warning = warning, information = info). |
errors | The messages by kind: format, business_rules, carrier. |
hints | Hints (IW-*), separate from the result. |
rulesets | The versions of the rule sets used for this check. |
evidence.document_sha256 | SHA-256 of the checked file, for the validation report. |
e_rechnung_de.status | The category under the letter of the German Federal Ministry of Finance (BMF) of Oct 15, 2025, for example e_rechnung or keine_e_rechnung. |
Check in the build
The step checks every file under invoices/. On errors it prints the rules and fails the job. Store the API key as the secret INVOWERK_KEY.
# .github/workflows/e-rechnung.yml: as a step after your build
- name: Check e-invoices
env:
INVOWERK_KEY: ${{ secrets.INVOWERK_KEY }}
run: |
status=0
for f in invoices/*.xml invoices/*.pdf; do
[ -e "$f" ] || continue
if ! res=$(curl -sS --fail-with-body -H "X-API-Key: $INVOWERK_KEY" \
--data-binary @"$f" https://api.invowerk.dev/v1/validate); then
echo "$f: request failed: $res"; status=1; continue
fi
if echo "$res" | jq -e '.valid' >/dev/null; then
echo "$f: passed"
else
echo "$f: failed"
echo "$res" | jq -r '.findings[] | select(.severity == "fatal") | "\(.rule_id): \(.message)"'
status=1
fi
done
exit "$status"# .gitlab-ci.yml: create INVOWERK_KEY as a masked CI/CD variable
e-rechnungen-pruefen:
image: alpine:3
before_script:
- apk add --no-cache curl jq
script:
- |
status=0
for f in invoices/*.xml invoices/*.pdf; do
[ -e "$f" ] || continue
if ! res=$(curl -sS --fail-with-body -H "X-API-Key: $INVOWERK_KEY" \
--data-binary @"$f" https://api.invowerk.dev/v1/validate); then
echo "$f: request failed: $res"; status=1; continue
fi
if echo "$res" | jq -e '.valid' >/dev/null; then
echo "$f: passed"
else
echo "$f: failed"
echo "$res" | jq -r '.findings[] | select(.severity == "fatal") | "\(.rule_id): \(.message)"'
status=1
fi
done
exit "$status"# .forgejo/workflows/e-rechnung.yml: as a step after your build,
# the runner image needs curl and jq
- name: Check e-invoices
env:
INVOWERK_KEY: ${{ secrets.INVOWERK_KEY }}
run: |
status=0
for f in invoices/*.xml invoices/*.pdf; do
[ -e "$f" ] || continue
if ! res=$(curl -sS --fail-with-body -H "X-API-Key: $INVOWERK_KEY" \
--data-binary @"$f" https://api.invowerk.dev/v1/validate); then
echo "$f: request failed: $res"; status=1; continue
fi
if echo "$res" | jq -e '.valid' >/dev/null; then
echo "$f: passed"
else
echo "$f: failed"
echo "$res" | jq -r '.findings[] | select(.severity == "fatal") | "\(.rule_id): \(.message)"'
status=1
fi
done
exit "$status"MCP
The MCP server at https://invowerk.dev/mcp/ offers the same check to AI assistants. To add it to Claude Code:
claude mcp add --transport http invowerk https://invowerk.dev/mcp/ \
--header "X-API-Key: $INVOWERK_KEY"
Other clients: connect over MCP.
More endpoints
invowerk creates e-invoices from JSON (/v1/generate), converts between UBL and CII (/v1/convert), reads invoice data as JSON (/v1/parse) and shows invoices as HTML (/v1/render). invowerk checks created and converted invoices before it returns them. The API reference describes every endpoint.
Plans and credits per call (1 check = 1 credit) are on the pricing page.