Errors, limits and pagination
The Invoa API error format and codes, the 600 requests a minute rate limit and its headers, offset pagination, and safe retries with idempotency keys.
Every Invoa API error has the same JSON shape and a standard HTTP status code. This page lists the codes, the rate limit, how list endpoints page, and how to retry safely.
Error format
{
"error": {
"code": "validation",
"message": "Invalid request body.",
"details": [
{ "path": "lines.0.unitPrice", "message": "Invalid input: expected number, received string" }
]
}
}| Field | Type | Description |
|---|---|---|
error.code | string | Machine-readable code from the table below. Branch on this, not on message. |
error.message | string | Human-readable explanation. May change wording. |
error.details | array or object | Present on some errors. For validation errors, a list of { path, message } pointing at each invalid field. |
Status codes and error codes
| Status | code | When it happens |
|---|---|---|
| 400 | invalid_json | The body is not valid JSON. |
| 400 | validation | A field is missing or invalid. details lists each problem. Also returned by send when the contact has no email and you did not pass to. |
| 401 | unauthenticated | No API key in Authorization or x-api-key. |
| 401 | invalid_api_key | The key is unknown, malformed or revoked. |
| 403 | insufficient_scope | The key lacks the scope this endpoint needs. details.required_scope names it. |
| 403 | plan_limit | The company is on the Free plan; the API is part of Core and Pro. details has plan, requiredPlan and upgradeUrl. |
| 403 | forbidden | The company may not use this option, for example a PDF template not on its plan. |
| 404 | not_found | The ID does not exist in this company. |
| 409 | conflict | The record clashes with an existing one, such as an item sku already in use, or a concurrent change got there first. Fetch the latest state and try again. |
| 409 | invalid_state | Only from GET /documents/{id}/pdf when the document is still a draft. |
| 422 | invalid_state | The action does not fit the record's current state, for example issuing a document that is not a draft, sending a draft, voiding an accepted or converted quotation, recording a payment on a void or fully paid invoice, triggering a milestone that is already invoiced, or a date inside a period locked by your accountant. |
| 422 | plan_limit | The plan does not allow this: the monthly invoice allowance is used up (quotations, proformas, delivery orders and receipts are not counted; details has used, limit and plan), or the feature needs Pro (details has feature, requiredPlan, trialAvailable and upgradeUrl). |
| 422 | einvoice_window_closed | Voiding a validated e-invoice more than 72 hours after validation. Issue a credit note instead. |
| 429 | rate_limited | Too many requests. See Rate limits. |
| 500 | internal_error | Something failed on our side. Retry later, and quote the x-request-id if you contact support. |
Successful requests return 200, 201 (created) or 202 (accepted for background processing). DELETE returns 204 with no body.
Rate limits
Each API key can make 600 requests per minute. The window resets at the start of each minute.
Every authenticated response carries these headers:
| Header | Meaning |
|---|---|
x-ratelimit-limit | Requests allowed per minute (600). |
x-ratelimit-remaining | Requests left in the current minute. |
x-ratelimit-reset | When the current window ends, as a Unix timestamp in seconds. |
When you go over the limit, Invoa answers 429 rate_limited with details.reset:
{
"error": {
"code": "rate_limited",
"message": "Too many requests; slow down.",
"details": { "limit": 600, "reset": 1760494860 }
}
}Wait until the reset time before sending more requests.
Pagination
List endpoints use offset pagination.
| Parameter | Type | Description |
|---|---|---|
limit | integer | Items per page, 1 to 200. Default 50 (100 for GET /items). |
offset | integer | How many items to skip. Default 0. |
curl "https://app.invoa.io/api/v1/documents?type=invoice&limit=50&offset=50" \
-H "Authorization: Bearer $INVOA_API_KEY"{
"data": [],
"pagination": { "total": 123, "limit": 50, "offset": 50, "has_more": true }
}| Field | Description |
|---|---|
pagination.total | Total items matching your filters. |
pagination.limit | The limit applied. |
pagination.offset | The offset applied. |
pagination.has_more | true if there are more items after this page. Request the next page with offset + limit. |
GET /webhooks is the one list that is not paginated.
Idempotency and safe retries
Invoa does not read an Idempotency-Key header. Retry safety depends on the endpoint:
| Request | Safe to retry? |
|---|---|
Any GET | Yes. |
POST /documents with idempotencyKey | Yes. A retry with the same key returns the document already created for that key, instead of creating a second draft. Keys are unique per company and never expire. |
POST /documents/{id}/issue, /void | Yes in effect. A repeat fails with 422 invalid_state because the document is already issued or void. Fetch the document to confirm the outcome. |
POST /schedules/items/{id}/trigger | Yes in effect. Each milestone creates one invoice; a repeat fails with 422 invalid_state. |
POST /einvoice/{documentId}/submit | Yes in effect. A repeat fails with 422 invalid_state once the submission is queued. |
POST /payments | No. A retry records a second payment. Before retrying, check GET /payments?documentId=…. |
POST /documents/{id}/send | No. A retry emails the customer again. |
POST /items with a sku | Yes in effect. A repeat fails with 409 conflict because the SKU is taken. |
POST /contacts, POST /items without a sku, POST /schedules, POST /webhooks | No. A retry creates a duplicate. Search first. |
Use an idempotencyKey that comes from your own system, such as shop-order-8812 or booking-1001-deposit. It must be 8 to 128 characters.
For 429 and 5xx responses, retry with exponential backoff, but only for requests that are safe to repeat.
Related
Something unclear or out of date? Tell us at hello@invoa.io and we'll fix the page.