Skip to content

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}`);
}