Loading...
Loading...
Complete reference for the OpenOCR REST API.
https://api.open-ocr.com/v1All authenticated endpoints return rate limit headers. Limits are per API key using a sliding window.
Rate limits by tier:
# Rate limit headers included in every response:
# X-RateLimit-Limit: 1000 # max requests per window
# X-RateLimit-Remaining: 742 # requests left in current window
# X-RateLimit-Reset: 1709251200 # Unix timestamp when window resets
# When rate limited (429):
# Retry-After: 30 # seconds to waitSee the Error Codes page for retry strategies.
| Method | Path | Description |
|---|---|---|
| POST | /v1/ocr | Process a document with OCR (sync or async) |
| POST | /v1/ocr/batch | Process up to 20 images concurrently |
| GET | /v1/ocr/:request_id | Recover a saved result or check an in-flight request |
| GET | /v1/engines | List available OCR engines with status |
| GET | /v1/usage | Get usage statistics and credit balance |
| GET | /api/usage | Per-user usage history with pagination and date filters (dashboard endpoint) |
| GET | /v1/health | Health check (no auth required) |
| GET | /v1/status | Service status (minimal without auth, full with auth) |
| GET | /v1/engines/health | Live health check of all local engines |
| GET | /v1/engines/stats | Aggregated performance stats for all engines |
| GET | /v1/engines/:engine_id/stats | Performance stats for a specific engine |
| GET | /v1/engines/:engine_id/latency | Hourly latency time-series (p50/p95/p99) |
| GET | /v1/engines/:engine_id/uptime | Daily uptime history with check counts |
| GET | /v1/changelog | API changelog — version history and breaking changes |
The primary endpoint for extracting text from documents. Returns the result synchronously.
| Parameter | Type | Required | Description |
|---|---|---|---|
| Idempotency-Key (header) | string | Yes | Send a new unique value (for example a UUID) for each logical request, and reuse the SAME value when you retry that request so it is processed and billed once. Use a NEW value for a different payload: Reusing a key with a DIFFERENT request body returns 409 idempotency_key_reused; the same key with the same request (even with different base64 line breaks or omitted default fields) replays the original result. The comparison is on the effective request (decoded base64 bytes, resolved engine, defaults applied such as page_count 1, output_format json, mode sync and language en), not its spelling. A request first stored before 2026-09-29 has no fingerprint and replays even a changed request. The text is replayed only while OCR history is retained: with history off a replayed success has a null extracted_text and a warning that the text was not retained, so clients with history off must keep the first response. Use a fresh key for every distinct document. Without this header the call fails with 400 (Idempotency-Key header is required). |
| engine | string | Yes | Engine ID, e.g. "mistral-mistral-ocr-4-1". See GET /v1/engines. |
| input | object | Yes | The document to process. |
| input.type | string | Yes | Input type: "url" or "base64". Other values, including "file", are rejected. |
| input.url | string | If type=url | Public URL of the document. |
| input.data_base64 | string | If type=base64 | Raw base64-encoded document data. Do not include a data: URI prefix (it is rejected as invalid base64). |
| input.mime_type | string | No | MIME type hint for base64 input (e.g. "image/png"). |
| options.language_hints | string[] | No | Language hints, e.g. ["eng", "fra"], honored by OpenOCR-hosted engines. Prefer settings.language. |
| options.detect_tables | boolean | No | Enable table detection where the engine supports it. |
| fallback_engine | string | No | Engine ID used for scanned pages in a PDF when scanned_handling is fallback. |
| scanned_handling | string | No | "skip", "fallback" or "error" for scanned (image-only) PDF pages. Default: skip, or fallback when fallback_engine is set. |
| fallback | string | boolean | No | Opt-in automatic retry. Set "similar" (or true) and, if your engine refuses the page, returns no text, or hits a temporary error or timeout, OpenOCR retries once on an engine from a different vendor that scores within 5 points on our hard benchmark and costs the same or less per page. You are never charged more than the engine you asked for; engine, warning and metadata.fallback name the engine that produced the text. Default "off". Not the same as fallback_engine, which only covers scanned PDF pages. |
| settings.dpi | integer | No | Resolution hint, 72 to 1200. |
| settings.page_range | string | No | Page selection such as "1-3,5" for engines that support it (OCRmyPDF). |
| settings.detect_tables | boolean | No | Same as options.detect_tables. |
| mode | string | No | "sync" (default) or "async". |
| webhook_url | string | No | Webhook URL for async results. Requires mode="async". |
| page_count | integer | No | Number of pages to process for multi-page PDFs (up to the engine's max_pages limit; see max_pages_per_request in GET /v1/engines). Default: 1, so only the first page is processed unless you set this. Set page_count to the number of pages you want. |
| output_format | string | No | Response format: "json" (default), "text" (plain text, Content-Type: text/plain), or "markdown" (Content-Type: text/markdown). |
| settings | object | No | Unified engine settings. Automatically mapped to engine-native parameters. |
| settings.language | string | No | ISO language code (e.g. "en", "ja", "zh"). Mapped to each engine's native language parameter. Default: "en". |
| native_settings | object | No | Engine-specific passthrough parameters. Overrides unified settings on conflict. See engine docs for supported keys. |
| Parameter | Type | Required | Description |
|---|---|---|---|
| request_id | string | Always | Unique request identifier. |
| status | string | Always | "succeeded", "failed", or "processing". |
| engine | string | Always | Engine ID that processed the request. |
| extracted_text | string | On success | Full extracted text content. |
| extracted_text_preview | string | On success | Truncated preview of extracted text. |
| pages | integer | On success | Number of pages processed. |
| pages_total | integer | When known | Number of pages in the source document. Omitted when the router cannot tell (for example a single image URL). If it is larger than pages, only part of the document was processed; set page_count to process more. |
| warnings | string[] | When applicable | Non-fatal notes, for example "document has 3 pages; processed 1. Set page_count to process more." Check it when you send a multi-page PDF. |
| poll_url | string | Async only | Path to poll for the result, /v1/ocr/{request_id}. Returned with status "processing". |
| cost_debited | number | Always | Credits charged (USD). Non-zero on a failed request when the engine ran and the provider billed us for the attempt; 0.0 when nothing is owed. See Billing on Errors. |
| provider_latency_ms | integer | On success | Engine processing time in milliseconds. |
| confidence | number | On success | OCR confidence score (0–1). Present when the engine supports it (e.g. Tesseract, EasyOCR). Absent for engines like OCRmyPDF. |
| applied_settings | object | On success | Echoes the actual settings applied to the engine, including language mapping and any native overrides. |
# POST /v1/ocr
curl -X POST https://api.open-ocr.com/v1/ocr \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-ocr-your-api-key" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"engine": "mistral-mistral-ocr-4-1",
"input": {
"type": "url",
"url": "https://example.com/document.png"
}
}'
# Response (HTTP 200 for JSON output; still check the status field):
# {
# "request_id": "req_abc123",
# "status": "succeeded",
# "engine": "mistral-mistral-ocr-4-1",
# "extracted_text": "Extracted text content...",
# "extracted_text_preview": "Extracted text con...",
# "pages": 1,
# "cost_debited": 0.005,
# "provider_latency_ms": 150
# }import uuid
import requests
response = requests.post(
'https://api.open-ocr.com/v1/ocr',
headers={
'Content-Type': 'application/json',
'Authorization': 'Bearer sk-ocr-your-api-key',
'Idempotency-Key': str(uuid.uuid4()),
},
json={
'engine': 'mistral-mistral-ocr-4-1',
'input': {
'type': 'url',
'url': 'https://example.com/document.png',
},
},
)
response.raise_for_status() # an error envelope has no "status" field
data = response.json()
if data["status"] != "succeeded":
raise RuntimeError(f"OCR ended as {data['status']}: {data.get('provider_error')}")
print(data['extracted_text'])const response = await fetch('https://api.open-ocr.com/v1/ocr', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer sk-ocr-your-api-key',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
engine: 'mistral-mistral-ocr-4-1',
input: {
type: 'url',
url: 'https://example.com/document.png',
},
}),
});
const data = await response.json();
if (data.status !== "succeeded") throw new Error("OCR ended as " + (data.status ?? data.error?.code)); // error envelopes have no status
console.log(data.extracted_text);# All errors use a nested envelope: { "error": { "code", "message" } }
# 401 Unauthorized — missing or invalid API key
# { "error": { "code": "authentication_error", "message": "Invalid API key" } }
# 402 Payment Required — insufficient credits
# { "error": { "code": "insufficient_balance", "message": "Insufficient balance to process any pages. Need $0.005, have $0.0001" } }
# 400 Bad Request — invalid engine or input
# { "error": { "code": "unknown_engine", "message": "'mistral-ocr-4-1' is not a known engine id. Did you mean: mistral-mistral-ocr-4-1?", "suggestions": ["mistral-mistral-ocr-4-1"] } }
# 429 Rate Limited
# { "error": { "code": "rate_limited", "message": "Rate limit exceeded" } }Use settings to pass unified parameters that work across all engines. The router automatically maps them to each engine's native format. Use native_settings for engine-specific passthrough — native settings override unified settings on conflict.
# Set language to Japanese. Honored by OpenOCR-hosted engines such as local-tesseract.
# Some engines, including Mistral OCR, ignore language hints and detect the language automatically.
curl -X POST https://api.open-ocr.com/v1/ocr \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-ocr-your-api-key" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"engine": "local-tesseract",
"input": { "type": "url", "url": "https://example.com/japanese-doc.png" },
"settings": { "language": "ja" }
}'
# Response includes applied_settings showing what was mapped:
# {
# "request_id": "req_xyz789",
# "status": "succeeded",
# "engine": "local-tesseract",
# "extracted_text": "日本語のテキスト...",
# "applied_settings": { "language": "ja" },
# "cost_debited": 0.002,
# "provider_latency_ms": 147
# }Pass engine-specific parameters directly. These are forwarded to the engine without transformation. If both settings and native_settings are provided, native settings win on conflict.
# Tesseract-specific: set page segmentation mode
curl -X POST https://api.open-ocr.com/v1/ocr \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-ocr-your-api-key" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"engine": "local-tesseract",
"input": { "type": "url", "url": "https://example.com/table.png" },
"settings": { "language": "en" },
"native_settings": { "--psm": "6", "--oem": "1" }
}'The router maps ISO language codes to each engine's native format automatically:
| settings.language | Tesseract | EasyOCR | LLM engines |
|---|---|---|---|
| "en" | --lang eng | ["en"] | Prompt: English |
| "ja" | --lang jpn | ["ja"] | Prompt: Japanese |
| "zh" | --lang chi_sim | ["ch_sim"] | Prompt: Chinese |
| "ko" | --lang kor | ["ko"] | Prompt: Korean |
| "ar" | --lang ara | ["ar"] | Prompt: Arabic |
Set mode: "async" to get a 202 Accepted immediately. Poll for results or provide a webhook_url to receive the result when processing completes.
# POST /v1/ocr (async mode)
curl -X POST https://api.open-ocr.com/v1/ocr \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-ocr-your-api-key" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"engine": "mistral-mistral-ocr-4-1",
"mode": "async",
"webhook_url": "https://your-server.com/webhook",
"input": {
"type": "url",
"url": "https://example.com/document.png"
}
}'
# Response (202):
# {
# "request_id": "req_xyz789",
# "status": "processing",
# "poll_url": "/v1/ocr/req_xyz789"
# }
# Poll for result:
curl https://api.open-ocr.com/v1/ocr/req_xyz789 \
-H "Authorization: Bearer sk-ocr-your-api-key"
# Response (inspect the status field; completed results return 200):
# {
# "request_id": "req_xyz789",
# "status": "succeeded",
# "extracted_text": "Full OCR text...",
# "engine": "mistral-mistral-ocr-4-1",
# "cost_debited": 0.005
# }
#
# If status is "partial": extracted_text is null. Read extracted_text_preview (first 2,000
# characters) and warning; the full partial text is not stored.Closing the tab or losing the HTTP connection does not cancel an accepted OCR request. Processing continues within the engine's execution limit, and completed text is saved. Use the same recovery endpoint for synchronous and asynchronous requests: GET /v1/ocr/{request_id}. Polling reads the saved request; it does not run OCR again or charge for another attempt.
Recovery requires saved history. In Dashboard → Settings → OCR history, you can turn retention off and remove existing results. Requests already running will also stop saving text. Their recovery endpoint returns 410 result_not_retained. Turning history back on applies to new requests and does not restore deleted results. With history off, use synchronous processing and save the live response yourself; submitting mode: "async" returns 400 history_disabled before processing starts. Reusing an idempotency key still prevents duplicate billing, but cannot restore unretained text.
mode: "async" and save the request_id from the acceptance response before you begin polling.request_id from the JSON response or X-Request-Id from the response headers. Slow responses send this header before the final JSON body; save it even if reading the body later times out.status is queued or processing. HTTP 200 only means the lookup succeeded; it does not mean OCR finished.succeeded, read the full extracted_text. A partial result contains incomplete text, and only extracted_text_preview is stored for it (extracted_text is null): read the preview and keep its warning too. Other terminal statuses should be handled as failures using provider_error. A provider failure or execution timeout is terminal; polling does not restart it.import os
import time
import requests
# Persist this ID from POST /v1/ocr; reuse it after reconnecting.
request_id = "req_xyz789"
headers = {"Authorization": f"Bearer {os.environ['OPENOCR_API_KEY']}"}
url = f"https://api.open-ocr.com/v1/ocr/{request_id}"
# This is your polling window, not a cancellation deadline for the OCR job.
stop_waiting_at = time.monotonic() + 60
while time.monotonic() < stop_waiting_at:
try:
response = requests.get(url, headers=headers, timeout=15)
except requests.RequestException:
time.sleep(5)
continue # Keep the same ID; do not submit a new OCR request.
response.raise_for_status() # 404: missing/wrong owner; 410: text not retained.
result = response.json()
if result["status"] in ("queued", "processing"):
time.sleep(5)
continue
if result["status"] == "succeeded":
print(result.get("extracted_text") or "")
elif result["status"] == "partial":
print("Incomplete result:", result.get("warning"))
# Only the preview is stored for a partial result; extracted_text is null.
print(result.get("extracted_text_preview") or "")
else:
raise RuntimeError(result.get("provider_error") or result["status"])
break
else:
print(f"Still waiting. Save {request_id} and check the same URL later.")Authenticate as the account that created the request. Another account receives 404. You can also open saved text in Dashboard → Activity, including results that finished after you left the original page. For a completion callback, provide a webhook URL when submitting an asynchronous request, then fetch the full text using its request ID.
Process up to 20 images in a single request. Each item can use a different engine. Items are billed independently, and a failed item follows the same rule as a single request: nothing to pay if its engine never ran, and the provider's cost for the attempt — at cost, including costs above the initial estimate — if it ran and then failed. See Billing on Errors.
Batch requests are not idempotent. POST /v1/ocr/batch ignores the Idempotency-Key header and creates new requests every time. Retrying a batch re-processes and re-bills every item, including the ones that already succeeded. If a batch call fails partway, read the per-item results and resubmit only the failed items.
| Parameter | Type | Required | Description |
|---|---|---|---|
| items | array | Yes | Array of OCR requests (max 20). Each item takes engine, input, options and page_count. |
# POST /v1/ocr/batch
curl -X POST https://api.open-ocr.com/v1/ocr/batch \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-ocr-your-api-key" \
-d '{
"items": [
{
"engine": "mistral-mistral-ocr-4-1",
"input": { "type": "url", "url": "https://example.com/page1.png" }
},
{
"engine": "google-gemini-3.1-flash-lite",
"input": { "type": "url", "url": "https://example.com/page2.png" }
}
]
}'
# Response (200):
# {
# "batch_id": "batch_abc123",
# "results": [
# { "index": 0, "status": "succeeded", "extracted_text": "Page 1...", "cost_debited": 0.005 },
# { "index": 1, "status": "succeeded", "extracted_text": "Page 2...", "cost_debited": 0.0006 }
# ],
# "total_cost_debited": 0.0056
# }All engines support PDF documents. Send PDFs the same way as images — via URL or base64. Each page of a PDF counts as one credit.
page_count parameter to set how many pages are processed, up to the engine's limit (max_pages_per_request in GET /v1/engines).page_count to process more.# Process first 5 pages of a PDF
curl -X POST https://api.open-ocr.com/v1/ocr \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-ocr-your-api-key" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"engine": "mistral-mistral-ocr-4-1",
"page_count": 5,
"input": {
"type": "url",
"url": "https://example.com/report.pdf"
}
}'
# Response (HTTP 200 for JSON output; still check the status field):
# {
# "request_id": "req_pdf456",
# "status": "succeeded",
# "engine": "mistral-mistral-ocr-4-1",
# "extracted_text": "Page 1 text...\n--- Page 2 ---\nPage 2 text...",
# "pages": 5,
# "cost_debited": 0.025,
# "provider_latency_ms": 820
# }Control the response format with output_format. Default is json.
# Plain text response (Content-Type: text/plain)
curl -X POST https://api.open-ocr.com/v1/ocr \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-ocr-your-api-key" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"engine": "mistral-mistral-ocr-4-1",
"output_format": "text",
"input": { "type": "url", "url": "https://example.com/receipt.png" }
}'
# Response (HTTP 200): plain text (no JSON wrapper)
# Markdown response (Content-Type: text/markdown)
curl -X POST https://api.open-ocr.com/v1/ocr \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-ocr-your-api-key" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"engine": "mistral-mistral-ocr-4-1",
"output_format": "markdown",
"input": { "type": "url", "url": "https://example.com/receipt.png" }
}'
# Response (HTTP 200): the markdown wrapper plus engine output (no JSON wrapper)Some engines return a confidence score (0–1) in the JSON response. This indicates how confident the engine is in the extracted text. Engines that don't support confidence (e.g. OCRmyPDF) omit the field.
# JSON response with confidence
# {
# "request_id": "req_abc123",
# "status": "succeeded",
# "engine": "mistral-mistral-ocr-4-1",
# "extracted_text": "Invoice #12345...",
# "confidence": 0.92,
# "cost_debited": 0.005,
# "provider_latency_ms": 340
# }Returns all engines with their pricing, capabilities, and availability status (available or coming_soon).
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | Always | Engine ID, e.g. "mistral-mistral-ocr-4-1". |
| name | string | Always | Human-readable engine name. |
| provider | string | Always | Provider name. |
| price_per_page_usd | number | Always | Per-page reserve or fallback charge in USD. Token-billed engines settle from actual token usage; this is not a fixed final price. |
| status | string | Always | "available" or "coming_soon". |
| benchmark | object | null | Always | { hard_score, easy_score, ci, measured_at } from our benchmarks; null when the engine has not been measured. GET /v1/engines and GET /v1/models both return it. |
| recommended | boolean | Always | True for engines we recommend as defaults. |
| accepts | string[] | GET /v1/engines | Input types and formats the engine accepts. For an engine with native_pdf true the provider reads the PDF itself. For every other engine, pdf means the router rasterizes the PDF first. It recognizes a PDF only when the input is base64 with mime_type application/pdf (or data starting with the %PDF magic), or a URL whose path ends in .pdf (query string ignored). An extensionless PDF URL such as /download/123 is not detected and is sent to the engine as an image: download it and send base64, or use a .pdf URL. |
| max_pages_per_request | integer | GET /v1/engines | Largest page_count a single request may use. |
| native_pdf | boolean | GET /v1/engines | True when the provider reads the PDF itself. Otherwise the router rasterizes PDFs first (see accepts). |
| supports | string[] | Always | Supported input types: "image_url", "image_base64". |
| strengths | string[] | Always | Engine strengths, e.g. "handwriting", "tables". |
Add ?sort=accuracy to order the list by hard-corpus benchmark score, best first. Engines with a null benchmark come last.
# GET /v1/engines
curl https://api.open-ocr.com/v1/engines \
-H "Authorization: Bearer sk-ocr-your-api-key"
# Response:
# {
# "object": "list",
# "data": [
# {
# "id": "mistral-mistral-ocr-4-1",
# "name": "Mistral OCR 4.1",
# "provider": "Mistral",
# "price_per_page_usd": 0.005,
# "status": "available",
# "supports": ["image_url", "image_base64"],
# "strengths": ["printed_text", "multilingual"]
# },
# ...
# ]
# }import requests
response = requests.get(
'https://api.open-ocr.com/v1/engines',
headers={'Authorization': 'Bearer sk-ocr-your-api-key'},
)
engines = response.json()['data']
available = [e for e in engines if e['status'] == 'available']
print(f'{len(available)} engines available')const response = await fetch('https://api.open-ocr.com/v1/engines', {
headers: { 'Authorization': 'Bearer sk-ocr-your-api-key' },
});
const { data: engines } = await response.json();
const available = engines.filter(e => e.status === 'available');
console.log(`${available.length} engines available`);Check your credit balance, total usage, and request counts. Requires authentication.
| Parameter | Type | Required | Description |
|---|---|---|---|
| balance_usd | number | Always | Current credit balance in USD. |
| total_spent_usd | number | Always | Total credits spent. |
| total_requests | integer | Always | Total OCR requests made. |
| total_pages | integer | Always | Total pages processed. |
| period | string | Always | Time period: "all_time". |
# GET /v1/usage
curl https://api.open-ocr.com/v1/usage \
-H "Authorization: Bearer sk-ocr-your-api-key"
# Response (200):
# {
# "balance_usd": 0.95,
# "total_spent_usd": 0.05,
# "total_requests": 100,
# "total_pages": 100,
# "period": "all_time"
# }import requests
response = requests.get(
'https://api.open-ocr.com/v1/usage',
headers={'Authorization': 'Bearer sk-ocr-your-api-key'},
)
usage = response.json()
print(f'Balance: ${usage["balance_usd"]:.2f}')
print(f'Requests: {usage["total_requests"]}')const response = await fetch('https://api.open-ocr.com/v1/usage', {
headers: { 'Authorization': 'Bearer sk-ocr-your-api-key' },
});
const usage = await response.json();
console.log(`Balance: $${usage.balance_usd.toFixed(2)}`);
console.log(`Requests: ${usage.total_requests}`);Returns paginated per-user usage history with date and engine filters. This is a dashboard endpoint on open-ocr.com/api/usage. Preferred authentication is Authorization: Bearer <api_key>.
api_key=...is supported only as a fallback for existing clients and will be removed in a future release. Migrate to header auth now.| Parameter | Type | Required | Description |
|---|---|---|---|
| user_id | string | Yes | User UUID for the usage timeline query. |
| from | string (YYYY-MM-DD) | No | Start date. Defaults to 30 days ago. |
| to | string (YYYY-MM-DD) | No | End date. Defaults to today. |
| engine | string | No | Optional engine filter (e.g. mistral-mistral-ocr-4-1). |
| page | integer | No | Page number (1-indexed). Default: 1. |
| per_page | integer | No | Results per page. Default: 50, max: 100. |
# Preferred (header auth)
curl "https://open-ocr.com/api/usage?user_id=YOUR_USER_ID&page=1&per_page=50" \
-H "Authorization: Bearer sk-ocr-your-api-key"
# Migration fallback only (deprecated)
# curl "https://open-ocr.com/api/usage?user_id=YOUR_USER_ID&api_key=sk-ocr-your-api-key"All authenticated endpoints are rate-limited to 60 requests per minute per API key. Every response includes rate-limit metadata so you can throttle before hitting 429s.
| Header | Meaning | How to use it |
|---|---|---|
| X-RateLimit-Limit | Total requests allowed in the current 60s window. | Use as your hard per-minute cap. |
| X-RateLimit-Remaining | Requests left before limit is reached. | Slow down when this drops below ~5. |
| X-RateLimit-Reset | Unix timestamp when quota resets. | Sleep until reset on burst workloads. |
| Retry-After | Seconds to wait before retrying (429 responses only). | Always prefer this over hardcoded waits. |
#!/usr/bin/env bash
set -euo pipefail
API_KEY="sk-ocr-your-api-key"
while true; do
response_headers=$(mktemp)
status=$(curl -sS -o /tmp/openocr-body.json -D "$response_headers" -w "%{http_code}" -X POST https://api.open-ocr.com/v1/ocr -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" -d '{"engine":"mistral-mistral-ocr-4-1","input":{"type":"url","url":"https://example.com/doc.png"}}')
if [ "$status" = "429" ]; then
retry_after=$(awk 'tolower($1)=="retry-after:" {print $2}' "$response_headers" | tr -d '\r')
sleep "${retry_after:-2}"
continue
fi
cat /tmp/openocr-body.json
break
doneFor POST /v1/ocr(not /v1/ocr/batch, which ignores the header), send a unique Idempotency-Key per logical operation. If the network fails mid-request, retry with the same key to prevent duplicate charges.
# One key per logical request
IDEMPOTENCY_KEY="ocr-$(uuidgen | tr '[:upper:]' '[:lower:]')"
curl -X POST https://api.open-ocr.com/v1/ocr -H "Authorization: Bearer sk-ocr-your-api-key" -H "Content-Type: application/json" -H "Idempotency-Key: $IDEMPOTENCY_KEY" -d '{
"engine": "mistral-mistral-ocr-4-1",
"input": {"type": "url", "url": "https://example.com/invoice.png"}
}'Set explicit client timeouts and retry only transient failures.
| Request type | Suggested timeout | Retry policy |
|---|---|---|
| POST /v1/ocr (sync) | Connect 5s, total 240s | Retry 429/500/502/503 up to 3 times |
| POST /v1/ocr (async) | Connect 5s, total 30s | Same retry set; then poll by request_id |
| GET polling endpoints | Connect 5s, total 20s | Back off 1s → 2s → 4s (max 10s) |
import random
import time
import uuid
import requests
TRANSIENT = {429, 500, 502, 503}
def post_ocr_with_backoff(payload, api_key, idem_key, max_retries=3):
for attempt in range(max_retries + 1):
resp = requests.post(
"https://api.open-ocr.com/v1/ocr",
json=payload,
headers={
"Authorization": f"Bearer {api_key}",
"Idempotency-Key": idem_key,
},
timeout=(5, 240), # connect 5s, read 240s: above the router's 215s max wall
)
if resp.status_code not in TRANSIENT:
return resp
if resp.status_code == 503 and resp.json().get("status") == "timeout":
return resp # stored timeout: the same key returns the same 503, so do not retry it
if attempt == max_retries:
return resp
if resp.status_code == 429 and resp.headers.get("Retry-After"):
delay = float(resp.headers["Retry-After"])
else:
delay = min(2 ** attempt, 10) + random.uniform(0, 0.25)
time.sleep(delay)
return resp
payload = {"engine": "mistral-mistral-ocr-4-1", "input": {"type": "url", "url": "https://example.com/doc.png"}}
api_key = "sk-ocr-your-api-key"
resp = post_ocr_with_backoff(payload, api_key, str(uuid.uuid4()))
# Use it: HTTP 2xx only means the call completed. Check the body status.
body = resp.json()
if body.get("status") == "timeout":
# A timed-out request replays as the same 503 for this key, forever. Report it, or start a
# NEW attempt with a NEW Idempotency-Key. Never loop on the same key.
raise RuntimeError("OCR timed out; retry with a NEW Idempotency-Key")
if not resp.ok:
# Error envelope: {"error": {"code", "message"}} with no "status" field, for example
# 409 idempotency_key_reused (the key was used with a different request).
err = body.get("error", {})
raise RuntimeError(f"HTTP {resp.status_code} {err.get('code')}: {err.get('message')}")
if body["status"] != "succeeded":
raise RuntimeError(f"OCR ended as {body['status']}: {body.get('provider_error')}")
print(body["extracted_text"])For detailed error-by-error remediation, see the Error Codes page.
Two endpoints for monitoring. Neither requires authentication for basic checks.
# GET /v1/health — simple health check
curl https://api.open-ocr.com/v1/health
# Response (200):
# { "ok": true }
# GET /v1/status — service status (unauthenticated)
curl https://api.open-ocr.com/v1/status
# Response (200):
# {
# "ok": true,
# "engine_count": 20
# }
# GET /v1/status — full details (authenticated)
curl https://api.open-ocr.com/v1/status \
-H "Authorization: Bearer sk-ocr-your-api-key"
# Response (200):
# {
# "ok": true,
# "engine_count": 20,
# "version": "0.1.0",
# "uptime_seconds": 86400
# }import requests
# Basic check (no auth)
status = requests.get('https://api.open-ocr.com/v1/status').json()
print(f'OK: {status["ok"]}, Engines: {status["engine_count"]}')
# Full details (with auth)
status = requests.get(
'https://api.open-ocr.com/v1/status',
headers={'Authorization': 'Bearer sk-ocr-your-api-key'},
).json()
print(f'Version: {status["version"]}, Uptime: {status["uptime_seconds"]}s')// Basic check (no auth)
const status = await fetch('https://api.open-ocr.com/v1/status')
.then(r => r.json());
console.log(`OK: ${status.ok}, Engines: ${status.engine_count}`);
// Full details (with auth)
const full = await fetch('https://api.open-ocr.com/v1/status', {
headers: { 'Authorization': 'Bearer sk-ocr-your-api-key' },
}).then(r => r.json());
console.log(`Version: ${full.version}`);Live health check that probes all local engine endpoints. Returns per-engine health status and in-memory performance metrics. No authentication required.
# GET /v1/engines/health
curl https://api.open-ocr.com/v1/engines/health
# Response (200):
# {
# "object": "engine_health",
# "total": 5,
# "healthy": 4,
# "engines": [
# {
# "engine_id": "local-tesseract",
# "engine_name": "Tesseract OCR",
# "health_url": "http://localhost:8101/health",
# "status": "healthy",
# "detail": null,
# "metrics": {
# "total_requests": 1024,
# "window_size": 100,
# "success_rate": 0.98,
# "avg_latency_ms": 166,
# "p50_latency_ms": 145,
# "p95_latency_ms": 320,
# "p99_latency_ms": 580
# }
# }
# ]
# }Aggregated performance statistics from the database. Available for all engines or a specific engine. No authentication required.
# GET /v1/engines/stats — all engines
curl https://api.open-ocr.com/v1/engines/stats
# Response (200):
# {
# "object": "list",
# "data": [
# {
# "engine_id": "mistral-mistral-ocr-4-1",
# "total_requests": 5230,
# "total_pages": 8410,
# "avg_latency_ms": 166,
# "error_rate": 0.02,
# "last_used": "2026-02-28T05:30:00Z"
# }
# ]
# }
# GET /v1/engines/:engine_id/stats — specific engine
curl https://api.open-ocr.com/v1/engines/mistral-mistral-ocr-4-1/stats
# Response (200): same fields as above for one engine
# Returns 404 if engine has no recorded statsHourly latency time-series for a specific engine. Useful for monitoring performance trends. No authentication required.
| Parameter | Type | Required | Description |
|---|---|---|---|
| hours | integer | No | Hours of history to return. Default: 168 (7 days). Max: 720 (30 days). |
# GET /v1/engines/:engine_id/latency?hours=48
curl "https://api.open-ocr.com/v1/engines/mistral-mistral-ocr-4-1/latency?hours=48"
# Response (200):
# {
# "engine_id": "mistral-mistral-ocr-4-1",
# "hours": 48,
# "data": [
# {
# "bucket_hour": "2026-02-27T10:00:00Z",
# "request_count": 42,
# "p50_ms": 145,
# "p95_ms": 310,
# "p99_ms": 580,
# "error_count": 1
# }
# ]
# }Daily uptime history for a specific engine. Shows health check totals and uptime percentage per day. No authentication required.
| Parameter | Type | Required | Description |
|---|---|---|---|
| days | integer | No | Days of history to return. Default: 30. Max: 365. |
# GET /v1/engines/:engine_id/uptime?days=7
curl "https://api.open-ocr.com/v1/engines/mistral-mistral-ocr-4-1/uptime?days=7"
# Response (200):
# {
# "engine_id": "mistral-mistral-ocr-4-1",
# "days": 7,
# "overall_uptime_pct": 99.85,
# "total_checks": 672,
# "data": [
# {
# "date": "2026-02-27",
# "checks_total": 96,
# "checks_healthy": 96,
# "uptime_pct": 100.0
# }
# ]
# }Programmatic access to API version history. Useful for monitoring breaking changes. No authentication required.
# GET /v1/changelog
curl https://api.open-ocr.com/v1/changelog
# Response (200):
# {
# "api_version": "2026-02-28",
# "changelog": [
# {
# "version": "2026-02-28",
# "date": "2026-02-28",
# "changes": [
# "Added X-API-Version header to all responses",
# "Added /v1/changelog endpoint",
# "Added rate limit dashboard visibility"
# ]
# }
# ]
# }