OCR scanned documents and images through one API call
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. First, detect that a file is an image. Then route it to a separate OCR step and merge the result back in. That’s a second dependency to install. It’s also a second set of language packs and model files to keep updated. And it’s 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 waste a Tesseract call on a document that didn’t need it. Or you silently skip OCR on a page that did. If you want to check which case one of your own files is, use the free PDF text-layer checker before writing any routing logic. It answers that on the spot, in your browser, with no upload involved.
One endpoint, format detection included
txtfetch runs Tesseract as a Lambda layer alongside Apache Tika. It decides
which one a request needs by inspecting the actual bytes, not the file
extension or 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. That’s
a silent gap in your retrieval. 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, and one thing to monitor. The alternative
is 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.