Errors & status codes
Every Clearvo product returns the same HTTP status codes and a consistent JSON error envelope. Product-specific rejection reasons — e.g. e-invoicing authority clearance codes — are documented on each product's own page and linked from here.
HTTP errors
Clearvo uses standard HTTP status codes. Error responses include a JSON body (usually with ok: false).
| Status | Meaning | Common cause |
|---|---|---|
| 401 | Unauthorized | Missing or invalid x-api-key. Check the header name (lowercase) and that the key is active. |
| 400 | Bad Request | The request cannot be read: invalid JSON, a missing required field or an invalid value. The response includes field and message for each violation. On POST /send this is only a malformed request (unparseable JSON, no country or invoiceNumber, a bad x-idempotency-key, an unrecognised country code, or a platform-set field such as channel or source). A well-formed document that fails a validation check is stored and answered 200 instead, see below. |
| 403 | Forbidden | The key is valid but not authorized for this entity or product — e.g. an entity-scoped key used against a different entity, or a read-only key on a write operation. Purchase-side tax calculation is the Enterprise-only Purchases (AP) sub-solution: ap_not_included (plan does not include it) or ap_not_enabled (switch Purchases (AP) on under Settings > Solutions). |
| 404 | Not Found | No record exists for the id you passed — a wrong referenceId on GET /status, an invoice id that belongs to another entity, or a resource that was deleted. Check the identifier. |
| 409 | Conflict | Conflict. On POST /send, most often IDEMPOTENT_BODY_MISMATCH — you re-sent an already processed document with different content; the stored document is not changed and the refused edit is recorded in its history, so send the change as a correction instead. (Reusing an x-idempotency-key for a different document is no longer an error: the document is processed under its own identity and the response carries the warning IDEMPOTENCY_KEY_REUSED_ACROSS_DOCUMENTS.) Other conflicts carry their own code, for example DUPLICATE_INVOICE_NUMBER (the invoice number already exists for this entity; invoices, credit notes and debit notes share one number space) and ALREADY_CORRECTED (the invoice already has an active or successful correction). |
| 422 | Unprocessable Entity | A well-formed request whose values fail validation on an endpoint that does not store a document (for example POST /tax/calculate or the configuration endpoints). POST /send does not answer 422: a missing customer taxId for reverse charge or an invalid Italian Partita IVA check digit is stored as a rejection and answered 200 with the error in errors[]. |
| 413 | Payload Too Large | An uploaded file or batch is over the limit — for example a document sent to POST /receive (FILE_TOO_LARGE), too many files in POST /receive/bulk (TOO_MANY_FILES), or an oversized POST /send/bulk or POST /mx/inbound/cfdi upload. Split the upload. |
| 415 | Unsupported Media Type | The uploaded file is not a supported document type (POST /receive). Send a supported XML or PDF file. |
| 429 | Too Many Requests | This API key has used up its per-minute budget for the endpoint class, or a bulk upload does not fit the remaining rows budget. Nothing was processed. Wait Retry-After seconds and retry — see Rate limits. |
| 500 | Internal Server Error | Unexpected Clearvo error. The response includes a requestId — include this when contacting support. |
| 503 | Service Unavailable | On POST /send, INGEST_UNAVAILABLE means the document could not be stored at all. Nothing was recorded: retry with the same body and the same x-idempotency-key, after the Retry-After seconds. |
An errors[] entry with UNKNOWN_FIELD_BUYER_RENAMED (POST /send body, stored as a rejection with HTTP 200) or UNKNOWN_HEADER_BUYER_RENAMED (bulk CSV header row) means the request still carries a retired buyer* input — see E-Invoicing for the current customer* field names.
POST /send answers 200 for every document it stores. A validation failure, a document that needs information, a hold, a document not supported yet, and one an authority later rejects all come back as 200 with the outcome in the body: read status, reason and nextStep, and the error and errors[] on a rejection. status is one of ten values (RECEIVED means stored and not processed yet: poll GET /v1/status or wait for the invoice.status_changed webhook). An unroutable Peppol customer is WAITING_FOR_YOU with reason: UNROUTABLE. outcome is derived from status. ok comes from the reason alone: it is false for REJECTED, NEEDS_INFO, SETUP_NEEDED and HELD_PLATFORM_FAULT, true otherwise, and the same on every replay of the document (a replay carries the same error, errors[], fixUrl and nextStep). When enabled for your account, a temporary problem after the document is stored gives 200 with status: RECEIVED and a PROCESSING_DELAYED warning: do not resend it. Otherwise a temporary failure is a 503 with Retry-After: retry the same request with the same x-idempotency-key. See Clearance errors.Rate limits
Requests are counted against the API key that made them, in fixed 60-second windows, however many IP addresses the key is used from. Each endpoint class has its own budget, and sandbox and production keys are counted separately. The limits sit well above normal integration traffic — they exist to keep one runaway client from slowing everyone else down.
| Endpoint class | Endpoints | Limit per key |
|---|---|---|
| Bulk submission | POST /send/bulk, POST /receive/bulk | 30 requests/min |
| Single submission | POST /send, POST /receive | 120 requests/min |
| Tax Calculations | POST /tax/calculate, POST /tax/calculate/{id}/refund, POST /integrations/coupa/tax-callout | 600 requests/min |
| Heavy operations | GET /pt/saft, POST /invoices/resend-notification/batch, POST /mx/inbound/cfdi, POST /mx/inbound/poll | 60 requests/min |
| Everything else | Every other authenticated endpoint — reads, status polls, entity and registration management, webhook configuration | 300 requests/min |
POST /query and GET /export have their own per-key budgets, documented on those endpoints, and are not counted above. When they are exceeded they return RATE_LIMITED (HTTP 429) rather than the RATE_LIMIT_EXCEEDED code used by the per-key limits in the table above. GET /health is unauthenticated and not counted.
Bulk rows budget
On top of the request count, bulk uploads are budgeted by size. The synchronous POST /send/bulk transport (uploads under 25MB) charges each CSV data row, and POST /receive/bulk charges each document, against 20,000 rows per minute per key. An upload larger than what remains of that budget in the current window is rejected whole with a 429 — nothing in it is processed — so split the file or wait for the window to reset. Uploads of 25MB or more to POST /send/bulk (up to 200MB) are stored and processed asynchronously and are not row-budgeted.
Response headers
Every counted response, success or error, tells you where you stand:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute for this key on this endpoint class. |
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Reset | Unix epoch seconds at which the window resets. |
X-RateLimit-Scope | api-key — the budget the headers above describe. |
X-RateLimit-Rows-Limit, X-RateLimit-Rows-Remaining, X-RateLimit-Rows-Reset | The bulk rows budget — on POST /send/bulk (synchronous) and POST /receive/bulk only. |
Retry-After | On a 429 only: seconds to wait before retrying. |
Handling a 429
A rate-limited request is not processed. Wait Retry-After seconds, then retry. If you send in bursts, spread the calls across the minute, or use the bulk endpoints instead of many single submits. Every rate-limited response has the same body:
{
"ok": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded for this API key: 120 requests per minute on this endpoint class. Retry after 37 seconds.",
"retryable": true
},
"hint": "You've exceeded the rate limit for this API key. Spread requests out, honour Retry-After, or contact support@clearvo.io for higher limits."
}
A separate network-level protection against abusive traffic also exists, set well above the per-key limits. It returns the same body with X-RateLimit-Scope: ip; a legitimate integration will not encounter it. If your integration genuinely needs higher limits, contact support@clearvo.io.
Error object
Most error responses include ok: false and an error object with code and message, plus an optional hint. Two envelope shapes exist, so read defensively:
{ "ok": false, "error": { "code", "message", "retryable" }, "hint" }— the shape described below.{ "error": "message string", "hint": "…" }— a bare string inerror, used by several endpoints (for exampleGET /invoices, the upload endpoints and the invoice action endpoints). Some of these add a siblingcodefield (for example"code": "DUPLICATE_INVOICE_NUMBER"or"FILE_TOO_LARGE");POST /sendcan return either shape depending on the failure.
If error is a string, use the top-level code when present, otherwise the HTTP status.
| Field | Type | Description |
|---|---|---|
| ok | boolean | Always false on an error response. |
| error.code | string | Stable, machine-readable identifier — branch on this, never on message. |
| error.message | string | Human-readable explanation. |
| error.retryable | boolean, optional | Present only when meaningfully true or false — e.g. true on a 429. |
| hint | string, optional | One plain-language sentence on what to do about it. |
A specific endpoint may add its own fields on top of this base envelope — e.g. violations[] on a structural validation failure, or source/detail on an authority clearance rejection (both shown below).
{
"ok": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Request body validation failed",
"violations": [
{ "field": "lines[0].taxRate", "message": "taxRate is required and must be a number between 0 and 100" }
]
}
}
{
"status": "REJECTED",
"reason": "REJECTED",
"terminal": true,
"rejectedAt": "2024-01-15T10:47:12Z",
"error": {
"source": "SDI",
"code": "00102",
"message": "Partita IVA fornitore non trovata",
"detail": "Supplier tax ID 01234567890 is not registered with the Italian Revenue Agency"
}
}
{
"ok": false,
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred",
"requestId": "req_01J9XKZ4MFBVQH7PRTD8CNWY5"
}
}