Error handling

The API distinguishes two things that are easy to confuse: request errors (your call could not be processed — reported as RFC 9457 problem+json with a 4xx status) and validation findings (the invoice has problems — reported inside the report, with HTTP 200). An invalid invoice is a successful validation.

Request errors: RFC 9457 problem+json

Whenever a request fails, the response has Content-Type: application/problem+json and this body structure:

HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json

{
  "type": "https://billhorse.com/errors/unauthorized",
  "title": "unauthorized",
  "status": 401,
  "detail": "API key missing or invalid. Send header: Authorization: Bearer <key>"
}
FieldTypeMeaning
typestring (URI)Stable identifier for the error kind, https://billhorse.com/errors/<title>. Match on this (or title), not on detail.
titlestringMachine-readable error code: unauthorized, empty_body or rate_limited.
statusintegerThe HTTP status code, repeated in the body.
detailstringHuman-readable explanation (English). May change; don't parse it.

HTTP status codes

StatusError codeEndpointsWhen
200allSuccess. Also returned when the invoice is invalid — check valid in the report.
400empty_body/validate, /parseEmpty request body. Send the invoice as raw bytes (XML or PDF) — no multipart, no base64.
401unauthorized/validate, /parseMissing or invalid API key. Send Authorization: Bearer <key>. /health is public.
413/validate, /parseRequest body exceeds the 10 MB limit.
422/parse onlyThe input could not be parsed as an e-invoice (no UBL/CII/ZUGFeRD structure found). See below.
429rate_limited/validate, /parseRate limit exceeded (default 120 requests/minute per key). The Retry-After header tells you how many seconds to wait.

422 on /parse

Unlike /validate — which always answers 200 with a report — /parse answers 422 Unprocessable Content when no semantic invoice model could be extracted. The response body contains the validation report ({ "report": … }, without invoice); its findings explain what was detected, e.g. BH-XML-01 when the input is not well-formed XML:

HTTP/1.1 422 Unprocessable Content

{
  "report": {
    "valid": false,
    "findings": [
      { "id": "BH-XML-01", "severity": "error",
        "message": "The file is not well-formed XML." }
    ],
    …
  }
}

Message language (?lang=)

Finding messages come in English by default (the contract language of the API). Add ?lang=de or ?lang=fr to /validate and /parse to get German or French messages for your end users:

curl -s -X POST "https://api.billhorse.com/v1/validate?lang=de" \
  -H "Authorization: Bearer $BILLHORSE_API_KEY" \
  --data-binary @rechnung.xml

Validation findings

Validation problems never surface as HTTP errors. They are entries in the report's findings array; valid is true iff no finding has severity error, and counts aggregates findings per severity:

{
  "valid": false,
  "findings": [
    { "id": "BR-DE-15", "severity": "error",
      "message": "XRechnung: buyer reference (BT-10, …) is mandatory." },
    { "id": "BR-CO-16", "severity": "error",
      "message": "Amount due (BT-115) does not match …",
      "expected": "178.50", "actual": "170.00" }
  ],
  "counts": { "error": 2, "warning": 0, "info": 0 }
}
FieldPresenceMeaning
idalwaysRule id — stable, language-neutral. Every id has a human explanation page: billhorse.com/en/rules/<id>/ (also in DE and FR).
severityalwayserror (invoice is invalid), warning (should be fixed) or info (notice, e.g. a migration hint).
messagealwaysHuman-readable explanation, localizable via ?lang=.
detailoptionalTechnical detail (e.g. the raw XML parser error). Not translated.
expected / actualoptionalComputed vs. stated value — set on arithmetic rules (e.g. BR-CO-16).
foundoptionalThe offending value (e.g. a malformed IBAN or Leitweg-ID).

Optional fields are omitted entirely when not set (never null).

Rule id families

PrefixSourceExamples
BR-*, BR-CO-*, BR-S/Z/E/G/IC-*EN 16931 business rules (field presence, arithmetic, VAT categories)BR-01, BR-CO-16, BR-S-10
BR-DE-*German XRechnung rules (applied when profile.xrechnung is true)BR-DE-15 (Leitweg-ID)
PEPPOL-EN16931-*Peppol BIS Billing 3.0 rules (applied when profile.peppol is true)PEPPOL-EN16931-R010
BH-*Billhorse checks: file/structure, profiles, plausibility, French rulesBH-XML-01, BH-PDF-01, BH-IBAN-01, BH-FR-01

The full catalog with fixes lives in the rule index — in EN, DE and FR.

© 2026 Billhorse · Legal & privacy · Questions: [email protected]