Skip to navigation

Document OCR API

PDFs and images to structured Markdown, page by page, with tables, images, and layout

Document OCR models read PDFs and images and return every page as Markdown: text in reading order, headings, lists, tables, math, headers and footers, and the position of every image on the page. Send a document as a URL or a base64 data URI, choose the pages, and each page comes back as its own object.

Endpoint

POST https://api.empiriolabs.ai/v1/ocr

Authenticate with your API key in the Authorization header:

Authorization: Bearer $EMPIRIOLABS_API_KEY

The full request and response schema is in the API reference.

Models

ModelInputPages per request
lightonocr-3-4bPDF, PNG, JPEG, WebP, TIFF, BMP, GIF, AVIFUp to 1,000

Your first request

This request reads the first two pages of a PDF.

curl https://api.empiriolabs.ai/v1/ocr \
-H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "lightonocr-3-4b",
"document": {
"type": "document_url",
"document_url": "https://arxiv.org/pdf/2412.13663"
},
"pages": [0, 1]
}'

An image works the same way with "type": "image_url":

{"model": "lightonocr-3-4b", "document": {"type": "image_url", "image_url": "https://example.com/receipt.png"}}

Send a local file

Either URL can be a base64 data URI, so a file on your machine needs no hosting:

Python
import base64
import os
import requests
with open("contract.pdf", "rb") as handle:
encoded = base64.b64encode(handle.read()).decode()
response = requests.post(
"https://api.empiriolabs.ai/v1/ocr",
headers={"Authorization": f"Bearer {os.environ['EMPIRIOLABS_API_KEY']}"},
json={
"model": "lightonocr-3-4b",
"document": {"type": "document_url", "document_url": f"data:application/pdf;base64,{encoded}"},
},
timeout=300,
)

A document can be up to 50 MB. A URL must be publicly reachable over HTTPS or HTTP.

The response

Response shape
{
"pages": [
{
"index": 0,
"markdown": "# ModernBERT\n\n![img-0.jpeg](img-0.jpeg)\n\nEncoder-only transformer models such as BERT ...",
"images": [
{"id": "img-0.jpeg", "top_left_x": 182, "top_left_y": 410, "bottom_right_x": 1401, "bottom_right_y": 902, "image_base64": null}
],
"tables": [],
"hyperlinks": [],
"header": null,
"footer": null,
"dimensions": {"dpi": 186, "height": 2048, "width": 1583},
"blocks": [
{"type": "title", "top_left_x": 268, "top_left_y": 162, "bottom_right_x": 1315, "bottom_right_y": 230, "content": "# ModernBERT"}
]
}
],
"model": "lightonocr-3-4b",
"document_annotation": null,
"usage_info": {"pages_processed": 1, "doc_size_bytes": 445479},
"usage": {"billable_units": 1, "billing_unit": "page"}
}
FieldWhat it holds
pages[].indexThe page number, starting at 0.
pages[].markdownThe page as Markdown. An image appears as ![img-0.jpeg](img-0.jpeg) where it sits on the page.
pages[].imagesEach image region on the page, with its box in pixels of the page image.
pages[].tablesTables, when table_format is set. Otherwise tables stay in markdown as HTML.
pages[].header, pages[].footerThe page’s header and footer text, when extract_header or extract_footer is true. Otherwise they stay in markdown.
pages[].dimensionsThe size of the page image the boxes refer to, and its resolution.
pages[].blocksEvery region of the page in reading order: title, text, list, table, image, equation, caption, code, header, footer, aside_text, each with its box and content. Set include_blocks to false to leave them out.
usage_info.pages_processedThe pages read.
usage.billable_unitsThe pages read, the unit the request is billed in. A request is billed at least the model’s minimum pages.

Choose pages

pages takes zero-based page numbers as a list ([0, 2, 3]) or a string ("0,2-4"). Leave it out to read every page. A request reads at most 1,000 pages; for a longer document, send it in parts with pages.

Images, tables, headers, and footers

OptionDefaultEffect
include_image_base64falseReturns each image region as a JPEG data URI in images[].image_base64.
image_limitnoneReturns at most this many images for the whole document, the first ones first.
image_min_sizenoneLeaves out image regions narrower or shorter than this many pixels.
table_formatnonemarkdown or html moves each table into tables[] in that format and leaves a link such as [tbl-0.html](tbl-0.html) in markdown.
extract_headerfalseMoves the header text out of markdown into header.
extract_footerfalseMoves the footer text out of markdown into footer.
include_blockstrueIncludes blocks.

Image and table ids run across the whole document: the second page continues where the first stopped.

Long documents

A request answers when its pages are read. A request still running after about four and a half minutes answers 202 with a job_id instead; poll GET /v1/jobs/{job_id} until status is completed, and the response is in result. To get the job id at once, send the header Prefer: respond-async.

Start a job and poll it
curl https://api.empiriolabs.ai/v1/ocr \
-H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
-H "Content-Type: application/json" \
-H "Prefer: respond-async" \
-d '{"model": "lightonocr-3-4b", "document": {"type": "document_url", "document_url": "https://example.com/report.pdf"}}'
curl https://api.empiriolabs.ai/v1/jobs/<job_id> \
-H "Authorization: Bearer $EMPIRIOLABS_API_KEY"

Billing

Billed per page read, at the price on the model page, with the minimum per request shown there. A request that fails is not billed for pages it did not return.

Errors

StatusWhen
400The document is missing, not a PDF or a supported image, larger than 50 MB, password-protected, or unreadable; a URL cannot be downloaded; pages names a page the document does not have or more than 1,000 pages; an option has the wrong type. The error.param field names the field to fix.
402The balance or plan allowance does not cover the pages.
502, 503The document could not be processed. Retry the request; no page is billed.

bbox_annotation_format, document_annotation_format, and document_annotation_prompt are not supported, and a request that sends them answers 400 before any page is read.