Error Handling¶
All API errors return a consistent JSON envelope with an error object.
Error Response Format¶
{
"error": {
"status": 404,
"code": "not_found",
"message": "Document not found",
"request_id": "38f24dafcf099a38a8eb287afef6b3c0"
}
}
| Field | Type | Description |
|---|---|---|
status |
integer | HTTP status code, mirroring the response status line. Included so a logged or forwarded body is self-describing |
code |
string | Machine-readable error code (see table below) |
message |
string | Human-readable description |
request_id |
string | Trace id for this request, also returned as the X-Trace-Id response header. Quote it in support requests — it identifies every log line for the request across our services. |
details |
object | Additional context (only present when the error carries extra data) |
The details key is omitted entirely when there is no extra context.
HTTP Status Codes¶
| Status | Meaning | When |
|---|---|---|
400 |
Bad Request | Invalid JSON or a malformed request the server rejects |
401 |
Unauthorized | Missing or invalid API key / bearer token |
403 |
Forbidden | Account disabled or insufficient permissions |
404 |
Not Found | Resource does not exist or belongs to another account |
409 |
Conflict | State transition not allowed (e.g. rotating an already-revoked key) |
422 |
Unprocessable Entity | Request body failed schema validation, or the payload is semantically invalid |
429 |
Too Many Requests | Rate limit exceeded or monthly render quota exceeded |
500 |
Internal Server Error | Unexpected server error |
501 |
Not Implemented | A backend the request needs is not configured (e.g. auth or billing) |
Error Codes¶
Every error envelope carries one of the following machine-readable code values. This is the complete set.
| Code | Status | Description |
|---|---|---|
bad_request |
400 | Malformed request |
unauthorized |
401 | Missing or invalid credentials |
forbidden |
403 | Valid credentials but the account is disabled or not permitted |
not_found |
404 | The requested resource does not exist or belongs to another account |
conflict |
409 | The request conflicts with current state — a duplicate resource, a disallowed state transition, or an Idempotency-Key reused with a different request body |
unprocessable_entity |
422 | Request body failed schema validation, or rendering/processing rejected the payload |
rate_limited |
429 | Too many requests — retry after the delay in Retry-After |
quota_exceeded |
429 | Monthly render quota has been exceeded |
not_configured |
501 | A required backend (auth, billing, etc.) is not configured on this deployment |
invalid_base_template |
422 | The referenced base template is not valid |
internal_error |
500 | Unexpected server error |
Validation Errors¶
Request bodies that fail schema validation return HTTP 422 in the same error envelope as every other error. Failing fields appear under details.fields, each with a dotted path:
{
"error": {
"code": "unprocessable_entity",
"message": "Request validation failed",
"status": 422,
"request_id": "38f24dafcf099a38a8eb287afef6b3c0",
"details": {
"fields": [
{
"loc": "body.data.line_items.0.unit_price",
"msg": "Field required",
"type": "missing"
}
]
}
}
}
Every error carries extra context under details, for example a quota error:
{
"error": {
"status": 429,
"code": "quota_exceeded",
"message": "Monthly render quota exceeded",
"request_id": "38f24dafcf099a38a8eb287afef6b3c0",
"details": { "monthly_quota": 100, "used": 100 }
}
}
Rate Limit Headers¶
| Header | Description |
|---|---|
Retry-After |
Seconds to wait before retrying (sent on a 429 rate-limit response) |
X-RateLimit-Remaining |
Requests remaining in the current window (sent on successful responses) |
A rate-limited response uses the standard envelope with code rate_limited:
{
"error": {
"status": 429,
"code": "rate_limited",
"message": "Too many requests. Please slow down.",
"request_id": "38f24dafcf099a38a8eb287afef6b3c0"
}
}
Handling Errors in Code¶
import httpx
response = httpx.post(
"https://invoicepdfs.com/api/v1/documents/render",
headers={"Authorization": "Bearer inv_live_..."},
json=render_request,
)
if response.status_code >= 400:
body = response.json()
if "error" in body:
error = body["error"]
print(f"Error {error['code']}: {error['message']}")
if error.get("details"):
print(f" context: {error['details']}")
else:
# Schema validation failures use FastAPI's default `detail` array
print(f"Validation error: {body.get('detail')}")
# Check the HTTP status code
HTTP_CODE=$(curl -s -o response.json -w "%{http_code}" \
-X POST https://invoicepdfs.com/api/v1/documents/render \
-H "Authorization: Bearer inv_live_..." \
-H "Content-Type: application/json" \
-d @request.json)
if [ "$HTTP_CODE" -ge 400 ]; then
echo "Error response:"
cat response.json | jq '.error'
fi
const response = await fetch(
"https://invoicepdfs.com/api/v1/documents/render",
{
method: "POST",
headers: {
Authorization: "Bearer inv_live_...",
"Content-Type": "application/json",
},
body: JSON.stringify(renderRequest),
}
);
if (!response.ok) {
const { error } = await response.json();
console.error(`Error ${error.code}: ${error.message}`);
}