OCR scanned documents and images through one API call

Feb 3, 2026 · 3 min read · ocr, api, llm-ingestion

A large share of “documents” in any real intake pipeline aren’t documents in the structured sense at all — they’re images. A scanned invoice from a supplier who still uses a fax-to-email service. A photographed receipt from an expense app. A signed contract that was printed, signed, and re-scanned because someone didn’t trust the e-signature. All of them arrive as PNGs, JPEGs, or image-only PDFs with zero extractable text.

If your extraction pipeline is built around a text-layer library — pdftotext, python-docx, anything that reads structured content — these files return nothing. Not an error, often just an empty string, which is the worst possible failure mode because it looks like success.

Why OCR needs to be a first-class path, not a special case

The usual fix is bolting Tesseract onto the side of a pipeline: detect that a file is an image, route it to a separate OCR step, merge the result back in. That’s a second dependency to install, a second set of language packs and model files to keep updated, and a second code path that inevitably drifts out of sync with whatever error handling and retry logic the “real” text extraction path has.

It also pushes a classification problem onto you: is this PDF text-based or scanned? Some are both — a scanned cover page stapled to a native PDF body. Get the routing wrong and you either waste a Tesseract call on a document that didn’t need it, or silently skip OCR on a page that did.

One endpoint, format detection included

txtfetch runs Tesseract as a Lambda layer alongside Apache Tika, and decides which one a request needs by inspecting the actual bytes — not the file extension, not a flag you have to set. A .jpg receipt, a scanned .pdf, and a native .docx all go through the same call:

curl -X POST https://api.txtfetch.com/v1/extract \
  -H "Authorization: Bearer $TXTFETCH_KEY" \
  -F file=@invoice-scan.jpg

Or, if the image is already sitting on a URL — say, an attachment in an inbound email pipeline:

curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/receipts/inv-4471.png" \
  -H "Authorization: Bearer $TXTFETCH_KEY"

The response shape doesn’t change based on whether OCR ran or the file had a native text layer:

{
  "status": "success",
  "extracted_text": "INVOICE #4471\nBill to: Acme Corp\nTotal due: $1,240.00"
}

Your downstream code — the code that reads extracted_text and does something with it — never needs to know which path a given file took.

Handling OCR failures explicitly

OCR is probabilistic in a way native text extraction isn’t: a blurry photo or a low-contrast fax can legitimately fail to yield usable text. txtfetch surfaces that as an explicit error rather than an empty success:

{
  "status": "error",
  "error": "No text could be extracted"
}

That distinction matters in a pipeline. An empty string that reports "status": "success" gets embedded and indexed like any other chunk — a silent gap in your retrieval that nobody notices until a user asks about a document that’s technically “in” the system. An explicit error is something you can catch, log, and route to a re-scan queue or a human reviewer.

Where this fits

Scanned intake is rarely the whole pipeline — it’s the long tail: the supplier who still faxes, the older records that predate digital originals, the field team uploading phone photos of paperwork. Routing that long tail through the same /v1/extract call as everything else means one retry policy, one error format, one thing to monitor, instead of a special-cased OCR branch that only gets tested when someone notices it’s broken.

Have a batch of scanned documents you want to test against? Get in touch and we’ll set you up with an API key.

Stop parsing. Start shipping.

Create an account and get an API key in minutes — the free Hobby plan needs no card.

Get started →