https://txtfetch.com/extract/pdf/
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 -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@quarterly-report.pdfimport 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"])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);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 -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/whitepaper.pdf" \
-H "Authorization: Bearer $TXTFETCH_KEY"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"])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);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)
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)
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)
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
PDF & scans
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 →