# PDF to Markdown
URL: https://docs.munito.ai/pdf-to-markdown

The Munito AI OCR API converts a PDF or an image to Markdown, with the processing in the EU or the US. Send the document to `POST /ocr` by URL or as base64. The answer holds the Markdown of each page.
A short document answers at once. A long one answers `202` with a `location` to poll.

The base URL is `https://api.munito.ai/inference/v1`, and the key is in `MUNITO_API_KEY`:

```bash
export MUNITO_API_KEY="munitoai_..."
```

## Send a PDF by URL [#send-a-pdf-by-url]

```bash
curl https://api.munito.ai/inference/v1/ocr \
  -H "Authorization: Bearer $MUNITO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "lightonai/lightonocr-2-1b",
    "document": {"type": "url", "url": "https://arxiv.org/pdf/1706.03762"},
    "pages": [0, 1]
  }'
```

Use a public `https` URL on port 443. Munito AI follows up to 3 redirects. A URL that Munito AI cannot fetch gets `422` with `Could not fetch the document from the provided URL.`

The answer has one entry in `pages` for each page, and the totals in `usage`:

```json
{
  "request_id": "01a165c4-00db-7c89-8558-034e9b948463",
  "model": "lightonai/lightonocr-2-1b",
  "service_tier": "standard",
  "pages": [
    {
      "index": 0,
      "markdown": "# Attention Is All You Need\n\n...",
      "header": null,
      "footer": null,
      "page_number": null,
      "aside": null,
      "images": [],
      "tables": [],
      "dimensions": { "width": 1190, "height": 1540, "dpi": 140 },
      "confidence_scores": null,
      "error": null,
      "error_code": null,
      "price_usd": 0.004
    }
  ],
  "usage": {
    "pages_processed": 2,
    "pages_failed": 0,
    "doc_size_bytes": 0,
    "cost_usd": 0.008,
    "price_per_page_usd": 0.004,
    "inference_region": "eu"
  }
}
```

A page that fails has `error` and `error_code` set, and it is not billed. `usage.inference_region` names the region that read the pages.

## Send a local file as base64 [#send-a-local-file-as-base64]

Use `"type": "base64"` with the bytes of the file in `data`. Add `filename` to help Munito AI detect the format.
This script sends a local PDF, polls until the request ends, and writes the Markdown to a `.md` file beside the PDF:

```python
# pip install requests
import base64
import os
import sys
import time

import requests

HOST = "https://api.munito.ai"
HEADERS = {"Authorization": f"Bearer {os.environ['MUNITO_API_KEY']}"}


def submit(path: str) -> str:
    """Send the PDF as base64 and return the location to poll."""
    with open(path, "rb") as f:
        data = base64.b64encode(f.read()).decode()
    response = requests.post(
        f"{HOST}/inference/v1/ocr",
        headers={**HEADERS, "Prefer": "respond-async"},
        json={
            "model": "munito/ocr-auto",
            "document": {"type": "base64", "data": data, "filename": os.path.basename(path)},
        },
        timeout=120,
    )
    response.raise_for_status()
    return response.json()["location"]


def wait(location: str) -> dict:
    """Poll the request until its status is final."""
    while True:
        response = requests.get(HOST + location, headers=HEADERS, timeout=60)
        if response.status_code == 503:
            time.sleep(int(response.headers.get("Retry-After", "2")))
            continue
        response.raise_for_status()
        body = response.json()
        if body["status"] != "running":
            return body
        print(f"{body.get('completed', 0)} of {body.get('total', '?')} pages done", file=sys.stderr)
        time.sleep(2)


if __name__ == "__main__":
    pdf = sys.argv[1]
    result = wait(submit(pdf))
    if result["status"] != "completed":
        sys.exit(f"OCR ended with status {result['status']}")
    markdown = "\n\n".join(page["markdown"] for page in result["pages"])
    out = os.path.splitext(pdf)[0] + ".md"
    with open(out, "w") as f:
        f.write(markdown)
    usage = result["usage"]
    print(f"Wrote {out}: {usage['pages_processed']} pages, {usage['pages_failed']} failed")
```

```bash
python pdf_to_markdown.py paper.pdf
```

