Scanned PDFs, OCR'd without a flag.

No text layer, skewed feeder scans, low-DPI faxes — txtfetch detects the blank pass itself and retries through OCR, same request.

the-problem

A scanned PDF has no text layer at all — it's a picture of a document wearing PDF packaging, and a naive text-extraction pass reads back nothing but whitespace. Real scans add their own damage on top: skew and rotation from the sheet feeder, low-DPI captures under ~200 dpi that blur character edges past recognition, and fax-style CCITT Group 4 compression on bitonal pages. The sharpest trap is the mixed document — a digitally-created cover page stapled ahead of a faxed, scanned body. A page-one text pass comes back non-empty, so a system that checks 'did we get any text' never learns that the rest of the file is a picture.

one-request-solution

txtfetch always tries the fast path first: a direct text-layer extraction. Only when the *entire* document comes back whitespace does it retry the whole file through Tika's ocr_only PDF strategy with Tesseract underneath — same request, same { status, extracted_text } shape, no ocr=true flag to set. That escalation is deliberately whole-document, which means the mixed cover-page-plus-scanned-body case above won't auto-escalate, because the first pass isn't blank. For those documents — or anywhere page-by-page fidelity matters more than the extra latency — pass ?quality=premium: every page routes through a vision-capable model instead of the text/OCR fork, so a digital cover page and a scanned body get consistently read.

curl
curl -X POST https://api.txtfetch.com/v1/extract \
  -H "Authorization: Bearer $TXTFETCH_KEY" \
  -F file=@faxed-inspection-report.pdf
Python
import os
import requests

with open("faxed-inspection-report.pdf", "rb") as f:
    r = requests.post(
        "https://api.txtfetch.com/v1/extract",
        headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
        files={"file": f},
    )

print(r.json()["extracted_text"])
JavaScript
import { readFile } from "node:fs/promises";

const file = new Blob([await readFile("faxed-inspection-report.pdf")]);
const form = new FormData();
form.append("file", file, "faxed-inspection-report.pdf");

const res = await fetch("https://api.txtfetch.com/v1/extract", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` },
  body: form,
});

const { extracted_text } = await res.json();
console.log(extracted_text);
Go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
)

type extractResponse struct {
	Status        string `json:"status"`
	ExtractedText string `json:"extracted_text"`
}

func main() {
	f, err := os.Open("faxed-inspection-report.pdf")
	if err != nil {
		panic(err)
	}
	defer f.Close()

	var body bytes.Buffer
	writer := multipart.NewWriter(&body)
	part, err := writer.CreateFormFile("file", "faxed-inspection-report.pdf")
	if err != nil {
		panic(err)
	}
	if _, err := io.Copy(part, f); err != nil {
		panic(err)
	}
	writer.Close()

	req, err := http.NewRequest("POST", "https://api.txtfetch.com/v1/extract", &body)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY"))
	req.Header.Set("Content-Type", writer.FormDataContentType())

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	var result extractResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		panic(err)
	}
	fmt.Println(result.ExtractedText)
}

Or skip the download — pass a url parameter and txtfetch fetches the document server-side:

curl
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/scans/site-survey.pdf" \
  -H "Authorization: Bearer $TXTFETCH_KEY"
Python
import os
import requests

r = requests.post(
    "https://api.txtfetch.com/v1/extract",
    headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
    params={"url": "https://example.com/scans/site-survey.pdf"},
)

print(r.json()["extracted_text"])
JavaScript
const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", "https://example.com/scans/site-survey.pdf");

const res = await fetch(endpoint, {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` },
});

const { extracted_text } = await res.json();
console.log(extracted_text);
Go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
)

type extractResponse struct {
	Status        string `json:"status"`
	ExtractedText string `json:"extracted_text"`
}

func main() {
	endpoint, err := url.Parse("https://api.txtfetch.com/v1/extract")
	if err != nil {
		panic(err)
	}
	q := endpoint.Query()
	q.Set("url", "https://example.com/scans/site-survey.pdf")
	endpoint.RawQuery = q.Encode()

	req, err := http.NewRequest("POST", endpoint.String(), nil)
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY"))

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	var result extractResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		panic(err)
	}
	fmt.Println(result.ExtractedText)
}
{
  "status": "success",
  "extracted_text": "..."
}

formats-covered

  • .pdf

response-options

Mixed text-and-scan PDF? ?quality=premium reads every page through a vision model instead of the whole-document text/OCR fork, so a digital cover page and a scanned body both come back accurately.

faq

Why is my scanned PDF coming back empty or blank?
If Tesseract still finds no readable text after the automatic OCR retry — an extremely low-resolution scan, a blank page, or a corrupted image stream — the request returns an extraction_failed error rather than guessing. Try ?quality=premium for a second read via a vision model.
Do I need to tell txtfetch a PDF is scanned?
No — there's no ocr=true parameter for PDFs. txtfetch runs a text pass first and only escalates to OCR automatically when that pass comes back whitespace across the whole document.
What about a PDF with a digital cover page and scanned pages after it?
That's the one case whole-document escalation misses — the first pass isn't blank, so OCR never kicks in for the rest. Pass ?quality=premium for these mixed documents; it reads every page through a vision model instead of relying on the text-or-OCR fork.
Does OCR handle skewed or low-quality fax scans?
Tesseract corrects moderate skew and reads CCITT Group 4 fax-compressed bitonal pages, but accuracy drops on very low-DPI (under ~150 dpi) source scans. See /benchmarks for measured accuracy by document category.

go-further

Stop parsing. Start shipping.

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

Get started →