Errors and limits¶
Every failure returns the same envelope. Branch on code, never on message.
{
"error": {
"code": "validation_failed",
"message": "Check the highlighted fields",
"fields": {
"registrationNumber": "invalid",
"amount.currency": "unsupported"
}
}
}
fields appears only on validation_failed. Keys are dotted paths into the request body, so they map directly onto form fields.
Error codes¶
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request |
Malformed JSON, or a body over 256 KB |
| 401 | unauthenticated |
Missing, expired, or invalid token |
| 403 | forbidden |
Token lacks the scope for this call |
| 403 | consent_required |
No consent on record for this client. Send the consent object or onboard the client first |
| 404 | not_found |
No such client or invoice |
| 409 | client_exists |
Registration number already registered. Existing clientId is in the body |
| 409 | already_submitted |
Onboarding was already submitted |
| 409 | not_financeable |
Invoice is settled, credited, or already financed |
| 409 | terms_not_available |
Insufficient history, or client not yet active. reason says which |
| 422 | validation_failed |
Field-level problems, enumerated in fields |
| 429 | rate_limited |
Too many requests. Honour Retry-After |
| 500 | internal_error |
Our fault. Safe to retry with the same idempotency key |
| 503 | signing_unavailable |
Signing provider unreachable. Retry — nothing was consumed |
Which errors are worth retrying¶
| Behaviour | Codes |
|---|---|
| Retry with backoff | rate_limited, internal_error, signing_unavailable |
| Fix, then resend | validation_failed, bad_request |
| Treat as success | client_exists, already_submitted — the work is already done |
| Do not retry | forbidden, not_found, not_financeable |
consent_required is worth calling out separately: the request itself is well-formed and will succeed unchanged once consent exists. Hold the invoice and resend after the client signs, rather than discarding it.
Always retry with the same Idempotency-Key. A new key on a retry is how duplicates get created.
Validation field codes¶
The values inside fields are stable machine codes, suitable for mapping to your own messages.
| Code | Meaning |
|---|---|
required |
Field is missing or empty |
invalid |
Present but malformed — wrong shape, pattern, or length |
unsupported |
A valid value we do not accept here, e.g. a non-EUR currency |
out_of_range |
Numeric or date value outside the permitted bounds |
before_issue_date |
A dueDate or paymentDate earlier than issueDate |
inconsistent |
Valid alone, but contradicts another field |
Rate limits¶
| Surface | Limit |
|---|---|
| Reads | 600 per minute |
| Writes | 120 per minute |
| Bulk invoice jobs | 10 concurrent per partner |
| Token requests | 60 per minute |
Every response carries:
X-RateLimit-Reset is a Unix timestamp. On 429, honour Retry-After rather than reading the reset header.
Bulk loads bypass the write limit
Sending twelve months of history through a loop over POST /invoices will hit the write limit within seconds. Use POST /invoices/batch instead — it is exempt, and it is an order of magnitude faster.
Request size¶
Bodies are capped at 256 KB. In practice this only matters for POST /invoices/batch; if a batch exceeds it, split into two calls rather than trimming fields.
Timeouts¶
| Operation | Typical | Timeout |
|---|---|---|
| Reads | under 200 ms | 10 s |
| Writes | under 500 ms | 10 s |
| Onboarding submit | 1–3 s (calls the signing provider) | 30 s |
| Batch accept | under 1 s (processing is async) | 30 s |
Set your client timeout to at least 30 seconds on onboarding submit. A client-side timeout there can leave a signing contract created without you knowing — replay the same idempotency key to recover the result rather than submitting again.