## Choose the pages [#choose-the-pages]

Send `pages` with zero-based page numbers to read only those pages. Each page counts one time.

| Request              | Pages read            |
| -------------------- | --------------------- |
| No `pages` field     | Every page            |
| `"pages": [0]`       | The first page        |
| `"pages": [0, 1, 2]` | The first three pages |

At most 1000 pages go in one request. A negative or repeated number gets `422`. A number past the last page gets `422` with a text such as `pages [20] are past the last page: the document has 15 pages, numbered from 0`.

## Long documents: 202 and polling [#long-documents-202-and-polling]

By default, `POST /ocr` waits up to 120 seconds for the result. When the work takes longer, the answer is `202` with a `Location` header and a body:

```json
{
  "request_id": "01a165c4-d049-78e1-b194-ddd4016c515c",
  "status": "accepted",
  "location": "/inference/v1/ocr/01a165c4-d049-78e1-b194-ddd4016c515c"
}
```

Send `Prefer: respond-async` to get the `202` at once, as the script above does. Then poll [`GET /ocr/{request_id}`](https://docs.munito.ai/api/getOcr) at `https://api.munito.ai` plus the `location`:

| `status`                           | Meaning                                                                                                 |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `running`                          | The work continues. `total` and `completed` count the pages, and `pages` holds the pages that are done. |
| `completed`                        | The full result, in the same shape as a `200` answer.                                                   |
| `failed`, `cancelled`, `timed_out` | The request ended with no result.                                                                       |

<Callout type="warn">
  Munito AI deletes the result when it delivers it. After the first answer with the full result, and the next poll gets
  `410` with `The result expired or was already retrieved.` Save the result when you get it.
</Callout>

To stop a request that runs, send [`DELETE /ocr/{request_id}`](https://docs.munito.ai/api/cancelOcr). A poll that gets `503` can try again after the `Retry-After` time.

## Choose a model [#choose-a-model]

| Model                       | What it does                                                                                                                                                                                          | Input      |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `munito/ocr-auto`           | The default when you omit `model`. Reads the text layer of a page that is mostly text, and sends other pages, such as scans, to an OCR model.                                                         | PDF, image |
| `munito/ocr-fast`           | Reads only the text layer of a PDF, with no OCR model. A page with no text layer, such as a scan, gives empty Markdown. [Models and pricing](https://docs.munito.ai/models#munito-ocr-fast) lists its page price. | PDF only   |
| `lightonai/lightonocr-2-1b` | Reads each page with the [LightOnOCR-2-1B](https://docs.munito.ai/models/lightonai-lightonocr-2-1b) model.                                                                                                        | PDF, image |

Use `munito/ocr-fast` for PDFs that software made, such as exports and reports. Use `munito/ocr-auto` or `lightonai/lightonocr-2-1b` for scans and photos.
An image sent to `munito/ocr-fast` gets `422` with `munito/ocr-fast accepts only PDF documents. Use munito/ocr-auto for this document.`
See [Models and pricing](https://docs.munito.ai/models#ocr-models) for the price of each page.

## Other options [#other-options]

| Field               | Effect                                                                                                                   |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `tables`            | `{"mode": "extracted"}` moves each table from the page text to `tables`. `format` is `markdown` (the default) or `html`. |
| `images`            | `annotate` reads the text of each image into `alt_text`. `include_base64` adds each image as base64.                     |
| `confidence_scores` | `true` returns a confidence for each page.                                                                               |
| `preferred_region`  | `global`, `eu` or `us`: where the request can run.                                                                       |

See the [OCR reference](https://docs.munito.ai/api/createOcr) for each field.

## Formats and limits [#formats-and-limits]

- A PDF, or an image such as JPEG, PNG, WebP, TIFF or HEIC. Another type gets `422` with `This document type is not supported. Send a PDF or an image.`
- At most 50 MiB. A larger file gets `413` with `Document exceeds the configured size limit`.
- An OCR request keeps data while it runs, so send it to the global host or to the host of your data region. The other regional host answers `421`. See [Regions](https://docs.munito.ai/regions).

See [Errors](https://docs.munito.ai/errors) for each status and its body.
