space ocr
GuidesArticlesPricingDocs
developer

Same documents, same schema, different answers: space-ocr vs Mistral

A controlled benchmark: 8 cases over 7 documents, 463 fields, 3 runs per engine. Field accuracy, run-to-run stability, and the coordinate gap no score table shows.

11 min read· 2026-08-31

space-ocr is our document reading API. You send a photo and a list of the values you want out of it, and you get those values back as data, each one carrying the exact spot on the page it was read from. This post is a comparison of it against Mistral Document AI, run on the documents that are actually hard.

Every OCR vendor's demo reads a clean printed invoice perfectly, ours included, and that tells you nothing about the decision you are making. Three questions do. What happens on awkward documents. Whether the answer changes when you run the same page twice. And how you find the wrong values without re-checking every one of them by hand. We measured all three, and everything below is in here, including the parts that don't flatter us.

What we ran

Eight scoring cases, 463 values in total, over seven photographs: three Japanese delivery slips, a supermarket receipt that prints an item's name on one line and its quantity and price on the next, a 24-row order sent as a chat message, a commission statement, and a 22-row wholesale price sheet photographed off a monitor. They are the image-bearing cases of our own regression suite, the set we use to catch ourselves breaking things.

Seven photographs but eight cases, because two of the cases are the same chat-order photo with the same answer key, kept separate in our suite to exercise different code paths. Both engines simply saw that image twice. It is worth knowing that one photo therefore accounts for 144 of the 463 values.

Three arms, all given identical images, an identical list of fields with identical descriptions, and the same instruction to copy what is printed:

armwhat we calledhow the fields were asked for
space-ocrproduction POST /ocr/fieldsfield list with names and descriptions
Mistral OCRmistral-ocr-latest, /v1/ocr with document_annotationthe same list as a strict JSON schema
Mistral vision LLMmistral-medium-latest, image in chatthe same list as response_format: json_schema, temperature 0

Three runs per arm per document, 72 runs in total, all of them archived. Scoring is one script for all three arms: strip whitespace, compare to a hand-written answer key, exact match or nothing. August 2026.

Benchmark result card: space-ocr 91.1%, Mistral OCR 70.0%, Mistral vision LLM 73.5% on 8 scoring cases over 7 documents and 463 fields
8 scoring cases over 7 documents, 463 fields, 3 runs each, same schema and scorer. August 2026.

The headline, and the caveat that goes with it

space-ocr read 91.1% of the 463 values to an exact match, averaged over three runs. Mistral's OCR annotations landed at 70.0%, its vision LLM at 73.5%.

Some of every engine's misses are cosmetic: a currency symbol kept, a unit suffix left on a number. So we also stripped out every mismatch that differed only in delimiters or spacing and counted content errors alone. Roughly 23 per run for space-ocr, against 119 for Mistral OCR and 93 for the vision LLM. The gap survives the cleanup.

One more thing before the numbers. The answer key was transcribed against our own pipeline's conventions for where one value ends and the next begins, so every live engine, ours included, loses points to boundary disagreements. Read the gap between the arms. The absolute scores are floors, not grades.

Every document, every score

Mean values correct out of the total, averaged over the three runs.

documentvaluesspace-ocrMistral OCRMistral VLM
Price sheet, 22 rows, photographed off a monitor141139.0 (98.6%)139.0 (98.6%)133.7 (94.8%)
Commission statement4442.7 (97.0%)39.0 (88.6%)34.7 (78.8%)
Delivery slip C5350.3 (95.0%)43.7 (82.4%)47.3 (89.3%)
Delivery slip B3734.3 (92.8%)18.0 (48.6%)24.3 (65.8%)
Two-line supermarket receipt1312.0 (92.3%)2.3 (17.9%)1.3 (10.3%)
Chat order, 24 rows (case 2 of 2)7261.0 (84.7%)21.3 (29.6%)41.7 (57.9%)
Chat order, 24 rows (case 1 of 2)7258.0 (80.6%)39.7 (55.1%)42.3 (58.8%)
Delivery slip A3124.7 (79.6%)21.0 (67.7%)15.0 (48.4%)
all463422.0 (91.1%)324.0 (70.0%)340.3 (73.5%)

The top row is the one to look at first. On the cleanest document in the set, a dense printed table with 141 values, Mistral OCR scored exactly what we did. Reading characters is not where these engines differ, and any comparison that hides that is selling you something.

The bottom of the table is where they do differ, and the pattern is consistent: the harder the layout, the wider the gap. Note also the two chat-order rows. Same photo, same answer key, same schema, sent twice. We came out 3 values apart on the two passes. Mistral OCR came out 18 apart.

