{"components":{"parameters":{"IdempotencyKey":{"description":"Opaque client-generated key that makes a POST replay-safe. A retry with the SAME key +\nrequest replays the original response (with `Idempotency-Replayed: true`); the same key\nwith a DIFFERENT request → 409 `idempotency_key_reused`; a concurrent duplicate while the\nfirst is in flight → 409 `idempotency_in_progress`. Records expire after 24h. Zero-retention:\nonly content-free metadata is stored — an inline (sync) `convert` result is NOT replayable\n(a retry gets 409); use the async path (202 job) for replay-safe convert.\n","in":"header","name":"Idempotency-Key","schema":{"maxLength":255,"type":"string"}}},"responses":{"BadRequest":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The request was malformed."}},"schemas":{"ApiKey":{"additionalProperties":false,"properties":{"created_at":{"type":"string"},"key_id":{"type":"string"},"last4":{"type":"string"},"revoked_at":{"type":"string"},"scopes":{"items":{"type":"string"},"type":"array"},"tier":{"type":"string"}},"required":["key_id","last4","scopes","created_at"],"type":"object"},"ApiKeyCreated":{"additionalProperties":false,"properties":{"created_at":{"type":"string"},"key":{"description":"The full API key — returned ONCE, never retrievable again.","type":"string"},"key_id":{"type":"string"},"last4":{"type":"string"},"scopes":{"items":{"type":"string"},"type":"array"}},"required":["key","key_id","last4","scopes"],"type":"object"},"ApiKeyList":{"additionalProperties":false,"properties":{"keys":{"items":{"$ref":"#/components/schemas/ApiKey"},"type":"array"}},"required":["keys"],"type":"object"},"AsyncJob":{"additionalProperties":false,"properties":{"job_id":{"type":"string"},"links":{"$ref":"#/components/schemas/JobLinks"},"status":{"$ref":"#/components/schemas/JobStatusEnum"}},"required":["job_id","status","links"],"type":"object"},"ConvertMeta":{"additionalProperties":false,"properties":{"backend":{"description":"Conversion backend that served the request.","type":"string"},"model":{"description":"Model id that produced this result (e.g. `mineru-2.5`). With `auto`, the model the router chose.","example":"mineru-2.5","type":"string"},"pages":{"type":"integer"},"route_reason":{"description":"When `auto` chose the model, the reason tag (e.g. `scanned_or_photo`); empty for an explicit selection.","type":"string"},"sha256":{"description":"SHA-256 of the source bytes (integrity/idempotency; not stored).","type":"string"},"timing_ms":{"type":"integer"}},"required":["pages","sha256","timing_ms","backend"],"type":"object"},"ConvertPage":{"additionalProperties":false,"properties":{"index":{"description":"Zero-based page index (enables progressive page-by-page rendering).","type":"integer"},"markdown":{"description":"Markdown for this page.","type":"string"}},"required":["index","markdown"],"type":"object"},"ConvertResult":{"additionalProperties":false,"properties":{"markdown":{"description":"Full document Markdown.","type":"string"},"meta":{"$ref":"#/components/schemas/ConvertMeta"},"pages":{"items":{"$ref":"#/components/schemas/ConvertPage"},"type":"array"}},"required":["markdown","pages","meta"],"type":"object"},"CreateKeyRequest":{"additionalProperties":false,"properties":{"scopes":{"items":{"type":"string"},"type":"array"}},"type":"object"},"Error":{"additionalProperties":false,"description":"Canonical error envelope. Every error response uses this shape.","properties":{"code":{"description":"Machine-readable error code (stable enum). Each code maps to a stable HTTP status and\nCLI exit code; see https://docs.hexread.com/errors.\n","enum":["already_subscribed","api_not_available_on_tier","billing_unavailable","concurrency_limit","conversion_failed","cross_origin_blocked","disposable_email","document_too_complex","encrypted_pdf","exchange_failed","file_too_large","forbidden","forbidden_tier","idempotency_in_progress","idempotency_key_reused","internal_error","invalid_api_key","invalid_plan","invalid_request","invalid_signature","invalid_state","job_not_found","key_not_found","malformed_document","missing_token","model_not_available","no_billing_account","no_subscription","nonce_mismatch","not_found","oidc_error","org_has_members","page_too_large","quota_exceeded","rate_limited","reauth_required","result_consumed","signup_required","subscription_active","too_many_pages","trial_expired","unauthenticated","unsupported_media_type"],"example":"file_too_large","type":"string"},"doc_url":{"description":"Link to documentation for this error.","example":"https://docs.hexread.com/errors/file_too_large","format":"uri","type":"string"},"message":{"description":"Human-readable explanation (never contains document content).","example":"The uploaded file exceeds the size limit for your tier.","type":"string"},"request_id":{"description":"Correlates with the structured server log line.","example":"req_01J9Z3K7Q2","type":"string"},"type":{"description":"Stable error category (URI-style), e.g. `validation_error`.","example":"validation_error","type":"string"}},"required":["type","code","request_id"],"type":"object"},"HealthStatus":{"additionalProperties":false,"properties":{"deps":{"additionalProperties":{"enum":["up","down"],"type":"string"},"description":"Per-dependency status (present when wired).","example":{"objectstore":"up","postgres":"up","queue":"up","redis":"up"},"type":"object"},"status":{"enum":["ok","degraded","down"],"type":"string"}},"required":["status"],"type":"object"},"JobLinks":{"additionalProperties":false,"properties":{"events":{"type":"string"},"result":{"type":"string"},"self":{"type":"string"}},"type":"object"},"JobStatus":{"additionalProperties":false,"properties":{"backend":{"type":"string"},"error_code":{"type":"string"},"job_id":{"type":"string"},"pages":{"type":"integer"},"pages_done":{"type":"integer"},"status":{"$ref":"#/components/schemas/JobStatusEnum"}},"required":["job_id","status"],"type":"object"},"JobStatusEnum":{"enum":["queued","warming","processing","completed","failed"],"type":"string"},"ModelInfo":{"additionalProperties":false,"properties":{"attribution":{"example":"Powered by MinerU","type":"string"},"attribution_url":{"example":"https://github.com/opendatalab/MinerU","type":"string"},"id":{"example":"mineru-2.5","type":"string"},"label":{"example":"MinerU 2.5","type":"string"},"license":{"example":"Apache-2.0","type":"string"}},"required":["id","label","license","attribution","attribution_url"],"type":"object"},"UsageMeters":{"additionalProperties":false,"description":"Content-free current-window meters. No filenames, content, or PII.","properties":{"anonymous":{"type":"boolean"},"api_access":{"type":"boolean"},"backend_lane":{"type":"string"},"concurrency":{"additionalProperties":false,"properties":{"in_use":{"type":"integer"},"limit":{"type":"integer"}},"required":["in_use","limit"],"type":"object"},"pages":{"additionalProperties":false,"description":"Whole-page usage for the current window. Metering is flat — 1 physical page = 1 page (an A3, an image, a blank, or a dense page all count as one). Values are integers.","properties":{"allowance":{"type":"integer"},"remaining":{"type":"integer"},"reserved":{"type":"integer"},"used":{"type":"integer"}},"required":["used","reserved","remaining","allowance"],"type":"object"},"period":{"description":"The calendar month \"YYYY-MM\" the monthly allowance is metered in (empty for the lifetime trial).","type":"string"},"resets_at":{"description":"When the monthly page allowance resets (RFC3339; omitted for the lifetime trial).","type":"string"},"source":{"description":"Which allowance window applies — the lifetime free trial or a paid plan's monthly period.","enum":["trial","period"],"type":"string"},"tier":{"type":"string"},"unlock_url":{"description":"Where a blocked anonymous caller creates a free account (anon only).","type":"string"}},"required":["tier","anonymous","period","pages","concurrency","backend_lane","api_access"],"type":"object"},"VersionInfo":{"additionalProperties":false,"properties":{"commit":{"example":"none","type":"string"},"date":{"example":"unknown","type":"string"},"models":{"description":"The selectable parser registry (id, label, attribution, license).","items":{"$ref":"#/components/schemas/ModelInfo"},"type":"array"},"service":{"example":"bff","type":"string"},"version":{"example":"dev","type":"string"}},"required":["service","version"],"type":"object"}},"securitySchemes":{"bearerAuth":{"description":"HexRead API key: `Authorization: Bearer hr_live_\u003cid\u003e_\u003csecret\u003e`. The credential for\nprogrammatic access — convert, jobs, keys, and usage.\n","scheme":"bearer","type":"http"}}},"info":{"contact":{"name":"HexWorld Solutions GmbH"},"description":"HexRead converts PDFs and images into clean, LLM-ready Markdown (and structured\nJSON), privacy-first and zero-retention, EU-hosted.\n\nAuthenticate with an API key: `Authorization: Bearer hr_live_...`. Convert a small\ndocument synchronously with `POST /convert`; larger documents are processed\nasynchronously — poll or stream the job, then fetch the read-once result.\n","license":{"name":"Proprietary"},"title":"HexRead API","version":"0.1.0"},"openapi":"3.0.3","paths":{"/convert":{"post":{"description":"Uploads a single document and returns Markdown + page-grouped content inline.\nThis endpoint is synchronous and size-gated (≤10 pages / ≤8 MB); larger documents return an\nerror pointing to the async path. The source is streamed in memory and never\nwritten to durable storage.\n","operationId":"convert","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"content":{"multipart/form-data":{"schema":{"properties":{"delivery":{"description":"Async result delivery. `webhook` POSTs a signed `job.completed`/`job.failed`\nevent (see the Webhooks guide) to `webhook` on completion instead of buffering\nthe result for polling.\n","enum":["inline","sse","webhook"],"type":"string"},"file":{"description":"The PDF or image to convert.","format":"binary","type":"string"},"formats":{"description":"Comma-separated output formats (`md`, `json`). Default `md,json`.","type":"string"},"lang":{"description":"OCR language hint (optional).","type":"string"},"model":{"description":"Parser to use. `auto` (recommended) routes by document content; or pick an\nexplicit model (`mineru-2.5`, `granite-docling`, `paddleocr-vl`). Omit for the\ndefault (`mineru-2.5`).\n","enum":["auto","mineru-2.5","granite-docling","paddleocr-vl"],"type":"string"},"prefer":{"description":"Force the sync or async path (else the server decides by size/pages).","enum":["sync","async"],"type":"string"},"webhook":{"description":"Publicly reachable HTTPS endpoint for `delivery=webhook` callbacks.","format":"uri","type":"string"},"webhook_secret":{"description":"Per-subscription HMAC signing secret for the webhook payload.","type":"string"}},"required":["file"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConvertResult"}}},"description":"Conversion result (inline, small documents)."},"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AsyncJob"}}},"description":"Accepted for async processing (large documents)."},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Quota exhausted for the period (`quota_exceeded`)."},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden for the caller's tier or sign-up state: `signup_required` (anonymous or\nemail-unverified — create a free account to convert), `forbidden_tier`,\n`model_not_available`, or `api_not_available_on_tier`.\n"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Idempotency-Key conflict (`idempotency_key_reused` or `idempotency_in_progress`)."},"413":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"File exceeds the maximum upload size."},"415":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unsupported media type (not a PDF or supported image)."},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Document exceeds the per-request page limit (`too_many_pages`)."},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit or concurrency slot limit hit (`rate_limited`, `concurrency_limit`)."},"502":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Conversion failed (`conversion_failed`)."}},"summary":"Convert a PDF or image to Markdown (synchronous, small docs)","tags":["convert"]}},"/health":{"get":{"operationId":"getHealth","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthStatus"}}},"description":"Service healthy (or degraded with per-dependency detail)."},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthStatus"}}},"description":"Service unhealthy."}},"security":[],"summary":"Liveness/readiness of the BFF and its dependencies","tags":["meta"]}},"/jobs/{id}":{"delete":{"operationId":"cancelJob","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Cancelled; the held reservation + concurrency slot are released (no charge)."},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Programmatic access not available on the caller's tier (`api_not_available_on_tier`)."},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown job, already terminal, or not owned by the caller."}},"summary":"Cancel a queued/in-flight job — releases the reservation + slot, purges the source","tags":["jobs"]},"get":{"operationId":"getJob","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatus"}}},"description":"Job status."},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Authentication required — anonymous callers cannot read job status (`unauthenticated`)."},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Programmatic access not available on the caller's tier (`api_not_available_on_tier`)."},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown job."}},"summary":"Async job status (metadata only — no document content)","tags":["jobs"]}},"/jobs/{id}/events":{"get":{"operationId":"getJobEvents","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"text/event-stream":{"schema":{"type":"string"}}},"description":"SSE stream of progress events. The stream itself returns 200 even for an unknown/expired\njob — that case is delivered as an in-stream `error` event carrying `job_not_found`, not\nan HTTP status.\n"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Authentication required — anonymous callers cannot open the progress stream (`unauthenticated`)."},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Programmatic access not available on the caller's tier (`api_not_available_on_tier`)."}},"summary":"Server-sent progress stream (queued/warming/page/completed/error)","tags":["jobs"]}},"/jobs/{id}/json":{"get":{"operationId":"getJobResultJSON","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"One-time download token from the SSE `completed` event.","in":"query","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConvertResult"}}},"description":"The conversion result as JSON (purged after this read)."},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The required one-time download `token` is missing (`missing_token`)."},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Programmatic access not available on the caller's tier (`api_not_available_on_tier`)."},"410":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Result already retrieved or expired (read-once)."}},"security":[],"summary":"Read-once result as JSON (alias of /result)","tags":["jobs"]}},"/jobs/{id}/markdown":{"get":{"operationId":"getJobResultMarkdown","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"One-time download token from the SSE `completed` event.","in":"query","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"text/markdown":{"schema":{"type":"string"}}},"description":"The conversion result as Markdown (purged after this read)."},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The required one-time download `token` is missing (`missing_token`)."},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Programmatic access not available on the caller's tier (`api_not_available_on_tier`)."},"410":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Result already retrieved or expired (read-once)."}},"security":[],"summary":"Read-once result as Markdown (alias of /result)","tags":["jobs"]}},"/jobs/{id}/result":{"get":{"operationId":"getJobResult","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"One-time download token from the SSE `completed` event.","in":"query","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConvertResult"}},"text/markdown":{"schema":{"type":"string"}}},"description":"The conversion result (purged after this read)."},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"The required one-time download `token` is missing (`missing_token`)."},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Programmatic access not available on the caller's tier (`api_not_available_on_tier`)."},"410":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Result already retrieved or expired (read-once)."}},"security":[],"summary":"Read-once result — succeeds once, then 410 Gone (zero-retention)","tags":["jobs"]}},"/keys":{"get":{"operationId":"listKeys","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyList"}}},"description":"The caller's keys (masked — never the secret)."}},"summary":"List the caller's API keys (masked)","tags":["keys"]},"post":{"operationId":"createKey","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyRequest"}}}},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreated"}}},"description":"The created key, including the full secret (shown once)."},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not allowed (requires a paid-plan session or `keys:manage`)."}},"summary":"Create an API key (the full secret is returned ONCE)","tags":["keys"]}},"/keys/{key_id}":{"delete":{"operationId":"revokeKey","parameters":[{"in":"path","name":"key_id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Revoked."},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Unknown key."}},"summary":"Revoke an API key (effective immediately)","tags":["keys"]}},"/keys/{key_id}/rotate":{"post":{"operationId":"rotateKey","parameters":[{"in":"path","name":"key_id","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreated"}}},"description":"The new key (the old one is revoked)."}},"summary":"Rotate an API key — revokes the old, returns a new secret once","tags":["keys"]}},"/openapi.json":{"get":{"operationId":"getOpenAPI","responses":{"200":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object"}}},"description":"The OpenAPI specification."}},"security":[],"summary":"This OpenAPI document, as JSON","tags":["meta"]}},"/usage":{"get":{"operationId":"getUsage","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageMeters"}}},"description":"The caller's meters. Anonymous callers get a trimmed view (zeroed page usage) with a sign-up unlock URL."}},"security":[{"bearerAuth":[]},{}],"summary":"Current-period quota \u0026 concurrency meters (content-free)","tags":["usage"]}},"/version":{"get":{"operationId":"getVersion","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionInfo"}}},"description":"Version info."}},"security":[],"summary":"Service version and the selectable model registry","tags":["meta"]}}},"security":[{"bearerAuth":[]}],"servers":[{"description":"Production","url":"https://api.hexread.com/v1"}],"tags":[{"description":"Service health, version, and this API document.","name":"meta"},{"description":"Convert a PDF or image to Markdown (and structured JSON). Small documents convert synchronously; larger ones are processed asynchronously as a job.","name":"convert"},{"description":"Async job status, live progress (SSE), and read-once result retrieval.","name":"jobs"},{"description":"API-key management — create, list, rotate, and revoke keys.","name":"keys"},{"description":"Current-period quota and concurrency meters (content-free).","name":"usage"}]}
