On this pageErrors & retries
Errors & retries
Aly Sawft · Founder & Engineer, Sawftware LLC ·
What does HTTP 402 mean on DocImprint?
402 Payment Required — either x402 USDC payment needed (retry with X-Payment header) or API key quota exceeded (upgrade plan or switch to x402). Response body includes structured error code and quota details.
Errors & retries
You are only charged on successful 2xx responses. All 4xx and 5xx errors are never billed. Retrying 5xx and 429 with backoff is always safe.
| Code | Meaning | When | Action | Retry? |
|---|---|---|---|---|
400 | Bad Request | Missing or invalid field in the request body. | Fix the request — do not retry unchanged. | — |
402 | Payment Required | x402: first call before payment. API key: quota exhausted. | x402: sign and retry with X-Payment. API key: upgrade plan. | — |
403 | Forbidden | API key is revoked or suspended. | Check key status. Contact support if unexpected. | — |
409 | Conflict | Bundle verification detected tampered artifacts. | Do not retry — the bundle has been modified. | — |
410 | Gone | Retired route called (e.g. POST /v1/screenshot). | Use POST /v1/extract with the equivalent include or mode param. | — |
413 | Payload Too Large | Uploaded file exceeds the size limit. | Compress the file or use a hosted source URL instead. | — |
422 | Unprocessable | Document could not be parsed — password-protected, corrupt, or unreadable. | Verify the file is readable. Remove password protection. | — |
429 | Too Many Requests | Rate limit exceeded (120 req/min on all plans). | Wait for the Retry-After duration, then retry. | ✓ |
500 | Server Error | Unexpected gateway error. | Retry with exponential backoff. Never charged. | ✓ |
503 | Service Unavailable | Gateway temporarily unavailable. | Retry with backoff. | ✓ |
400 Bad Request
# 400 Bad Request — fix the request before retrying
{
"error": "Missing required field: source"
}
# Common causes:
# - source or file body missing from extract request
# - Unsupported mode value
# - Malformed JSON body
# - Invalid bundle_id format402 Quota exhausted (API key)
Returned when your monthly credits are used up. Check remaining credits at any time with GET /v1/quota — that call is always free.
# 402 — API key quota exhausted
{
"error": "Monthly quota exhausted",
"quota": { "used": 100, "limit": 100, "remaining": 0 },
"upgrade_url": "https://docimprint.com/pricing"
}
# Check remaining credits at any time (free call):
curl https://api.docimprint.com/v1/quota \
-H "Authorization: Bearer dr_live_..."429 Rate limit
All plans are limited to 120 requests per minute. The response includes a Retry-After header with the number of seconds to wait before retrying.
# 429 Too Many Requests — rate limit exceeded (120 req/min)
# Response includes Retry-After header with seconds to wait
HTTP/1.1 429 Too Many Requests
Retry-After: 8
Content-Type: application/json
{
"error": "Rate limit exceeded",
"retry_after": 8
}
# Retry after the indicated delay. Both Free and Pro plans share the 120 RPM limit.422 Unprocessable
# 422 Unprocessable — document could not be parsed
{
"error": "OCR extraction failed: document appears to be a scanned image with no readable text"
}
# Common causes:
# - Password-protected PDF
# - Corrupt or truncated file
# - Image resolution too low for OCR
# - Unsupported file encodingRetry with backoff
Retry 429 and 5xx responses using exponential backoff. Never retry 4xx without changing the request.
# Python — error handling with exponential backoff
import time
import httpx
def extract_with_retry(url: str, headers: dict, body: bytes, max_retries=3):
for attempt in range(max_retries):
r = httpx.post(url, content=body, headers=headers)
if r.status_code == 200:
return r.json()
if r.status_code == 429:
wait = int(r.headers.get("Retry-After", 2 ** attempt))
time.sleep(wait)
continue
if r.status_code >= 500:
time.sleep(2 ** attempt) # 1s, 2s, 4s
continue
# 4xx (except 429) — don't retry, fix the request
r.raise_for_status()
raise RuntimeError("Max retries exceeded")// TypeScript — retry on 5xx and 429
async function extractWithRetry(url: string, init: RequestInit, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const res = await fetch(url, init)
if (res.ok) return res.json()
if (res.status === 429) {
const wait = Number(res.headers.get('Retry-After') ?? 2 ** attempt) * 1000
await new Promise(r => setTimeout(r, wait))
continue
}
if (res.status >= 500) {
await new Promise(r => setTimeout(r, 2 ** attempt * 1000))
continue
}
// 4xx — throw immediately
const body = await res.json().catch(() => ({}))
throw Object.assign(new Error(body.error ?? res.statusText), { status: res.status, body })
}
throw new Error('Max retries exceeded')
}