Error Handling
Understand API errors and learn how to handle them in your application. All errors follow a consistent format with helpful details.
Error Response Format
All API errors are returned in the following JSON format:
1{2 "error": "VALIDATION_FAILED",3 "message": "Die Rechnung entspricht nicht dem XRechnung 3.0.2 Schema"4}error
Machine-readable error code for programmatic handling
message
Human-readable error description
429 Quota Exceeded Response
When quota is exceeded, the response includes an additional quota object with usage details.
1{2 "error": "QUOTA_EXCEEDED",3 "message": "Usage limit reached for pdf:de:generate",4 "quota": {5 "current": 100,6 "limit": 100,7 "period": "monthly"8 }9}quota.current
Current usage count
quota.limit
Usage limit
quota.period
Quota period type, e.g. monthly or yearly — never a calendar label. Absent when the rejection belongs to no plan quota.
HTTP Status Codes
Invalid request data (JSON, XML, missing required fields)
Missing or invalid API key
API key has no permission for this resource
Resource (e.g. invoice) not found
Compliance check failed — the request JSON was valid, but an EN 16931 / KoSIT rule fired. The rule (e.g. BR-61) is listed in errors[].
Rate limit reached — the response carries **no** `quota` object. Retry with backoff.
Monthly quota exhausted (`error: "QUOTA_EXCEEDED"`). Only this case carries a `quota` object; retrying does not help.
Server error, please try again
API Error Codes
| Code | HTTP | Description | Solution |
|---|---|---|---|
| Bad Request | 400 | The request body or a path parameter is invalid — the most common error string of the API. | Read `message` — it names the field. For format errors, check the path token: `xrechnung`, `zugferd`, `pdf`, `facturx`, `ubl`, `peppol-ubl`, `qr-bill`, `fatturapa`, `facturae`, `ebinterface`, `isdoc`, `nav`, `mydata`, `ksef` — lower case and without a version number. |
| INVALID_REQUEST | 400 | Invalid input on the VeriFactu route (`POST /api/v1/invoice/es/verifactu-qr`) — the only endpoint that returns this code. | Check the VeriFactu fields against `/docs/api/verifactu-qr`. On every other route the same case is called `Bad Request`. |
| FORMAT_NOT_DETECTED | 400 | Format detection was not confident enough (confidence < 50) — applies to `POST /api/v1/invoice/parse` and `POST /api/v1/invoice/convert`. | Call the explicit parse endpoint with country and format: `POST /api/v1/invoice/{countryCode}/{format}/parse`. The response also carries `detection` with the intermediate result. |
| COUNTRY_NOT_DETECTED | 400 | The format was detected, the country was not (`POST /api/v1/invoice/parse`). | Call the parse endpoint with a country path: `POST /api/v1/invoice/{countryCode}/{format}/parse`. |
| TOO_MANY_LINE_ITEMS | 400 | The invoice carries more entries in `items[]` than the plan allows — applies to generating, both validate routes and converting, not to parsing. `lineItems.count` and `lineItems.limit` name the counted number and the cap. | Split the document into several invoices or change plan (Free 25, Starter 100, Premium 1,000, Enterprise individual). Across all plans an absolute ceiling of 15,000 line items per invoice applies. Rejected means: no invoice produced, no call billed. |
| UNAUTHORIZED | 401 | API key is missing or invalid | Check the Authorization: Bearer <key> header |
| FORBIDDEN | 403 | API key has no permission | Check if the key is enabled for this endpoint |
| Not Found | 404 | Resource not found | Check the ID or endpoint path |
| PARSE_FAILED | 422 | The source document was detected but could not be read (HTTP 422). The individual errors are in `errors[]`. | Inspect `errors[]`. Most common cause: a PDF without embedded XML, or a truncated upload. |
| CONVERSION_FAILED | 422 | The source could not be converted into a conformant target document (HTTP 422). `complianceErrors` names the rules that failed. | Read `complianceErrors` and `conversionWarnings` — usually a field the target format requires is missing from the source. |
| QUOTA_EXCEEDED | 429 | Monthly quota exhausted | Upgrade your plan or wait until the end of the month |
| Internal Server Error | 500 | Internal server error | Try again, contact support if it persists |
XRechnung Validation Errors (BR-DE)
These errors come from KoSIT Schematron validation and are returned in errors[] / warnings[]:
An entry in `errors[]` or `warnings[]` has exactly three fields: code, message, field. There is no `details` object. `MISSING_FIELD` and `MISSING_OPTIONAL` are values of `errors[].code` and `warnings[].code`, not top-level error codes.
Payment instructions missing: an invoice must contain payment information (BG-16)
Field: paymentMethods
Buyer reference missing: the element Buyer reference (BT-10) must be provided — for public-sector invoices this carries the Leitweg ID. Its shape: 2–12 digits of coarse addressing, optionally 0–30 digits of fine addressing, 2 check digits, separated by `-` (example: `04011000-12345-34`).
Field: countrySpecific.leitwegId
Invalid invoice type: the invoice type code (BT-3) must be 326, 380, 384, 389, 381, 875, 876 or 877
Field: type
No invoice line: an invoice must contain at least one line (BG-25)
Field: items
Sum of line items does not equal invoice net amount
Field: items[].quantity * items[].unitPrice
Invoice net total calculated wrongly: BT-109 = sum of line amounts minus allowances plus charges
Field: totals
VAT category taxable amount for category S is wrong: BT-116 does not match the sum of the lines at that rate
Field: totals / items[].taxRate
Invalid scheme identifier: an identifier's schemeID must come from the ISO 6523 list
Field: PartyIdentification/ID/@schemeID
Credit transfer without a payee account: if paymentMethods is a credit transfer (BT-81 = 30/58), the payment account identifier (BT-84, IBAN) must be present
Field: seller.bankAccount.iban
Unit of measure not a code: items[].unit must be a UN/ECE Rec 20 code (C62 piece, HUR hour, KGM, MTR, DAY) — no free text
Field: items[].unit
Full list: KoSIT XRechnung Specification
Retry Strategy
Implement retries for temporary errors (429, 5xx):
1async function createInvoice(data, retries = 3) {2 for (let i = 0; i < retries; i++) {3 try {4 const response = await fetch('https://service.invoice-api.xhub.io/api/v1/invoice/de/xrechnung/generate', {5 method: 'POST',6 headers: {7 'Authorization': 'Bearer sk_live_...',8 'Content-Type': 'application/json'9 },10 body: JSON.stringify(data)11 });12 13 if (response.status === 429) {14 const body = await response.json();15 // Only the quota rejection carries a `quota` object. Retrying it is16 // pointless — the monthly allowance is gone until the period rolls over.17 if (body.quota) throw new Error(body.message);18 // Rate limit: no Retry-After header is sent, so back off exponentially.19 await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));20 continue;21 }22 23 if (response.status >= 500) {24 // Server error - retry with exponential backoff25 await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));26 continue;27 }28 29 const result = await response.json();30 31 if (!response.ok) {32 // Client error - don't retry, handle the error33 throw new Error(result.message);34 }35 36 return result;37 } catch (error) {38 if (i === retries - 1) throw error;39 }40 }41}Best Practices
Log Errors
Always log the response header x-request-id. There is no `requestId` field in the response body. With the header we can trace the request for support inquiries.
Retry Only on 5xx/429
Client errors (4xx) won't be fixed by retries. Only retry on server or rate limit errors.
Exponential Backoff
For retries: double the wait time after each failed attempt (1s, 2s, 4s, ...) to avoid overloading the server.
Tell the two 429s apart
No `Retry-After` header is sent. Instead, check whether the 429 response carries a quota object: with the object the quota is exhausted (upgrade, or wait for the period to roll over); without it the rate limit fired (retry with exponential backoff).
Support
For recurring errors or unclear error messages, contact us at support@xhub.io with the response header x-request-id.