Errors
Envelope, typed codes, rate limits, and versioning policy.
The API uses HTTP-native status codes so resp.ok and standard
handling work correctly. Failure bodies always follow one shape:
{
"error": {
"type": "validation_failed",
"code": "VALIDATION_FAILED",
"message": "Body failed schema validation.",
"field_errors": {
"name": ["Required"],
"email": ["Invalid email address"]
},
"request_id": "req_01H…"
}
}Branch on code, not on message — code is stable; message is
free-form and may improve over time.
Status code map
| Status | Family | Typical code |
|---|---|---|
400 | Bad request | Malformed JSON |
401 | Auth | UNAUTHORIZED / INVALID_API_KEY |
403 | Scope / platform | SCOPE_INSUFFICIENT / PLATFORM_UNSUPPORTED |
404 | Not found | RESOURCE_NOT_FOUND / LINE_NOT_FOUND |
409 | Business conflict | see below |
422 | Validation | VALIDATION_FAILED (with field_errors) |
429 | Rate limit | RATE_LIMITED (with Retry-After header) |
500 | Internal | INTERNAL_ERROR |
Typed 409 codes
Codes you can branch on to distinguish business-rule conflicts:
INVOICE_ALREADY_ISSUED— you tried to edit a frozen invoice; raise a credit note.INVOICE_NOT_ISSUED— you tried to credit-note an unissued invoice.SERIES_ENDED— the series' end date is final (it has passed, or a credit note already reversed part of it) and cannot be moved.OCCURRENCE_NOT_FOUND— no current occurrence (past horizon, paused).OCCURRENCE_SKIPPED— the occurrence was explicitly skipped.DELETE_BLOCKED_BY_BOOKINGS— a customer delete blocked because the accounting platform has entries against them.PLATFORM_NOT_CONFIGURED— the connection is missing a choice its platform requires of every record (an e-conomic agreement whose document defaults were never confirmed). Nothing about your request is wrong; an admin finishes the Configure step in the workspace. See Accounting platforms.DELETE_BLOCKED_BY_REFERENCES— a customer or product delete blocked because Invoia still holds the record: invoices, recurring series or credit notes for a customer; invoice lines, series lines or one-time lines for a product. Themessagenames which. Nothing was deleted on either side — the reference is permanent, so retrying will not help.
Platform-independent by contract
Codes do not vary by accounting platform. PLATFORM_UNSUPPORTED (403)
is the one refusal a platform difference produces, and
DINERO_API_ERROR (502) is the frozen name for "the accounting
platform behind your integration failed" whichever platform that is.
Accounting platforms has the full list.
request_id
Every response — success or failure — includes a request_id. Log it
alongside your integration's own trace id. If you open a support
ticket, the request_id lets us find your exact call in seconds.
Rate limiting
Every response carries three rate-limit headers:
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 287
X-RateLimit-Reset: 1717000000On 429, the response adds:
Retry-After: 12Two buckets:
- General — reads and writes that stay inside Invoia. A generous default.
- Platform-writes — a separate, stricter bucket for the endpoints
that write to the accounting platform (
issue,credit-note), whichever platform your key is on. This protects the expensive operations without throttling your reads.
Retry with exponential backoff on Retry-After. Do not retry on
400/401/403/404/422 — those will not succeed on retry with the same
payload.
Versioning
The URL major (/v1) is additive-only. Within a major, we may:
- Add new endpoints.
- Add new optional fields on request bodies.
- Add new fields on response bodies.
- Add new event types to webhooks.
- Add new error codes (branch defensively: an unknown
codeshould fall through to a generic handler).
We will NOT, within /v1:
- Remove endpoints.
- Remove or rename fields.
- Change the meaning of an existing
code. - Tighten validation on an existing endpoint in a way that would reject payloads that used to succeed.
A breaking change ships as /v2. Both majors are supported in parallel
during a deprecation window announced in the changelog.