Run the same page three times

Totals out of 463, one column per run.

armrun 1run 2run 3spread
space-ocr43041442216
Mistral OCR322275375100
Mistral vision LLM36032933231

A 100-value spread is not noise around a mean, it is two different products. One Mistral OCR run returned 3 of 72 values on a document it had scored 58 of 72 on an hour earlier, because the annotation layer scrambled every column of a 43-row table. Nothing in the response distinguishes that run from a good one.

What the misses actually look like

Here is the two-line receipt, run 1, the document with the widest gap. The receipt prints the product name on one line and the quantity and unit price on the next, and the totals block sits directly underneath in the same column.

valueprinted on the pagespace-ocrMistral OCR
item 1 nameポッカサッポロ果実のcorrect006142 ポッカサッポロ 果実の
item 1 quantity12correct12コ
item 1 unit price98correct単98 ¥1,176
item 2 name塩パンcorrect011102 塩パン
date2017年07月30日(日)correct2017年07月30日(日) No.2805
total1,451¥1,451¥1,451

Read the last row before you read the rest. On the total, both engines returned the same thing and both are scored wrong, because the answer key has no currency symbol. That is the kind of miss that inflates every engine's error count, ours included, and it is why the content-only number above matters more than the raw one.

The rest of the column is a different failure. Mistral is not misreading characters there. It is stapling the shelf code to the product name and the price to the quantity line, because it never worked out that two printed lines are one record.

The 24-row chat order fails in a way that is worth seeing in full, because it is the failure mode that costs the most downstream:

rowprintedMistral OCR returned
1, noteヒチョウ(empty)
2, itemクエヒチョウ
2, note頭落とし
3, itemブリクエ
3, noteサクラブリ(empty)

Every row has slid up by one. Individually each value is a real string from the page, spelled correctly. As a table it is wrong from the second row down, and nothing about the response looks unusual.

Our own misses on that page were smaller but they were there: ヒチョウ came back as ヒチョウ背, 冷凍 as 冷凍 2L. We merged two adjacent cells instead of shifting the table. On delivery slip B we read a unit price of 1510 as 510, dropping a digit that the neighbouring column had glued on.

The part no score table can show

Ask both engines for the same 141 values on the price sheet and you get two answers back that look equally usable, until you ask where each value came from.

Side by side: space-ocr draws 138 per-field boxes on a 22-row price sheet; Mistral returns a single block rectangle covering the whole table
The same price sheet through both engines. Left: one box per value. Right: the whole table as a single block. Business names and phone numbers are pixelated by us before publishing.

space-ocr returns a box per value, anchored to the pixels it was read from. In the Mistral responses we archived for this benchmark (mistral-ocr-latest, August 2026), the same price sheet came back as a single rectangle covering the whole table. Those responses carried no per-value coordinates, and a paragraph block was the finest granularity we saw in them. What the API returns today is worth checking against Mistral's current documentation before you design around either behaviour.

Those responses carried no verification signal either. Every space-ocr value arrives in data.cells[path]: a box and a quad on a 0 to 1000 grid, a verified verdict, a review object whose reasons array names in machine-readable codes what did not check out, and an evidence object holding the character cross-check itself (text_match), the match_ratio behind it and, when the recogniser can report one, ocr_confidence. The values that want a human are collected in one place, data.review.flagged. Declaring a label on a field anchors which occurrence to take when the same value repeats down a page. Read verified: true for what it is: the checks that ran found nothing, not a promise that the value is the one you wanted.

That is what changes the work. With boxes and a review list, values that are quietly wrong get surfaced, and a person starts from the handful that are flagged instead of re-reading all 141. It is not a net that catches everything, since a cross-check has nothing to raise when both readings agree on the same answer, which is exactly what the total row above looks like. But a flagged wrong value costs a glance, and an unflagged one costs whatever you built on top of it.

The whole set, both engines

All seven photographs, left and right the same image, with the boxes drawn straight from the archived responses of the benchmark's first run. Green is one space-ocr value, orange is one the engine flagged for review in that run, and blue is one Mistral OCR block. Third-party names, addresses, phone numbers, account numbers and staff names are pixelated before publishing, which is also why some boxes sit over grey rectangles.

