Tools,

A Tesseract OCR API in 53 lines, no key needed

55 of 55

pages matched the tesseract command, on a Mac

Or call ours: $0.006 a page

There is no Tesseract API key

The project runs no hosted service, so wrap the command in a small server.

The server: 53 lines of Python

Standard library only: no Flask, no pytesseract. The image goes to Tesseract on stdin, and the text comes back. No temp files, no shell.

run = subprocess.run(["tesseract", "stdin", "stdout", "-l", lang],
                     input=page, capture_output=True, timeout=120)
server.py, all 53 lines
# A small Tesseract OCR REST API: POST a page image, get its text back.
# Run: python3 server.py
# Try: curl --data-binary @page.png "localhost:8884/?lang=eng"
import hmac
import os
import subprocess
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlparse

TOKEN = os.environ.get("OCR_TOKEN", "")  # set one before it goes online
MAX_BYTES = 20 * 1024 * 1024
listed = subprocess.run(["tesseract", "--list-langs"],
                        capture_output=True, text=True).stdout
LANGS = set(listed.split("\n")[1:]) - {""}


class OCR(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"  # so curl gets "100 Continue" at once

    def do_POST(self):
        size = self.headers.get("Content-Length", "")
        if not size.isdigit() or not 0 < int(size) <= MAX_BYTES:
            self.close_connection = True  # the body was not read
            return self.reply(413, "Send one image of up to 20 MB\n")
        page = self.rfile.read(int(size))
        auth = self.headers.get("Authorization", "")
        if TOKEN and not hmac.compare_digest(auth, f"Bearer {TOKEN}"):
            return self.reply(401, "Send Authorization: Bearer <token>\n")
        query = parse_qs(urlparse(self.path).query)
        lang = query.get("lang", ["eng"])[0].replace(" ", "+")  # a + in a URL means space
        if not all(code in LANGS for code in lang.split("+")):
            return self.reply(400, f"Languages: {' '.join(sorted(LANGS))}\n")
        try:
            run = subprocess.run(["tesseract", "stdin", "stdout", "-l", lang],
                                 input=page, capture_output=True, timeout=120)
        except subprocess.TimeoutExpired:
            return self.reply(504, "Took over 120 seconds\n")
        if run.returncode:
            return self.reply(422, "Tesseract could not read that file\n")
        self.reply(200, run.stdout.decode())

    def reply(self, code, text):
        body = text.encode()
        self.send_response(code)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


host = os.environ.get("HOST", "127.0.0.1")
port = int(os.environ.get("PORT", "8884"))
ThreadingHTTPServer((host, port), OCR).serve_forever()

Run it, then send a page:

python3 server.py
curl --data-binary @page.png "localhost:8884/?lang=eng"

Nothing lost over HTTP

55 book scans, 14,692 true words
Run Words found Pages a minute
tesseract command 14,167 65.0
server.py on the Mac 14,167 56.8
server.py in Docker 14,166 53.7

On the Mac, every answer matched the command byte for byte. Docker runs Debian's older build of Tesseract. Only 12 pages matched exactly, and it found 1 word fewer in all.

Docker: 7 lines, 339 MB

FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends python3 tesseract-ocr && rm -rf /var/lib/apt/lists/*
COPY server.py /app/server.py
ENV HOST=0.0.0.0 OMP_THREAD_LIMIT=1
USER nobody
EXPOSE 8884
CMD ["python3", "/app/server.py"]
docker build -t tesseract-api .
docker run -p 127.0.0.1:8884:8884 -e OCR_TOKEN=pick-one tesseract-api
curl -H "Authorization: Bearer pick-one" --data-binary @page.png "localhost:8884/?lang=eng"

-p 127.0.0.1 keeps the port on this machine. With OCR_TOKEN set, a request without the token gets a 401.

One line: 209 pages a minute, not 24

That line is OMP_THREAD_LIMIT=1. Debian's Tesseract is built with OpenMP, so each request starts its own threads. With 4 requests at once, that cost 89% of the speed.

Pages a minute in Docker, 8 CPUs
OMP_THREAD_LIMIT 1 at a time 4 at a time
1 (the Dockerfile) 53.7 209.2
Not set 57.1 23.9

What it says to a bad request

Request Answer
No token, or the wrong one 401
Empty, or over 20 MB 413
Language xyz or eng;ls 400
A PDF, or not an image 422
A GET 501

?lang=eng+osd works as typed. A plus in a URL means a space, so the server puts it back. My first draft answered 400, and my own test had missed it.

There is no limit on requests at once. Keep it private, or add one.

Or call ours: it reads PDFs too

What server.py Our Actor
PDF files No, 422 Yes
Output Plain text Markdown with tables
You run A server Nothing
curl -X POST "https://api.apify.com/v2/acts/brasspage~pdf-ocr-to-markdown/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"documentUrls": ["https://example.com/scan.pdf"]}'

It is slower for one page: 5.8 seconds against 0.12 on server.py, on Tesseract's own test page. Both returned 60 words.

Each run adds a $0.00005 start fee. Blank pages and failed pages are free.

Questions people ask

Does Tesseract need an API key?

No. Tesseract is free software that you run, under the Apache 2.0 license. Only a hosted service, like ours, asks for a key.

Where are the Tesseract API docs?

The C and C++ API is in libtesseract. Start with the API examples page in the Tesseract docs, then the headers capi.h and baseapi.h.

Can this server read a PDF?

No. Tesseract does not read PDF files, so the server answers 422. Turn each page into an image first, or send the PDF to our Actor.

Method, limits and sources

How I tested it
  • Pages: 55 printed pages from 11 public domain books on Wikisource, as JPEGs 1,920 px wide. I left out one hand-lettered book.
  • Truth: each page's proofread text. A word counts if the output holds it, in any order.
  • "Matched" means the SHA-256 of the output was the same.
  • Speed: one run each on an Apple M5 Max with other jobs running, so read it as rough.
  • The server and Docker commands ran as printed, with a test token.
What I did not test
  • Ready-made Tesseract server images from Docker Hub.
  • Windows, servers on the open internet, and the 120 second timeout.
  • Phone photos, handwriting, and languages other than English.
  • Hosted OCR APIs other than ours.
Versions and sources
  • Mac: Tesseract 5.5.3 from Homebrew.
  • Docker 29.8.2: Debian 13, Tesseract 5.5.0, Python 3.13.5.
  • Tesseract README, for the library and the command.
  • Tesseract FAQ, for OMP_THREAD_LIMIT.

Two ways to skip the work

Run it yourself. Our Apify Actor turns scanned PDFs into Markdown, tables included.

Try the Actor

Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First page free.

Get one page free