Document OCR API hosted in Europe: PDFs, scans and handwriting to Markdown

Convert PDFs, scans and handwritten pages to Markdown with an OCR API in the EU or the US. Extract tables and invoice fields, pay for each page, and keep zero data retention under a GDPR DPA.

The Munito AI OCR API reads PDFs and images, such as scans and handwritten pages, and returns the Markdown of each page. It runs in the EU or the US, and it bills each page. Munito AI deletes the result when it delivers it.

Send the document to POST /ocr by URL or as base64. The base URL is https://api.munito.ai/inference/v1. To keep the work in the EU, use https://api.eu.munito.ai/inference/v1.

Convert a PDF to Markdown

The answer has one entry in pages for each page, with the Markdown of the page in markdown. PDF to Markdown gives a complete script, with page selection and long documents.

The model sets how Munito AI reads each page:

ModelHow it reads a pageWhat markdown holds
lightonai/lightonocr-2-1bWith the OCR model LightOnOCR-2-1BMarkdown with headings, paragraphs and tables
munito/ocr-auto, the defaultFrom the text layer when the PDF page is mostly text. With an OCR model for other pages, such as scans.Plain text for a page from its text layer, Markdown for a page that an OCR model reads
munito/ocr-fastFrom the text layer of a PDF onlyPlain text with no headings and no tables. A scan gives empty text and costs nothing.

Read scans and handwriting

A page with no text layer, such as a scan or a handwritten page, needs an OCR model. lightonai/lightonocr-2-1b and munito/ocr-auto send such a page to an OCR model, and munito/ocr-fast returns empty text for it. Send an image, such as a PNG, in the same way as a PDF. See formats and limits.

LightOnOCR-2-1B reads printed text and handwriting, also cursive. Its accuracy depends on how clear the writing is, so check the Markdown of a page that is hard to read.

Extract tables from scanned PDFs

LightOnOCR-2-1B writes each table as an HTML table in the Markdown of its page: one <tr> for each row, and one <th> or <td> for each cell. This is the Markdown of a scanned page with a title, a sentence and a table:

# Regional sales report, Q3 2026

This report gives the units sold and the revenue of each region in the third quarter.

<table>
  <tr>
    <th>Region</th>
    <th>Units</th>
    <th>Revenue (EUR)</th>
    <th>Change</th>
  </tr>
  <tr>
    <td>France</td>
    <td>1,240</td>
    <td>86,800</td>
    <td>+12%</td>
  </tr>
  <tr>
    <td>Germany</td>
    <td>980</td>
    <td>68,600</td>
    <td>+4%</td>
  </tr>
  <tr>
    <td>Spain</td>
    <td>415</td>
    <td>29,050</td>
    <td>-3%</td>
  </tr>
  <tr>
    <td>Italy</td>
    <td>530</td>
    <td>37,100</td>
    <td>+8%</td>
  </tr>
</table>

The total revenue is 221,550 EUR.

This script sends a scanned PDF and prints each table. pandas.read_html reads the HTML tables of the Markdown into data frames.

# pip install requests pandas lxml
import base64
import os
import sys
from io import StringIO

import pandas as pd
import requests

with open(sys.argv[1], "rb") as f:
    data = base64.b64encode(f.read()).decode()
response = requests.post(
    "https://api.munito.ai/inference/v1/ocr",
    headers={"Authorization": f"Bearer {os.environ['MUNITO_API_KEY']}"},
    json={"model": "lightonai/lightonocr-2-1b", "document": {"type": "base64", "data": data}},
    timeout=300,
)
response.raise_for_status()
for page in response.json()["pages"]:
    if "<table" in page["markdown"]:
        for table in pd.read_html(StringIO(page["markdown"])):
            print(f"Page {page['index']}:")
            print(table.to_string(index=False))
export MUNITO_API_KEY="munitoai_..."
python scan_tables.py scan.pdf

For the scanned page above, the script prints:

Page 0:
 Region  Units  Revenue (EUR) Change
 France   1240          86800   +12%
Germany    980          68600    +4%
  Spain    415          29050    -3%
  Italy    530          37100    +8%

The script expects the full result in the answer, as for a short document. A long document answers 202 with a location to poll instead. See Long documents.

A page that Munito AI reads from its text layer gives each cell of a table on its own line, with no table markup. This applies to munito/ocr-fast, and to munito/ocr-auto on a page that is mostly text. To keep the tables of a PDF that software made, use lightonai/lightonocr-2-1b.

Extract invoices to structured data

Read the invoice with POST /ocr. Then send its Markdown to a chat model, such as qwen/qwen3.8-27b, with a JSON schema in response_format. The answer is JSON that matches the schema, for example the invoice number, the seller, the buyer, the total and each line.

Both requests use the same key and the same host. Send both to the EU host to keep the invoice in the EU. Invoice extraction gives the complete script.

OCR in the EU, for GDPR

  • Send the request to the EU host, https://api.eu.munito.ai/inference/v1. The OCR model then reads the pages in the EU. The response has the header munito-region: EUROPE, and usage.inference_region is eu.
  • Munito AI deletes the result when it delivers it. A second request for the same result gets 410.
  • Zero data retention is the default, and Munito AI does not train models on your documents. See Data privacy.
  • The GDPR Data Processing Agreement (DPA) forms part of the Terms of Service. See GDPR and EU data residency.

An OCR request keeps data while it runs, so it must run in the data region of your workspace. Send it to the global host or to the host of that region. The other regional host answers 421. See Regions.

Price per page

Munito AI bills OCR for each page, at a posted price for each 1,000 pages. The model, the region and the service tier set the price of a page. Models and pricing lists each price.

Each answer shows what the request costs:

FieldMeaning
pages[].price_usdThe price of the page
usage.pages_processedThe number of pages that Munito AI reads and bills
usage.pages_failedThe number of pages that failed. Munito AI does not bill them.
usage.cost_usdThe price of the request: the sum of the page prices
usage.price_per_page_usdThe average price of one page

To pay for fewer pages, send pages with the numbers of the pages to read. The flex service tier costs less for each page than standard: send the header munito-service-tier: flex.