Skip to content

LearnTutorials

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.

By Recaply EditorsAdvanced20 min5 stepsUpdated

A hand drops a stack of paper sheets into a wide funnel, and a single card falls into a tray below

What you need

  1. Step 1: Get a key and install the SDK

    3 min

    The SDK reads your key from an environment variable, so the key never sits inside your scripts.

    1. In Mistral Studio, open API Keys and select New key.
    2. Give it a name and an expiration date, then create it and copy the key. It's shown only once.
    3. In your terminal, run the commands below with your key in place of [YOUR API KEY].
    Run this
    export 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_KEY holds 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.

  2. Step 2: Build the batch file

    5 min

    A batch file holds one OCR request per line, and the whole batch runs at half the normal price.

    1. Put your PDFs in a folder named docs, and create make_batch.py next to it.
    2. Paste the script below and run it with python make_batch.py.
    3. Each line uses the file name as its custom_id, so you can match every result to its PDF later.
    Your script
    import 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 added line for each PDF, and a new batch.jsonl file 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.

  3. Step 3: Upload the file and start the job

    3 min

    The job runs on Mistral's servers, so it keeps going even if you close the terminal.

    1. Create start_job.py, paste the script below and run it.
    2. Copy the job ID it prints.
    3. Save the ID for the next step: run export JOB_ID="[JOB ID]" in the same terminal.
    Your script
    import 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 QUEUED or RUNNING.

    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-latest points to OCR 4.1 today. Use mistral-ocr-4-1 instead to pin the version.

  4. Step 4: Wait for the results

    5 min

    Results arrive all at once, when every request in the batch is done.

    1. Create get_results.py, paste the script below and run it.
    2. It checks the job every 10 seconds, then saves results.jsonl when the job ends.
    3. If some requests failed, it also saves errors.jsonl so you can see why.
    Your script
    import 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: SUCCESS and a new results.jsonl file.

    The status ends as TIMEOUT_EXCEEDED?

    A job expires after 24 hours by default. Split the batch, or pass a longer timeout_hours when 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.

  5. Step 5: Save each PDF as Markdown

    4 min

    Each result holds every page as Markdown, plus labelled blocks that show where each part sits on the page.

    1. Create save_markdown.py, paste the script below and run it.
    2. Open the new markdown folder and compare one file with its PDF.
    3. Use each page's blocks list to find titles, tables or signatures by their type.
    Your script
    import 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 .md file 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-latest and that each line sets include_blocks.

    Each block has a type, such as title, table or signature, and its corners as top_left_x, top_left_y, bottom_right_x and bottom_right_y.

You get

A folder of Markdown files from your PDFs, with labelled blocks and positions for every page, at $2 per 1,000 pages

Did this guide work for you?

Sources