docimprint

← All docs

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.

CodeMeaningWhenActionRetry?
400Bad RequestMissing or invalid field in the request body.Fix the request — do not retry unchanged.
402Payment Requiredx402: first call before payment. API key: quota exhausted.x402: sign and retry with X-Payment. API key: upgrade plan.
403ForbiddenAPI key is revoked or suspended.Check key status. Contact support if unexpected.
409ConflictBundle verification detected tampered artifacts.Do not retry — the bundle has been modified.
410GoneRetired route called (e.g. POST /v1/screenshot).Use POST /v1/extract with the equivalent include or mode param.
413Payload Too LargeUploaded file exceeds the size limit.Compress the file or use a hosted source URL instead.
422UnprocessableDocument could not be parsed — password-protected, corrupt, or unreadable.Verify the file is readable. Remove password protection.
429Too Many RequestsRate limit exceeded (120 req/min on all plans).Wait for the Retry-After duration, then retry.
500Server ErrorUnexpected gateway error.Retry with exponential backoff. Never charged.
503Service UnavailableGateway temporarily unavailable.Retry with backoff.

400 Bad Request

json400 response
# 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 format

402 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.

bash402 quota response + quota check
# 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.

bash429 rate limit response
# 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

json422 response
# 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 encoding

Retry with backoff

Retry 429 and 5xx responses using exponential backoff. Never retry 4xx without changing the request.

pythonPython — retry helper
# 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")
typescriptTypeScript — retry helper
// 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')
}

Related