API
Validate documents from your pipeline. API keys ship with the beta (join here) — the same engine powers the free validator page, no key needed there.
POST /v1/validate — validate one document
curl -X POST https://api.normdrift.com/v1/validate \ -H "Authorization: Bearer rk_live_…" \ -F "file=@invoice.xml"
Optional ?vesid= to force a ruleset (e.g. de.xrechnung:ubl-invoice:3.0.2); PDF Factur-X are auto-routed by profile. Max size 20 MB. Every 200 response carries X-RateLimit-Remaining (your monthly quota left).
Response
{
"verdict": "fail",
"ruleset": "eu.peppol.bis3:invoice:2026.5",
"duration_ms": 131,
"truncated": false,
"findings": [
{
"rule_code": "BR-CO-16",
"severity": "error",
"message": "[BR-CO-16]-Amount due for payment (BT-115) = …",
"location": "/:Invoice[1]/cac:LegalMonetaryTotal[1]"
}
]
} POST /v1/runs — re-validate your whole corpus
curl -X POST https://api.normdrift.com/v1/runs \
-H "Authorization: Bearer rk_live_…" \
-H "Content-Type: application/json" \
-d '{"project_public_id":"<shown on your project page>"}' Returns 202 {"run_id":"run_…","docs_total":12}. One run at a time per project (409 run_in_progress); 422 empty_corpus if no documents. The run compares against your baseline and fires your alert channels on regressions.
GET /v1/rulesets — what we track
curl -H "Authorization: Bearer rk_live_…" https://api.normdrift.com/v1/rulesets
Current and upcoming rulesets per format family, with effective_date. Public variant without key: GET /v1/public/releases (adds the latest upstream releases detected).
Status badge

SVG badge of your last run (pass / fail / running), cached 5 min. The public id is on your project page — it is unguessable, that is its only protection: treat it like a semi-secret.
GitHub Action
name: e-invoicing compliance
on:
push: { branches: [main] }
schedule: [{ cron: "0 7 * * 1" }] # weekly — catches ruleset releases
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate sample invoice
run: |
curl -sf -X POST https://api.normdrift.com/v1/validate \
-H "Authorization: Bearer ${{ secrets.NORMDRIFT_KEY }}" \
-F "file=@samples/invoice.xml" | tee result.json
test "$(jq -r .verdict result.json)" = "pass"
- name: Re-validate reference corpus
run: |
curl -sf -X POST https://api.normdrift.com/v1/runs \
-H "Authorization: Bearer ${{ secrets.NORMDRIFT_KEY }}" \
-H "Content-Type: application/json" \
-d '{"project_public_id":"'"${{ vars.NORMDRIFT_PROJECT }}"'"}' Errors
| Status | Slug | Meaning |
|---|---|---|
| 400 | unknown_format · unknown_vesid · file_missing | Bad request — not detectable as UBL/CII/PDF, or vesid not served |
| 402 | api_requires_paid_plan | Free plan has no API quota — upgrade to Pro |
| 413 | file_too_large | Over 20 MB |
| 429 | quota_exceeded | Monthly quota exhausted — resets on the 1st (UTC) |
| 502 | validator_unavailable | Engine outage — not counted against your quota, retry |
| 503 | {"warming":true} | Engine warming up — not counted, retry in a few seconds |