Convert PDFs to Markdown in bulk with Mistral OCR 4.1
Turn a folder of PDFs into Markdown with labelled blocks using Mistral OCR 4.1's batch API, at $2 per 1,000 pages, in about 20 minutes.

What you need
Step 1: Get a key and install the SDK
3 minThe SDK reads your key from an environment variable, so the key never sits inside your scripts.
- In Mistral Studio, open API Keys and select New key.
- Give it a name and an expiration date, then create it and copy the key. It's shown only once.
- In your terminal, run the commands below with your key in place of [YOUR API KEY].
Run thisexport MISTRAL_API_KEY="[YOUR API KEY]" pip install "mistralai>=2"
You should see the SDK install finish with no errors.
Requests fail with
Invalid API Key?Check that
MISTRAL_API_KEYholds the whole key. The variable only lasts for one terminal session, so set it again in each new window.Keep the key out of scripts, notebooks and shared files. If it leaks, delete it in Studio and make a new one.
Step 2: Build the batch file
5 minA batch file holds one OCR request per line, and the whole batch runs at half the normal price.
- Put your PDFs in a folder named
docs, and createmake_batch.pynext to it. - Paste the script below and run it with
python make_batch.py. - Each line uses the file name as its
custom_id, so you can match every result to its PDF later.
Your scriptimport base64, json, pathlib with open("batch.jsonl", "w") as out: for pdf in sorted(pathlib.Path("docs").glob("*.pdf")): data = base64.b64encode(pdf.read_bytes()).decode() line = { "custom_id": pdf.name, "body": { "document": { "type": "document_url", "document_url": f"data:application/pdf;base64,{data}", }, "include_blocks": True, }, } out.write(json.dumps(line) + "\n") print("added", pdf.name)You should see an
addedline for each PDF, and a newbatch.jsonlfile in the folder.One PDF is over 50 MB or 1,000 pages?
Split it into smaller files before you build the batch. The OCR API only takes files within both limits.
You don't need to turn pages into images first. OCR 4.1 reads the PDF itself, and it also reads Word, PowerPoint and image files.
- Put your PDFs in a folder named
Step 3: Upload the file and start the job
3 minThe job runs on Mistral's servers, so it keeps going even if you close the terminal.
- Create
start_job.py, paste the script below and run it. - Copy the job ID it prints.
- Save the ID for the next step: run
export JOB_ID="[JOB ID]"in the same terminal.
Your scriptimport os from mistralai.client import Mistral client = Mistral(api_key=os.environ["MISTRAL_API_KEY"]) with open("batch.jsonl", "rb") as f: batch_file = client.files.upload( file={"file_name": "batch.jsonl", "content": f}, purpose="batch" ) job = client.batch.jobs.create( input_files=[batch_file.id], model="mistral-ocr-latest", endpoint="/v1/ocr", ) print(f"Job ID: {job.id}, status: {job.status}")You should see a job ID and the status
QUEUEDorRUNNING.Python can't import
mistralai.client?You have the older 1.x SDK. Run
pip install --upgrade "mistralai>=2", then run the script again.mistral-ocr-latestpoints to OCR 4.1 today. Usemistral-ocr-4-1instead to pin the version.- Create
Step 4: Wait for the results
5 minResults arrive all at once, when every request in the batch is done.
- Create
get_results.py, paste the script below and run it. - It checks the job every 10 seconds, then saves
results.jsonlwhen the job ends. - If some requests failed, it also saves
errors.jsonlso you can see why.
Your scriptimport os, time from mistralai.client import Mistral client = Mistral(api_key=os.environ["MISTRAL_API_KEY"]) job = client.batch.jobs.get(job_id=os.environ["JOB_ID"]) while job.status in ("QUEUED", "RUNNING"): print(f"Status: {job.status}, {job.succeeded_requests} done") time.sleep(10) job = client.batch.jobs.get(job_id=job.id) print(f"Status: {job.status}") if job.output_file: with open("results.jsonl", "wb") as f: f.write(client.files.download(file_id=job.output_file).read()) if job.error_file: with open("errors.jsonl", "wb") as f: f.write(client.files.download(file_id=job.error_file).read())You should see
Status: SUCCESSand a newresults.jsonlfile.The status ends as
TIMEOUT_EXCEEDED?A job expires after 24 hours by default. Split the batch, or pass a longer
timeout_hourswhen you create the job. It must stay under 7 days.The wait depends on the batch size and on demand. Batch results don't expire, so you can download them later.
- Create
Step 5: Save each PDF as Markdown
4 minEach result holds every page as Markdown, plus labelled blocks that show where each part sits on the page.
- Create
save_markdown.py, paste the script below and run it. - Open the new
markdownfolder and compare one file with its PDF. - Use each page's
blockslist to find titles, tables or signatures by theirtype.
Your scriptimport json, pathlib out_dir = pathlib.Path("markdown") out_dir.mkdir(exist_ok=True) for line in open("results.jsonl"): result = json.loads(line) pages = result["response"]["body"]["pages"] text = "\n\n".join(page["markdown"] for page in pages) name = pathlib.Path(result["custom_id"]).stem + ".md" (out_dir / name).write_text(text) blocks = sum(len(page.get("blocks") or []) for page in pages) print(name, len(pages), "pages,", blocks, "blocks")You should see one
.mdfile per PDF, with a page and block count printed for each.The block count is 0?
Blocks need OCR 4 or newer, and older models return an empty list. Check that the job used
mistral-ocr-latestand that each line setsinclude_blocks.Each block has a
type, such astitle,tableorsignature, and its corners astop_left_x,top_left_y,bottom_right_xandbottom_right_y.- Create
You get
A folder of Markdown files from your PDFs, with labelled blocks and positions for every page, at $2 per 1,000 pages
Try next
Did this guide work for you?
Thanks. It helps us fix the steps.


