Loading...
Loading...
OpenOCR automatically classifies each page of a PDF — digital text or scanned image — and routes it to the engine best suited for it. On engines without native PDF support, requests for more than 20 pages are dispatched as background jobs (see below). Engines that read PDFs natively can process more than 20 pages in a single synchronous request.
Not all PDF pages are the same. A single document might contain:
Smart routing inspects every page before processing, picks the cheapest viable path, and falls back gracefully when the primary engine can't handle a page.
When you send a PDF the router runs a lightweight classifier before OCR:
digital.scanned.mixed and processed by a full OCR engine.Classification metadata is returned in the API response under pdf_classification.
{
"request_id": "req_abc123",
"status": "succeeded",
"extracted_text": "Invoice #4521\n\n---\n\nThank you for your business.",
"pdf_classification": {
"total_pages": 2,
"digital_pages": 1,
"scanned_pages": 1,
"mixed_pages": 0,
"primary_type": "mixed"
}
}PDF routing options are top-level request fields (alongside engine and input), except dpi, which lives under settings.
| Parameter | Type | Default | Description |
|---|---|---|---|
| page_count | integer | 1 | Pages to process, up to the engine's limit. Only page 1 is processed unless you set it. |
| scanned_handling | string | skip (fallback when fallback_engine is set) | What to do with scanned (image-only) pages: skip, fallback or error. See below. |
| fallback_engine | string | none | Engine ID (typically a vision model) used for scanned pages when scanned_handling is fallback. |
| settings.dpi | integer | engine default | Resolution hint, 72 to 1200. Mapped to Tesseract --dpi and OCRmyPDF --image-dpi. |
When the engine does not read PDFs natively and you request more than 20 pages (page_count above 20; without it only page 1 is requested and no job starts), the router dispatches a background job without you asking for async. The POST returns 202 with status: "processing", a warning of the form job_id: job_..., and a poll_url of /v1/ocr/jobs/{job_id}/status, not /v1/ocr/{request_id}. That status endpoint reports progress only; once it shows the job is done, fetch the text from GET /v1/ocr/jobs/{job_id}/result. While the job is still running, /result answers HTTP 202 with {"error":{"code":"processing",...}}, not a result with a status field.
# Progress (job_id, status, total_pages, pages_completed, current_page_type,
# estimated_seconds_remaining, progress_pct)
curl https://api.open-ocr.com/v1/ocr/jobs/job_abc123/status \
-H "Authorization: Bearer sk-ocr-your-api-key"
# Result (job_id, request_id, status, engine, total_pages, extracted_text, error,
# plus pages / pages_total / warnings). While the job runs it answers HTTP 202 with
# {"error":{"code":"processing","message":"Job is still processing. ..."}}.
curl https://api.open-ocr.com/v1/ocr/jobs/job_abc123/result \
-H "Authorization: Bearer sk-ocr-your-api-key"scanned_handling: "fallback"
Scanned (image-only) pages that the primary engine cannot read are sent to fallback_engine. Requires fallback_engine to be set. Best for maximum coverage; slightly higher cost when the fallback is used.
scanned_handling: "skip" (default without fallback_engine)
Scanned pages are omitted from the result. When the request ran with PDF classification, the response's pdf_classification block reports skipped_pages and marks each skipped page with skipped: true. (stopped_at_page and stop_reason are not set for skipped pages; they describe stopping early for credit exhaustion.) Good for batch pipelines where a few scanned pages are acceptable.
scanned_handling: "error"
If any scanned page is detected the request fails as a provider failure: the response has status: "refunded" (no charge), not an HTTP 422, and provider_error reads "PDF routing failed: document contains scanned pages but scanned_handling=Error". Use when you need complete, verified output and can retry the whole document.
For long PDFs, send mode: "async" together with page_count (without it only page 1 is processed). The API returns 202 Accepted immediately with a request_id and a poll_url. Async mode requires OCR history to be enabled on your account; with history off the request is rejected with 400 history_disabled, so use synchronous mode and save the response yourself.
mode: "async" and page_count.GET {poll_url} (that is /v1/ocr/{request_id}) until status is no longer queued or processing. Polling returns succeeded, partial, failed or rejected as terminal statuses (refunded and timeout appear only in the POST response; GET reports them as failed). For a partial result only extracted_text_preview is stored, so the full partial text is not available from polling. Stop on any terminal status.extracted_text, pages and cost_debited.# 1. Submit the PDF asynchronously, for the first 60 pages
curl -X POST https://api.open-ocr.com/v1/ocr \
-H "Authorization: Bearer sk-ocr-your-api-key" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"engine": "mistral-mistral-ocr-4-1",
"mode": "async",
"page_count": 60,
"input": { "type": "url", "url": "https://example.com/annual-report.pdf" }
}'
# 202 Accepted
# { "request_id": "req_abc123", "status": "processing", "poll_url": "/v1/ocr/req_abc123" }
# 2. Poll until status is neither "queued" nor "processing"
# (terminal: succeeded, partial, failed, rejected; for partial read extracted_text_preview)
curl https://api.open-ocr.com/v1/ocr/req_abc123 \
-H "Authorization: Bearer sk-ocr-your-api-key"import time
import base64
import uuid
import requests
API_KEY = "sk-ocr-your-api-key"
BASE_URL = "https://api.open-ocr.com"
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()), # required on POST /v1/ocr; new UUID per logical request
}
with open("large-document.pdf", "rb") as f:
pdf_b64 = base64.b64encode(f.read()).decode()
# Submit asynchronously. page_count is required to process more than page 1.
resp = requests.post(f"{BASE_URL}/v1/ocr", headers=headers, json={
"engine": "mistral-mistral-ocr-4-1",
"mode": "async",
"page_count": 60,
"input": {"type": "base64", "data_base64": pdf_b64, "mime_type": "application/pdf"},
})
resp.raise_for_status()
poll_url = resp.json()["poll_url"] # e.g. "/v1/ocr/req_abc123"
while True:
poll = requests.get(f"{BASE_URL}{poll_url}", headers=headers)
poll.raise_for_status() # an HTTP error body has no "status"; do not keep polling on it
result = poll.json()
# queued and processing are the only non-terminal statuses. GET returns succeeded, partial,
# failed or rejected as terminal.
if result["status"] not in ("queued", "processing"):
break
time.sleep(3)
if result["status"] == "partial":
# Only extracted_text_preview is stored for a partial result (extracted_text is null).
print("WARNING: partial result:", result.get("warning"))
text = result["extracted_text_preview"]
elif result["status"] != "succeeded":
raise RuntimeError(f"OCR ended as {result['status']}: {result.get('provider_error')}")
else:
text = result["extracted_text"]
print(text)const API_KEY = "sk-ocr-your-api-key";
const BASE_URL = "https://api.open-ocr.com";
async function ocrPdf(pdfBase64: string): Promise<string> {
const headers = {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(), // required on POST /v1/ocr
};
// Submit asynchronously. page_count is required to process more than page 1.
const submitRes = await fetch(`${BASE_URL}/v1/ocr`, {
method: "POST",
headers,
body: JSON.stringify({
engine: "mistral-mistral-ocr-4-1",
mode: "async",
page_count: 60,
input: { type: "base64", data_base64: pdfBase64, mime_type: "application/pdf" },
}),
});
// Async needs OCR history enabled; otherwise this is a 400 history_disabled with no poll_url.
if (!submitRes.ok) throw new Error(`submit failed: ${submitRes.status} ${await submitRes.text()}`);
const { poll_url } = await submitRes.json();
while (true) {
await new Promise((r) => setTimeout(r, 3000));
const res = await fetch(`${BASE_URL}${poll_url}`, { headers });
if (!res.ok) throw new Error(`poll failed: ${res.status} ${await res.text()}`);
const result = await res.json();
// queued and processing are the only non-terminal statuses.
if (result.status === "queued" || result.status === "processing") continue;
if (result.status === "partial") {
// Only extracted_text_preview is stored for a partial result (extracted_text is null).
console.warn("partial result:", result.warning);
return result.extracted_text_preview as string;
}
if (result.status !== "succeeded") {
throw new Error(`OCR ended as ${result.status}: ${result.provider_error ?? ""}`);
}
return result.extracted_text as string;
}
}Before processing a multi-page PDF, the router checks your balance against the estimated cost. If your balance is insufficient:
402 Payment Required.affordable_pages showing how many pages your balance covers. Processing continues up to that limit and returns results for the affordable pages, still with status: "succeeded" and stop_reason: "credit_exhaustion".{
"request_id": "req_abc123",
"status": "succeeded",
"extracted_text": "Page 1 content\n\n---\n\nPage 2 content",
"stopped_at_page": 3,
"stop_reason": "credit_exhaustion",
"remaining_pages": 17,
"affordable_pages": 3
}What about PDFs with digital signatures?
Signature fields and embedded form widgets are classified as mixed pages and run through full OCR. The visual signature image is captured; the cryptographic signature itself is not verified.
Are password-protected PDFs supported?
No. Submit an unencrypted copy. The API will return a 422 if the PDF cannot be opened.
How long are async job results stored?
Completed job results are held in memory for 1 hour. Fetch the result promptly; after expiry the endpoint returns 404.
Can I force async mode for small PDFs?
Not currently. Async mode triggers automatically at 20+ pages. Smaller PDFs always complete synchronously.
What if a page fails during async processing?
Failed pages are logged and skipped by default. The job still succeeds with partial text; failed pages appear as empty gaps in the output.