Extract text from any PDF, one request.

Multi-column academic papers. Dense financial tables. Scanned contracts. One endpoint returns all of them in the same response shape, with no per-layout tuning.

the-problem

PDF is the least standardized 'standard' in document formats. Multi-column layouts confuse naive text extraction. Tables collapse into unreadable strings. Some pages are scans with no text layer at all. Most teams bolt together a PDF library for the easy cases and a separate OCR pipeline for the scanned ones. That means two code paths to maintain and two sets of edge cases to debug. VLM-based extractors that read pages as images do fine on a single clean scan. They struggle with mixed batches, like a folder of digital-native reports next to faxed scans. That breaks the assumption that every page is a picture.

one-request-solution

txtfetch takes any PDF, digital-native or scanned, and always gives back the same JSON shape. Text-layer pages go straight through Apache Tika. Pages with no text layer route through Tesseract OCR automatically, in the same request. That's one code path for every PDF in your pipeline.

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

with open("quarterly-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("quarterly-report.pdf")]);
const form = new FormData();
form.append("file", file, "quarterly-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("quarterly-report.pdf")
	if err != nil {
		panic(err)
	}
	defer f.Close()

	var body bytes.Buffer
	writer := multipart.NewWriter(&body)
	part, err := writer.CreateFormFile("file", "quarterly-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/whitepaper.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/whitepaper.pdf"},
)

print(r.json()["extracted_text"])
JavaScript
const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", "https://example.com/whitepaper.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/whitepaper.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": "..."
}

what-comes-back

That is the shape. This is the text. A real .pdf from the benchmark corpus, word-diffed against the raw parser output. Nothing here was written for this page. Every character comes from a committed fixture.

Q3 2026 Regional Performance Report (2-column PDF)

q3-2026-regional-performance.pdf · application/pdf · left pane read via Apache Tika, right pane at premium (VLM)

raw parser output

Q3 2026 Regional Performance Report

Q3 was a strong quarter across every region, driven Support tickets rose 8% quarter over quarter, concentrated
by the launch of the structured-output beta and continued in the API-key rotation flow; a fix shipped mid-quarter
expansion in EMEA. Renewal rates held above 92% despite reduced volume by half within two weeks.
the price increase that took effect in July.

Region Revenue Growth North America $5.1M 11% EMEA $3.4M 24% APAC $1.8M 15%

what txtfetch returns

# Q3 2026 Regional Performance Report

Q3 was a strong quarter across every region, driven by the launch of the structured-output beta and continued expansion in EMEA. Renewal rates held above 92% despite the price increase that took effect in July.

Support tickets rose 8% quarter over quarter, concentrated in the API-key rotation flow; a fix shipped mid-quarter reduced volume by half within two weeks.

| Region | Revenue | Growth |
| --- | --- | --- |
| North America | $5.1M | 11% |
| EMEA | $3.4M | 24% |
| APAC | $1.8M | 15% |
Show 2 more .pdf documents

Q3 Product Catalog (5-column pricing table)

q3-product-catalog.pdf · application/pdf · left pane read via Apache Tika, right pane at premium (VLM)

raw parser output

Q3 Product Catalog

Pricing reflects the July rate card; stock levels are warehouse totals as of quarter-end.

Product	Monthly	Annual	Stock
WD-100 Widget Pro	$29	$290	120
WD-200 Widget Mini	$9	$90	340
WD-300 Widget Max	$59	$590	45

what txtfetch returns

# Q3 Product Catalog

Pricing reflects the July rate card; stock levels are warehouse totals as of quarter-end.

| SKU | Product | Monthly | Annual | Stock |
| --- | --- | --- | --- | --- |
| WD-100 | Widget Pro | $29 | $290 | 120 |
| WD-200 | Widget Mini | $9 | $90 | 340 |
| WD-300 | Widget Max | $59 | $590 | 45 |
| WD-400 | Widget Nano | $5 | $50 | 610 |

Security Brief (single-column PDF)

security-brief.pdf · application/pdf · left pane read via Apache Tika, right pane: the human-checked expected.json

raw parser output

Security Brief

This document describes the security posture of the txtfetch extraction pipeline.

Every document is treated as hostile until parsed: zip-bomb guards, SSRF-guarded fetches, and a hard extraction budget all run before Tika ever sees the bytes.

Threat Model

Untrusted input arrives as arbitrary bytes from a URL fetch or a direct upload.

Secrets and document content are never logged; only allowlisted metadata fields reach stdout.

the human-checked ideal

Security Brief

This document describes the security posture of the txtfetch extraction pipeline.

Every document is treated as hostile until parsed: zip-bomb guards, SSRF-guarded fetches, and a hard extraction budget all run before Tika ever sees the bytes.

Threat Model

Untrusted input arrives as arbitrary bytes from a URL fetch or a direct upload.

Secrets and document content are never logged; only allowlisted metadata fields reach stdout.

See all ten documents, with the full explanation →

formats-covered

  • .pdf

response-options

Need Markdown with real tables instead of flattened text? Or a typed element JSON tree with page and offset per block? ?format=markdown and ?format=json both cover PDFs. See /output for the same document rendered all three ways.

faq

How do I extract text from a PDF?
POST the file as multipart form data to https://api.txtfetch.com/v1/extract. Or pass a url parameter, and txtfetch fetches it server-side. Either way you get back { "status": "success", "extracted_text": "..." }.
Does it handle scanned PDFs, not just digital-native ones?
Yes. Pages with no text layer route through Tesseract OCR automatically, in the same request, with the same response shape. You don't need to detect or flag scanned pages yourself.
What about multi-column layouts and tables?
Apache Tika parses the underlying PDF structure rather than guessing from pixel positions. That means multi-column academic papers and tabular financial reports come out as readable, ordered text.
Is there a page limit?
No. One extraction request is one document, regardless of length. A 300-page PDF still counts as a single request.

go-further

Send a real .pdf (digital) through it.

One HTTP call returns the text. Read one in your browser first, for free.

Get an API key →

Open the free .pdf (digital) reader →