PDF to Markdown

Convert a printed, scanned or handwritten PDF or image to Markdown with the Munito AI OCR API, in the EU or the US, by URL or as base64.

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:

export MUNITO_API_KEY="munitoai_..."

Send a PDF by URL

curl https://api.munito.ai/inference/v1/ocr \
  -H "Authorization: Bearer $MUNITO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "lightonai/lightonocr-3-4b",
    "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:

{
  "request_id": "01a165c4-00db-7c89-8558-034e9b948463",
  "model": "lightonai/lightonocr-3-4b",
  "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

Use "type": "base64" with the bytes of the file in data. Munito AI reads the format from the bytes, so filename is optional. This script sends a local PDF, polls until the request ends, and writes the Markdown to a .md file beside the PDF:

# 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")
python pdf_to_markdown.py paper.pdf

Choose the pages

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

RequestPages read
No pages fieldEvery 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

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:

{
  "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} at https://api.munito.ai plus the location:

statusMeaning
runningThe work continues. total and completed count the pages, and pages holds the pages that are done.
completedThe full result, in the same shape as a 200 answer.
failed, cancelled, timed_outThe request ended with no result.

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.

To stop a request that runs, send DELETE /ocr/{request_id}. A poll that gets 503 can try again after the Retry-After time.

Choose a model

ModelWhat it doesInput
munito/ocr-autoThe 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-fastReads only the text layer of a PDF, with no OCR model. A page with no text layer, such as a scan, gives empty Markdown. Pricing lists its page price.PDF only
lightonai/lightonocr-3-4bReads each page with the LightOnOCR-3-4B model.PDF, image

Use munito/ocr-fast for PDFs that software made, such as exports and reports. Use munito/ocr-auto or lightonai/lightonocr-3-4b 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 Pricing for the price of each page.

Other options

FieldEffect
tables{"mode": "extracted"} adds each HTML table to pages[].tables. The page Markdown stays unchanged. format is markdown or html.
imagesannotate reads the text of each image into alt_text. include_base64 adds each image as base64.
confidence_scorestrue returns a confidence for each page.
preferred_regionglobal, eu or us: where the request can run.

See the OCR reference for each field.

Tables

Send "tables": {"mode": "extracted", "format": "html"} to get separate table entries from the HTML tables in each page. Each entry has an id, such as tbl-0.html, its content, and its format. Table IDs start at zero on each page. The markdown field keeps the full page, including its tables.

Use html to retain header cells and merged cells with rowspan and colspan. The table content uses normalized HTML and keeps the cell text. The default format, markdown, returns a Markdown table. A merged cell keeps its text in its first position; its other positions are empty. Multiple header rows become one header row, with labels separated by /. A table with no header gets an empty header row, so its first data row stays in the body.

A page with no HTML tables returns tables: []. Empty tables also produce no entries. For broken HTML, the response includes the cells that the parser can read. A table parse error does not fail the page. If you omit tables, or set mode to inline, the response keeps its default behavior.

Use lightonai/lightonocr-3-4b when you need table structure. munito/ocr-fast returns text without table markup. munito/ocr-auto also returns plain text for pages that it reads from their text layer. See Document AI for a complete example.

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.

See Errors for each status and its body.