Extract invoice data to structured JSON. Read the invoice PDF with the Munito AI OCR API, then get its fields from a chat model with a JSON schema.
To extract an invoice to structured JSON with Munito AI, send the PDF to POST /ocr to get Markdown, then send the Markdown to a chat model with a JSON schema.
The answer is JSON that matches the schema: the number, the date, the seller, the buyer, the total and each line.
How it works
| Step | Endpoint | Model | Result |
|---|---|---|---|
| 1. Read the document | POST /ocr | munito/ocr-auto | The Markdown of each page |
| 2. Extract the fields | POST /chat/completions | qwen/qwen3.8-27b | JSON that matches your schema |
Both steps use the same key and the base URL https://api.munito.ai/inference/v1.
To keep the processing in the EU, use https://api.eu.munito.ai/inference/v1. See Regions.
Complete script
Install the clients and set your key:
pip install openai requests
export MUNITO_API_KEY="munitoai_..."Save this file as extract_invoice.py:
# pip install openai requests
import base64
import json
import os
import sys
import time
import requests
from openai import OpenAI
HOST = "https://api.munito.ai"
BASE_URL = f"{HOST}/inference/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['MUNITO_API_KEY']}"}
INVOICE_SCHEMA = {
"type": "object",
"properties": {
"invoice_number": {"type": "string"},
"invoice_date": {"type": "string", "description": "ISO 8601 date, YYYY-MM-DD"},
"seller": {"type": "string"},
"buyer": {"type": "string"},
"currency": {"type": "string", "description": "ISO 4217 code, such as EUR"},
"total": {"type": "number"},
"lines": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {"type": "string"},
"quantity": {"type": "number"},
"unit_price": {"type": "number"},
"amount": {"type": "number"},
},
"required": ["description", "quantity", "unit_price", "amount"],
"additionalProperties": False,
},
},
},
"required": ["invoice_number", "invoice_date", "seller", "buyer", "currency", "total", "lines"],
"additionalProperties": False,
}
def ocr_markdown(path: str) -> str:
"""Read the PDF with Munito OCR and return the Markdown of all pages."""
with open(path, "rb") as f:
data = base64.b64encode(f.read()).decode()
response = requests.post(
f"{BASE_URL}/ocr",
headers=HEADERS,
json={
"model": "munito/ocr-auto",
"document": {"type": "base64", "data": data, "filename": os.path.basename(path)},
},
timeout=300,
)
response.raise_for_status()
body = response.json()
# A long document answers 202. Poll its location until the status is final.
while response.status_code == 202 or body.get("status") == "running":
time.sleep(2)
response = requests.get(HOST + body["location"], headers=HEADERS, timeout=60)
response.raise_for_status()
body = {**response.json(), "location": body["location"]}
if body.get("status", "completed") != "completed":
sys.exit(f"OCR ended with status {body['status']}")
return "\n\n".join(page["markdown"] for page in body["pages"])
def extract_fields(markdown: str) -> dict:
"""Ask a chat model for the invoice fields, as JSON that matches the schema."""
client = OpenAI(base_url=BASE_URL, api_key=os.environ["MUNITO_API_KEY"])
completion = client.chat.completions.create(
model="qwen/qwen3.8-27b",
messages=[
{"role": "system", "content": "Extract the invoice fields. Use only values that the invoice shows."},
{"role": "user", "content": markdown},
],
response_format={
"type": "json_schema",
"json_schema": {"name": "invoice", "schema": INVOICE_SCHEMA, "strict": True},
},
reasoning_effort="low",
max_tokens=4096,
)
return json.loads(completion.choices[0].message.content)
if __name__ == "__main__":
path = sys.argv[1] if len(sys.argv) > 1 else "invoice.pdf"
fields = extract_fields(ocr_markdown(path))
print(json.dumps(fields, indent=2))Run it on your invoice:
python extract_invoice.py invoice.pdfThe script prints the fields:
{
"invoice_number": "INV-2026-0417",
"invoice_date": "2026-10-15",
"seller": "Nordlicht Software GmbH",
"buyer": "Atelier Vert SARL",
"currency": "EUR",
"total": 1499.4,
"lines": [
{
"description": "Support plan, October",
"quantity": 1,
"unit_price": 900,
"amount": 900
},
{
"description": "Onboarding workshop (hours)",
"quantity": 3,
"unit_price": 120,
"amount": 360
}
]
}Choices in the script
munito/ocr-autoreads a page that is mostly text from its text layer, and sends a scanned page to an OCR model. For a scan or a photo, you can also namelightonai/lightonocr-2-1b. See PDF to Markdown.response_formatwithjson_schemamakes the answer match the schema. Add a field to the schema to extract it, such asdue_dateorvat_id.reasoning_effort: "low"and a largemax_tokenskeep the answer incontent.qwen/qwen3.8-27breasons before it answers, and a smallmax_tokenscan end the request before the answer, withcontentset tonull.- Base64 sends a local file. To send a file that is on the web, use
{"type": "url", "url": "https://..."}in place of the base64 document.
Cost
One extraction costs the OCR pages plus the chat tokens: the input tokens of the Markdown and the schema, and the output tokens of the JSON.
The OCR answer shows the page cost in usage.cost_usd, and the chat answer shows its tokens in usage. A page that fails is not billed.
See Models and pricing for the price of each page and each token.
Errors
| Status | Cause | Fix |
|---|---|---|
413 Document exceeds the configured size limit | The file is larger than 50 MiB. | Split the PDF. |
422 This document type is not supported. Send a PDF or an image. | The file is not a PDF or an image. | Convert the file to PDF. |
422 Invalid base64 data | The data field is not valid base64. | Encode the raw bytes of the file. |
503 | No capacity now. | Wait for the Retry-After time, then send the request again. |
See Errors for each status and its body.
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.
Retrieval-augmented generation
Read documents with Munito AI OCR, index them with a local keyword index, and answer questions with citations from a Munito AI chat model.