IronStratum Get an API key

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:

  1. The diplomatic flag. Verbatim-fidelity transcription for archaic typography, on or off per request.
  2. 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.
  3. 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.

Pagedots.mocr CERdots.ocr CER
Title page0.66320.6824
Dense body page0.28750.9453
Body page0.48150.5309
Mixed page0.54500.3365
Light page0.21080.2054
Mean0.43760.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 categorydots.mocrdots.ocr
Old scans48.240.9
Old scans math85.564.2
Tables90.788.3
Overall83.9 +/- 0.979.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

  1. 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.
  2. 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.
  3. Create a key and send a page. Two file parts and a Bearer key against POST /v1/ocr is the whole first call; the response's pages field is the number that was metered.
  4. 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.

What the platform serves

Specialty models — kind and unit price
ModelKindPrice
ocr-archivalocr—

This model is not on the public list yet. Price cells stay dashed until it is, and nothing here is a live rate.

Questions

What is dots.ocr archival, and which model answers the calls?
It is this platform's archival OCR lane, built on the dots.ocr model family from the rednote lab. dots.ocr was released in July 2025 with a 1.7B-parameter language model foundation. dots.mocr is the 3B successor, released in March 2026 under its earlier name dots.ocr-1.5. Both sets of weights ship under MIT. The lane can run either candidate. The shipped default is dots.mocr because it won the platform's paired bake-off on historical printed pages. The older dots.ocr remains available as the in-image fallback. That is a deploy-time selection, not a request parameter.
dots.ocr or dots.mocr: which is better for old scans?
For old printed scans, the evidence points to dots.mocr, and two independent sources agree. On the official olmOCR-bench table, dots.mocr scores 48.2 on the Old scans category against 40.9 for dots.ocr. It scores 85.5 against 64.2 on Old scans math. On this platform's own paired bench, five historical book pages scored a mean character error rate of 0.4376 for dots.mocr. dots.ocr scored 0.5401. dots.ocr over-generates on the densest page, wins the mixed page outright, and edges the light page by half a point of CER, inside the fixture class the bench itself describes. It also runs about 1.8 times faster in serial wall time, so a speed-first pipeline has a real tradeoff to weigh. Accuracy is the ruling axis here, which is why dots.mocr is the default.
Does archival OCR handle handwriting?
No, and this page will not claim it. The dots.ocr family is a print-and-layout parser. It handles scanned books, serials, typeset documents, and the typography problems of old print such as the long s, ligatures, and mixed reading order. Handwriting recognition is a different model class with its own training and its own specialist vendors. If your corpus is manuscript material, use a handwriting tool for that part of the job. Use this lane for the typeset pages.
How does per-page metering work?
Successful calls bill by the page count the engine reports. On a batch, when the engine does not report a positive count, the platform counts the file parts you sent instead. A successful batch is never billed as free. The job read has no body to recount. A job that finishes without reporting a page count leaves its completion row unpriced and visible to reconciliation, never guessed. A batch returns its full answer in one response. A job meters once, on the read that observes the job finish. Refused requests bill nothing. A request that fails validation, or that the model side rejects, never touches the wallet. The /pricing page carries current rates.
What does the diplomatic flag do?
It switches the lane toward verbatim transcription. Old printed pages carry typography a modern normalizer quietly rewrites. The list includes the long s that looks like an f, ligatures, archaic case, and spacing. With diplomatic set to t, the lane is instructed to preserve what is printed rather than modernize it. That is what a citation-grade transcription of a 19th-century page needs. The flag is a form field, t or f. The platform implements it as a verbatim-fidelity instruction on the model side, because neither dots repo ships a native diplomatic mode.
Can I self-host dots.ocr instead of calling an API?
Yes, fully. The weights are MIT-licensed, and vLLM lists dots.ocr in its supported-models table under the dots_ocr architecture. A current vLLM server runs it natively, and a transformers path exists too. What self-hosting does not give you is the work around the model. That work includes the job route that carries full-resolution decodes past a proxy timeout. It includes the output-side loop guard for the repeat failure mode the model card documents, the blank-page scrub, and per-page metering against a wallet. The lane here exists so you can rent that work per page. The MIT license means you can always take the weights and leave.