Formats, limits, errors

Supported formats

Documents and images (text recognition, 16 formats): .pdf · .doc · .docx · .xls · .xlsx · .ppt · .pptx · .jpg · .jpeg · .png · .tiff · .tif · .heic · .txt · .rtf · .odt

Archives and containers (recursive unpacking, depth ≤ 3; 5 formats): .zip · .7z · .rar · .eml · .msg (Outlook emails are converted to .eml)

Files inside an archive whose extension isn’t on the list aren’t processed (they’re silently skipped during unpacking). Digital-signature files (.sig, .sign) and files without an extension aren’t processed.

Don’t see the format you need? Email hello@hotdoc.io — we’ll add it for free.

Limits

The values below are defaults; they may differ in your deployment/plan.

LimitValueScope
Total job size300 MiB (default, configurable)checked before and after each source
Upload size (POST /v1/jobs/upload and gRPC Upload)20 MiB (config grpc.maxRecvMsgBytes)per file/message
Size of a single downloaded source20 MiB (default)per file
Number of input files per job20 (default)sourceUrls
Number of unpacked files20 (default)after unpacking
Concurrent jobs per account5 (default)in-flight
Converter response size64 MiBper file
Converter timeout90 sper file
Source download timeout30 s, ≤ 3 redirectsper source
OCR retries3 attempts × 10 sper file
Archive unpacking depth≤ 3 (default)recursive
Maximum prompt size64 KiBexceeding it → 400 error
Request rate120 req/min per principal (burst 40), per-replicaAPI key or user

Error codes

The API uses standard HTTP codes. The code in the response body is the numeric gRPC code (for example, 8 = ResourceExhausted). On REST endpoints (except POST /v1/jobs/upload), the body is {"code": <int>, "message": "…", "details": [...]}. For POST /v1/jobs/upload, it’s {"code": <int>, "message": "…"}, with no details.

CodeWhen it happens
400bad request: prompt larger than 64 KiB, unknown neural.type, empty model/apiKey, missing ocr.provider / empty ocr.model / empty ocr.providerKey, invalid reasoningEffort, neural.chunkBudgetTokens out of the 160002000000 range, creating a key with expiresAt in the past, or idempotency key reused with different request parameters
401token missing or unrecognized (no Authorization header, or the Bearer prefix is missing)
403token or key is invalid, or the action is forbidden; this includes using an expired, revoked, or unverified key/token
404the job or key doesn’t belong to your account
429three causes (all gRPC ResourceExhausted): (1) free-plan quota exhausted — message: "job quota exceeded" plus a QuotaExceededDetail{ jobsUsed, jobsLimit }; (2) concurrent-job limit exceeded — message: "too many concurrent jobs" (no details); (3) rate limit exceeded — on gRPC/REST the message reads "<method> is rejected by grpc_ratelimit middleware, please retry later. rate limit exceeded", and on POST /v1/jobs/upload it’s message: "rate limit exceeded" (no details).
5xxinternal service error; retry with exponential backoff

To tell the three 429 cases apart: quota — by the presence of QuotaExceededDetail; concurrency — by the too many concurrent jobs substring; rate — by the grpc_ratelimit / rate limit exceeded substring.

Errors within a job (a single file failed to process) aren’t returned as an HTTP code — they arrive in the job result at the file level, and the job status becomes JOB_STATUS_PARTIAL (if at least one file got a successful model answer) or JOB_STATUS_FAILED (if none did).

OCR-stage problems surface as job-level markers (not create-time HTTP errors); how the job ends depends on the problem: Auth/config markers that fail the whole job: provider_auth_failed, provider_key_required, provider_required, model_required, unsupported_provider. Per-file permanent markers (those files fail, job may end JOB_STATUS_PARTIAL): too_many_pages, unsupported_request, file_too_large, unsupported_format. Transient backend issues are retried internally and don’t surface as job failures: rate_limited, ocr_backend_overloaded, ocr_backend_error, ocr_backend_unavailable, ocr_timeout. Create-time validation errors (missing ocr.provider, empty ocr.model, or empty ocr.providerKey) → 400 before processing begins.