API Reference
Docira REST API
Eleven endpoints. JSON in, JSON out. Sync, async, and streaming variants for every common workflow. Live OpenAPI: https://api.docira.io/openapi.json.
Authentication
Every request needs an API key in the X-API-Key header. Get one from the dashboard under API Keys. Keys are scoped to a single account and roll over without downtime.
X-API-Key: docira_live_sk_...
The /v1/health endpoint doesn't require auth.
Operation modes
Most parse endpoints take an operation_mode that controls what the VLM does on each page:
| Mode | What it does |
|---|---|
| ocr | Default. Returns Markdown faithful to the page layout. |
| extract | Schema-driven JSON output. Provide extraction_schema (a JSON schema) on /parse or /parse/stream. |
| hybrid | Returns both Markdown and the JSON extraction in one pass. |
Tier selection (fast / pro / premium) is decided automatically by the router on a per-page basis from the page's complexity score. Each page's response includes tier_used and provider so you can see what was picked.
Errors & rate limits
Conventional HTTP status codes. FastAPI returns errors as JSON {"detail": "..."} for top-level failures, or a structured validation list for schema errors.
| Code | Meaning |
|---|---|
| 400 | Bad request — malformed body, missing fields, unsupported file type |
| 401 | Missing or invalid API key (X-API-Key header) |
| 402 | Plan limit reached — upgrade or wait for the next billing cycle |
| 404 | Request ID not found OR belongs to another tenant |
| 413 | File exceeds max upload size (default 50 MB) |
| 422 | Document accepted but pipeline failed sanity checks (e.g., empty pages) |
| 429 | Rate limit exceeded — retry after the Retry-After header |
| 500 | Server error — request_id is included, please report |
| 503 | All providers in the requested tier are circuit-broken |
Rate limits scale with plan. Free: 10 req/min, 25 pages/day. Pro: 60 req/min, 10k pages/day. Scale: custom — talk to us.
/v1/parse/uploadParse an uploaded file
Multipart file upload. Runs the full pipeline (ingest → classify → route → VLM → verify → deliver) and returns the result synchronously. The most common entry point.
Request body
Multipart fields: file (required) — PDF, image, DOCX, PPTX, XLSX, HTML, MD operation_mode ocr | extract | hybrid default: ocr maintain_format bool — preserve heading/list shape default: true max_pages int — cap pages processed default: null enable_verification bool — re-route low-confidence pgs default: null (auto) dpi int — rasterization resolution default: null (auto)
Response
{
"id": "req_01JEXAMPLE...",
"status": "completed",
"document": {
"hash": "612b65b...",
"pages": 3,
"format": "pdf",
"cached": false,
"filename": "invoice.pdf",
"size_bytes": 200838
},
"pages": [
{
"page_number": 1,
"content_markdown": "# Heading\n\nBody text...",
"content_json": null,
"tier_used": "pro",
"provider": "anthropic",
"model": "claude-sonnet-4.5",
"confidence": {
"layout_score": 0.94,
"ocr_score": 0.91,
"verbatim_score": 0.92,
"table_score": null,
"mean_grade": "good",
"low_grade": "good"
},
"grounding": [{"text": "...", "bbox": {...}, "element_type": "paragraph"}],
"tokens": {"input_tokens": 1240, "output_tokens": 380},
"latency_ms": 2840,
"re_routed": false,
"re_route_result": null
}
],
"extracted": null,
"usage": {
"total_input_tokens": 12480,
"total_output_tokens": 3210,
"estimated_cost_usd": 0.018,
"pages_by_tier": {"fast": 2, "pro": 1}
},
"timing": {
"total_ms": 4210,
"classification_ms": 1120,
"routing_ms": 2,
"vlm_processing_ms": 2630,
"verification_ms": 458
},
"document_type": null,
"created_at": "2026-05-01T17:42:18Z"
}Examples
curl -X POST https://api.docira.io/v1/parse/upload \ -H "X-API-Key: $DOCIRA_API_KEY" \ -F "file=@invoice.pdf" \ -F "operation_mode=ocr"
import httpx, os
with open("invoice.pdf", "rb") as f:
r = httpx.post(
"https://api.docira.io/v1/parse/upload",
headers={"X-API-Key": os.environ["DOCIRA_API_KEY"]},
files={"file": ("invoice.pdf", f, "application/pdf")},
data={"operation_mode": "ocr"},
timeout=120.0,
)
r.raise_for_status()
print(r.json()["pages"][0]["content_markdown"])const fd = new FormData();
fd.append("file", fileBlob, "invoice.pdf");
fd.append("operation_mode", "ocr");
const res = await fetch("https://api.docira.io/v1/parse/upload", {
method: "POST",
headers: { "X-API-Key": process.env.DOCIRA_API_KEY },
body: fd,
});
const data = await res.json();
console.log(data.pages[0].content_markdown);/v1/parseParse from a URL
Same pipeline as /parse/upload, but Docira fetches the document for you. The URL must be publicly reachable and respond with a supported content-type.
Request body
{
"file_url": "https://example.com/report.pdf", // required
"operation_mode": "ocr", // default: "ocr"
"extraction_schema": null, // JSON schema for extract mode
"maintain_format": true,
"webhook_url": null, // POST result here when done
"max_pages": null
}Response
Identical shape to /v1/parse/upload.
Examples
curl -X POST https://api.docira.io/v1/parse \
-H "X-API-Key: $DOCIRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"file_url": "https://example.com/report.pdf",
"operation_mode": "ocr"
}'import httpx, os
r = httpx.post(
"https://api.docira.io/v1/parse",
headers={"X-API-Key": os.environ["DOCIRA_API_KEY"]},
json={
"file_url": "https://example.com/report.pdf",
"operation_mode": "ocr",
},
timeout=120.0,
)
print(r.json())/v1/parse/streamParse with live progress (SSE)
Same multipart upload as /parse/upload, but Docira streams pipeline events back as Server-Sent Events. Use this to render a progress UI without polling. The final event is always result.final carrying the full ParseResponse.
Request body
Multipart fields (same as /parse/upload, plus extraction_schema): file (required) operation_mode default: "ocr" maintain_format default: true max_pages default: null extraction_schema JSON schema for extract mode enable_verification default: null (auto) dpi default: null (auto)
Response
data: {"event":"ingest.start","page_count":3}
data: {"event":"classify.page","page":1,"complexity":0.42}
data: {"event":"route.page","page":1,"tier":"pro","provider":"anthropic"}
data: {"event":"vlm.page.done","page":1,"confidence":0.94}
data: {"event":"result.final","payload":{ ...full ParseResponse... }}Examples
const fd = new FormData();
fd.append("file", fileBlob, "doc.pdf");
const res = await fetch("https://api.docira.io/v1/parse/stream", {
method: "POST",
headers: { "X-API-Key": apiKey },
body: fd,
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buf = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop()!;
for (const line of lines) {
if (line.startsWith("data: ")) {
const evt = JSON.parse(line.slice(6));
console.log(evt.event, evt);
}
}
}import httpx, json, os
with open("doc.pdf", "rb") as f, httpx.stream(
"POST",
"https://api.docira.io/v1/parse/stream",
headers={"X-API-Key": os.environ["DOCIRA_API_KEY"]},
files={"file": ("doc.pdf", f, "application/pdf")},
timeout=None,
) as r:
for line in r.iter_lines():
if line.startswith("data: "):
evt = json.loads(line[6:])
print(evt["event"], evt)/v1/parse/structuredExtract tables, equations, and figures
Returns typed structured blocks (tables as cell grids, equations as LaTeX, figures with captions and bboxes) instead of free-form Markdown. Use this when downstream code needs to consume tables programmatically.
Request body
Multipart fields: file (required) max_pages int — cap pages processed default: null model string — extractor VLM default: "claude-sonnet-4-6"
Response
{
"document": {...},
"blocks": [
{ "type": "table", "table_id": "t1", "page": 2,
"cells": [{"row":0,"col":0,"text":"Q1","row_span":1,"col_span":1}, ...],
"bbox": [120, 540, 880, 720] },
{ "type": "equation", "equation_id": "eq1", "page": 4,
"latex": "\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}",
"bbox": [...] },
{ "type": "figure", "figure_id": "f1", "page": 3,
"caption": "Figure 1: ...", "bbox": [...] }
],
"warnings": [],
"usage": {...},
"timing": {...}
}Examples
curl -X POST https://api.docira.io/v1/parse/structured \ -H "X-API-Key: $DOCIRA_API_KEY" \ -F "file=@statement.pdf"
/v1/parse/estimateEstimate cost before parsing
Runs only the ingestion + classification stages (cheap) and returns the projected per-tier mix and dollar cost. Useful for previewing batch jobs.
Request body
Multipart fields: file (required) max_pages int — cap pages default: null
Response
{
"page_count": 42,
"tier_mix": { "fast": 28, "pro": 12, "premium": 2 },
"estimated_cost_usd": 0.094,
"doc_type": "textbook",
"applied_preset": { "tier_floor": "pro", "return_grounding": true }
}Examples
curl -X POST https://api.docira.io/v1/parse/estimate \ -H "X-API-Key: $DOCIRA_API_KEY" \ -F "file=@textbook.pdf"
/v1/compareCompare 1–4 providers side-by-side
Runs the same document through multiple providers in parallel. The providers field is a JSON-encoded array of {id, model} entries. Returns one merged payload tagged by provider+model so you can render a side-by-side compare. Powers the playground compare view.
Request body
Multipart fields:
file (required)
providers (required) JSON-encoded array of 1–4 entries:
[{"id":"aimlapi","model":"gpt-4o"},
{"id":"nvidia","model":"meta/llama-3.2-90b-vision-instruct"}]
operation_mode default: "ocr"
max_pages default: nullResponse
{
"compare_id": "cmp_b96f552bd249",
"filename": "doc.pdf",
"providers_requested": [{"id":"aimlapi","model":"gpt-4o"}, ...],
"runs": [
{
"provider_id": "aimlapi",
"model_id": "gpt-4o",
"request_id": "req_...",
"status": "completed",
"response": { ...full ParseResponse for this provider... }
}
]
}Examples
curl -X POST https://api.docira.io/v1/compare \
-H "X-API-Key: $DOCIRA_API_KEY" \
-F "file=@page.pdf" \
-F 'providers=[{"id":"aimlapi","model":"gpt-4o"},{"id":"nvidia","model":"meta/llama-3.2-90b-vision-instruct"}]'import httpx, json, os
with open("page.pdf", "rb") as f:
r = httpx.post(
"https://api.docira.io/v1/compare",
headers={"X-API-Key": os.environ["DOCIRA_API_KEY"]},
files={"file": ("page.pdf", f, "application/pdf")},
data={
"providers": json.dumps([
{"id": "aimlapi", "model": "gpt-4o"},
{"id": "nvidia", "model": "meta/llama-3.2-90b-vision-instruct"},
]),
},
timeout=240.0,
)
print(r.json()["runs"])/v1/parse/batchSubmit a batch (async)
Submit up to settings.max_batch_documents documents at once for background processing (currently 100). Each entry is a full ParseRequest. Returns a batch ID immediately. Poll /v1/batch/{batch_id} or set webhook_url for delivery.
Request body
{
"documents": [
{
"file_url": "https://example.com/doc1.pdf",
"operation_mode": "ocr",
"maintain_format": true
},
{
"file_url": "https://example.com/doc2.pdf",
"operation_mode": "extract",
"extraction_schema": { "type": "object", "properties": { ... } }
}
],
"webhook_url": "https://your.app/webhook",
"priority": "normal"
}Response
{
"batch_id": "batch_01JEXAMPLE...",
"status": "queued",
"item_count": 2
}Examples
import httpx, os
r = httpx.post(
"https://api.docira.io/v1/parse/batch",
headers={"X-API-Key": os.environ["DOCIRA_API_KEY"]},
json={
"documents": [
{"file_url": u, "operation_mode": "ocr"} for u in urls
],
"webhook_url": "https://your.app/webhook",
"priority": "normal",
},
)
batch_id = r.json()["batch_id"]/v1/parse/{request_id}Retrieve a parse result
Fetch a previously-completed (or in-progress) parse by ID. Returns 404 for both 'not found' and 'found but belongs to another user' to prevent existence leakage across tenants.
Response
Same shape as POST /v1/parse/upload, plus a status field while in progress.
Examples
curl https://api.docira.io/v1/parse/req_01JEXAMPLE \ -H "X-API-Key: $DOCIRA_API_KEY"
/v1/batch/{batch_id}Retrieve batch status
Returns counts (queued / in_progress / completed / failed) and per-item request IDs once each item starts processing.
Response
{
"batch_id": "batch_01JEXAMPLE",
"status": "in_progress",
"totals": { "queued": 0, "in_progress": 12, "completed": 88, "failed": 0 },
"items": [
{ "request_id": "req_...", "status": "completed", "file_url": "https://..." }
]
}Examples
curl https://api.docira.io/v1/batch/batch_01JEXAMPLE \ -H "X-API-Key: $DOCIRA_API_KEY"
/v1/providersList provider pool + circuit-breaker state
Operational endpoint. Shows which VLM providers are currently registered in the pool and their circuit-breaker state (CLOSED / HALF_OPEN / OPEN / IDLE). Requires authentication.
Response
{
"providers": [
{ "provider_id": "nvidia", "circuit_breaker_state": "IDLE", "healthy": true },
{ "provider_id": "together", "circuit_breaker_state": "IDLE", "healthy": true },
{ "provider_id": "aimlapi", "circuit_breaker_state": "CLOSED", "healthy": true }
]
}Examples
curl https://api.docira.io/v1/providers \ -H "X-API-Key: $DOCIRA_API_KEY"
/v1/healthHealth check
Liveness check used by Fly.io and uptime monitoring. No auth required.
Response
{
"status": "healthy",
"version": "0.1.0",
"uptime_seconds": 73821.4,
"providers": {
"groq": { "state": "CLOSED", "healthy": true },
"aimlapi": { "state": "CLOSED", "healthy": true }
}
}Examples
curl https://api.docira.io/v1/health
Need help?
Open an issue on GitHub, or email hello@docira.io. Security issues: security@docira.io.