> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.empiriolabs.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.empiriolabs.ai/_mcp/server.

# Document OCR API

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

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

Authenticate with your API key in the `Authorization` header:

```text
Authorization: Bearer $EMPIRIOLABS_API_KEY
```

The full request and response schema is in the [API reference](/api-reference/api-reference/document-ocr/create-ocr).

## Models

| Model                                        | Input                                      | Pages per request |
| -------------------------------------------- | ------------------------------------------ | ----------------- |
| [`lightonocr-3-4b`](/models/lightonocr-3-4b) | PDF, PNG, JPEG, WebP, TIFF, BMP, GIF, AVIF | Up to 1,000       |

## Your first request

This request reads the first two pages of a PDF.

**`cURL`**

```bash title="cURL"
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]
  }'
```

**`Python`**

```python title="Python"
import os

import requests

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": "https://arxiv.org/pdf/2412.13663"},
        "pages": [0, 1],
    },
    timeout=300,
)
response.raise_for_status()
for page in response.json()["pages"]:
    print(f"--- page {page['index']} ---")
    print(page["markdown"])
```

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

```json
{"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`**

```python title="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`**

```json title="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"}
}
```

| Field                              | What it holds                                                                                                                                                                                                                          |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pages[].index`                    | The page number, starting at 0.                                                                                                                                                                                                        |
| `pages[].markdown`                 | The page as Markdown. An image appears as `![img-0.jpeg](img-0.jpeg)` where it sits on the page.                                                                                                                                       |
| `pages[].images`                   | Each image region on the page, with its box in pixels of the page image.                                                                                                                                                               |
| `pages[].tables`                   | Tables, when `table_format` is set. Otherwise tables stay in `markdown` as HTML.                                                                                                                                                       |
| `pages[].header`, `pages[].footer` | The page's header and footer text, when `extract_header` or `extract_footer` is true. Otherwise they stay in `markdown`.                                                                                                               |
| `pages[].dimensions`               | The size of the page image the boxes refer to, and its resolution.                                                                                                                                                                     |
| `pages[].blocks`                   | Every 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_processed`       | The pages read.                                                                                                                                                                                                                        |
| `usage.billable_units`             | The 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

| Option                 | Default | Effect                                                                                                                                   |
| ---------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `include_image_base64` | `false` | Returns each image region as a JPEG data URI in `images[].image_base64`.                                                                 |
| `image_limit`          | none    | Returns at most this many images for the whole document, the first ones first.                                                           |
| `image_min_size`       | none    | Leaves out image regions narrower or shorter than this many pixels.                                                                      |
| `table_format`         | none    | `markdown` 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_header`       | `false` | Moves the header text out of `markdown` into `header`.                                                                                   |
| `extract_footer`       | `false` | Moves the footer text out of `markdown` into `footer`.                                                                                   |
| `include_blocks`       | `true`  | Includes `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`**

```bash title="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](/models/lightonocr-3-4b), with the minimum per request shown there. A request that fails is not billed for pages it did not return.

## Errors

| Status       | When                                                                                                                                                                                                                                                                                               |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`        | The 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. |
| `402`        | The balance or plan allowance does not cover the pages.                                                                                                                                                                                                                                            |
| `502`, `503` | The 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.