Two-line supermarket receipt: 13 space-ocr field boxes against 8 Mistral blocks
Two-line receipt, 13 values. space-ocr 12.0, Mistral OCR 2.3. The paired lines are visible in the boxes on the left: name, then quantity and price directly under it.
A 24-row order sent as a chat message: 72 space-ocr field boxes against 5 Mistral blocks
A 24-row order sent as a chat message, 72 values. space-ocr 58.0 and 61.0 on the two passes, Mistral OCR 39.7 and 21.3. Five blocks on the right for the whole order.
Japanese delivery slip photographed on a table: 27 space-ocr field boxes against 23 Mistral blocks
Delivery slip A, 31 values, the lowest score of the set for both engines. space-ocr 24.7, Mistral OCR 21.0. Shadow across the paper and a form printed in faint grey rules.
Delivery slip photographed from above on a warehouse floor: 38 space-ocr field boxes against 22 Mistral blocks
Delivery slip B, 37 values. space-ocr 34.3, Mistral OCR 18.0. Six item rows, each with a code line under the product name.
Printed delivery slip on a dark surface: 53 space-ocr field boxes against 16 Mistral blocks
Delivery slip C, 53 values. space-ocr 50.3, Mistral OCR 43.7. A cleaner print, and the gap narrows accordingly.
Commission statement: 44 space-ocr field boxes against 15 Mistral blocks
Commission statement, 44 values. space-ocr 42.7, Mistral OCR 39.0. This one is our own remittance statement, so the partner names are pixelated and the amounts left as they were.
✓ Verified

How the verification works, since it is the whole argument: the language model returns values and word-level hints, never coordinates. The engine then matches each returned value character by character against the symbols the OCR pass actually detected on the page, lands the box on those symbols, and scores the match as evidence.match_ratio, where 0.85 and above counts as a confident match. That ratio is one piece of evidence behind the verdict, not a gate on its own: where the two readings disagree, review.reasons picks up a code such as text_mismatch, verified comes back false, and the value joins data.review.flagged, the list of what to look at. Coordinates arrive per value as box, which is xmin/ymin/xmax/ymax on a 0 to 1000 grid, plus quad, four ordered corner points for pages that sit at an angle.

What this does not measure

Field extraction, on eight cases, in August 2026. Not speed, not price, not Mistral's markdown conversion, which is a different product doing a different job and was not part of any run here. Not general document reading either: eight cases from one company's regression suite is a small, deliberately nasty sample, weighted towards Japanese business forms and photographs taken in bad light.

The raw material is all kept: the runner script, the 72 scored runs, every raw response from both vendors, and the script that drew the images above.

The most useful benchmark is still your own. Take five documents that give you trouble, write the answer key by hand first, send the same field list to both APIs, and run each of them three times. The steps below are what we did. A successful scan costs $0.05, failed scans are never charged, and the first 100 pages every month are free, so the running cost of checking this yourself is roughly nothing.

  1. Pick hard documents, not demos
    Choose 5 to 10 real pages that hurt: multi-line records, repeated values, photos of screens, dense tables. Clean samples cannot separate vendors.
  2. Freeze one schema and one instruction
    Write the field list once, with the same names and descriptions, and send it identically to every engine. Ask for values exactly as printed.
  3. Write the ground truth by hand
    Transcribe the expected value for every field before running anything, and do not edit it afterwards.
  4. Run each engine at least three times
    One run hides instability. Score every run with the same script and look at the spread, not just the mean.
  5. Count the silent errors separately
    For each wrong value, note whether the engine flagged it. A wrong value with a review flag costs a glance; a wrong value with no signal costs whatever your process built on top of it.
Is Mistral OCR bad at reading documents?
No. On clean dense print its character reading tied ours. The gap we measured is in document structure (pairing multi-line records, keeping table columns straight), in run-to-run stability, and in what came back alongside the values: in the responses we archived in August 2026, ours carried per-field coordinates and a review list and Mistral's did not.
Can I reproduce this benchmark?
Yes. The corpus definition, the exact requests, the scoring script and all 72 raw outputs are archived. The post describes the full method: same images, same field schemas, same instruction, same scorer, three runs per arm.
Why should I trust a vendor's own benchmark?
Treat it as a starting point, not a verdict. We published the method, the caveats, and the case where Mistral tied us. The strongest evidence is structural and you can check it in a single call of your own: ask both APIs for the same fields and look at what comes back beside each value. In our August 2026 runs one returned per-field coordinates and a `review.flagged` list and the other did not, so check what each vendor returns today before you decide.
Does space-ocr win on every document?
No. On the cleanest printed table the two engines read the same number of fields. The differences that held across the corpus were structure handling, consistency between runs, and the verification layer.
What do per-field coordinates actually give me?
Auditability. A human can click a value and see exactly where it was read, review only the flagged fields instead of everything, and catch wrong values before they reach a spreadsheet or an ERP.

Run your own benchmark

100 pages free every month. Every value comes back with its box and a trust verdict.