ocr-archival: dots.ocr archival OCR as an API
The dots.ocr family from the rednote lab holds state-of-the-art benchmark results among open document parsers its size. dots.ocr shipped in July 2025 as a single vision-language model that reads layout and text together. Its 3B successor dots.mocr arrived in March 2026. On this platform the family runs behind the ocr-archival alias, in a measured configuration pointed at old scanned books. That is the class of print where older OCR engines age worst. The lane is built and benchmarked against its own candidates, and its per-page rate is set and carried in this page's search listing. It is not on the public models list yet, and no date is promised. This page covers what the lane is, the exact request, what our own measurements and the official benchmarks say, and where the self-host road starts.
Last verified: 2026-09-25
What it is
Two models, one lane. dots.ocr parses multilingual documents in a single vision-language model built on a compact 1.7B-parameter language model foundation, released 2025-07-30 under MIT. dots.mocr, described in the paper Multimodal OCR: Parse Anything from Documents, is the 3B successor that also converts charts and figures toward SVG. It was first released under the name dots.ocr-1.5 and rebranded on 2026-03-19, per the project README. So a "dots.ocr-1.5" you see referenced anywhere is this same dots.mocr.
The archival lane is the platform's name for running this family pointed at historical print. dots.mocr is the default, selected by a paired bake-off on historical book pages rather than by spec sheet. The benchmarks section carries the numbers, including the mixed page dots.ocr wins. Its slot is deliberately model-agnostic. Both candidates are pinned in the serving image, and the older dots.ocr remains the built-in fallback.
The lane is not on the public list yet, and no date is promised. The console carries the ocr-archival alias with its per-page rate. Metering is per page, from a prepaid wallet, and the models index shows the catalog as it stands.
Use cases
- Re-OCR of scanned book corpora. Libraries and archives hold millions of pages whose existing text layer came from an older engine. A modern VLM parser run over those page images again is how stale text layers get replaced. The job route exists because full-resolution book pages decode slowly.
- Verbatim transcription of historical print. Citations, scholarly editions, and digitization programs that must record what the page actually says need transcription rather than normalization. That means long s and ligatures included. That is the difference the diplomatic flag controls, and the lane was measured with it on.
- Newspaper and serial page text layers. Multi-column layouts with mixed reading order are the class of page where pipeline OCR historically falls apart. Reading order is one of the axes the dots family is benchmarked on.
- Search and RAG over scanned sources. Faithful page text is the input every retrieval pipeline needs. Grounding a collection of scanned books so it can be asked questions starts with getting the page text right. Then the text goes to the embedding and rerank stages.
What archival means here
Three behaviors, all measured, separate this lane from pointing a generic vision model at an old page:
- The diplomatic flag. Verbatim-fidelity transcription for archaic typography, on or off per request.
- Blank-page honesty. A blank page returns empty text. The lane scrubs whitespace-only rambling and does not hallucinate content onto a page that has none, a documented failure class for language-model OCR.
- A loop guard. The dots.ocr card warns that continuous special characters such as ellipses and underscores can make output repeat endlessly. The platform watches output for that signature and stops it, because neither dots repo ships a guard of its own.
API usage
Calls go to POST /v1/ocr as multipart/form-data with a console-issued Bearer key. The accepted form set includes model and mode (print or archival; when present it must match the alias's lane). It also includes diplomatic (t or f, the fidelity flag) and language. Keys outside the set are refused with a parameter error before anything is relayed. file parts carry the pages: one image part per page, and every file part counts. The whole body lives under a single 25 MiB cap.
A batch answers in one response. The first byte carries the full batch's answer, with a 90-second first-byte budget under the platform edge's 100-second wall. Long documents and full-resolution pages that decode slowly belong to the job route. POST /v1/ocr/jobs returns 202 with a job_id and meters nothing at creation. GET /v1/ocr/jobs/{id}?model=ocr-archival reads the status. The model query parameter on the status read is required. The read that observes the job finish is the one that meters, once. A finished job that reports no page count leaves its completion row unpriced rather than guessed. The batch face has no such hole, because the platform can count the file parts it relayed. Persist the payload when you read a finished job. The status read consumes it, so a re-poll of the same job id returns job_not_found.
curl
curl -X POST "https://api.ironstratum.com/v1/ocr" \
-H "Authorization: Bearer $KEY" \
-F "model=ocr-archival" \
-F "diplomatic=t" \
-F "file=@page-0001.png" \
-F "file=@page-0002.png"
The response is the card's answer as JSON:
{
"text": "BIRDS OF GREAT BRITAIN AND IRELAND. ORDER PASSERES...",
"pages": 2,
"mode": "archival",
"diplomatic": true,
"model": "dots.mocr",
"loop_detected": false,
"loop_pct": 0.0
}
python
import os
import requests
base = "https://api.ironstratum.com/v1"
headers = {"Authorization": "Bearer " + os.environ["KEY"]}
# batch: two pages, verbatim fidelity on
with open("page-0001.png", "rb") as a, open("page-0002.png", "rb") as b:
resp = requests.post(
base + "/ocr",
headers=headers,
files=[("file", a), ("file", b)],
data={"model": "ocr-archival", "diplomatic": "t"},
timeout=120,
)
resp.raise_for_status()
print(resp.json()["pages"], resp.json()["text"][:120])
# job route: full-resolution pages that decode past the batch budget
create = requests.post(
base + "/ocr/jobs",
headers=headers,
files={"file": open("title-page-full.png", "rb")},
data={"model": "ocr-archival", "diplomatic": "t"},
timeout=120,
)
create.raise_for_status()
job_id = create.json()["job_id"]
# the model query parameter is required on every status read
status = requests.get(
base + f"/ocr/jobs/{job_id}",
headers=headers,
params={"model": "ocr-archival"},
timeout=120,
)
status.raise_for_status()
payload = status.json() # persist this: the finished payload reads once
openai-sdk
Verified against openai-python 3.13 in docker, wire and response. The one non-obvious part: the low-level post needs the multipart Content-Type passed through options. Without it the SDK drops the form fields and pins application/json. The request is refused before it reaches the model.
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.ironstratum.com/v1",
api_key=os.environ["KEY"],
)
with open("page-0001.png", "rb") as f:
page = client.post(
"/ocr",
cast_to=dict,
body={"model": "ocr-archival", "diplomatic": "t"},
files={"file": ("page-0001.png", f, "image/png")},
options={"headers": {"Content-Type": "multipart/form-data"}},
)
print(page["pages"], page["text"][:120])
Benchmarks
The evidence runs on two dated layers.
Our lane. A paired bake-off, run 2026-09-07 on five text-bearing pages from historical English and Latin printed books. Ground truth came from the finebooks/bhl-impact-gt corpus, CC-BY-3.0. The render was the same 900-pixel render and the scoring was the same: character error rate on verbatim transcription with the diplomatic setting on, lower is better.
| Page | dots.mocr CER | dots.ocr CER |
|---|---|---|
| Title page | 0.6632 | 0.6824 |
| Dense body page | 0.2875 | 0.9453 |
| Body page | 0.4815 | 0.5309 |
| Mixed page | 0.5450 | 0.3365 |
| Light page | 0.2108 | 0.2054 |
| Mean | 0.4376 | 0.5401 |
Raw CERs here carry the fixture class. The ground truth itself contains digitization errors and reading-order artifacts, so read the paired difference, not the absolute level. The mean difference of 0.1025 is what ruled dots.mocr the default. dots.ocr wins the mixed page outright and edges the light page by 0.0054, inside the fixture class. It also finished the five pages in 977 seconds against 1741, roughly 1.8 times faster in serial wall time. A throughput-first pipeline has a real number to weigh.
Two lane behaviors were measured beside it. A 997 KB full-resolution page through the job route completed in 50 seconds with a clean verbatim read of the title page, a page class the batch budget cannot carry. And a blank page returns empty text rather than generated content, with the loop guard idle because there is no loop to stop.
The official numbers. From the dots.mocr card, whose olmOCR-bench table carries both models side by side (higher is better on all olmOCR-bench rows):
| olmOCR-bench category | dots.mocr | dots.ocr |
|---|---|---|
| Old scans | 48.2 | 40.9 |
| Old scans math | 85.5 | 64.2 |
| Tables | 90.7 | 88.3 |
| Overall | 83.9 +/- 0.9 | 79.1 +/- 1.0 |
On OmniDocBench v1.5, where lower is better, the same card lists dots.mocr at 0.031 TextEdit and 0.029 ReadOrder. dots.ocr sits at 0.048 and 0.053. The direction is the interesting part. The official Old scans gap (48.2 against 40.9) and this platform's independent paired bake-off point the same way on old print. Both cards still list complex tables and formulas as open problems. For self-hosting, vLLM's supported-models table lists the dots_ocr architecture natively. The dots.mocr card notes official integration since vLLM 0.11.0.
Getting started
- Open a console account through the invite flow. The console is invite-only while the platform is in beta; the keys, the wallet, and the spend history live there.
- Fund the wallet once. Prepaid balance stops the spend at arrival: once it is gone the next call is refused with a named error, and a key can be revoked at any time without touching the rest of the account.
- Create a key and send a page. Two file parts and a Bearer key against
POST /v1/ocris the whole first call; the response'spagesfield is the number that was metered. - Look up the rate once. Metering is per page, so one visit to the pricing page settles the budget, and any change to rates lands there first.
From there, the parsing family page covers the platform's other document routes, including the print-lane sibling for clean print pages. The models index lays out the full catalog.