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+jsonand 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, quota_exceeded, rate_limited, invalid_model, unsupported_format or unparseable.
statusintegerThe HTTP status code, repeated in the body.
detailstringHuman-readable explanation (English). May change; don't parse it.

HTTP status codes

StatusError codeEndpointsWhen
200allSuccess. On /validate, also returned when the invoice is invalid — check valid. On /generate and /convert the body is the resulting XML (application/xml).
400empty_bodyall POSTEmpty request body. Send raw bytes (XML/PDF) — or, for /generate, the JSON model.
400invalid_model/generateThe JSON invoice model could not be parsed — see the InvoiceModel schema.
400unsupported_format/generate, /convertUnknown ?format= value. Use cii or ubl.
401unauthorizedall except /healthMissing or invalid API key. Send Authorization: Bearer <key>. /health is public.
402quota_exceededall POSTMonthly quota exhausted. Upgrade or buy credits in the console — nothing is charged automatically.
413all POSTRequest body exceeds the 10 MB limit.
422— (report)/parseNo semantic invoice model could be extracted. Body is the report (without invoice). See below.
422unparseable/convertThe input could not be parsed as an e-invoice — reported as problem+json.
429rate_limitedall POSTRate 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 — /parseanswers 422 Unprocessable Content when no semantic invoice model could be extracted. The response body contains the validation report ({ "report": … }, withoutinvoice); its findings explain what was detected, e.g. BH-XML-01when 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'sfindings array; valid is true iff no finding has severityerror, 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.