# txtfetch — full-site corpus
Every page on txtfetch.com, concatenated as plain text and generated at build time. See https://txtfetch.com/text for the convention and https://txtfetch.com/llms.txt for a curated link index instead of the full text.
---
# https://txtfetch.com/
# Any document in.
Clean text out.
PDFs, Office docs, HTML, email, EPUB, even scans, plus captions and tagged audio/video files. One HTTP endpoint turns any of it into clean text, Markdown, or element JSON. Built for LLM ingestion, RAG pipelines, and search indexing. No parsers to write, nothing to host.
[Get started →](https://app.txtfetch.com/signup)
[Read the docs](https://txtfetch.com/docs)
[1,000+ formats detected, checked →](https://txtfetch.com/formats/coverage) · OCR for scans · free tier stays free
[Interactive demo — try it live: https://txtfetch.com/playground]
Drop a file below. It stays in your browser.
* * *
the-problem
## Every pipeline ends up with a parser zoo.
The document formats never stop coming. Each one grows its own library, its own edge cases, its own 2 a.m. failure mode. You wanted to build a product. Instead you're maintaining an extraction stack.
Delete the parser zoo. txtfetch collapses it into one call: Apache Tika and Tesseract OCR, run for you, behind an endpoint that always answers with text.
- `pdf-parse` # chokes on scanned pages
- `mammoth` # .docx only, loses tables
- `cheerio` # HTML only, you write the selectors
- `xlsx` # one more dependency to patch
- `mailparser` # attachments are your problem
- `tesseract.js` # now you run OCR infra
- `$ npm uninstall --all-of-it`
* * *
see it, don't take our word for it
## A scanned invoice with no text layer. Left is raw. Right is txtfetch.
One of the ten documents in our benchmark corpus, read on the premium tier. The corpus is a small, synthetic set of recorded fixtures. Differences are marked, not just colored, so this reads the same for a screen reader as it does on screen.
Scanned Invoice #9042 (image-only PDF)
`invoice-9042-scan.pdf` · application/pdf · left pane read via Apache Tika, **right pane at premium (VLM)**
raw parser output
```
lnvoice #9O42
Bill to: Northwind Tradcrs, 44 Harbor R0w
ltem Qty Unit Pricc Total Onsite installatlon 2 $45O.OO $9OO.OO Annual support pian 1 $1,2OO.OO $1,2OO.OO
Tota1 due: $2,1OO.OO
```
what txtfetch returns
```
# Invoice #9042
Bill to: Northwind Traders, 44 Harbor Row
| Item | Qty | Unit Price | Total |
| --- | --- | --- | --- |
| Onsite installation | 2 | $450.00 | $900.00 |
| Annual support plan | 1 | $1,200.00 | $1,200.00 |
Total due: $2,100.00
```
This scan has no text layer, so the standard tier's automatic Tesseract OCR escalation is what reads it at all. Tesseract reads most of the page, but confuses look-alike characters (O for 0, l and I for 1) throughout. It also has no table model, so the two-line item table survives only as one run-on blur. The premium tier reads the same image through a vision-language model and gets both the characters and the table right. See /fixes/pdf-extracts-no-text for the OCR-escalation path this document takes.
Table cell-F1 and reading order, standard vs. premium: 0.0% → 100.0%. See [/benchmarks](https://txtfetch.com/benchmarks) for the full methodology.
[PDF extracts no text (the OCR escalation path) →](https://txtfetch.com/fixes/pdf-extracts-no-text)
[See all ten documents, including the two-column report and the table that loses a row →](https://txtfetch.com/diff)
* * *
how-it-works
## Three steps. Two of them are ours.
1. ### Send anything
POST a file or pass a URL and txtfetch fetches it for you. PDF, DOCX, PPTX, XLSX, HTML, EPUB, email — 615 formats with a real parser behind them.
2. ### We detect & extract
Format detection, structure-aware parsing with Apache Tika, and Tesseract OCR when the document turns out to be pixels.
3. ### Get clean text
A JSON response with the document's plain text — ready to chunk, embed, index, classify, or archive. Nothing stored.
[Read the full pipeline →](https://txtfetch.com/how-it-works)
* * *
output
## Text, Markdown, or element JSON — you pick.
Every request answers the same way. Pick the shape your pipeline wants: a flat string, GFM Markdown with real tables, or a typed element tree with page and offset per block.
- format=text
### Plain text
One flat string. Default, unchanged since day one.
- format=markdown
### Markdown
GFM headings, lists, and tables. Reading order preserved.
- format=json
### Element JSON
Typed heading/paragraph/list/table/image/code nodes.
[See the same document rendered all three ways →](https://txtfetch.com/output)
* * *
formats
## Every extension is a parser you don't write.
Most extraction tools stop at PDFs and scanned pages. txtfetch also handles email (.eml/.msg/.mbox), Office documents natively, EPUB, ODF, spreadsheets, archives, and caption/media files. It is one path for every file type, not just the ones that look like paper.
- `.pdf`
- `.docx`
- `.doc`
- `.pptx`
- `.ppt`
- `.xlsx`
- `.xls`
- `.csv`
- `.html`
- `.xml`
- `.epub`
- `.rtf`
- `.odt`
- `.ods`
- `.odp`
- `.md`
- `.eml`
- `.msg`
- `.mbox`
- `.txt`
- `.json`
- `.png`
- `.jpg`
- `.tiff`
- `.srt`
- `.vtt`
- `.mp4`
- `.mp3`
- `+ ~976 more`
**Apache Tika** powers it: two decades of format archaeology, maintained by the people who wrote the book on it. Scans and images route through **Tesseract OCR** automatically.
[Browse all 1,000+ formats →](https://txtfetch.com/formats)
* * *
use-cases
## Text is the universal interface.
### [RAG & LLM ingestion](https://txtfetch.com/solutions/rag-ingestion)
Your retrieval is only as good as your extraction. Feed models clean text from any source your users throw at you — uploads, links, inboxes, archives.
chunk → embed → retrieve
### [Search indexing](https://txtfetch.com/solutions/search-indexing)
Make every contract, deck, and attachment findable. One ingestion path for your whole document estate, OCR included.
extract → index → find
### [Document workflows](https://txtfetch.com/solutions/document-workflows)
Classify inbound email, route invoices, screen resumes, mine archives. Anything you can do with text, you can now do with any file.
receive → extract → act
[See all 7 use cases →](https://txtfetch.com/solutions)
* * *
pricing
## Priced per document, not per surprise.
**Hobby** — $0/mo — 500 documents / month
**Developer** — $19/mo — 10,000 documents / month
**Scale** — $99/mo — 100,000 documents / month
Hobby is free, permanently, no card needed. [Full pricing →](https://txtfetch.com/pricing)
## Stop parsing. Start shipping.
Create an account and get an API key in minutes. The free Hobby plan needs no card.
[Get started →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/benchmarks
# How accurate is it? Here are the numbers, and how we got them.
Most vendors quote a self-reported figure and move on. Here's our full methodology, our per-category results, and where the numbers should and shouldn't be trusted.
These are scores. Want the actual extracted text these numbers come from? [See it, side by side, at /diff →](https://txtfetch.com/diff)
aggregate results
This benchmark runs the real extraction engine against a committed, human-checked corpus, using dependency-injected mock Tika and mock VLM fixtures. It needs no JVM and no network calls, and it is fully reproducible. All four tiers, whole corpus, 10 documents (premium-vlm ran on the 3 hardest documents only; see the quality climb below).
| Tier | Docs | Text similarity | Table cell-F1 | Table structure | Reading order | Structure valid |
| --- | --- | --- | --- | --- | --- | --- |
| baseline-text (Tika plain text, E1) | 10 | 92.0% | – | – | – | – |
| markdown (structured, standard) | 10 | 91.7% | 57.1% | 67.4% | 75.2% | 100.0% |
| json (element-JSON, standard) | 10 | 91.2% | 57.1% | 67.4% | 81.7% | 100.0% |
| premium-vlm (VLM tier) | 3 | 100.0% | 100.0% | 100.0% | 100.0% | 100.0% |
the quality climb
## Standard vs premium, on the hard PDFs.
This section covers table fidelity and reading order on the hardest documents: multi-column layouts, scanned pages, wide tables. Tika's structural recovery is weakest here. This is the clearest view of what the premium VLM tier buys. On every hard-PDF category, cell-F1 climbs from 0.0% to 100.0%.
| Category | Tier | Table cell-F1 | Table structure | Reading order |
| --- | --- | --- | --- | --- |
| pdf-complex | json (element-JSON, standard) | 0.0% | 0.0% | 50.0% |
| pdf-complex | premium-vlm (VLM tier) | 100.0% | 100.0% | 100.0% |
| pdf-scanned | json (element-JSON, standard) | 0.0% | 0.0% | 0.0% |
| pdf-scanned | premium-vlm (VLM tier) | 100.0% | 100.0% | 100.0% |
| table-heavy | json (element-JSON, standard) | 0.0% | 72.0% | 66.7% |
| table-heavy | premium-vlm (VLM tier) | 100.0% | 100.0% | 100.0% |
full per-category breakdown
**Show all 10 categories, all tiers**
| Category | Tier | Docs | Text similarity | Table cell-F1 | Reading order |
| --- | --- | --- | --- | --- | --- |
| email-eml | baseline-text (Tika plain text, E1) | 1 | 100.0% | – | – |
| email-eml | markdown (structured, standard) | 1 | 98.7% | 100.0% | 75.0% |
| email-eml | json (element-JSON, standard) | 1 | 100.0% | 100.0% | 100.0% |
| email-msg | baseline-text (Tika plain text, E1) | 1 | 100.0% | – | – |
| email-msg | markdown (structured, standard) | 1 | 98.8% | – | 75.0% |
| email-msg | json (element-JSON, standard) | 1 | 100.0% | – | 100.0% |
| html | baseline-text (Tika plain text, E1) | 1 | 100.0% | – | – |
| html | markdown (structured, standard) | 1 | 100.0% | 100.0% | 100.0% |
| html | json (element-JSON, standard) | 1 | 100.0% | 100.0% | 100.0% |
| office-docx | baseline-text (Tika plain text, E1) | 1 | 100.0% | – | – |
| office-docx | markdown (structured, standard) | 1 | 100.0% | 100.0% | 85.7% |
| office-docx | json (element-JSON, standard) | 1 | 92.3% | 100.0% | 100.0% |
| office-pptx | baseline-text (Tika plain text, E1) | 1 | 100.0% | – | – |
| office-pptx | markdown (structured, standard) | 1 | 100.0% | – | 100.0% |
| office-pptx | json (element-JSON, standard) | 1 | 100.0% | – | 100.0% |
| office-xlsx | baseline-text (Tika plain text, E1) | 1 | 100.0% | – | – |
| office-xlsx | markdown (structured, standard) | 1 | 100.0% | 100.0% | 100.0% |
| office-xlsx | json (element-JSON, standard) | 1 | 100.0% | 100.0% | 100.0% |
| pdf-complex | baseline-text (Tika plain text, E1) | 1 | 47.9% | – | – |
| pdf-complex | markdown (structured, standard) | 1 | 47.9% | 0.0% | 50.0% |
| pdf-complex | json (element-JSON, standard) | 1 | 47.9% | 0.0% | 50.0% |
| pdf-complex | premium-vlm (VLM tier) | 1 | 100.0% | 100.0% | 100.0% |
| pdf-scanned | baseline-text (Tika plain text, E1) | 1 | 84.6% | – | – |
| pdf-scanned | markdown (structured, standard) | 1 | 84.6% | 0.0% | 0.0% |
| pdf-scanned | json (element-JSON, standard) | 1 | 84.6% | 0.0% | 0.0% |
| pdf-scanned | premium-vlm (VLM tier) | 1 | 100.0% | 100.0% | 100.0% |
| pdf-simple | baseline-text (Tika plain text, E1) | 1 | 100.0% | – | – |
| pdf-simple | markdown (structured, standard) | 1 | 100.0% | – | 100.0% |
| pdf-simple | json (element-JSON, standard) | 1 | 100.0% | – | 100.0% |
| table-heavy | baseline-text (Tika plain text, E1) | 1 | 87.0% | – | – |
| table-heavy | markdown (structured, standard) | 1 | 87.0% | 0.0% | 66.7% |
| table-heavy | json (element-JSON, standard) | 1 | 87.0% | 0.0% | 66.7% |
| table-heavy | premium-vlm (VLM tier) | 1 | 100.0% | 100.0% | 100.0% |
methodology
This harness runs the real extraction engine (extract.js, render.js, premium.js) against a committed corpus. It uses a dependency-injected mock Tika and a mock VLM. The mock Tika returns each corpus entry's recorded tika.xhtml/tika.txt. The mock VLM returns each entry's recorded vlm.md. This is the same dependency-injection seam that api/extract/src's own unit tests use. The harness needs no JVM and no network calls. It runs on node:22-alpine with no npm install. Each corpus document carries a human-checked expected.json file. This file holds the ideal reading order, text, and table structure. It is not a copy of what Tika produces. On easy formats (Office, HTML, email) Tika's own structure already matches the ideal closely. On hard PDFs (multi-column layouts, scanned/image-only pages, wide tables) the recorded tika.xhtml/tika.txt faithfully encode Tika's real failure modes. These failure modes include column-scrambled reading order and flattened or partially-recovered tables. The recorded vlm.md encodes the correction a vision-capable model makes from the same page. The gap between the standard tiers and the premium tier on those documents is the quality climb. This report exists to measure that gap.
metric definitions
**Text similarity**: Normalized Levenshtein edit similarity between each tier's text projection and expected.text.
**Table cell-F1**: Table cells are aligned by (row, col) position. A cell is "correct" when its normalized text matches the ideal. Headline table fidelity metric.
**Table structure ("TEDS-style")**: A lightweight cell-grid score: rewards matching (rows, cols) dimensions and matching cell positions, independent of text. This is not the full academic tree-edit-distance TEDS metric. That metric diffs the underlying HTML/tree structure. This score is a cheap, zero-dependency stand-in for the same intuition.
**Reading order**: LCS of normalized block-text fingerprint sequences divided by the expected block count. This penalizes both dropped and reordered blocks (the multi-column-PDF failure mode).
**Structure validity**: markdown: lightweight GFM well-formedness (balanced fences, consistent table column counts, no unclosed inline
). json: the shared validateElements from render-json.js (also used by api/extract/src's own tests).
Source of truth: api/extract/bench/REPORT.md, generated by \`node api/extract/bench/run.js\` (regenerate with \`make bench\`). This page hand-mirrors that committed artifact. It is not computed at request time.
caveats
- This corpus is small and synthetic. It uses recorded fixtures that stand in for real Tika/VLM responses. It demonstrates the shape of the quality climb and guards against regression. It is not a claim of population-level accuracy.
- "TEDS-style" is a cell-grid F1/structural score, not the full academic TEDS metric. See Methodology above.
- Competitor figures are vendors' own published numbers, on their own evaluation sets. Vendors gathered these numbers independently of this corpus. They are never apples-to-apples with the tables above. Several carry an explicit dispute or self-consistency caveat. Read the per-vendor notes before citing any of them externally.
competitor context
**Other vendors' own published numbers**, on their own evaluation sets. These are NOT measured on our corpus. They are NOT apples-to-apples with the tables above. Read every note below before citing any of these figures externally.
These are OTHER VENDORS' OWN PUBLISHED NUMBERS, on THEIR OWN evaluation sets and metric definitions. We never measured them on this repo's corpus. We never independently reproduced them. They are included for directional market context only, not as an apples-to-apples comparison against the tiers above. Every entry is flagged directional:true. Most are also selfReported:true (the vendor's own blog/docs/marketing page). Where an entry notes a dispute or integrity concern, treat the number as contested, not authoritative.
LlamaParse (LlamaIndex)
- Overall parse accuracy — "ParseBench": 84.9% directional self-reported
Dataset: LlamaIndex's own ~2,000-page private ParseBench set (LlamaParse Agentic mode)
Source: [LlamaIndex blog — "ParseBench"](https://www.llamaindex.ai/blog/parsebench) (accessed 2026-07)
- Table TEDS — OmniDocBench (standardized dataset): 0.63 / 0.38 (two reported variants) directional self-reported
Dataset: OmniDocBench (public/standardized)
Source: [LlamaIndex blog — OmniDocBench post](https://www.llamaindex.ai/blog) (accessed 2026-07)
This figure is vendor-published, on a standardized benchmark. The post itself argues the metric undersells LlamaParse. Read the scores alongside that framing.
- Document QA accuracy — RealDoc-Bench (third party): 92.1% directional
Dataset: Extend.ai's RealDoc-Bench
Source: [Extend.ai — RealDoc-Bench](https://www.extend.ai/blog) (accessed 2026-07)
This figure is independent of LlamaIndex. Extend is itself a competing document-AI vendor. It ranks its own product (95.7%) above LlamaParse and Reducto (91.1%) on this set.
Unstructured.io
- Table extraction score — "SCORE": 0.844 (blog post) / 0.880 (live benchmarks page, same claim) directional self-reported
Dataset: Unstructured's own ~1,000-page private set
Source: [Unstructured.io blog — SCORE benchmark](https://unstructured.io/blog) (accessed 2026-07)
The two self-published figures for the same claim disagree. Cite with caution.
- Table TEDS — DP-Bench (standardized dataset): 65.56 (ranked 6th of 7 tools) directional
Dataset: Upstage DP-Bench (public)
Source: [Upstage — DP-Bench paper](https://arxiv.org/abs/2412.04824) (accessed 2026-07)
Azure AI Document Intelligence
Microsoft does not publish a fixed vendor accuracy percentage for Document Intelligence itself. Every figure here is third-party. Third-party figures scatter roughly 65-89% depending on dataset.
- Table TEDS — DP-Bench (standardized dataset): 77.85 directional
Dataset: Upstage DP-Bench (public)
Source: [Upstage — DP-Bench paper](https://arxiv.org/abs/2412.04824) (accessed 2026-07)
- Document QA accuracy — RealDoc-Bench (third party): 88.8% directional
Dataset: Extend.ai's RealDoc-Bench
Source: [Extend.ai — RealDoc-Bench](https://www.extend.ai/blog) (accessed 2026-07)
AWS Textract
AWS does not publish a fixed accuracy percentage for Textract itself.
- Table TEDS — third-party comparison: 80.75 directional
Dataset: Tensorlake's published comparison set
Source: [Tensorlake blog — parser comparison](https://www.tensorlake.ai/blog) (accessed 2026-07)
Tensorlake is itself a competing document-parsing vendor.
- Text-similarity accuracy — independent study: 99.3% (SBERT similarity, n=300 docs) directional
Dataset: AIMultiple's independent research sample
Source: [AIMultiple Research — OCR accuracy comparison](https://research.aimultiple.com/ocr-accuracy/) (accessed 2026-07)
Mindee
- Field-level accuracy (vendor claim): \>95% for most fields directional self-reported
Dataset: Mindee's own private/rolling evaluation set (receipts/invoices)
Source: [Mindee — product pages](https://mindee.com) (accessed 2026-07)
Vague vendor marketing claim, not a fixed benchmark with a published dataset or date.
Reducto
- Table extraction — "RD-TableBench": 90.2% (vs Azure DI 82.7%, Textract 80.9%, LlamaParse 74.6%, Unstructured 60.2%) directional self-reported
Dataset: Reducto's own ~1,000-table private set, custom similarity metric (labelled "TEDS" but not the standard TEDS formula)
Source: [Reducto blog — RD-TableBench](https://reducto.ai/blog/rd-tablebench) (accessed 2026-07)
DISPUTED: an independent audit found a number of RD-TableBench's ground-truth files byte-identical to Reducto's own output. The audit also found errors shared with Reducto's own extraction. This is a material integrity concern. Treat this comparison as contested, not authoritative.
Dispute: [Runpulse (Pulse AI) — RD-TableBench audit](https://runpulse.com/blog) (accessed 2026-07)
Chunkr
No public vendor-vs-vendor accuracy benchmark for Chunkr was found, as of this writing. Its published benchmark posts compare LLM/VLM choices inside its own pipeline, on private eval sets. They do not compare Chunkr against other document-parsing products. We deliberately cite no number here rather than infer one.
Nanonets
- Overall score — OCR/parsing leaderboard: 85.9 (self-reported #1 of 29 models) directional self-reported
Dataset: OlmOCR-bench / OmniDocBench / IDP Core (standardized public datasets, self-run and self-scored)
Source: [Nanonets — public benchmarking page](https://benchmarking.nanonets.com) (accessed 2026-07)
The underlying datasets are standardized, but the ranking/run was performed and reported by Nanonets itself.
Mistral OCR
- Overall accuracy / table accuracy — launch benchmark: 94.89% overall, 96.12% tables (vs GPT-4o 89.77%, Azure OCR 89.52%, Google Doc AI 83.42%) directional self-reported
Dataset: Mistral's own private text-only evaluation set
Source: [Mistral AI — "Mistral OCR" launch post](https://mistral.ai/news/mistral-ocr) (accessed 2026-07)
- Contradicting third-party comparison: Gemini 80.1% vs Mistral OCR 45.3% on forms directional
Dataset: Reducto's own private RD-FormsBench
Source: [Reducto blog — LVM OCR accuracy comparison](https://reducto.ai/blog/lvm-ocr-accuracy-mistral-gemini) (accessed 2026-07)
Reducto is a competing vendor with its own commercial interest in this comparison; the two vendors' benchmarks rank the field in opposite orders.
## Stop parsing. Start shipping.
Create an account and get an API key in minutes. The free Hobby plan needs no card.
[Get started →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/blog
# Guides for document-to-text pipelines.
Practical write-ups on extracting, OCR-ing, and parsing documents for RAG, search, and LLM ingestion. No fluff, just how to do it.
- **[Getting text out of video and audio: extraction vs. transcription](https://txtfetch.com/blog/text-from-video-and-audio)**
Aug 15, 2026 — Text in a media file can live in a sidecar, an embedded track, a container tag, or nowhere at all. Extraction and speech-to-text are different jobs.
- **[Per-document vs per-page pricing for document extraction](https://txtfetch.com/blog/per-document-vs-per-page-pricing)**
Jul 14, 2026 — Per-page pricing looks cheaper until you multiply it by a 300-page report. This post frames what actually predicts your bill, and when per-page wins.
- **[Ingesting large documents and big batches without falling over](https://txtfetch.com/blog/batch-and-large-document-ingestion)**
Jun 23, 2026 — A 500-page PDF and a ten-thousand-file backfill stress the same two things: single-request time and concurrency.
- **[Extracting tables from PDFs and spreadsheets for RAG](https://txtfetch.com/blog/extract-tables-for-rag)**
Jun 2, 2026 — Tables don't survive naive text extraction. They collapse into number-soup with no row or column structure. Structure-aware extraction keeps them usable.
- **[Using txtfetch as a LangChain and LlamaIndex document loader](https://txtfetch.com/blog/langchain-llamaindex-document-loader)**
May 5, 2026 — There's no official txtfetch SDK yet. Wiring the endpoint into a LangChain or LlamaIndex loader takes about fifteen lines.
- **[Chunking strategies for RAG: from clean text to good retrieval](https://txtfetch.com/blog/chunking-strategies-for-rag)**
Apr 7, 2026 — Fixed-size, recursive, and structure-aware chunking all assume clean extracted text. Extraction quality bounds chunk quality, whichever one you pick.
- **[Parsing DOCX, PPTX, and XLSX into clean text for LLM ingestion](https://txtfetch.com/blog/parse-office-docs-docx-pptx-xlsx-for-llms)**
Mar 10, 2026 — python-docx, python-pptx, and openpyxl each have their own API and their own way of failing on real files. One endpoint replaces all three.
- **[OCR scanned documents and images through one API call](https://txtfetch.com/blog/ocr-scanned-documents-api)**
Feb 3, 2026 — Scanned invoices, faxed contracts, and photographed receipts have no text layer, so they OCR through the same endpoint as everything else.
- **[How to extract text from a PDF for RAG (without maintaining a parser)](https://txtfetch.com/blog/extract-text-from-pdf-for-rag)**
Jan 12, 2026 — Feeding PDFs into a RAG pipeline breaks the usual parser stack: multi-column layouts, embedded tables, scanned pages. One request handles all three.
## Try it on your own file.
The free reader runs in your browser. Nothing gets uploaded.
[Open the file reader →](https://txtfetch.com/tools/file-to-text)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/blog/batch-and-large-document-ingestion
# Ingesting large documents and big batches without falling over
Jun 23, 2026 · 3 min read · rag, batch, llm-ingestion, api
Two kinds of scale break ingestion pipelines that work fine in a demo. One is a single very large document. The other is a very large number of ordinary ones. They look different. But they stress the same two things: how long a single request takes, and how many you can run at once.
## Large single documents
[`/v1/extract`](https://txtfetch.com/extract/pdf) is a synchronous endpoint. There’s no async job or webhook mode. You send a request and get `extracted_text` back on the same connection. For a 500-page report or a multi-hundred-slide deck, the request itself takes longer. Your client needs a longer timeout too. Don’t use the short timeout you’d set for a two-page memo:
```bash
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@annual-report-500pg.pdf \
--max-time 300
```
Many HTTP client libraries default to a short timeout, sometimes 10 or 30 seconds. Raise it explicitly for extraction calls. Otherwise you’ll discover the hard way that your largest documents are the ones that fail silently.
## Large batches
A backfill means ingesting an existing document archive, not a steady trickle of new uploads. There the constraint shifts. It’s no longer “how long does one request take” but “how many can I run at once without overwhelming either side.” A plain `for` loop making requests one at a time works. But it wastes most of your batch window waiting on network I/O. Some client-side concurrency, capped at a sane number, gets through a large batch faster than a sequential wait:
```python
import asyncio
import httpx
SEM = asyncio.Semaphore(8) # cap concurrent in-flight requests
async def extract(client, path, api_key):
async with SEM:
with open(path, "rb") as f:
resp = await client.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {api_key}"},
files={"file": f},
timeout=120,
)
body = resp.json()
if body["status"] != "success":
raise ValueError(f"{path}: {body.get('error')}")
return path, body["extracted_text"]
async def run_batch(paths, api_key):
async with httpx.AsyncClient() as client:
results = await asyncio.gather(
*(extract(client, p, api_key) for p in paths),
return_exceptions=True,
)
return results
```
`return_exceptions=True` matters here: one bad file in a ten-thousand-file backfill shouldn’t take down the other 9,999. Collect the failures separately and retry or route them to a review queue instead of letting one exception cancel the whole batch.
## Retry and backoff
Treat a failed request as retryable unless the response tells you otherwise. An explicit `{"status": "error", "error": "..."}` for a genuinely unreadable file (corrupt, unsupported, empty) isn’t worth retrying. Retrying the same broken input just wastes another request. A transport-level failure (timeout, connection reset) is usually worth a retry. Add backoff so a batch of failures doesn’t turn into a retry storm:
```python
async def extract_with_retry(client, path, api_key, attempts=3):
for attempt in range(attempts):
try:
return await extract(client, path, api_key)
except (httpx.TimeoutException, httpx.TransportError):
if attempt == attempts - 1:
raise
await asyncio.sleep(2 ** attempt)
```
There’s no idempotency key or job ID here. Each request is independent. A retry just means “send the same file again,” not resuming a partially completed job. That’s the tradeoff of a synchronous, no-job-state API. It’s simpler to reason about. But the client owns concurrency and retry logic instead of polling a job status endpoint.
## Where this fits into cost
Once ingestion completes reliably at whatever batch size you’re running, the next question is usually cost. What does it cost to run this repeatedly? See the [per-document vs per-page pricing guide](https://txtfetch.com/blog/per-document-vs-per-page-pricing) for how document count and page count factor in.
If scanned documents are part of the batch, read the [OCR guide](https://txtfetch.com/blog/ocr-scanned-documents-api) too. OCR requests can take longer than native-text extraction. Account for that in your timeout and concurrency tuning.
Planning a backfill and want to talk through concurrency limits? [Get in touch](https://txtfetch.com/contact) and we’ll set you up with an API key.
keep reading
- **[Using txtfetch as a LangChain and LlamaIndex document loader](https://txtfetch.com/blog/langchain-llamaindex-document-loader)**
There's no official txtfetch SDK yet. Wiring the endpoint into a LangChain or LlamaIndex loader takes about fifteen lines.
May 5, 2026 · 3 min read
- **[Chunking strategies for RAG: from clean text to good retrieval](https://txtfetch.com/blog/chunking-strategies-for-rag)**
Fixed-size, recursive, and structure-aware chunking all assume clean extracted text. Extraction quality bounds chunk quality, whichever one you pick.
Apr 7, 2026 · 3 min read
- **[Parsing DOCX, PPTX, and XLSX into clean text for LLM ingestion](https://txtfetch.com/blog/parse-office-docs-docx-pptx-xlsx-for-llms)**
python-docx, python-pptx, and openpyxl each have their own API and their own way of failing on real files. One endpoint replaces all three.
Mar 10, 2026 · 3 min read
See also: [RAG & LLM ingestion →](https://txtfetch.com/solutions/rag-ingestion)
## Try it on your own file.
The free reader runs in your browser. Nothing gets uploaded.
[Open the file reader →](https://txtfetch.com/tools/file-to-text)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/blog/chunking-strategies-for-rag
# Chunking strategies for RAG: from clean text to good retrieval
Apr 7, 2026 · 3 min read · rag, chunking, llm-ingestion
Chunking gets a lot of attention in RAG write-ups: overlap sizes, token counts, semantic splitters. Most of it assumes a precondition that rarely gets checked. The text needs to be clean and in the right order before you chunk it. Chunking strategy is a real lever, but it’s the second lever. The first is whether the text you’re chunking is actually right.
## Three common strategies
**Fixed-size chunking** splits text every N tokens or characters, usually with some overlap so a sentence that straddles a boundary isn’t lost entirely. It’s simple, fast, and format-agnostic. That’s why most RAG tutorials use it by default.
**Recursive chunking** tries to split on natural boundaries first: paragraphs, then sentences, then words. It falls back to a hard character cut only when a single unit is still too big. It respects sentence and paragraph structure better than a fixed-size cut. The cost is variable chunk sizes.
**Structure-aware chunking** uses document structure as split points: headings, sections, list items. A chunk then matches one idea, not N tokens starting at an arbitrary offset. This produces the best retrieval quality when the structure is real. It produces the worst when the structure isn’t real. A chunker that trusts heading markers on text without clean headings will split in the wrong places. It does this just as confidently as it splits in the right ones.
## Why extraction quality bounds all three
Every one of these strategies operates on whatever text extraction handed it. Say the source document’s extraction step interleaved two PDF columns into one garbled line. No chunking strategy recovers the original reading order then, not fixed, recursive, or structure-aware. The chunker doesn’t know two columns exist. It just sees text with sentences from two unrelated ideas mashed together. It will happily draw chunk boundaries through the middle of that mess. Structure-aware chunking is actually more exposed to bad extraction than fixed-size, not less. That’s because it trusts signals like heading patterns and indentation, and a mangled extraction can fake or destroy those signals.
This is the case for [extracting the text well before you think about chunking at all](https://txtfetch.com/blog/extract-text-from-pdf-for-rag). txtfetch’s [PDF extraction endpoint](https://txtfetch.com/extract/pdf) runs Apache Tika, with Tesseract OCR for scanned pages. Multi-column reading order and table content come back as one coherent `extracted_text` string:
```bash
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/whitepaper.pdf" \
-H "Authorization: Bearer $TXTFETCH_KEY"
```
```json
{
"status": "success",
"extracted_text": "Executive summary\n\nQ3 revenue grew 34%..."
}
```
That single string is what every chunker downstream operates on, whether fixed-size, recursive, or structure-aware. Getting it right once, upstream, is cheaper than trying to compensate for it later in the chunker.
## A practical default
If you don’t have a strong reason to do something fancier, use recursive chunking. A target size of 300-500 tokens with roughly 10-15% overlap is a reasonable default for most prose-heavy documents. Reach for structure-aware chunking when the source genuinely has reliable structure, such as technical docs with real heading hierarchies or contracts with numbered clauses. Reach for fixed-size chunking only when you need predictable chunk counts more than clean boundaries, for example when estimating batch cost.
Whichever you pick, feed it into your embedding model the way you already do. Chunking is downstream of extraction. It’s not a replacement for getting extraction right. If you’re wiring this into a LangChain or LlamaIndex pipeline, the [document loader guide](https://txtfetch.com/blog/langchain-llamaindex-document-loader) covers the integration point.
Want to see this argument on your own text instead of taking it on faith? The [chunk previewer](https://txtfetch.com/tools/chunk-preview) runs all three strategies above against text you paste in. It shows the resulting boundaries, token estimates, and overlap. It also scans for specific extraction-damage signals that wreck good chunking: hyphenated line breaks, mojibake, collapsed tables. It’s free and runs entirely in your browser. It also includes a clean-vs-naive-PDF-extraction sample of the same source page, so you can see the difference directly.
Working through a chunking strategy for your own document set? [Get in touch](https://txtfetch.com/contact) and we’ll set you up with an API key.
keep reading
- **[Ingesting large documents and big batches without falling over](https://txtfetch.com/blog/batch-and-large-document-ingestion)**
A 500-page PDF and a ten-thousand-file backfill stress the same two things: single-request time and concurrency.
Jun 23, 2026 · 3 min read
- **[Using txtfetch as a LangChain and LlamaIndex document loader](https://txtfetch.com/blog/langchain-llamaindex-document-loader)**
There's no official txtfetch SDK yet. Wiring the endpoint into a LangChain or LlamaIndex loader takes about fifteen lines.
May 5, 2026 · 3 min read
- **[How to extract text from a PDF for RAG (without maintaining a parser)](https://txtfetch.com/blog/extract-text-from-pdf-for-rag)**
Feeding PDFs into a RAG pipeline breaks the usual parser stack: multi-column layouts, embedded tables, scanned pages. One request handles all three.
Jan 12, 2026 · 3 min read
See also: [RAG & LLM ingestion →](https://txtfetch.com/solutions/rag-ingestion)
## Try it on your own file.
The free reader runs in your browser. Nothing gets uploaded.
[Open the file reader →](https://txtfetch.com/tools/file-to-text)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/blog/extract-tables-for-rag
# Extracting tables from PDFs and spreadsheets for RAG
Jun 2, 2026 · 3 min read · rag, tables, pdf, office-docs
Ask any RAG pipeline builder which content type causes the most retrieval failures. A lot of them will say tables. A paragraph survives bad extraction gracefully. A jumbled sentence is still mostly readable. A table doesn’t survive as well. Strip out the column alignment and you’re left with a wall of numbers and labels. The order means nothing without the grid they came from.
## Why tables break naive extraction
A table’s meaning lives entirely in its two-dimensional layout. Which cell is in which row, and which row belongs under which column header, both matter. Most “extract the text” libraries only understand one-dimensional text: a stream of characters in reading order. Fed a table, they have to guess how to flatten two dimensions into one. The guess is often wrong. Cells read top-to-bottom instead of left-to-right. Headers get separated from the rows they label by hundreds of characters. Merged cells get duplicated or dropped entirely.
The result is number-soup, and that’s worse than an outright error. It looks like extracted text. It gets chunked and embedded like extracted text. It only fails visibly later. That happens when someone asks a question whose answer was in row 14 of a table that got scrambled into row 3’s neighborhood.
## What “extraction” for a table actually means here
It’s worth being precise about what txtfetch does and doesn’t give you. `/v1/extract` returns a single flat `extracted_text` string. There’s no structured `rows`/`columns` object, and no JSON representation of cell boundaries. What Tika does well is flatten a table into that string in row-major reading order. It keeps each row’s cells together and headers attached to the data below them, instead of scattering them across the output:
```bash
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@quarterly-report.pdf
```
```json
{
"status": "success",
"extracted_text": "Region\tQ1\tQ2\tQ3\nNorth America\t3.1M\t3.6M\t4.2M\nEMEA\t1.8M\t2.0M\t2.3M..."
}
```
Spreadsheets go through the same path. A sheet’s rows and columns come back as linear text in the same shape. An [XLSX model](https://txtfetch.com/extract/docx) and a [PDF table](https://txtfetch.com/extract/pdf) both resolve to the same kind of `extracted_text` your pipeline already knows how to handle:
```bash
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/models/forecast.xlsx" \
-H "Authorization: Bearer $TXTFETCH_KEY"
```
If your pipeline needs structured, per-cell output, such as a JSON object per row with typed columns, that’s a different problem than text extraction. `/v1/extract` doesn’t claim to solve it. What it solves is getting the table’s content into linear text without scrambling row/column correspondence. That’s the part that actually breaks most pipelines.
## Chunking tabular text so retrieval survives
Once a table is flattened, the chunking decision matters more than usual. Splitting a table in the middle of its rows, the same fixed-size cut you’d apply to prose, divorces a row from its header. That makes the chunk unanswerable on its own. Two things help:
- **Keep small tables whole.** A table under a few hundred tokens should usually be its own chunk rather than getting split, so the header stays attached to every row.
- **Repeat the header row into each chunk of a large table.** For tables too big to keep whole, prefix every chunk with the header row’s text. That way each chunk is self-describing, instead of assuming the header survived from a previous chunk.
See the [chunking strategies guide](https://txtfetch.com/blog/chunking-strategies-for-rag) for the general reasoning. Tables are the case where getting this specific decision right matters most.
Tables are common enough inside Office documents that it’s also worth reading the [DOCX/PPTX/XLSX parsing guide](https://txtfetch.com/blog/parse-office-docs-docx-pptx-xlsx-for-llms). Read it if spreadsheets and embedded tables inside Word docs are a big share of your document set.
Wrestling with a document set that’s mostly tabular? [Get in touch](https://txtfetch.com/contact) and we’ll set you up with an API key.
keep reading
- **[How to extract text from a PDF for RAG (without maintaining a parser)](https://txtfetch.com/blog/extract-text-from-pdf-for-rag)**
Feeding PDFs into a RAG pipeline breaks the usual parser stack: multi-column layouts, embedded tables, scanned pages. One request handles all three.
Jan 12, 2026 · 3 min read
- **[Per-document vs per-page pricing for document extraction](https://txtfetch.com/blog/per-document-vs-per-page-pricing)**
Per-page pricing looks cheaper until you multiply it by a 300-page report. This post frames what actually predicts your bill, and when per-page wins.
Jul 14, 2026 · 3 min read
- **[Ingesting large documents and big batches without falling over](https://txtfetch.com/blog/batch-and-large-document-ingestion)**
A 500-page PDF and a ten-thousand-file backfill stress the same two things: single-request time and concurrency.
Jun 23, 2026 · 3 min read
See also: [PDF extraction →](https://txtfetch.com/extract/pdf)
## Try it on your own file.
The free reader runs in your browser. Nothing gets uploaded.
[Open the file reader →](https://txtfetch.com/tools/file-to-text)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/blog/extract-text-from-pdf-for-rag
# How to extract text from a PDF for RAG (without maintaining a parser)
Jan 12, 2026 · 3 min read · rag, pdf, llm-ingestion
Every RAG pipeline eventually has to answer the same boring question: how do you turn a PDF into text you can chunk and embed? The obvious answer is `pdftotext`, or a Python library like `pdfminer` or `PyPDF2`. That works right up until it doesn’t. Multi-column academic papers interleave columns into nonsense. Tables collapse into runs of numbers with no structure. Scanned contracts and invoices, which are just images wrapped in a PDF container, return nothing at all.
None of these are edge cases if you’re ingesting real-world documents at any volume. They’re the median case.
## Why PDF text extraction is harder than it looks
A PDF doesn’t store “text” the way a Markdown file does. It stores a sequence of drawing instructions: put this glyph at this (x, y) coordinate, with this font. There is no inherent concept of a paragraph or a table cell. Extractors have to reconstruct reading order from glyph positions alone. This is where multi-column and table layouts fall apart. Two columns of text at the same vertical position look like one long garbled line to a naive extractor.
Sometimes the PDF is a scan: a photographed invoice, a faxed contract, a flattened image-only export. In those cases, there’s no text layer to extract in the first place. You need OCR, which most “extract text from PDF” library calls silently don’t do. Not sure which case one of your own PDFs falls into? [Drop it into the free PDF text-layer checker](https://txtfetch.com/tools/pdf-text-check). It runs entirely in your browser and tells you before you write a line of code.
For a RAG pipeline, garbled or missing text is worse than an error. It gets chunked, embedded, and retrieved with confidence. Nobody notices until a user asks a question the source document already answers correctly on page one.
## A one-request approach
txtfetch handles all three cases behind a single HTTP call: normal PDFs, multi-column layouts, and scanned pages. It runs Apache Tika for structural text extraction and layout-aware reading order. It falls back to Tesseract OCR automatically when a page has no extractable text layer. You don’t tell it which path to take. It detects the document and picks one.
If you already have a PDF at a URL:
```bash
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/whitepaper.pdf" \
-H "Authorization: Bearer $TXTFETCH_KEY"
```
Or if you’re pulling files out of an upload flow, POST the bytes directly:
```bash
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@quarterly-report.pdf
```
Either way, the response is the same shape:
```json
{
"status": "success",
"extracted_text": "Q3 revenue grew 34% year over year, driven by..."
}
```
That’s `extracted_text`, ready to chunk and hand to your embedding model. There’s no intermediate layout object to parse, and no separate OCR branch to write or maintain.
## Fitting it into a RAG pipeline
The integration point is wherever your ingestion pipeline currently calls a local PDF library. Replace that function call with a request to txtfetch. Keep everything downstream (your chunker, your embedder, your vector store) exactly as it is:
```bash
curl -s -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@handbook.pdf | jq -r .extracted_text | your-chunker
```
Failures come back as explicit JSON (`{"status": "error", "error": "..."}`). This replaces an empty string silently swallowed into an empty chunk. Bad documents fail loudly in your pipeline logs instead of quietly poisoning your index.
The result: no `pdfminer` version pinned in your `requirements.txt`, no separate Tesseract install to keep patched, no special-casing for scanned intake. One endpoint, and it’s already handled the PDF you’re about to throw at it.
Want to try it against your own documents? [Get in touch](https://txtfetch.com/contact) and we’ll set you up with an API key.
keep reading
- **[Ingesting large documents and big batches without falling over](https://txtfetch.com/blog/batch-and-large-document-ingestion)**
A 500-page PDF and a ten-thousand-file backfill stress the same two things: single-request time and concurrency.
Jun 23, 2026 · 3 min read
- **[Extracting tables from PDFs and spreadsheets for RAG](https://txtfetch.com/blog/extract-tables-for-rag)**
Tables don't survive naive text extraction. They collapse into number-soup with no row or column structure. Structure-aware extraction keeps them usable.
Jun 2, 2026 · 3 min read
- **[Using txtfetch as a LangChain and LlamaIndex document loader](https://txtfetch.com/blog/langchain-llamaindex-document-loader)**
There's no official txtfetch SDK yet. Wiring the endpoint into a LangChain or LlamaIndex loader takes about fifteen lines.
May 5, 2026 · 3 min read
See also: [PDF extraction →](https://txtfetch.com/extract/pdf)
## Try it on your own file.
The free reader runs in your browser. Nothing gets uploaded.
[Open the file reader →](https://txtfetch.com/tools/file-to-text)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/blog/langchain-llamaindex-document-loader
# Using txtfetch as a LangChain and LlamaIndex document loader
May 5, 2026 · 3 min read · rag, langchain, llamaindex, llm-ingestion
LangChain and LlamaIndex both ship a long list of built-in document loaders, covering PDFs, S3 buckets, Notion, and a dozen other sources. Each one wraps some underlying parsing library. txtfetch doesn’t have an official loader in either ecosystem yet. What it does have is a plain HTTP endpoint. That’s enough to write a thin custom loader yourself in about fifteen lines, without waiting on us to ship one.
## Why write your own instead of waiting
A custom loader that calls `/v1/extract` gives you the same one-endpoint handling as the rest of txtfetch’s guides: PDFs, Office docs, scanned images, and HTML. That covers multi-column PDFs, DOCX/PPTX/XLSX, and OCR fallback. You don’t pin a parsing library in your `requirements.txt` for each format. The loader itself is a thin adapter. It calls the endpoint, reads `extracted_text` out of the JSON response, and hands it to the framework’s document object. There’s no hidden complexity to wait for an official package to solve.
## A LangChain loader
LangChain’s `BaseLoader` interface just needs a `load()` method that returns a list of `Document` objects:
```python
import requests
from langchain_core.document_loaders import BaseLoader
from langchain_core.documents import Document
class TxtfetchLoader(BaseLoader):
def __init__(self, file_path: str, api_key: str):
self.file_path = file_path
self.api_key = api_key
def load(self) -> list[Document]:
with open(self.file_path, "rb") as f:
resp = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {self.api_key}"},
files={"file": f},
)
resp.raise_for_status()
body = resp.json()
if body["status"] != "success":
raise ValueError(body.get("error", "extraction failed"))
return [Document(
page_content=body["extracted_text"],
metadata={"source": self.file_path},
)]
```
Use it exactly like any built-in loader:
```python
loader = TxtfetchLoader("quarterly-report.pdf", api_key=TXTFETCH_KEY)
docs = loader.load()
# feed docs into your text splitter, then your vector store, as usual
```
## A LlamaIndex reader
LlamaIndex’s equivalent is a `BaseReader` with a `load_data()` method:
```python
import requests
from llama_index.core.readers.base import BaseReader
from llama_index.core.schema import Document
class TxtfetchReader(BaseReader):
def __init__(self, api_key: str):
self.api_key = api_key
def load_data(self, file_path: str) -> list[Document]:
with open(file_path, "rb") as f:
resp = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {self.api_key}"},
files={"file": f},
)
resp.raise_for_status()
body = resp.json()
if body["status"] != "success":
raise ValueError(body.get("error", "extraction failed"))
return [Document(text=body["extracted_text"], metadata={"source": file_path})]
```
Both loaders are deliberately minimal: a single request, a status check, and a document object. There’s no batching, retry, or concurrency built in. For ingesting more than a handful of files at once, see the [batch ingestion guide](https://txtfetch.com/blog/batch-and-large-document-ingestion). It covers the concurrency and retry pattern to wrap around this same call. Full endpoint details (auth, request shape, response shape) are in the [docs](https://txtfetch.com/docs) and the [PDF extraction reference](https://txtfetch.com/extract/pdf).
## Feeding the URL variant instead of local files
If your documents already live somewhere reachable over HTTP (an S3 presigned URL, a CMS export), skip the local file and pass `?url=`:
```python
resp = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {api_key}"},
params={"url": document_url},
)
```
Same response shape, one fewer download step in your loader.
## What this is and isn’t
This is a self-written adapter around a plain HTTP API. It’s not an official `txtfetch` package on PyPI or npm. It’s also not a LangChain- or LlamaIndex-maintained integration. If you’d rather not maintain even fifteen lines of loader code, an official SDK is on our roadmap. Today, the pattern above is the fastest path to plugging txtfetch into either framework. The loader itself doesn’t change no matter what document type it’s parsing. Only the file you point it at does. See the [PDF extraction guide](https://txtfetch.com/blog/extract-text-from-pdf-for-rag) for the reasoning behind `/v1/extract`’s single-endpoint design. Once you have documents loaded, [chunking strategy](https://txtfetch.com/blog/chunking-strategies-for-rag) is the next decision to make before embedding.
Building this into a production pipeline and want a hand? [Get in touch](https://txtfetch.com/contact) and we’ll set you up with an API key.
keep reading
- **[Ingesting large documents and big batches without falling over](https://txtfetch.com/blog/batch-and-large-document-ingestion)**
A 500-page PDF and a ten-thousand-file backfill stress the same two things: single-request time and concurrency.
Jun 23, 2026 · 3 min read
- **[Chunking strategies for RAG: from clean text to good retrieval](https://txtfetch.com/blog/chunking-strategies-for-rag)**
Fixed-size, recursive, and structure-aware chunking all assume clean extracted text. Extraction quality bounds chunk quality, whichever one you pick.
Apr 7, 2026 · 3 min read
- **[How to extract text from a PDF for RAG (without maintaining a parser)](https://txtfetch.com/blog/extract-text-from-pdf-for-rag)**
Feeding PDFs into a RAG pipeline breaks the usual parser stack: multi-column layouts, embedded tables, scanned pages. One request handles all three.
Jan 12, 2026 · 3 min read
See also: [API docs →](https://txtfetch.com/docs)
## Try it on your own file.
The free reader runs in your browser. Nothing gets uploaded.
[Open the file reader →](https://txtfetch.com/tools/file-to-text)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/blog/ocr-scanned-documents-api
# 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. 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](https://txtfetch.com/tools/pdf-text-check) 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:
```bash
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:
```bash
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:
```json
{
"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:
```json
{
"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](https://txtfetch.com/contact) and we’ll set you up with an API key.
keep reading
- **[Ingesting large documents and big batches without falling over](https://txtfetch.com/blog/batch-and-large-document-ingestion)**
A 500-page PDF and a ten-thousand-file backfill stress the same two things: single-request time and concurrency.
Jun 23, 2026 · 3 min read
- **[Parsing DOCX, PPTX, and XLSX into clean text for LLM ingestion](https://txtfetch.com/blog/parse-office-docs-docx-pptx-xlsx-for-llms)**
python-docx, python-pptx, and openpyxl each have their own API and their own way of failing on real files. One endpoint replaces all three.
Mar 10, 2026 · 3 min read
- **[Getting text out of video and audio: extraction vs. transcription](https://txtfetch.com/blog/text-from-video-and-audio)**
Text in a media file can live in a sidecar, an embedded track, a container tag, or nowhere at all. Extraction and speech-to-text are different jobs.
Aug 15, 2026 · 4 min read
See also: [OCR for scanned documents & images →](https://txtfetch.com/extract/image)
## Try it on your own file.
The free reader runs in your browser. Nothing gets uploaded.
[Open the file reader →](https://txtfetch.com/tools/file-to-text)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/blog/parse-office-docs-docx-pptx-xlsx-for-llms
# Parsing DOCX, PPTX, and XLSX into clean text for LLM ingestion
Mar 10, 2026 · 3 min read · office-docs, llm-ingestion, api
Office documents are the default file format inside most organizations. Proposals use DOCX, decks use PPTX, financial models use XLSX. An ingestion pipeline that indexes everything the business has must read all three formats eventually. Each one lives in a different library, with a different API and different failure modes. Each library also carries its own maintenance burden.
## Three formats, three libraries, three sets of edge cases
A typical Python stack ends up with `python-docx` for Word, `python-pptx` for PowerPoint, and `openpyxl` or `pandas` for Excel. Each is reasonably good at what it does, but together they create real overhead:
- **Three APIs to learn and maintain.** Extracting “the text” means writing and testing three separate code paths. Each has its own object model: paragraphs and runs, slides and shapes, sheets and rows and cells.
- **Version drift.** DOCX, PPTX, and XLSX are all OOXML under the hood. But library versions lag behind Office’s own format changes. A document saved from a newer Office build can trip up an older library version. These failures are often hard to reproduce locally.
- **Inconsistent handling of “weird” content.** This content includes text inside a table cell, inside a text box on a slide, or inside a merged Excel range. It also includes text in headers, footers, and speaker notes. Each library supports these cases differently, and the support is often poorly documented. As a result, “extract the text” quietly means different things depending on which file type hit your pipeline.
- **No shared error handling.** A corrupt DOCX raises a different exception than a corrupt XLSX. Your pipeline’s retry and logging logic has to special-case each library’s failure modes.
None of this is hard, exactly. It’s just three times the surface area for a problem that stays conceptually the same each time: turn a document into text.
## One call, regardless of which Office format it is
txtfetch runs all three formats through Apache Tika. Tika normalizes DOCX, PPTX, and XLSX into the same extraction path, along with their legacy `.doc`/`.ppt`/`.xls` counterparts and ODF equivalents. You don’t branch on file type. You send the file, and the response is identical in shape, regardless of what came in.
A Word proposal:
```bash
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@proposal.docx
```
A slide deck:
```bash
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@quarterly-review.pptx
```
A spreadsheet, fetched straight from wherever it’s hosted instead of downloaded first:
```bash
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/models/forecast.xlsx" \
-H "Authorization: Bearer $TXTFETCH_KEY"
```
All three return the same JSON shape:
```json
{
"status": "success",
"extracted_text": "Q3 Forecast\nRegion: North America\nRevenue: 4.2M..."
}
```
Table cells, slide text and notes, and sheet contents all come back as part of `extracted_text`. This flattens everything into the same linear text your chunker already expects from a PDF or an HTML page. The rest of your ingestion code doesn’t need a special branch for “this one came from Excel.”
## Fewer parsers, fewer things to patch
Dropping three format-specific libraries for one HTTP call has real benefits. There’s one thing to version, and one place to fix when a document category starts behaving oddly. Errors come back in the same shape too: `{"status": "error", "error": "..."}`. You stop tracking `python-docx`, `python-pptx`, and `openpyxl` release notes for compatibility breaks. For a document-heavy ingestion pipeline, “read anything from the business” is a hard requirement. That’s the whole point: less code exists purely to keep up with file formats.
Want to point it at your own DOCX/PPTX/XLSX archive? [Get in touch](https://txtfetch.com/contact) and we’ll set you up with an API key.
keep reading
- **[Ingesting large documents and big batches without falling over](https://txtfetch.com/blog/batch-and-large-document-ingestion)**
A 500-page PDF and a ten-thousand-file backfill stress the same two things: single-request time and concurrency.
Jun 23, 2026 · 3 min read
- **[OCR scanned documents and images through one API call](https://txtfetch.com/blog/ocr-scanned-documents-api)**
Scanned invoices, faxed contracts, and photographed receipts have no text layer, so they OCR through the same endpoint as everything else.
Feb 3, 2026 · 3 min read
- **[Getting text out of video and audio: extraction vs. transcription](https://txtfetch.com/blog/text-from-video-and-audio)**
Text in a media file can live in a sidecar, an embedded track, a container tag, or nowhere at all. Extraction and speech-to-text are different jobs.
Aug 15, 2026 · 4 min read
See also: [Office doc extraction (DOCX/PPTX/XLSX) →](https://txtfetch.com/extract/docx)
## Try it on your own file.
The free reader runs in your browser. Nothing gets uploaded.
[Open the file reader →](https://txtfetch.com/tools/file-to-text)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/blog/per-document-vs-per-page-pricing
# Per-document vs per-page pricing for document extraction
Jul 14, 2026 · 3 min read · pricing, rag, cost
Extraction pricing usually comes in one of two shapes: a flat rate per document, or a rate per page. Per-page looks cheaper on the pricing page. The number is smaller, so it’s easier to feel good about. That’s exactly why it’s worth doing the multiplication before picking a vendor on sticker price alone.
## The 300-page report problem
Say a per-page vendor charges what looks like a trivially small amount per page. Run a two-page invoice through it and the bill is basically nothing. Now run a 300-page annual report, a compliance filing, or a technical manual through the same pricing. That “trivially small” number gets multiplied by 300. A single document now costs as much as a hundred two-page ones did. Per-document pricing charges the same amount whether the file is two pages or three hundred. The unit being priced is “a document ingested,” not “a page rendered.”
Page count varies enormously across any real-world document set: reports, contracts, manuals, filings. Under per-page pricing, the long documents dominate the bill, not the short ones. A handful of 300-page filings can cost more than the rest of your entire archive combined.
## Where per-page pricing actually wins
This isn’t a case against per-page pricing everywhere. It’s a case for knowing which shape matches your workload. Say your document set is genuinely single-page: receipts, one-page forms, individual invoices. Then per-page and per-document pricing converge to roughly the same number. Whichever has the lower headline rate for that page count wins outright. Per-page pricing gets worse in direct proportion to how many multi-page documents you have, not in general.
## Estimating your own mix
The number that predicts your actual bill isn’t the average page count. A mean gets dragged around by a handful of huge outliers, the same way it would for anything with a long tail. What matters is the shape of your document distribution. Mostly-short documents with an occasional long one behave very differently under per-page pricing than a mix spread evenly across page counts. Before comparing vendors, pull the page-count distribution of a representative sample of your actual archive. Don’t guess from a handful of documents you happen to have open.
```bash
# rough page-count histogram for a local PDF archive
for f in *.pdf; do pdfinfo "$f" | grep Pages; done | awk '{print $2}' | sort -n | uniq -c
```
txtfetch prices per document, not per page, specifically because of this. A 300-page report and a 2-page memo cost the same to ingest. Growing your average document length doesn’t grow your bill. If you want to run the exact numbers against your own archive, use the [interactive pricing comparison](https://txtfetch.com/compare/per-page-pricing). It walks through a real page-count distribution instead of a single averaged estimate.
## Once the pricing model is settled
Cost predictability matters most at the volumes covered in the [batch ingestion guide](https://txtfetch.com/blog/batch-and-large-document-ingestion). A pricing model that’s fine for occasional single documents can surprise you once a backfill runs a few thousand files through it. See our [pricing page](https://txtfetch.com/pricing) for the current plans. If you’re evaluating alternatives on cost as well as extraction quality, start with the [txtfetch vs Unstructured.io](https://txtfetch.com/compare/unstructured) comparison.
Want to run your own document mix against both pricing models? [Get in touch](https://txtfetch.com/contact) and we’ll set you up with an API key.
keep reading
- **[Ingesting large documents and big batches without falling over](https://txtfetch.com/blog/batch-and-large-document-ingestion)**
A 500-page PDF and a ten-thousand-file backfill stress the same two things: single-request time and concurrency.
Jun 23, 2026 · 3 min read
- **[Extracting tables from PDFs and spreadsheets for RAG](https://txtfetch.com/blog/extract-tables-for-rag)**
Tables don't survive naive text extraction. They collapse into number-soup with no row or column structure. Structure-aware extraction keeps them usable.
Jun 2, 2026 · 3 min read
- **[Using txtfetch as a LangChain and LlamaIndex document loader](https://txtfetch.com/blog/langchain-llamaindex-document-loader)**
There's no official txtfetch SDK yet. Wiring the endpoint into a LangChain or LlamaIndex loader takes about fifteen lines.
May 5, 2026 · 3 min read
See also: [Pricing →](https://txtfetch.com/pricing)
## Try it on your own file.
The free reader runs in your browser. Nothing gets uploaded.
[Open the file reader →](https://txtfetch.com/tools/file-to-text)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/blog/text-from-video-and-audio
# Getting text out of video and audio: extraction vs. transcription
Aug 15, 2026 · 4 min read · media, captions, llm-ingestion
Someone on the team says “we need to pull text out of our video library.” The ticket gets written as if that’s one job. It’s two. The first is reading text that’s already sitting in the file: a caption track, a sidecar, an ID3 tag. The second is speech recognition. It listens to audio and guesses at words that were never written down anywhere. Mix the two up, and a one-day integration turns into a week of confused debugging. The two failure modes are completely different. Neither one tells you which kind of failure you’re looking at.
## Four places text can hide in a media file
**A sidecar caption file** (`.srt`, `.vtt`, `.ttml`) is the easy case: a plain text file that happens to sit next to a video. Open it and the words are right there. They’re wrapped in cue numbers and timestamps you’ll want to strip before handing the text downstream.
**An embedded caption track** lives inside the container itself, a `tx3g` track in an `.mp4`, an `S_TEXT` track in a `.mkv`. The words exist, but a tool that only reads container tags (title, artist, duration) never looks at this track at all. It sits muxed in alongside the video and audio streams. Reading it means actually parsing the container’s structure, not just skimming its metadata block.
**A container tag** is metadata the encoder wrote, not a transcript of anything: a title, an artist, sometimes an album. It’s genuinely text, and it genuinely comes back from a text extractor. But it has nothing to do with what’s said in the recording. An `.mp3` with `title: "Q3 all-hands"` hands back exactly that string, nothing about what anyone actually said in the meeting.
**Burned-in text** is different: captions painted into the video frame as pixels. Audio with no caption track and no useful tags at all has no text to extract either. Full stop. That’s the case speech-to-text exists for, and no amount of clever parsing turns pixels or raw waveform samples back into a text file.
You need to know which of these four categories a file falls into. That’s most of the battle. Guessing wrong looks like an extractor reporting “success” with a one-line title string. What you actually wanted was the caption track, sitting two boxes deeper in the same file.
## Extraction reads; transcription writes
Extraction takes text that already exists in a file and returns it unmodified, wherever it happened to be typed, tagged, or muxed in. Transcription takes sound that was never written down and produces new text by guessing at words from audio waveforms. txtfetch does the first job. It reads a caption sidecar, an embedded track, a container tag, whatever text is genuinely in the file, and returns it as-is. It does not listen to audio, and it never will. That’s a different, much heavier problem: acoustic models, language models, speaker diarization. It belongs to a dedicated ASR tool, not a document-extraction API.
If a media file’s actual content only exists as sound, no caption track, no transcript file anywhere, extraction can’t produce what isn’t there. That’s not a bug to work around. It’s the honest boundary of what “extract the text from this file” can mean.
## Getting a caption track out with ffmpeg or yt-dlp
If a video already has an embedded caption or subtitle track and you just need it as a standalone file:
```bash
ffmpeg -i meeting-recording.mp4 -map 0:s:0 meeting-recording.srt
```
`-map 0:s:0` selects the first subtitle stream in the container. Run `ffprobe meeting-recording.mp4` first if a file carries more than one and you need to check the stream index.
For a video hosted somewhere with its own auto-generated or uploader-supplied captions, `yt-dlp` pulls those down without touching the video itself:
```bash
yt-dlp --write-auto-sub --skip-download --sub-lang en "https://example.com/watch?v=..."
```
Neither of these transcribes anything. Both just extract a caption track that already existed, the same distinction this whole post has been making. If a file genuinely has no caption track and no useful tags, a real speech-to-text pass has to run first. That could be Whisper, a hosted ASR API, or whatever your stack already uses. Once you have the resulting `.srt` or plain text file, it goes through txtfetch exactly like any other document.
## From transcript to RAG pipeline
Say you’ve got a `.srt`, a `.vtt`, or a plain-text transcript. It might come from a caption track or from ASR. Whatever the source, the rest of the pipeline doesn’t care that it started life as a video. If your file is still in caption form, use the [subtitle-to-text tool](https://txtfetch.com/tools/subtitles-to-text) to strip cue numbers and timestamps into readable prose first. Then treat the result like any other document: chunk it, embed it, index it.
[Chunk previewer](https://txtfetch.com/tools/chunk-preview) shows exactly where your chunk boundaries land before you commit to a chunk size. That matters more for transcripts than for prose. A chunker tuned for paragraphs will happily cut a sentence in half at a caption-cue boundary if you don’t check first. The [chunking strategies guide](https://txtfetch.com/blog/chunking-strategies-for-rag) covers the fixed-size/recursive/structure-aware tradeoff in more depth if you’re picking a strategy from scratch.
Want the exact behavior for a specific container format, what `.mp4`, `.mp3`, `.wav`, or `.mkv` actually return? [See the full breakdown on /extract/captions](https://txtfetch.com/extract/captions), checked against a real extraction run rather than assumed from the format’s documentation.
keep reading
- **[Ingesting large documents and big batches without falling over](https://txtfetch.com/blog/batch-and-large-document-ingestion)**
A 500-page PDF and a ten-thousand-file backfill stress the same two things: single-request time and concurrency.
Jun 23, 2026 · 3 min read
- **[Using txtfetch as a LangChain and LlamaIndex document loader](https://txtfetch.com/blog/langchain-llamaindex-document-loader)**
There's no official txtfetch SDK yet. Wiring the endpoint into a LangChain or LlamaIndex loader takes about fifteen lines.
May 5, 2026 · 3 min read
- **[Chunking strategies for RAG: from clean text to good retrieval](https://txtfetch.com/blog/chunking-strategies-for-rag)**
Fixed-size, recursive, and structure-aware chunking all assume clean extracted text. Extraction quality bounds chunk quality, whichever one you pick.
Apr 7, 2026 · 3 min read
See also: [RAG & LLM ingestion →](https://txtfetch.com/solutions/rag-ingestion)
## Try it on your own file.
The free reader runs in your browser. Nothing gets uploaded.
[Open the file reader →](https://txtfetch.com/tools/file-to-text)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/changelog
# What shipped, and when.
Every entry below is dated to the commit that shipped it, and links to the page that proves it. Three entries are corrections, not features — we'd rather list those than hide them.
Subscribe: [`/changelog.xml`](https://txtfetch.com/changelog.xml)
## September 2026
[2026-09-06](#2026-09-06-sources) new
### Extract from Drive, SharePoint, S3, and more
The /sources page names every place a document can live before txtfetch reads it. It states which ones hand over a URL, and which need a file upload.
[/sources →](https://txtfetch.com/sources)
[2026-09-05](#2026-09-05-clean-extracted-text) new
### A free tool to clean up damaged extracted text
Paste text that came out of any extractor and see the damage signals. Each one links to the fix that explains it, and nothing is uploaded.
[/tools/clean-extracted-text →](https://txtfetch.com/tools/clean-extracted-text)
[2026-09-04](#2026-09-04-ingest) new
### What pgvector, Pinecone, Qdrant, Chroma, and Weaviate need next
txtfetch stops at extracted text. /ingest covers the chunk, embed, and upsert step for five vector stores, cited and dated.
[/ingest →](https://txtfetch.com/ingest)
[2026-09-03](#2026-09-03-compare-libraries) new
### How txtfetch compares to five open-source parsers
/compare/libraries checks txtfetch against Docling, MarkItDown, PyMuPDF4LLM, Marker, and MinerU on licence, GPU need, format coverage, and OCR.
[/compare/libraries →](https://txtfetch.com/compare/libraries)
[2026-09-01](#2026-09-01-languages) new
### Document-language pages, with the honest OCR limit stated
/languages covers nine document languages: legacy encodings, chunking risk, and where OCR support actually stops.
[/languages →](https://txtfetch.com/languages)
## August 2026
[2026-08-30](#2026-08-30-solutions-reading-order-fix) fixed
### Stopped promising guaranteed reading order on multi-column PDFs
/solutions claimed txtfetch always kept multi-column PDFs in reading order. It does not: the engine gets most PDFs right, and /fixes/columns-out-of-order covers the rest. The page now says so.
[/solutions →](https://txtfetch.com/solutions)
[2026-08-29](#2026-08-29-integrations) new
### Wire txtfetch into n8n, Zapier, Make, Airflow, or S3 and Lambda
No plugin ships for any of them. /integrations shows the HTTP step each platform already has, pointed at txtfetch.
[/integrations →](https://txtfetch.com/integrations)
[2026-08-24](#2026-08-24-self-hosted-tika) new
### An honest answer to 'why not self-host Tika?'
Apache Tika is free. /compare/self-hosted-tika shows the pinned versions, the from-source Tesseract build, and the runtime limits behind the question.
[/compare/self-hosted-tika →](https://txtfetch.com/compare/self-hosted-tika)
[2026-08-17](#2026-08-17-glossary) new
### An extraction glossary
/glossary defines PDF text layers, OCR, mojibake, OOXML, and chunking in plain language. Each term links to its full page.
[/glossary →](https://txtfetch.com/glossary)
[2026-08-16](#2026-08-16-diff) new
### Raw parser output next to txtfetch's output, word for word
/diff shows ten committed documents with the raw parser text next to the corrected text, with every difference marked.
[/diff →](https://txtfetch.com/diff)
[2026-08-15](#2026-08-15-extract-captions) new
### Audio and video get their own extraction page
/extract/captions states exactly what comes back from an .srt, .vtt, .mp4, .mp3, or .mkv file. A file with no caption track and no tag data returns an error, not an empty success.
[/extract/captions →](https://txtfetch.com/extract/captions)
[2026-08-13](#2026-08-13-formats-coverage) fixed
### Corrected the '1,000+ formats' claim
That figure counted every media type Tika can detect, not every one it can parse. /formats/coverage now lists the real, checked count, measured against the exact jar this build ships.
[/formats/coverage →](https://txtfetch.com/formats/coverage)
[2026-08-12](#2026-08-12-file-to-text) new
### One drop zone for any file type
/tools/file-to-text detects a file's real type from its bytes and extracts its text in the browser, with no upload.
[/tools/file-to-text →](https://txtfetch.com/tools/file-to-text)
[2026-08-10](#2026-08-10-legacy-office-readers) new
### Free readers for legacy Word, Excel, and PowerPoint files
/tools/doc-to-text reads the 97-2003 binary Office formats (.doc, .xls, .ppt) in the browser, alongside the existing DOCX/XLSX/PPTX tools.
[/tools/doc-to-text →](https://txtfetch.com/tools/doc-to-text)
[2026-08-07](#2026-08-07-image-ocr-check) new
### Check whether a scan will OCR cleanly before you send it
/tools/image-ocr-check measures resolution, focus, skew, and inversion from a photo's actual pixels, in the browser.
[/tools/image-ocr-check →](https://txtfetch.com/tools/image-ocr-check)
[2026-08-06](#2026-08-06-subtitles-metadata-fix) fixed
### Stopped claiming the API returns media metadata it has no field for
Copy on /tools/subtitles-to-text said the API returns a title, artist, duration, and codec for media files. It has no such field. A success carries the extracted text, the content type, the byte and character counts, and whether OCR ran.
[/tools/subtitles-to-text →](https://txtfetch.com/tools/subtitles-to-text)
[2026-08-04](#2026-08-04-email-to-text) new
### Read .eml and Outlook .msg files, attachments included
/tools/email-to-text extracts a message and its attachments recursively, in the browser.
[/tools/email-to-text →](https://txtfetch.com/tools/email-to-text)
[2026-08-03](#2026-08-03-office-to-text-tools) new
### In-browser DOCX, XLSX, and PPTX readers
/tools/docx-to-text and its XLSX and PPTX siblings read modern Office files in the browser. No upload, no API call.
[/tools/docx-to-text →](https://txtfetch.com/tools/docx-to-text)
[2026-08-01](#2026-08-01-plain-text-mirror) new
### Every page on this site is also plain text
Append .md to any URL for a plain-text twin. /llms.txt indexes the whole site by section, and /llms-full.txt is the whole corpus in one file.
[/text →](https://txtfetch.com/text)
## July 2026
[2026-07-31](#2026-07-31-fixes) new
### A page for text that extracted but came out wrong
/fixes covers symptoms like mojibake, missing spaces, and scrambled columns: what causes each one, and how to fix it.
[/fixes →](https://txtfetch.com/fixes)
[2026-07-21](#2026-07-21-solutions) new
### Use-case pages: RAG ingestion, search indexing, document workflows
/solutions shows the same API applied to different jobs, with the parts of the response each job actually uses.
[/solutions →](https://txtfetch.com/solutions)
[2026-07-19](#2026-07-19-docs) new
### Developer docs: quickstarts, async jobs, idempotency, errors
/docs covers authentication and both ways to call the API. It also covers response formats, async jobs and webhooks, idempotency keys, and the full error reference.
[/docs →](https://txtfetch.com/docs)
[2026-07-16](#2026-07-16-status) new
### A public status page
/status shows current status and 90-day uptime history for the extraction API. No login is required.
[/status →](https://txtfetch.com/status)
[2026-07-16](#2026-07-16-extract-format-pages) new
### A dedicated page for every format
/extract lists format-specific pages covering PDF, Office, email, HTML, images, and more, each with its own working curl.
[/extract →](https://txtfetch.com/extract)
what this page is
## One hub, no roadmap.
This page has no roadmap section and no deprecation policy. The one fact worth stating about versioning: every endpoint lives under `/v1`. Everything else here already shipped.
## Stop parsing. Start shipping.
Create an account and get an API key in minutes. The free Hobby plan needs no card.
[Get started →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/compare
# Per document vs per page, compared honestly.
Named comparisons against the document-extraction tools people actually evaluate us against — capability tables, cited pricing, and a calculator for your own workload.
**[txtfetch vs Unstructured.io](https://txtfetch.com/compare/unstructured)**
Open-core ETL for RAG pipelines, priced per page.
per page (pay-as-you-go)
**[txtfetch vs LlamaParse](https://txtfetch.com/compare/llamaparse)**
Credit-metered parsing, tuned for complex PDFs.
credits (per page, tiered by fidelity)
**[txtfetch vs AWS Textract](https://txtfetch.com/compare/aws-textract)**
AWS-native OCR and document analysis, billed per 1,000 pages.
per 1,000 pages (feature-stacked)
**[txtfetch vs Azure AI Document Intelligence](https://txtfetch.com/compare/azure-document-intelligence)**
Microsoft's prebuilt-model document API, billed per 1,000 pages.
per 1,000 pages (tiered by model)
**[txtfetch vs Mindee](https://txtfetch.com/compare/mindee)**
Subscription + credits for structured field extraction.
subscription + credits (1 credit = 1 page)
**[Why per-page pricing punishes long documents](https://txtfetch.com/compare/per-page-pricing)**
The explainer — the math behind every comparison above, worked out in general.
read the explainer →
**[Why not just run Apache Tika myself?](https://txtfetch.com/compare/self-hosted-tika)**
Tika is free. Here is the real build and runtime work behind it, sourced from our own recipe.
see what it takes →
**[Docling, MarkItDown, PyMuPDF4LLM, Marker, and MinerU](https://txtfetch.com/compare/libraries)**
Five open-source parsers you'd run yourself, compared on licence, GPU need, and what self-hosting actually costs.
see the comparison →
**[Ready to switch? See what your code looks like after.](https://txtfetch.com/migrate)**
The call you run today, the call that replaces it, and a drop-in adapter, per vendor.
see the migration guides →
**[How accurate is txtfetch, really?](https://txtfetch.com/benchmarks)**
Pricing and capabilities are only half the picture — see the measured accuracy numbers and honest caveats.
see the benchmarks →
## Check the numbers yourself.
The benchmark runs against a committed corpus. You can re-run it.
[See the benchmarks →](https://txtfetch.com/benchmarks)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/compare/aws-textract
# txtfetch vs AWS Textract
AWS Textract is aws-native ocr and document analysis, billed per 1,000 pages. Here's an honest comparison: pricing math, capabilities, and where each tool wins.
at a glance
AWS Textract
**Billing**: per 1,000 pages (feature-stacked)
**Rate used below**: $0.0015/page
**Free tier**: 1,000 pages/month free (first 3 months of your AWS account only).
txtfetch
**Billing**: per document, regardless of page count
**Rate used below**: flat monthly quota (see plans)
**Free tier**: 500 documents/month, free, ongoing (Hobby plan)
capability table
| Capability | AWS Textract | txtfetch |
| --- | --- | --- |
| Billing unit | per 1,000 pages | per document |
| Format coverage | PDF, PNG, JPEG, TIFF only | 615 formats (Apache Tika, checked) |
| OCR for scans / images | yes | yes |
| Table extraction | yes | yes* |
| Complex layout fidelity | partial | partial* |
| Structured field extraction | yes | not yet* |
| Self-hosted option | no | no* |
| Single HTTP endpoint | partial | yes |
Vendor-published benchmarks are marked directional throughout. See sources below. \* txtfetch notes: Tables come back as structured rows and cells, not flattened text. Use ?format=markdown for GFM pipe tables, or ?format=json for table elements with a cells array. Output is clean linear or structured text, not a visual layout reconstruction. That's by design, but worth knowing. Structured markdown and element-JSON document output ship today (?format=markdown / ?format=json). Schema-defined field extraction means pulling typed fields per your own schema. That feature is on the roadmap, not shipped yet. We won't claim it early. No shipped self-host or VPC artifact today. It is on the roadmap. Apache Tika itself is free to run yourself. See the page below for what that takes.
Weighing self-hosting against either API? See [what running Apache Tika yourself actually takes](https://txtfetch.com/compare/self-hosted-tika).
the cost calculator
## Per page vs per document, worked out.
A 300-page PDF is 300 units of billing to AWS Textract. It's one document to txtfetch. Adjust the numbers to your own workload.
[Interactive cost calculator — adjust pages per document and documents per month to compare pricing against AWS Textract]
where textract wins
- If you're already on AWS, it's the path of least resistance: IAM, VPC, and billing are already wired up.
- Purpose-built forms and table analysis work at scale, covering key-value pairs and structured tables.
- Per-feature pricing lets you pay only for what you use.
- Proven at very large volumes with AWS's operational guarantees around the rest of the stack.
where txtfetch wins
- Breadth: 615 formats, each with a real Apache Tika parser behind it. That's [checked against the exact build we run](https://txtfetch.com/formats/coverage), not a curated list of a dozen file types.
- One HTTP endpoint. Pass a URL or upload a file, and get text back. No SDK and no async job polling required.
- Priced per document, so a 300-page report doesn't cost 300x a one-pager.
- A durable free tier (500 documents/month, ongoing) rather than a time-boxed trial.
What we don't claim yet: schema-defined structured field extraction, or an uptime SLA. Those ship later. See the capability table above.
next step
Ready to switch? See [what your code looks like after migrating from AWS Textract](https://txtfetch.com/migrate/aws-textract): the call you run today, the call that replaces it, and a drop-in adapter.
sources
- [AWS Textract: Pricing](https://aws.amazon.com/textract/pricing/) Accessed 2026-07
## Check the numbers yourself.
The benchmark runs against a committed corpus. You can re-run it.
[See the benchmarks →](https://txtfetch.com/benchmarks)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/compare/azure-document-intelligence
# txtfetch vs Azure AI Document Intelligence
Azure AI Document Intelligence is microsoft's prebuilt-model document api, billed per 1,000 pages. Here's an honest comparison: pricing math, capabilities, and where each tool wins.
at a glance
Azure AI Document Intelligence
**Billing**: per 1,000 pages (tiered by model)
**Rate used below**: $0.0015/page directional
**Free tier**: 500 pages/month free.
txtfetch
**Billing**: per document, regardless of page count
**Rate used below**: flat monthly quota (see plans)
**Free tier**: 500 documents/month, free, ongoing (Hobby plan)
capability table
| Capability | Azure AI Document Intelligence | txtfetch |
| --- | --- | --- |
| Billing unit | per 1,000 pages | per document |
| Format coverage | PDF, images, limited Office formats | 615 formats (Apache Tika, checked) |
| OCR for scans / images | yes | yes |
| Table extraction | yes | yes* |
| Complex layout fidelity | partial | partial* |
| Structured field extraction | yes | not yet* |
| Self-hosted option | no | no* |
| Single HTTP endpoint | partial | yes |
Vendor-published benchmarks are marked directional throughout. See sources below. \* txtfetch notes: Tables come back as structured rows and cells, not flattened text. Use ?format=markdown for GFM pipe tables, or ?format=json for table elements with a cells array. Output is clean linear or structured text, not a visual layout reconstruction. That's by design, but worth knowing. Structured markdown and element-JSON document output ship today (?format=markdown / ?format=json). Schema-defined field extraction means pulling typed fields per your own schema. That feature is on the roadmap, not shipped yet. We won't claim it early. No shipped self-host or VPC artifact today. It is on the roadmap. Apache Tika itself is free to run yourself. See the page below for what that takes.
Weighing self-hosting against either API? See [what running Apache Tika yourself actually takes](https://txtfetch.com/compare/self-hosted-tika).
the cost calculator
## Per page vs per document, worked out.
A 300-page PDF is 300 units of billing to Azure AI Document Intelligence. It's one document to txtfetch. Adjust the numbers to your own workload.
[Interactive cost calculator — adjust pages per document and documents per month to compare pricing against Azure AI Document Intelligence]
where document intelligence wins
- Prebuilt models for common structured documents (invoices, receipts, IDs, W-2s) that extract named fields out of the box.
- Deep integration with the rest of Azure Cognitive Services and enterprise Microsoft procurement.
- This matters for regulated organizations already committed to that stack.
- Confidence scores and bounding boxes per extracted field for auditability.
where txtfetch wins
- Breadth: 615 formats, each with a real Apache Tika parser behind it. That's [checked against the exact build we run](https://txtfetch.com/formats/coverage), not a curated list of a dozen file types.
- One HTTP endpoint. Pass a URL or upload a file, and get text back. No SDK and no async job polling required.
- Priced per document, so a 300-page report doesn't cost 300x a one-pager.
- A durable free tier (500 documents/month, ongoing) rather than a time-boxed trial.
What we don't claim yet: schema-defined structured field extraction, or an uptime SLA. Those ship later. See the capability table above.
next step
Ready to switch? See [what your code looks like after migrating from Azure AI Document Intelligence](https://txtfetch.com/migrate/azure-document-intelligence): the call you run today, the call that replaces it, and a drop-in adapter.
sources
- [Azure AI Document Intelligence: Pricing](https://azure.microsoft.com/en-us/pricing/details/document-intelligence/) Accessed 2026-07 directional
- [Microsoft Q&A: Document Intelligence per-page rate consensus](https://learn.microsoft.com/en-us/answers/questions/tagged/azure-ai-document-intelligence) Accessed 2026-07 directional
## Check the numbers yourself.
The benchmark runs against a committed corpus. You can re-run it.
[See the benchmarks →](https://txtfetch.com/benchmarks)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/compare/libraries
# Five open-source parsers, and what running one actually costs.
Docling, MarkItDown, PyMuPDF4LLM, Marker, and MinerU are all free to download. Every one still needs a model to host, a GPU decision, and a patch schedule. Here's the honest comparison, cited and dated.
## Why compare txtfetch to a library you'd run yourself?
Every parser on this page is a library. You install it, you host it, and you own its updates. txtfetch is a hosted API: one HTTP call, nothing to install. That's the same honest framing [our Apache Tika page](https://txtfetch.com/compare/self-hosted-tika) already uses, and it applies here too.
This page names real strengths for each library. It also names the licence terms and the operational cost that a "free" download doesn't advertise up front.
the five-way matrix
| Capability | Docling | MarkItDown | PyMuPDF4LLM | Marker | MinerU |
| --- | --- | --- | --- | --- | --- |
| Hosting | self-hosted only | self-hosted only | self-hosted only | self-hosted only | self-hosted only |
| Ships neural model weights | yes | no | no | yes | yes |
| GPU required | optional | no | no | optional | partial |
| Table extraction | yes | partial | yes | yes | yes |
| OCR engine bundled | yes | no | partial | yes | yes |
| Structured or JSON output | yes | no | yes | yes | yes |
| Built-in chunking for RAG | yes | no | yes | yes | no |
| Single HTTP endpoint | partial | no | no | partial | partial |
Full licence citations, dated, live on each spoke below. No row on this page states an accuracy or speed figure. See [/benchmarks](https://txtfetch.com/benchmarks) for txtfetch's own numbers.
## A short decision list
- Need the broadest bundled OCR language coverage? MinerU ships 109 languages by default.
- Have only native, born-digital PDFs and no GPU? PyMuPDF4LLM is the fastest path.
- Need one document object that exports to Markdown, JSON, and RAG-ready chunks? Try Docling.
- Want the lightest possible install, with no model weights at all? MarkItDown fits.
- Need the highest table and math fidelity, and can run a GPU? Marker's --use\_llm pass targets that.
- Want none of the above to manage? That's the case for a hosted call instead.
## What self-hosting any of them costs
Every library on this page is free to download. None of them is free to run in production. Each one needs somewhere to put its model weights, a decision about GPU spend, and a plan for cold starts on a fresh process. Someone has to patch the library itself, and its model dependencies, as new CVEs and file-format edge cases turn up.
Two of the five, PyMuPDF4LLM and MinerU, carry licence terms that need a real read before you ship. PyMuPDF4LLM's default is AGPL-3.0's network-copyleft clause. MinerU adds its own usage thresholds. Marker's model weights carry a separate revenue and funding threshold from its Apache-2.0 code. Each spoke below names its own checklist.
## When one HTTP call is simpler
txtfetch has no model to host and no GPU to provision. Apache Tika and Tesseract run on our infrastructure, checked against 615 formats with a real parser behind them (see [the coverage list](https://txtfetch.com/formats/coverage)). If your team would rather ship the product than run a document-parsing service, that's the case for a hosted call.
The standard OCR tier ships English trained data plus script detection only (`TESSDATA_LANGS=eng osd`). See [/languages](https://txtfetch.com/languages) for the full per-language detail, and request `quality=premium` for a hard non-English scan.
compare each one to txtfetch
**[txtfetch vs Docling](https://txtfetch.com/compare/libraries/docling)**
A document-conversion library built around one unified document object, with broad format coverage.
MIT
**[txtfetch vs MarkItDown](https://txtfetch.com/compare/libraries/markitdown)**
A lightweight, zero-model Python utility that converts files to Markdown for LLM prompts.
MIT
**[txtfetch vs PyMuPDF4LLM](https://txtfetch.com/compare/libraries/pymupdf4llm)**
A PDF-first extension of PyMuPDF, built for fast Markdown and JSON output with no GPU.
Dual-licensed: GNU AGPL-3.0, or a paid Artifex commercial licence
**[txtfetch vs Marker](https://txtfetch.com/compare/libraries/marker)**
A layout- and VLM-driven converter with an optional LLM pass for the highest-fidelity tables and math.
Apache-2.0
**[txtfetch vs MinerU](https://txtfetch.com/compare/libraries/mineru)**
A VLM-and-OCR document parser with the broadest native OCR language coverage of the five.
MinerU Open Source License, a custom licence based on Apache-2.0
## More comparisons
- [Compare hub](https://txtfetch.com/compare) — the commercial extraction APIs, priced per page.
- [Why not just run Apache Tika myself?](https://txtfetch.com/compare/self-hosted-tika) — the same question, asked of the engine behind txtfetch itself.
- [Benchmarks](https://txtfetch.com/benchmarks) — txtfetch's own measured accuracy, per category, with the full methodology.
frequently asked questions
**Are Docling, MarkItDown, PyMuPDF4LLM, Marker, and MinerU really free?**: The code for all five is open source. Three (Docling, MarkItDown, and PyMuPDF4LLM's dual option) use permissive or fully free terms. PyMuPDF4LLM's default is AGPL-3.0. Marker and MinerU add extra terms above a funding, revenue, or usage threshold. Check each spoke's licence section before you pick one.
**Which of these should I pick for a RAG pipeline?**: It depends on your documents and your infrastructure. A CJK-heavy scan set favors MinerU's OCR coverage. A native-PDF-only pipeline with no GPU favors PyMuPDF4LLM. A mixed Office and web estate favors Docling or MarkItDown. Each spoke names its own real strengths.
**Does txtfetch use any of these libraries internally?**: No. txtfetch runs Apache Tika and Tesseract, not any of the five libraries on this page. See /compare/self-hosted-tika for that build.
**Why doesn't this page show accuracy or speed numbers?**: txtfetch has no benchmark run against these five libraries, so publishing one would be a guess dressed up as a fact. See /benchmarks for txtfetch's own measured numbers and their caveats.
## Keep the library. Add the hard formats.
Some teams run both. Send only the files the library cannot read.
[Read the quickstart →](https://txtfetch.com/docs)
[Compare self-hosted Tika →](https://txtfetch.com/compare/self-hosted-tika)
---
# https://txtfetch.com/compare/libraries/docling
# txtfetch vs Docling
Docling is an open-source library you run yourself. txtfetch is one HTTP call. Here's an honest look at the licence, the operational cost, and where each one wins.
## What Docling is
Docling is built by The Docling project (LF AI & Data Foundation, originated at IBM Research). A document-conversion library built around one unified document object, with broad format coverage.
Docling's code is MIT. Its default layout, table-structure, and GraniteDocling vision-language models are published separately on Hugging Face under Apache-2.0. Both are permissive, so this is the simplest licence story of the five.
Licence checked 2026-09. See [the project's own LICENSE file](https://github.com/docling-project/docling/blob/main/LICENSE).
Checked against the project's own LICENSE file, not a summary of it. A licence can change between releases, so check the LICENSE of the exact version you pin, not this page. This is a factual summary, not legal advice.
at a glance
Docling
**Code licence**: MIT
**Model-weights licence**: Apache-2.0 (layout, table-structure, and GraniteDocling models)
**Install**: pip install docling (Python 3.10+, macOS, Linux, and Windows, on x86_64 or arm64)
**Model weights**: Bundled: a layout model and a table-structure model download on first use. An optional GraniteDocling vision-language model adds a heavier VLM pipeline for harder pages.
**GPU**: Optional. Docling runs on CPU. A GPU speeds up the layout and VLM models on large batches.
**Formats**: One of the widest input lists in this comparison. It covers PDF, DOCX, XLSX, and PPTX, plus legacy DOC, XLS, and PPT through LibreOffice. Other inputs include ODF, EPUB, Apple Pages, HTML, Markdown, AsciiDoc, LaTeX, and CSV. It also reads images, audio and video (with the asr extra), email, and a few schema-specific XML formats such as USPTO, JATS, and XBRL.
**Output**: Markdown, HTML, lossless JSON (DoclingDocument), DocTags, plain text, WebVTT, and chunked JSONL built for RAG pipelines.
**OCR**: EasyOCR by default, with Tesseract and other engines pluggable. Docling's docs list per-engine language coverage, wider than one hard-coded set.
txtfetch
**Code licence**: N/A — hosted API, no code to license
**Model-weights licence**: N/A — no weights on your infrastructure
**Install**: None. One HTTP call.
**Model weights**: None on your side. Tika and Tesseract run on ours.
**GPU**: None needed. quality=premium routes to a VLM on our infrastructure.
**Formats**: Every format on the coverage list, one endpoint.
**Output**: Text, Markdown, or element JSON.
**OCR**: Standard tier: eng, osd. quality=premium for a hard scan. See /languages.
capability table
| Capability | Docling | txtfetch |
| --- | --- | --- |
| Hosting | self-hosted only | hosted API* |
| Ships neural model weights | yes | no |
| GPU required | optional | no* |
| Table extraction | yes | yes |
| OCR engine bundled | yes | yes* |
| Structured or JSON output | yes | yes* |
| Built-in chunking for RAG | yes | no* |
| Single HTTP endpoint | partial | yes |
\* txtfetch notes: One HTTP call. No install, no model download, no server to run and patch. The standard tier runs on Tika and Tesseract, CPU only. quality=premium routes a document to a vision-language model on our infrastructure, not yours. The standard OCR tier ships English trained data plus script detection only (TESSDATA\_LANGS=eng osd). See /languages for the per-language detail and quality=premium for a hard scan. ?format=markdown and ?format=json ship today. Schema-defined field extraction, pulling named fields to your own schema, is on the roadmap, not shipped yet. txtfetch returns text, Markdown, or element JSON. Chunking stays a caller-side step, the same reasoning behind every guide on /fixes.
No accuracy or speed figure is measured against Docling on this page. See [/benchmarks](https://txtfetch.com/benchmarks) for txtfetch's own measured numbers and their caveats.
where docling wins
- DoclingDocument gives every parsed file one consistent internal format. A PDF, a DOCX, and an HTML page all export through the same Markdown, JSON, or chunking path.
- The broadest input list of the five. It covers audio and video transcription, Apple Pages, and schema formats like JATS and XBRL, on top of the usual PDF and Office set.
- A chunked JSONL output mode built specifically for RAG pipelines, not bolted on after the fact.
- An active project governed by the LF AI & Data Foundation, with frequent format and model additions.
the work you still own
Docling is free to run. Running it in production is a separate job. This is the checklist that job includes.
- Downloading and caching the layout and table-structure models on first run, and again for the optional GraniteDocling VLM.
- Picking and installing an OCR engine (EasyOCR ships by default; Tesseract and others are pluggable), plus that engine's own language packs.
- Deciding whether a GPU is worth the cost for your batch size. A cold start pays the model-load penalty on every fresh process, unless you keep a worker warm.
- Patching Docling itself and its model dependencies as new CVEs and format edge cases surface.
where txtfetch wins
- One HTTP call. No install, no model download, no GPU to provision, and no server to patch.
- The standard OCR tier ships English trained data plus script detection only (`TESSDATA_LANGS=eng osd`). See [/languages](https://txtfetch.com/languages) for the per-language detail, and request `quality=premium` for a hard non-English scan.
- A durable free tier (500 documents/month, ongoing), so trying it costs nothing and no card.
txtfetch strips HTML markup, not boilerplate, the same as /fixes describes it. Nav, footer, and cookie-banner text still comes back as ordinary text. Deciding what's chrome versus article stays a caller-side step. We also don't ship schema-defined field extraction yet. See the capability table above for the full list.
the same job, one call
This is the whole integration: no install, no model download, no GPU.
```
curl -s -X POST "https://api.txtfetch.com/v1/extract?format=markdown" \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@document.pdf
```
frequently asked questions
**Is Docling really free to use commercially?**: Yes. Docling's code is MIT-licensed, and its default layout, table-structure, and GraniteDocling models are Apache-2.0. Both are permissive open-source licences with no revenue threshold or attribution clause.
**Does Docling need a GPU?**: No. It runs on CPU. A GPU speeds up the layout model and the optional GraniteDocling vision-language pipeline on larger batches, but nothing in Docling requires one.
**Can Docling read scanned documents in languages other than English?**: Its default OCR engine, EasyOCR, ships broader language coverage than a single hard-coded set, and Tesseract is pluggable too. Check Docling's own documentation for the exact language list of the engine you configure.
sources
- [Docling: LICENSE (MIT)](https://github.com/docling-project/docling/blob/main/LICENSE) Accessed 2026-09
- [Docling: README (features, formats, install)](https://github.com/docling-project/docling) Accessed 2026-09
- [Docling: supported input and output formats](https://docling-project.github.io/docling/usage/supported_formats/) Accessed 2026-09
- [GraniteDocling-258M model card (Apache-2.0)](https://huggingface.co/ibm-granite/granite-docling-258M) Accessed 2026-09
Checked against the project's own LICENSE file, not a summary of it. A licence can change between releases, so check the LICENSE of the exact version you pin, not this page. This is a factual summary, not legal advice.
other open-source parsers
- [txtfetch vs MarkItDown →](https://txtfetch.com/compare/libraries/markitdown)
- [txtfetch vs PyMuPDF4LLM →](https://txtfetch.com/compare/libraries/pymupdf4llm)
- [txtfetch vs Marker →](https://txtfetch.com/compare/libraries/marker)
- [txtfetch vs MinerU →](https://txtfetch.com/compare/libraries/mineru)
- [All open-source parsers →](https://txtfetch.com/compare/libraries)
## Keep the library. Add the hard formats.
Some teams run both. Send only the files Docling cannot read.
[Read the quickstart →](https://txtfetch.com/docs)
[Compare self-hosted Tika →](https://txtfetch.com/compare/self-hosted-tika)
---
# https://txtfetch.com/compare/libraries/marker
# txtfetch vs Marker
Marker is an open-source library you run yourself. txtfetch is one HTTP call. Here's an honest look at the licence, the operational cost, and where each one wins.
## What Marker is
Marker is built by Datalab. A layout- and VLM-driven converter with an optional LLM pass for the highest-fidelity tables and math.
Marker's code and its weights carry two different licences. The code is plain Apache-2.0. The model weights use a modified Open RAIL-M licence. That licence is free for research, personal use, and startups under $5M in combined funding or revenue. Above that line, you need a paid commercial licence. Marker's terms have changed more than once, so re-check both links before you rely on this split.
Licence checked 2026-09. See [the project's own LICENSE file](https://github.com/datalab-to/marker/blob/master/LICENSE).
Checked against the project's own LICENSE file, not a summary of it. A licence can change between releases, so check the LICENSE of the exact version you pin, not this page. This is a factual summary, not legal advice.
at a glance
Marker
**Code licence**: Apache-2.0
**Model-weights licence**: Modified AI Pubs Open RAIL-M, free for research, personal use, and startups under $5M in funding or revenue
**Install**: pip install marker-pdf. Model weights download automatically on first run.
**Model weights**: A layout-detection model, plus a heavier vision-language model for balanced and highest-fidelity modes. Both download on first use.
**GPU**: Runs on GPU, CPU, or Apple MPS, per Marker's own README. GPU is the realistic choice for throughput once you enable the VLM or --use_llm paths.
**Formats**: PDF, images, PPTX, DOCX, XLSX, HTML, and EPUB, in any language, per the project's README.
**Output**: Markdown, JSON, HTML, and a chunked output mode.
**OCR**: A bundled layout-and-OCR model stack: a lightweight detector for the fast, CPU-oriented mode, and a heavier vision-language pipeline for the balanced and highest-fidelity modes. An optional --use_llm flag routes a page through an external LLM (Gemini, Claude, OpenAI-compatible, and others) for extra table, math, and form fidelity.
txtfetch
**Code licence**: N/A — hosted API, no code to license
**Model-weights licence**: N/A — no weights on your infrastructure
**Install**: None. One HTTP call.
**Model weights**: None on your side. Tika and Tesseract run on ours.
**GPU**: None needed. quality=premium routes to a VLM on our infrastructure.
**Formats**: Every format on the coverage list, one endpoint.
**Output**: Text, Markdown, or element JSON.
**OCR**: Standard tier: eng, osd. quality=premium for a hard scan. See /languages.
capability table
| Capability | Marker | txtfetch |
| --- | --- | --- |
| Hosting | self-hosted only | hosted API* |
| Ships neural model weights | yes | no |
| GPU required | optional | no* |
| Table extraction | yes | yes |
| OCR engine bundled | yes | yes* |
| Structured or JSON output | yes | yes* |
| Built-in chunking for RAG | yes | no* |
| Single HTTP endpoint | partial | yes |
\* txtfetch notes: One HTTP call. No install, no model download, no server to run and patch. The standard tier runs on Tika and Tesseract, CPU only. quality=premium routes a document to a vision-language model on our infrastructure, not yours. The standard OCR tier ships English trained data plus script detection only (TESSDATA\_LANGS=eng osd). See /languages for the per-language detail and quality=premium for a hard scan. ?format=markdown and ?format=json ship today. Schema-defined field extraction, pulling named fields to your own schema, is on the roadmap, not shipped yet. txtfetch returns text, Markdown, or element JSON. Chunking stays a caller-side step, the same reasoning behind every guide on /fixes.
No accuracy or speed figure is measured against Marker on this page. See [/benchmarks](https://txtfetch.com/benchmarks) for txtfetch's own measured numbers and their caveats.
where marker wins
- Strong table, equation, and form fidelity, especially with the optional --use\_llm hybrid pass enabled.
- Automatic removal of running headers, footers, and page-layout artifacts before they reach the output.
- Three run tiers (fast, balanced, and the --use\_llm hybrid pass) that let you trade speed for fidelity per document, instead of one fixed setting.
- Works entirely offline once its model weights are downloaded, with no required outbound call unless you turn on --use\_llm.
the work you still own
Marker is free to run. Running it in production is a separate job. This is the checklist that job includes.
- Downloading, caching, and updating Marker's layout and vision-language model weights, and provisioning a GPU if your volume needs the throughput.
- Reviewing the Open RAIL-M weights licence against your own funding and revenue, since it is not the same permissive terms as the Apache-2.0 code.
- Configuring and paying for an external LLM provider for the --use\_llm hybrid accuracy pass. That call goes to a service you connect, not one Marker bundles.
- Patching Marker itself and its model stack as new versions and CVEs land, and re-checking the licence split each time you upgrade.
where txtfetch wins
- One HTTP call. No install, no model download, no GPU to provision, and no server to patch.
- The standard OCR tier ships English trained data plus script detection only (`TESSDATA_LANGS=eng osd`). See [/languages](https://txtfetch.com/languages) for the per-language detail, and request `quality=premium` for a hard non-English scan.
- A durable free tier (500 documents/month, ongoing), so trying it costs nothing and no card.
txtfetch strips HTML markup, not boilerplate, the same as /fixes describes it. Nav, footer, and cookie-banner text still comes back as ordinary text. Deciding what's chrome versus article stays a caller-side step. We also don't ship schema-defined field extraction yet. See the capability table above for the full list.
the same job, one call
This is the whole integration: no install, no model download, no GPU.
```
curl -s -X POST "https://api.txtfetch.com/v1/extract?format=markdown" \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@document.pdf
```
frequently asked questions
**Is Marker free to use commercially?**: The code is Apache-2.0, free for any use including commercial. The model weights are different: a modified Open RAIL-M licence, free for research, personal use, and startups under $5M in combined funding or revenue. Above that, Datalab sells a separate commercial licence for the weights.
**Does Marker need a GPU?**: No, per its own README it runs on GPU, CPU, or Apple MPS. A GPU is the realistic choice once you enable the vision-language or --use_llm paths, where CPU-only throughput drops sharply.
**What does --use_llm actually add?**: It routes a page through an external LLM you configure: Gemini, Claude, an OpenAI-compatible endpoint, or others. That pass improves table, inline-math, and form extraction beyond Marker's bundled models alone.
sources
- [Marker: LICENSE (Apache-2.0, code)](https://github.com/datalab-to/marker/blob/master/LICENSE) Accessed 2026-09
- [Marker: README (commercial usage, formats, GPU/CPU/MPS)](https://github.com/datalab-to/marker) Accessed 2026-09
- [Datalab: pricing (model-weights commercial licence)](https://www.datalab.to/pricing) Accessed 2026-09
- [marker-pdf on PyPI](https://pypi.org/project/marker-pdf/) Accessed 2026-09
Checked against the project's own LICENSE file, not a summary of it. A licence can change between releases, so check the LICENSE of the exact version you pin, not this page. This is a factual summary, not legal advice.
other open-source parsers
- [txtfetch vs Docling →](https://txtfetch.com/compare/libraries/docling)
- [txtfetch vs MarkItDown →](https://txtfetch.com/compare/libraries/markitdown)
- [txtfetch vs PyMuPDF4LLM →](https://txtfetch.com/compare/libraries/pymupdf4llm)
- [txtfetch vs MinerU →](https://txtfetch.com/compare/libraries/mineru)
- [All open-source parsers →](https://txtfetch.com/compare/libraries)
## Keep the library. Add the hard formats.
Some teams run both. Send only the files Marker cannot read.
[Read the quickstart →](https://txtfetch.com/docs)
[Compare self-hosted Tika →](https://txtfetch.com/compare/self-hosted-tika)
---
# https://txtfetch.com/compare/libraries/markitdown
# txtfetch vs MarkItDown
MarkItDown is an open-source library you run yourself. txtfetch is one HTTP call. Here's an honest look at the licence, the operational cost, and where each one wins.
## What MarkItDown is
MarkItDown is built by Microsoft (built by the AutoGen team). A lightweight, zero-model Python utility that converts files to Markdown for LLM prompts.
MIT, plainly. MarkItDown ships no neural model weights of its own, so there is no second licence to check.
Licence checked 2026-09. See [the project's own LICENSE file](https://github.com/microsoft/markitdown/blob/main/LICENSE).
Checked against the project's own LICENSE file, not a summary of it. A licence can change between releases, so check the LICENSE of the exact version you pin, not this page. This is a factual summary, not legal advice.
at a glance
MarkItDown
**Code licence**: MIT
**Model-weights licence**: None — ships no model weights
**Install**: pip install 'markitdown[all]' (Python 3.10+); or install format extras individually, e.g. markitdown[pdf,docx,pptx]
**Model weights**: None. Every core converter is rule-based (parses the file format directly). No model download, no GPU.
**GPU**: Not applicable. MarkItDown has no bundled ML model to run on one.
**Formats**: PDF, PowerPoint, Word, and Excel files. Images return EXIF metadata only, plus an optional LLM description. Audio returns EXIF metadata and an optional transcription. It also reads HTML, CSV, JSON, XML, ZIP archives, YouTube URLs, and EPUB.
**Output**: Markdown only, printed to stdout or a file.
**OCR**: None bundled. A scanned image or PDF returns its EXIF metadata, not the pictured text. Reading the pictured text needs the separate markitdown-ocr plugin and your own vision-LLM API key.
txtfetch
**Code licence**: N/A — hosted API, no code to license
**Model-weights licence**: N/A — no weights on your infrastructure
**Install**: None. One HTTP call.
**Model weights**: None on your side. Tika and Tesseract run on ours.
**GPU**: None needed. quality=premium routes to a VLM on our infrastructure.
**Formats**: Every format on the coverage list, one endpoint.
**Output**: Text, Markdown, or element JSON.
**OCR**: Standard tier: eng, osd. quality=premium for a hard scan. See /languages.
capability table
| Capability | MarkItDown | txtfetch |
| --- | --- | --- |
| Hosting | self-hosted only | hosted API* |
| Ships neural model weights | no | no |
| GPU required | no | no* |
| Table extraction | partial | yes |
| OCR engine bundled | no | yes* |
| Structured or JSON output | no | yes* |
| Built-in chunking for RAG | no | no* |
| Single HTTP endpoint | no | yes |
\* txtfetch notes: One HTTP call. No install, no model download, no server to run and patch. The standard tier runs on Tika and Tesseract, CPU only. quality=premium routes a document to a vision-language model on our infrastructure, not yours. The standard OCR tier ships English trained data plus script detection only (TESSDATA\_LANGS=eng osd). See /languages for the per-language detail and quality=premium for a hard scan. ?format=markdown and ?format=json ship today. Schema-defined field extraction, pulling named fields to your own schema, is on the roadmap, not shipped yet. txtfetch returns text, Markdown, or element JSON. Chunking stays a caller-side step, the same reasoning behind every guide on /fixes.
No accuracy or speed figure is measured against MarkItDown on this page. See [/benchmarks](https://txtfetch.com/benchmarks) for txtfetch's own measured numbers and their caveats.
where markitdown wins
- The lightest install of the five: a pure-Python tool with no model weights, no GPU, and a near-instant cold start.
- Clean, direct Markdown for well-structured Office files, HTML, and data formats (CSV, JSON, XML) headed straight into an LLM prompt.
- A plugin system, so a third party can add a converter or an OCR path without waiting on the core project.
- Built and maintained by the team behind AutoGen, so it tracks closely with how Microsoft's own agent tooling expects document input to look.
the work you still own
MarkItDown is free to run. Running it in production is a separate job. This is the checklist that job includes.
- Supplying your own OCR path. MarkItDown ships none: a scanned page returns no text at all unless you install markitdown-ocr and wire up a vision-LLM API key and its cost.
- Table fidelity on complex PDFs. MarkItDown's PDF converter is text-layer-based, with no layout model behind it, so a multi-column or heavily tabular PDF often needs a different tool.
- Patching the tool and its many optional per-format dependencies as new file variants and CVEs surface.
- Running the OCR or image-description LLM call yourself, including its own cost, latency, and rate limits, since MarkItDown only forwards to a client you provide.
where txtfetch wins
- One HTTP call. No install, no model download, no GPU to provision, and no server to patch.
- The standard OCR tier ships English trained data plus script detection only (`TESSDATA_LANGS=eng osd`). See [/languages](https://txtfetch.com/languages) for the per-language detail, and request `quality=premium` for a hard non-English scan.
- A durable free tier (500 documents/month, ongoing), so trying it costs nothing and no card.
txtfetch strips HTML markup, not boilerplate, the same as /fixes describes it. Nav, footer, and cookie-banner text still comes back as ordinary text. Deciding what's chrome versus article stays a caller-side step. We also don't ship schema-defined field extraction yet. See the capability table above for the full list.
the same job, one call
This is the whole integration: no install, no model download, no GPU.
```
curl -s -X POST "https://api.txtfetch.com/v1/extract?format=markdown" \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@document.pdf
```
frequently asked questions
**Can MarkItDown OCR a scanned document?**: Not by itself. The core tool reads a scanned image's EXIF metadata, not its pictured text. The separate markitdown-ocr plugin adds OCR, but it works by calling a vision-LLM API you configure and pay for yourself, not a bundled local engine.
**Does MarkItDown need a GPU?**: No. It ships no neural model weights at all, so there is nothing to run on a GPU.
**What licence covers MarkItDown?**: MIT, the same permissive licence as its own README states, with no separate model-weights licence to track since it ships no models.
sources
- [MarkItDown: LICENSE (MIT)](https://github.com/microsoft/markitdown/blob/main/LICENSE) Accessed 2026-09
- [MarkItDown: README (formats, install, plugins)](https://github.com/microsoft/markitdown) Accessed 2026-09
- [MarkItDown on PyPI](https://pypi.org/project/markitdown/) Accessed 2026-09
Checked against the project's own LICENSE file, not a summary of it. A licence can change between releases, so check the LICENSE of the exact version you pin, not this page. This is a factual summary, not legal advice.
other open-source parsers
- [txtfetch vs Docling →](https://txtfetch.com/compare/libraries/docling)
- [txtfetch vs PyMuPDF4LLM →](https://txtfetch.com/compare/libraries/pymupdf4llm)
- [txtfetch vs Marker →](https://txtfetch.com/compare/libraries/marker)
- [txtfetch vs MinerU →](https://txtfetch.com/compare/libraries/mineru)
- [All open-source parsers →](https://txtfetch.com/compare/libraries)
## Keep the library. Add the hard formats.
Some teams run both. Send only the files MarkItDown cannot read.
[Read the quickstart →](https://txtfetch.com/docs)
[Compare self-hosted Tika →](https://txtfetch.com/compare/self-hosted-tika)
---
# https://txtfetch.com/compare/libraries/mineru
# txtfetch vs MinerU
MinerU is an open-source library you run yourself. txtfetch is one HTTP call. Here's an honest look at the licence, the operational cost, and where each one wins.
## What MinerU is
MinerU is built by OpenDataLab. A VLM-and-OCR document parser with the broadest native OCR language coverage of the five.
MinerU moved off AGPL-3.0 in its 3.1.0 release to a custom licence built on top of Apache-2.0. Commercial use is free below two thresholds: 100 million monthly active users, or $20 million in monthly revenue. Both thresholds count your affiliates too. Cross either one and you need a separate commercial licence. The licence also requires you to name MinerU in any product or service you build on it. MinerU changed licences this recently, so double-check which version, and which licence, you are actually pinning.
Licence checked 2026-09. See [the project's own LICENSE file](https://github.com/opendatalab/MinerU/blob/master/LICENSE.md).
Checked against the project's own LICENSE file, not a summary of it. A licence can change between releases, so check the LICENSE of the exact version you pin, not this page. This is a factual summary, not legal advice.
at a glance
MinerU
**Code licence**: MinerU Open Source License, a custom licence based on Apache-2.0
**Model-weights licence**: Covered by the same MinerU Open Source License as the code
**Install**: pip install mineru (or an extras variant). Model weights download from Hugging Face or ModelScope on first run.
**Model weights**: A VLM model (around 1.2B parameters in its current release) plus an OCR model stack, both downloaded on first use.
**GPU**: Depends on the backend. The CPU-capable pipeline backend runs without one; the higher-accuracy VLM and hybrid-engine backends need a GPU (or Apple mlx) for realistic throughput. Several Chinese domestic AI accelerators are also supported directly.
**Formats**: Native PDF, DOCX, PPTX, and XLSX parsing, plus images, per the project's README.
**Output**: Markdown and JSON, with formulas converted to LaTeX and tables to HTML.
**OCR**: A dual VLM-plus-OCR engine with 109-language OCR recognition. That's the broadest bundled OCR language list of the five.
txtfetch
**Code licence**: N/A — hosted API, no code to license
**Model-weights licence**: N/A — no weights on your infrastructure
**Install**: None. One HTTP call.
**Model weights**: None on your side. Tika and Tesseract run on ours.
**GPU**: None needed. quality=premium routes to a VLM on our infrastructure.
**Formats**: Every format on the coverage list, one endpoint.
**Output**: Text, Markdown, or element JSON.
**OCR**: Standard tier: eng, osd. quality=premium for a hard scan. See /languages.
capability table
| Capability | MinerU | txtfetch |
| --- | --- | --- |
| Hosting | self-hosted only | hosted API* |
| Ships neural model weights | yes | no |
| GPU required | partial | no* |
| Table extraction | yes | yes |
| OCR engine bundled | yes | yes* |
| Structured or JSON output | yes | yes* |
| Built-in chunking for RAG | no | no* |
| Single HTTP endpoint | partial | yes |
\* txtfetch notes: One HTTP call. No install, no model download, no server to run and patch. The standard tier runs on Tika and Tesseract, CPU only. quality=premium routes a document to a vision-language model on our infrastructure, not yours. The standard OCR tier ships English trained data plus script detection only (TESSDATA\_LANGS=eng osd). See /languages for the per-language detail and quality=premium for a hard scan. ?format=markdown and ?format=json ship today. Schema-defined field extraction, pulling named fields to your own schema, is on the roadmap, not shipped yet. txtfetch returns text, Markdown, or element JSON. Chunking stays a caller-side step, the same reasoning behind every guide on /fixes.
No accuracy or speed figure is measured against MinerU on this page. See [/benchmarks](https://txtfetch.com/benchmarks) for txtfetch's own measured numbers and their caveats.
where mineru wins
- 109-language OCR recognition, bundled by default. That's the strongest coverage of the five for non-Latin and CJK scripts.
- Cross-page table merging and reading-order reconstruction aimed specifically at complex, multi-page layouts.
- Native parsing of PDF, DOCX, PPTX, and XLSX without first converting through PDF, unlike tools that only read Office formats via a conversion step.
- Multiple backend choices (a CPU-only pipeline, plus GPU-backed VLM and hybrid engines) so you can trade cost against accuracy per workload.
the work you still own
MinerU is free to run. Running it in production is a separate job. This is the checklist that job includes.
- Downloading and caching MinerU's VLM and OCR model weights, and provisioning a GPU if you need the higher-accuracy backends at real volume.
- Naming MinerU in your product or service, a condition the licence attaches to any online service built on it.
- Tracking your own combined monthly active users and revenue against the licence's 100 million MAU / $20 million revenue thresholds.
- Patching MinerU and its model stack, and re-reading the licence on every upgrade, since it changed once already in 2026.
where txtfetch wins
- One HTTP call. No install, no model download, no GPU to provision, and no server to patch.
- The standard OCR tier ships English trained data plus script detection only (`TESSDATA_LANGS=eng osd`). See [/languages](https://txtfetch.com/languages) for the per-language detail, and request `quality=premium` for a hard non-English scan.
- A durable free tier (500 documents/month, ongoing), so trying it costs nothing and no card.
txtfetch strips HTML markup, not boilerplate, the same as /fixes describes it. Nav, footer, and cookie-banner text still comes back as ordinary text. Deciding what's chrome versus article stays a caller-side step. We also don't ship schema-defined field extraction yet. See the capability table above for the full list.
the same job, one call
This is the whole integration: no install, no model download, no GPU.
```
curl -s -X POST "https://api.txtfetch.com/v1/extract?format=markdown" \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@document.pdf
```
frequently asked questions
**Is MinerU still AGPL-licensed?**: No, not since its 3.1.0 release. MinerU moved to a custom MinerU Open Source License built on Apache-2.0. It's free below 100 million monthly active users, or $20 million in monthly revenue, counting your affiliates. It also requires naming MinerU in any service you build on it.
**Does MinerU need a GPU?**: Only for its higher-accuracy backends. The pipeline backend runs on CPU. The VLM and hybrid-engine backends need a GPU, or Apple's mlx, for realistic throughput.
**What makes MinerU's OCR different from the others here?**: It bundles OCR recognition across 109 languages by default. That's the broadest language list of the five projects on this page, and it matters most for non-Latin and CJK-script documents.
sources
- [MinerU: LICENSE.md (custom, Apache-2.0-based)](https://github.com/opendatalab/MinerU/blob/master/LICENSE.md) Accessed 2026-09
- [MinerU: README (changelog, formats, backends)](https://github.com/opendatalab/MinerU) Accessed 2026-09
- [mineru on PyPI](https://pypi.org/project/mineru/) Accessed 2026-09
Checked against the project's own LICENSE file, not a summary of it. A licence can change between releases, so check the LICENSE of the exact version you pin, not this page. This is a factual summary, not legal advice.
other open-source parsers
- [txtfetch vs Docling →](https://txtfetch.com/compare/libraries/docling)
- [txtfetch vs MarkItDown →](https://txtfetch.com/compare/libraries/markitdown)
- [txtfetch vs PyMuPDF4LLM →](https://txtfetch.com/compare/libraries/pymupdf4llm)
- [txtfetch vs Marker →](https://txtfetch.com/compare/libraries/marker)
- [All open-source parsers →](https://txtfetch.com/compare/libraries)
## Keep the library. Add the hard formats.
Some teams run both. Send only the files MinerU cannot read.
[Read the quickstart →](https://txtfetch.com/docs)
[Compare self-hosted Tika →](https://txtfetch.com/compare/self-hosted-tika)
---
# https://txtfetch.com/compare/libraries/pymupdf4llm
# txtfetch vs PyMuPDF4LLM
PyMuPDF4LLM is an open-source library you run yourself. txtfetch is one HTTP call. Here's an honest look at the licence, the operational cost, and where each one wins.
## What PyMuPDF4LLM is
PyMuPDF4LLM is built by Artifex Software (maintainer of PyMuPDF and the MuPDF engine). A PDF-first extension of PyMuPDF, built for fast Markdown and JSON output with no GPU.
PyMuPDF4LLM inherits PyMuPDF's own dual licence. Use it under AGPL-3.0, or pay Artifex for a commercial licence if AGPL's copyleft terms don't fit your product. This is the strictest default licence of the five. AGPL requires that a network service built on it also offer its own source. That's a real constraint for a hosted product, not just a library you vendor in.
Licence checked 2026-09. See [the project's own LICENSE file](https://github.com/pymupdf/pymupdf4llm/blob/master/LICENSE).
Checked against the project's own LICENSE file, not a summary of it. A licence can change between releases, so check the LICENSE of the exact version you pin, not this page. This is a factual summary, not legal advice.
at a glance
PyMuPDF4LLM
**Code licence**: Dual-licensed: GNU AGPL-3.0, or a paid Artifex commercial licence
**Model-weights licence**: None — ships no model weights
**Install**: pip install pymupdf4llm (pulls in PyMuPDF and pymupdf-layout). Office-format support needs the separate, paid PyMuPDF Pro add-on.
**Model weights**: None. PyMuPDF4LLM is a heuristics-and-C-engine parser, with no bundled neural model.
**GPU**: Not applicable. Nothing in PyMuPDF4LLM's own pipeline needs one.
**Formats**: PDF is the primary, fully supported format. Other formats PyMuPDF itself can open (XPS, EPUB, and a few image formats) pass through the same API. DOCX, XLSX, and PPTX need the separate paid PyMuPDF Pro package.
**Output**: Markdown, JSON (with bounding-box and layout metadata), plain text, and a page-chunked mode with metadata per chunk for a vector store.
**OCR**: Hybrid and selective: PyMuPDF4LLM inspects each page first and only routes the illegible or image-only regions to Tesseract, which you install and manage yourself. A clean, born-digital page skips OCR entirely.
txtfetch
**Code licence**: N/A — hosted API, no code to license
**Model-weights licence**: N/A — no weights on your infrastructure
**Install**: None. One HTTP call.
**Model weights**: None on your side. Tika and Tesseract run on ours.
**GPU**: None needed. quality=premium routes to a VLM on our infrastructure.
**Formats**: Every format on the coverage list, one endpoint.
**Output**: Text, Markdown, or element JSON.
**OCR**: Standard tier: eng, osd. quality=premium for a hard scan. See /languages.
capability table
| Capability | PyMuPDF4LLM | txtfetch |
| --- | --- | --- |
| Hosting | self-hosted only | hosted API* |
| Ships neural model weights | no | no |
| GPU required | no | no* |
| Table extraction | yes | yes |
| OCR engine bundled | partial | yes* |
| Structured or JSON output | yes | yes* |
| Built-in chunking for RAG | yes | no* |
| Single HTTP endpoint | no | yes |
\* txtfetch notes: One HTTP call. No install, no model download, no server to run and patch. The standard tier runs on Tika and Tesseract, CPU only. quality=premium routes a document to a vision-language model on our infrastructure, not yours. The standard OCR tier ships English trained data plus script detection only (TESSDATA\_LANGS=eng osd). See /languages for the per-language detail and quality=premium for a hard scan. ?format=markdown and ?format=json ship today. Schema-defined field extraction, pulling named fields to your own schema, is on the roadmap, not shipped yet. txtfetch returns text, Markdown, or element JSON. Chunking stays a caller-side step, the same reasoning behind every guide on /fixes.
No accuracy or speed figure is measured against PyMuPDF4LLM on this page. See [/benchmarks](https://txtfetch.com/benchmarks) for txtfetch's own measured numbers and their caveats.
where pymupdf4llm wins
- The fastest path in this comparison for a native, born-digital PDF: a pure C engine with no model to load and no GPU to provision.
- Selective, hybrid OCR that only touches the regions of a page that actually need it, instead of OCR-ing every page of a mixed document.
- Page-level chunking with metadata built directly into the output, ready for a vector store with no extra chunking step.
- A tiny dependency footprint for a PDF-only pipeline, with no model weights to download or version.
the work you still own
PyMuPDF4LLM is free to run. Running it in production is a separate job. This is the checklist that job includes.
- Installing and maintaining Tesseract yourself for the OCR path, including its own language packs, since PyMuPDF4LLM only calls out to it.
- Paying for and installing PyMuPDF Pro if your pipeline needs Word, Excel, or PowerPoint files, since the free package covers PDF only.
- Reviewing the AGPL-3.0 terms against your own product shape, or budgeting for Artifex's commercial licence if AGPL's network-copyleft clause doesn't fit.
- Patching PyMuPDF, PyMuPDF4LLM, and your own Tesseract install as new CVEs and PDF edge cases surface.
where txtfetch wins
- One HTTP call. No install, no model download, no GPU to provision, and no server to patch.
- The standard OCR tier ships English trained data plus script detection only (`TESSDATA_LANGS=eng osd`). See [/languages](https://txtfetch.com/languages) for the per-language detail, and request `quality=premium` for a hard non-English scan.
- A durable free tier (500 documents/month, ongoing), so trying it costs nothing and no card.
txtfetch strips HTML markup, not boilerplate, the same as /fixes describes it. Nav, footer, and cookie-banner text still comes back as ordinary text. Deciding what's chrome versus article stays a caller-side step. We also don't ship schema-defined field extraction yet. See the capability table above for the full list.
the same job, one call
This is the whole integration: no install, no model download, no GPU.
```
curl -s -X POST "https://api.txtfetch.com/v1/extract?format=markdown" \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@document.pdf
```
frequently asked questions
**Is PyMuPDF4LLM's AGPL licence a problem for a hosted product?**: It can be. AGPL-3.0 requires that a network service built on AGPL-licensed code also offer its own source to users of that service. Check the licence against your own product shape, or use Artifex's paid commercial licence if AGPL doesn't fit. This is not legal advice.
**Does PyMuPDF4LLM need a GPU?**: No. It's a C-engine PDF parser with no bundled neural model, so nothing in its own pipeline benefits from one.
**Can PyMuPDF4LLM read Word or Excel files?**: Only through PyMuPDF Pro, a separate paid add-on. The free, AGPL-licensed package parses PDF (and a few other formats PyMuPDF itself opens) only.
sources
- [pymupdf4llm: LICENSE (AGPL-3.0)](https://github.com/pymupdf/pymupdf4llm/blob/master/LICENSE) Accessed 2026-09
- [pymupdf4llm: README (features, hybrid OCR, install)](https://github.com/pymupdf/pymupdf4llm) Accessed 2026-09
- [PyMuPDF on PyPI (dual licence statement)](https://pypi.org/project/PyMuPDF/) Accessed 2026-09
- [pymupdf4llm on PyPI](https://pypi.org/project/pymupdf4llm/) Accessed 2026-09
Checked against the project's own LICENSE file, not a summary of it. A licence can change between releases, so check the LICENSE of the exact version you pin, not this page. This is a factual summary, not legal advice.
other open-source parsers
- [txtfetch vs Docling →](https://txtfetch.com/compare/libraries/docling)
- [txtfetch vs MarkItDown →](https://txtfetch.com/compare/libraries/markitdown)
- [txtfetch vs Marker →](https://txtfetch.com/compare/libraries/marker)
- [txtfetch vs MinerU →](https://txtfetch.com/compare/libraries/mineru)
- [All open-source parsers →](https://txtfetch.com/compare/libraries)
## Keep the library. Add the hard formats.
Some teams run both. Send only the files PyMuPDF4LLM cannot read.
[Read the quickstart →](https://txtfetch.com/docs)
[Compare self-hosted Tika →](https://txtfetch.com/compare/self-hosted-tika)
---
# https://txtfetch.com/compare/llamaparse
# txtfetch vs LlamaParse
LlamaParse is credit-metered parsing, tuned for complex pdfs. Here's an honest comparison: pricing math, capabilities, and where each tool wins.
at a glance
LlamaParse
**Billing**: credits (per page, tiered by fidelity)
**Rate used below**: $0.00125/page
**Free tier**: ~10,000 credits/month free (secondary source; treat as directional).
txtfetch
**Billing**: per document, regardless of page count
**Rate used below**: flat monthly quota (see plans)
**Free tier**: 500 documents/month, free, ongoing (Hobby plan)
capability table
| Capability | LlamaParse | txtfetch |
| --- | --- | --- |
| Billing unit | credits (per page) | per document |
| Format coverage | PDF-first, common Office formats | 615 formats (Apache Tika, checked) |
| OCR for scans / images | yes | yes |
| Table extraction | yes | yes* |
| Complex layout fidelity | yes | partial* |
| Structured field extraction | yes | not yet* |
| Self-hosted option | no | no* |
| Single HTTP endpoint | partial | yes |
Vendor-published benchmarks are marked directional throughout. See sources below. \* txtfetch notes: Tables come back as structured rows and cells, not flattened text. Use ?format=markdown for GFM pipe tables, or ?format=json for table elements with a cells array. Output is clean linear or structured text, not a visual layout reconstruction. That's by design, but worth knowing. Structured markdown and element-JSON document output ship today (?format=markdown / ?format=json). Schema-defined field extraction means pulling typed fields per your own schema. That feature is on the roadmap, not shipped yet. We won't claim it early. No shipped self-host or VPC artifact today. It is on the roadmap. Apache Tika itself is free to run yourself. See the page below for what that takes.
Weighing self-hosting against either API? See [what running Apache Tika yourself actually takes](https://txtfetch.com/compare/self-hosted-tika).
the cost calculator
## Per page vs per document, worked out.
A 300-page PDF is 300 units of billing to LlamaParse. It's one document to txtfetch. Adjust the numbers to your own workload.
[Interactive cost calculator — adjust pages per document and documents per month to compare pricing against LlamaParse]
where llamaparse wins
- Best-in-class fidelity on genuinely hard PDFs: multi-column layouts, embedded tables, math notation, and forms.
- Higher tiers (Premium/Accurate) trade credits for materially better structure recovery on messy scans. That's a real dial we don't offer.
- Native structured/JSON output modes for downstream LLM consumption, which we don't have yet.
where txtfetch wins
- Breadth: 615 formats, each with a real Apache Tika parser behind it. That's [checked against the exact build we run](https://txtfetch.com/formats/coverage), not a curated list of a dozen file types.
- One HTTP endpoint. Pass a URL or upload a file, and get text back. No SDK and no async job polling required.
- Priced per document, so a 300-page report doesn't cost 300x a one-pager.
- A durable free tier (500 documents/month, ongoing) rather than a time-boxed trial.
What we don't claim yet: schema-defined structured field extraction, or an uptime SLA. Those ship later. See the capability table above.
next step
Ready to switch? See [what your code looks like after migrating from LlamaParse](https://txtfetch.com/migrate/llamaparse): the call you run today, the call that replaces it, and a drop-in adapter.
sources
- [LlamaIndex: LlamaParse pricing](https://developers.llamaindex.ai/llamaparse/general/pricing/) Accessed 2026-07
- [LlamaParse free-tier credits (secondary source)](https://developers.llamaindex.ai/llamaparse/general/pricing/) Accessed 2026-07 directional
## Check the numbers yourself.
The benchmark runs against a committed corpus. You can re-run it.
[See the benchmarks →](https://txtfetch.com/benchmarks)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/compare/mindee
# txtfetch vs Mindee
Mindee is subscription + credits for structured field extraction. Here's an honest comparison: pricing math, capabilities, and where each tool wins.
at a glance
Mindee
**Billing**: subscription + credits (1 credit = 1 page)
**Rate used below**: $0.05/page directional
**Free tier**: Removed September 15, 2025. 14-day trial only, no ongoing free tier.
txtfetch
**Billing**: per document, regardless of page count
**Rate used below**: flat monthly quota (see plans)
**Free tier**: 500 documents/month, free, ongoing (Hobby plan)
capability table
| Capability | Mindee | txtfetch |
| --- | --- | --- |
| Billing unit | subscription + credits | per document |
| Format coverage | PDF, images (narrow, doc-type-specific models) | 615 formats (Apache Tika, checked) |
| OCR for scans / images | yes | yes |
| Table extraction | partial | yes* |
| Complex layout fidelity | partial | partial* |
| Structured field extraction | yes | not yet* |
| Self-hosted option | no | no* |
| Single HTTP endpoint | yes | yes |
Vendor-published benchmarks are marked directional throughout. See sources below. \* txtfetch notes: Tables come back as structured rows and cells, not flattened text. Use ?format=markdown for GFM pipe tables, or ?format=json for table elements with a cells array. Output is clean linear or structured text, not a visual layout reconstruction. That's by design, but worth knowing. Structured markdown and element-JSON document output ship today (?format=markdown / ?format=json). Schema-defined field extraction means pulling typed fields per your own schema. That feature is on the roadmap, not shipped yet. We won't claim it early. No shipped self-host or VPC artifact today. It is on the roadmap. Apache Tika itself is free to run yourself. See the page below for what that takes.
Weighing self-hosting against either API? See [what running Apache Tika yourself actually takes](https://txtfetch.com/compare/self-hosted-tika).
the cost calculator
## Per page vs per document, worked out.
A 300-page PDF is 300 units of billing to Mindee. It's one document to txtfetch. Adjust the numbers to your own workload.
[Interactive cost calculator — adjust pages per document and documents per month to compare pricing against Mindee]
where mindee wins
- Turnkey field extraction for common document types (invoices, receipts, passports, IDs) needs minimal setup.
- You get named fields, not raw text.
- Custom document APIs let non-ML teams train a field-extraction model on their own document types without building an ML pipeline.
- Confidence scores per extracted field, useful for automating approval thresholds.
where txtfetch wins
- Breadth: 615 formats, each with a real Apache Tika parser behind it. That's [checked against the exact build we run](https://txtfetch.com/formats/coverage), not a curated list of a dozen file types.
- One HTTP endpoint. Pass a URL or upload a file, and get text back. No SDK and no async job polling required.
- Priced per document, so a 300-page report doesn't cost 300x a one-pager.
- A durable free tier (500 documents/month, ongoing) rather than a time-boxed trial.
What we don't claim yet: schema-defined structured field extraction, or an uptime SLA. Those ship later. See the capability table above.
next step
Ready to switch? See [what your code looks like after migrating from Mindee](https://txtfetch.com/migrate/mindee): the call you run today, the call that replaces it, and a drop-in adapter.
sources
- [Mindee: Pricing](https://www.mindee.com/pricing) Accessed 2026-07
- [Mindee: derived effective per-page rate (not a published figure)](https://www.mindee.com/pricing) Accessed 2026-07 directional
## Check the numbers yourself.
The benchmark runs against a committed corpus. You can re-run it.
[See the benchmarks →](https://txtfetch.com/benchmarks)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/compare/per-page-pricing
# Per-page pricing punishes long documents.
Most extraction APIs meter by the page. That's a fair way to price OCR compute. But it means your bill scales with document length, not with how many documents you process.
Almost every document-extraction vendor bills per page. That list includes [Unstructured.io](https://txtfetch.com/compare/unstructured), [LlamaParse](https://txtfetch.com/compare/llamaparse), [AWS Textract](https://txtfetch.com/compare/aws-textract), [Azure AI Document Intelligence](https://txtfetch.com/compare/azure-document-intelligence), and [Mindee](https://txtfetch.com/compare/mindee). Per-page pricing is a fair proxy for the OCR and inference cost of a page. But it has a side effect. The price of extracting a document depends on the document's length, not on how much work it is for _you_. To you, it's one file, fetched or uploaded, and handled once.
Consider two customers sending 100 documents a month. Customer A sends one-page invoices. Customer B sends 300-page compliance reports. Under per-page billing, Customer B pays 300x more for the same "100 documents a month" workload. Under per-document billing, both customers pay the same amount. The unit of billing matches the unit of work: one document extracted, once.
This isn't an argument that per-page pricing is dishonest. It maps cleanly to compute cost. For vendors selling OCR as commodity infrastructure, it's a defensible model. The real argument is different: **the pricing unit should match how you think about your workload.** If you ingest long documents, per-page billing makes your costs unpredictable. Your bill depends on content you don't control. A vendor could send you a 50-page PDF instead of a 5-page one, and your bill jumps 10x. txtfetch bills per document instead. A 300-page PDF and a one-page memo cost the same to extract. From your side, they're both "one thing I needed turned into text."
## The math, worked out
Below is a representative per-page rate: the Read/OCR tier that AWS Textract and Azure AI Document Intelligence both charge. This is roughly what "commodity OCR" costs across the market. Try your own document length and monthly volume.
[Interactive cost calculator — adjust pages per document and documents per month to compare pricing against a typical per-page OCR vendor]
## Where per-page pricing is the right call
Per-page pricing can be the better deal in some cases. If your documents are uniformly short, such as single-page forms, receipts, or IDs, per-page billing may cost less than a flat document quota. The same is true if you need per-feature pricing. Then you pay only for the specific analysis you use, such as tables, key-value pairs, or a single prebuilt model. Per-page pricing is also more transparent for pure infrastructure: you buy compute, not a product tier. See the individual comparisons below for where each vendor's model wins for your workload.
## Read the individual comparisons
- [txtfetch vs Unstructured.io](https://txtfetch.com/compare/unstructured): Open-core ETL for RAG pipelines, priced per page.
- [txtfetch vs LlamaParse](https://txtfetch.com/compare/llamaparse): Credit-metered parsing, tuned for complex PDFs.
- [txtfetch vs AWS Textract](https://txtfetch.com/compare/aws-textract): AWS-native OCR and document analysis, billed per 1,000 pages.
- [txtfetch vs Azure AI Document Intelligence](https://txtfetch.com/compare/azure-document-intelligence): Microsoft's prebuilt-model document API, billed per 1,000 pages.
- [txtfetch vs Mindee](https://txtfetch.com/compare/mindee): Subscription + credits for structured field extraction.
Full plan details, including the durable free tier, are on [the pricing page](https://txtfetch.com/pricing).
## Sources
- [Unstructured.io: Pricing](https://unstructured.io/pricing) Accessed 2026-07
- [LlamaIndex: LlamaParse pricing](https://developers.llamaindex.ai/llamaparse/general/pricing/) Accessed 2026-07
- [LlamaParse free-tier credits (secondary source)](https://developers.llamaindex.ai/llamaparse/general/pricing/) Accessed 2026-07 directional
- [AWS Textract: Pricing](https://aws.amazon.com/textract/pricing/) Accessed 2026-07
- [Azure AI Document Intelligence: Pricing](https://azure.microsoft.com/en-us/pricing/details/document-intelligence/) Accessed 2026-07 directional
- [Microsoft Q&A: Document Intelligence per-page rate consensus](https://learn.microsoft.com/en-us/answers/questions/tagged/azure-ai-document-intelligence) Accessed 2026-07 directional
- [Mindee: Pricing](https://www.mindee.com/pricing) Accessed 2026-07
- [Mindee: derived effective per-page rate (not a published figure)](https://www.mindee.com/pricing) Accessed 2026-07 directional
## Check the numbers yourself.
The benchmark runs against a committed corpus. You can re-run it.
[See the benchmarks →](https://txtfetch.com/benchmarks)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/compare/self-hosted-tika
# Apache Tika is free. Why not run it yourself?
It is a fair question, and worth an honest answer. This page shows the build we run and the work behind it, so you can judge for yourself.
## Tika is free. The parsers are not the hard part.
Apache Tika is Apache-2.0 licensed. Anyone can download it and run it for free, including the exact parser set txtfetch uses. See [the full parser list](https://txtfetch.com/formats/coverage) for the build we run, and [how it fits into our pipeline](https://txtfetch.com/how-it-works).
Parsing is not where the work is. Tika and Tesseract are mature, well-documented projects. The work is packaging them for a size- and time-limited serverless environment. It also means picking the small limits a production service needs. The sections below show that work, sourced from our own build recipe.
## What you still have to build
This is the checklist we worked through to run Tika and Tesseract on AWS Lambda. Every entry below traces to a real constraint in our build.
**Trim the JDK with jlink.**: A full Corretto 21 JDK is about 350 MB unzipped. AWS Lambda caps a function's combined layers at 250 MB. jlink strips the JDK to about 100 MB, keeping every module, so no Tika parser hits a missing one.
**Compile Tesseract and Leptonica from source.**: The compiled binary must link against the exact glibc of the runtime it will execute on. We build both on Amazon Linux 2023 arm64 to match the Lambda execution environment, then copy every shared library ldd reports.
**Choose a trained-data set.**: tessdata_fast ships int8 models at about 4 MB each. tessdata_best ships float models at about 15 MB each. We chose fast, to keep cold starts short.
**Pin and verify every download.**: We check the Corretto tarball and the Tika jar against a pinned SHA-256 sum before use. This stops a swapped file at the vendor from reaching production unnoticed.
**Split the sync and async request paths.**: API Gateway caps a synchronous integration at 30 seconds. A request that could run longer needs its own job queue, a separate worker function, and either webhook delivery or a polling endpoint.
**Set your own limits.**: Memory, timeouts, input-size caps, and OCR language choice are judgment calls. Every one already has a number in this build. You would set each one again for your own traffic.
## The versions we pin
These are the exact component versions our build downloads, checksums, and ships. A self-hosted build starts from the same public releases.
| Component | Version | Source |
| --- | --- | --- |
| Java runtime | Amazon Corretto 21.0.11.10.1 | versions.env: CORRETTO\_VERSION |
| Tika server | tika-server-standard 3.3.1 | versions.env: TIKA\_VERSION |
| Leptonica | 1.87.0 | versions.env: LEPTONICA\_VERSION |
| Tesseract | 5.5.2 | versions.env: TESSERACT\_VERSION |
| Trained-data set | tessdata\_fast | versions.env: TESSDATA\_REPO |
| OCR languages shipped | eng, osd | versions.env: TESSDATA\_LANGS |
## Limits you must pick yourself
A synchronous API request through API Gateway has a hard 30-second ceiling. That is why a request above 8 MB in our system runs on an async worker path instead of blocking on the response. Every other number below is a limit we picked and now enforce. A self-hosted setup needs its own version of each one.
| Limit | Value | Source |
| --- | --- | --- |
| Worker memory | 2,048 MB | variables.tf: worker Lambda memory\_size |
| Worker timeout | 900 s | variables.tf: worker\_timeout\_seconds |
| Internal extraction deadline | 840,000 ms | variables.tf: worker\_budget\_ms |
| Max input size | 100 MB | variables.tf: worker\_max\_bytes |
| Sync-to-async threshold | 8 MB | variables.tf: async\_threshold\_bytes |
| API Gateway integration cap | 30,000 ms | apigateway.tf: timeout\_milliseconds |
## Where self-hosting wins
Self-hosting is the right call in real situations. We would rather name them than pretend they do not exist. txtfetch has no self-hosted or VPC-deployed artifact today. That is on the roadmap, not shipped. See [/compliance](https://txtfetch.com/compliance) for the current state.
- Your documents must never leave your own network, and you cannot wait for a VPC-deployed option to ship.
- You already run infrastructure like this, with someone to own patching, checksum verification, and scaling.
- Your volume is high enough that owning the infrastructure costs less than a per-document API bill. That is true even after you count the engineering time to build and run it.
- You need to change Tika itself: a custom parser, a different OCR engine, or trained data for a language we do not ship.
## What the API adds on top of Tika
Once Tika and Tesseract are running, an extraction engine is not yet a product. txtfetch adds the parts around the engine. For large or slow documents, it adds [async jobs and webhook delivery](https://txtfetch.com/docs/async). [Idempotency keys](https://txtfetch.com/docs/idempotency) stop a retried request from double-processing a document. A [typed error reference](https://txtfetch.com/docs/errors) replaces a stack trace. An optional [premium quality tier](https://txtfetch.com/docs#premium-quality) handles the documents Tika alone struggles with.
## Frequently asked questions
**Is Apache Tika really free?**: Yes. Tika is Apache-2.0 licensed. txtfetch pays nothing to run it, and neither would you.
**What is the hardest part of self-hosting Tika?**: Not the parsers. The hard part is the operational work: trimming the JDK to fit a size limit, and compiling Tesseract for your runtime's glibc. You also pick your own memory, timeout, and input-size limits.
**Does txtfetch offer a self-hosted or VPC-deployed version?**: No, not today. It is on the roadmap, not shipped. See /compliance for the current state.
**When does self-hosting make more sense than an API?**: When your documents cannot leave your network, or your volume is high enough to justify owning the infrastructure yourself.
**Can I run Tika alone and skip Tesseract?**: Yes, if none of your documents are scans or photos. Tika alone handles PDFs, Office files, HTML, and email. Add Tesseract only when you need OCR.
## Check the numbers yourself.
The benchmark runs against a committed corpus. You can re-run it.
[See the benchmarks →](https://txtfetch.com/benchmarks)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/compare/unstructured
# txtfetch vs Unstructured.io
Unstructured.io is open-core etl for rag pipelines, priced per page. Here's an honest comparison: pricing math, capabilities, and where each tool wins.
at a glance
Unstructured.io
**Billing**: per page (pay-as-you-go)
**Rate used below**: $0.03/page
**Free tier**: 15,000 pages/month free.
txtfetch
**Billing**: per document, regardless of page count
**Rate used below**: flat monthly quota (see plans)
**Free tier**: 500 documents/month, free, ongoing (Hobby plan)
capability table
| Capability | Unstructured.io | txtfetch |
| --- | --- | --- |
| Billing unit | per page | per document |
| Format coverage | ~25 file types | 615 formats (Apache Tika, checked) |
| OCR for scans / images | yes | yes |
| Table extraction | yes | yes* |
| Complex layout fidelity | partial | partial* |
| Structured field extraction | no | not yet* |
| Self-hosted option | yes | no* |
| Single HTTP endpoint | partial | yes |
Vendor-published benchmarks are marked directional throughout. See sources below. \* txtfetch notes: Tables come back as structured rows and cells, not flattened text. Use ?format=markdown for GFM pipe tables, or ?format=json for table elements with a cells array. Output is clean linear or structured text, not a visual layout reconstruction. That's by design, but worth knowing. Structured markdown and element-JSON document output ship today (?format=markdown / ?format=json). Schema-defined field extraction means pulling typed fields per your own schema. That feature is on the roadmap, not shipped yet. We won't claim it early. No shipped self-host or VPC artifact today. It is on the roadmap. Apache Tika itself is free to run yourself. See the page below for what that takes.
Weighing self-hosting against either API? See [what running Apache Tika yourself actually takes](https://txtfetch.com/compare/self-hosted-tika).
the cost calculator
## Per page vs per document, worked out.
A 300-page PDF is 300 units of billing to Unstructured.io. It's one document to txtfetch. Adjust the numbers to your own workload.
[Interactive cost calculator — adjust pages per document and documents per month to compare pricing against Unstructured.io]
where unstructured wins
- Purpose-built chunking strategies (by-title, by-similarity) tuned specifically for RAG retrieval, not just text extraction.
- Open-source core you can self-host and modify if you need full control over the pipeline.
- Deep partitioning options per element type (titles, narrative text, tables, images) for teams that want to post-process structure, not just plain text.
where txtfetch wins
- Breadth: 615 formats, each with a real Apache Tika parser behind it. That's [checked against the exact build we run](https://txtfetch.com/formats/coverage), not a curated list of a dozen file types.
- One HTTP endpoint. Pass a URL or upload a file, and get text back. No SDK and no async job polling required.
- Priced per document, so a 300-page report doesn't cost 300x a one-pager.
- A durable free tier (500 documents/month, ongoing) rather than a time-boxed trial.
What we don't claim yet: schema-defined structured field extraction, or an uptime SLA. Those ship later. See the capability table above.
next step
Ready to switch? See [what your code looks like after migrating from Unstructured.io](https://txtfetch.com/migrate/unstructured): the call you run today, the call that replaces it, and a drop-in adapter.
sources
- [Unstructured.io: Pricing](https://unstructured.io/pricing) Accessed 2026-07
## Check the numbers yourself.
The benchmark runs against a committed corpus. You can re-run it.
[See the benchmarks →](https://txtfetch.com/benchmarks)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/compliance
# SOC 2 is a roadmap item, not a badge we're wearing.
txtfetch is not SOC 2 certified today. Here's the honest scope of what that would take, and what we already do while we get there.
not certified today
txtfetch has not completed a SOC 2 Type II audit. Be suspicious of anyone who tells you otherwise about a pre-launch product. We'd rather say "not yet" than imply a badge we haven't earned.
## Planned scope
The intended scope is a SOC 2 Type II report covering the Security and Availability Trust Services Criteria. It would cover the extraction API and its supporting AWS infrastructure. Confidentiality would extend naturally from the no-content-retention posture described at [/security](https://txtfetch.com/security).
## Gaps between today and an audit-ready state
- Formal risk assessment and control documentation across the Trust Services Criteria (security, availability, confidentiality).
- Automated dependency and runtime vulnerability scanning wired into CI. Today it's a manual process; see /security.
- Independent third-party audit of controls over a defined observation period.
- Formal vendor-management and sub-processor review process (today: a maintained list at /subprocessors).
## Timeline
There is no committed audit date yet. We intend to close the operational gaps above before formally engaging an auditor, most urgently the request-logging fix and automated scanning noted on [/security](https://txtfetch.com/security). We'll update this page when a Type I observation period is scheduled.
## Tooling
Infrastructure runs entirely on AWS-managed services: Lambda, CloudFront, S3, SES, ACM, and CloudWatch. There are no self-managed servers. That keeps the control surface small, and most of it is inherited from AWS's own SOC 2 and ISO 27001 attestations for the underlying platform. Application-level controls (IAM scoping, patch cadence, dependency hygiene) are ours to build out and document.
## VPC / self-hosted deployment
Some customers need document content to never leave their own network. For them, a self-hosted or VPC-deployed variant of the extraction service is on the roadmap. The engine (Tika and Tesseract on Lambda-compatible runtimes) is already isolated enough to package this way. There's no shipped self-host artifact yet; if this is a blocker for your deal, tell us via [/contact](https://txtfetch.com/contact) and we'll factor it into prioritization.
## Stop parsing. Start shipping.
Create an account and get an API key in minutes. The free Hobby plan needs no card.
[Get started →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/contact
# Tell us what you're building.
Scale plans, support questions, weird formats: it all lands in the same place. Short and specific beats long and polished.
[Contact form — reach us at https://txtfetch.com/contact]
what-happens-next
1. A person reads it: not a queue, not a sales sequence.
2. Scale and support requests get a reply, usually within a day.
3. Everything else gets a straight answer.
while-you-wait
The whole API fits on one page. [Read the quickstart](https://txtfetch.com/docs) so you can paste your key and go.
---
# https://txtfetch.com/diff
# The numbers are on /benchmarks. Here's the actual text.
$ diff raw.txt txtfetch.txt — ten documents, the raw parser output on the left, the corrected text on the right, every difference marked. Nothing hidden.
before you read the rest of this page
This corpus is small and synthetic: ten recorded fixtures standing in for real Tika and VLM responses. What follows demonstrates the shape of each failure and its fix, not a claim of population-level accuracy. Every vendor in this market publishes a self-scored benchmark number; we'd rather show you the actual text and let you judge it yourself.
Want evidence on a document of your own instead of taking our word for any of this? [Run it in the playground](https://txtfetch.com/playground) or [drop a file into the free converter](https://txtfetch.com/tools/file-to-text). Both read your file in your browser, and nothing is uploaded.
the three hard ones
## Where Tika's structural recovery is weakest.
A two-column PDF, a scanned invoice, and a wide pricing table make up this part of the corpus. These three carry a recorded premium-tier (vision-language model) response. The standard tier's gap on each one is wide enough to be worth showing.
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% |
```
Tika's PDF parser reads the page in raw position order, not by column. Line one of the left column lands next to line one of the right, so two unrelated sentences run together mid-thought. Every word from the page survives. Only the order doesn't. The table in the right column degrades into one flattened line with no structure at all. The premium tier reads the page the way a person would instead of inferring order from coordinates, and recovers both paragraphs and the table.
Reading order, standard vs. premium: 50.0% → 100.0%. See [/benchmarks](https://txtfetch.com/benchmarks) for the full methodology.
[Columns out of order →](https://txtfetch.com/fixes/columns-out-of-order)
Scanned Invoice #9042 (image-only PDF)
`invoice-9042-scan.pdf` · application/pdf · left pane read via Apache Tika, **right pane at premium (VLM)**
raw parser output
```
lnvoice #9O42
Bill to: Northwind Tradcrs, 44 Harbor R0w
ltem Qty Unit Pricc Total Onsite installatlon 2 $45O.OO $9OO.OO Annual support pian 1 $1,2OO.OO $1,2OO.OO
Tota1 due: $2,1OO.OO
```
what txtfetch returns
```
# Invoice #9042
Bill to: Northwind Traders, 44 Harbor Row
| Item | Qty | Unit Price | Total |
| --- | --- | --- | --- |
| Onsite installation | 2 | $450.00 | $900.00 |
| Annual support plan | 1 | $1,200.00 | $1,200.00 |
Total due: $2,100.00
```
This scan has no text layer, so the standard tier's automatic Tesseract OCR escalation is what reads it at all. Tesseract reads most of the page, but confuses look-alike characters (O for 0, l and I for 1) throughout. It also has no table model, so the two-line item table survives only as one run-on blur. The premium tier reads the same image through a vision-language model and gets both the characters and the table right. See /fixes/pdf-extracts-no-text for the OCR-escalation path this document takes.
Table cell-F1 and reading order, standard vs. premium: 0.0% → 100.0%. See [/benchmarks](https://txtfetch.com/benchmarks) for the full methodology.
[PDF extracts no text (the OCR escalation path) →](https://txtfetch.com/fixes/pdf-extracts-no-text)
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 |
```
Unlike the financial report above, Tika does detect a table here. It just recovers it wrong. The SKU column folds into the product name, so five columns become four. The last row falls off the page-boundary extraction entirely, with no error and no warning. One product goes silently missing from whatever index this feeds. The premium tier keeps all five columns and all four rows.
Table cell-F1, standard vs. premium: 0.0% → 100.0%. See [/benchmarks](https://txtfetch.com/benchmarks) for the full methodology.
[Tables losing structure →](https://txtfetch.com/fixes/tables-lose-structure)
the other seven
## Office, HTML, and email — already close to exact.
No recorded premium response exists for these seven. There's little for one to fix: a real structural model, or a parser that already reads the format correctly. Worth showing anyway. This is the other half of "delete the parser zoo," with receipts.
One note on precision: the right pane for these seven is the human-checked ideal from `expected.json`. It is not a recording of what the engine returned. How close the standard tier actually lands on each is measured separately. It's 100.0% text similarity on six of the seven categories, and 92.3% on the DOCX. Those numbers, and the metric behind them, are on [/benchmarks](https://txtfetch.com/benchmarks).
**Show all 7 documents**
Invoice Follow-up (EML)
`invoice-followup.eml` · message/rfc822 · left pane read via Apache Tika, **right pane: the human-checked expected.json**
raw parser output
```
From: Priya Shah
To: Billing
Subject: Re: Invoice #7734
Hi team,
Following up on the invoice below — could you confirm receipt?
Description Qty Amount
Consulting hours 12 $1,800.00
Travel 1 $340.00
Invoice total: $2,140.00
Due date: 2026-08-15
Thanks,
Priya
```
the human-checked ideal
```
From: Priya Shah
To: Billing
Subject: Re: Invoice #7734
Hi team,
Following up on the invoice below — could you confirm receipt?
Description Qty Amount
Consulting hours 12 $1,800.00
Travel 1 $340.00
Invoice total: $2,140.00
Due date: 2026-08-15
Thanks, Priya
```
Tika's RFC822 parser reads a plain email like this one without much to prove. Headers, body, and the small table all land where you'd expect.
Sprint Status Update (Outlook MSG)
`sprint-status-update.msg` · application/vnd.ms-outlook · left pane read via Apache Tika, **right pane: the human-checked expected.json**
raw parser output
```
From: Dana Lee
To: Engineering
Subject: Sprint Status Update
Hi all,
Here is where things stand heading into the demo on Friday.
Benchmark harness: on track
Corpus curation: on track
CI wiring: at risk, needs a second reviewer
Ping me if you want to pair on the CI job.
Thanks,
Dana
```
the human-checked ideal
```
From: Dana Lee
To: Engineering
Subject: Sprint Status Update
Hi all,
Here is where things stand heading into the demo on Friday.
Benchmark harness: on track
Corpus curation: on track
CI wiring: at risk, needs a second reviewer
Ping me if you want to pair on the CI job.
Thanks, Dana
```
Legacy Outlook .msg goes through Tika's OutlookExtractor instead, and it's just as unremarkable here: headers, prose, a short list, nothing lost.
txtfetch Pricing Page (HTML)
`pricing.html` · text/html · left pane read via Apache Tika, **right pane: the human-checked expected.json**
raw parser output
```
Pricing
Turn any document into clean plain text over a simple HTTP API.
pricing tiers diagram
Plans
Plan Price Quota
Hobby $0 500 docs/mo
Developer $19 10,000 docs/mo
Scale $99 100,000 docs/mo
Questions? Use the contact form & skip the 'support' email.
```
the human-checked ideal
```
Pricing
Turn any document into clean plain text over a simple HTTP API.
pricing tiers diagram
Plans
Plan Price Quota
Hobby $0 500 docs/mo
Developer $19 10,000 docs/mo
Scale $99 100,000 docs/mo
Questions? Use the contact form & skip the 'support' email.
```
Tika strips the inline script and style noise and keeps the pricing table. An ordinary HTML page like this one is already a solved problem.
Q3 2026 Board Update (DOCX)
`board-update-q3-2026.docx` · application/vnd.openxmlformats-officedocument.wordprocessingml.document · left pane read via Apache Tika, **right pane: the human-checked expected.json**
raw parser output
```
Q3 2026 Board Update
This update covers Q3 2026 performance across all product lines.
Highlights
Annual recurring revenue grew 14% quarter over quarter
Shipped the structured-output beta
Markdown mode
Element JSON mode
Support headcount increased to 6 engineers
Revenue by Plan
Plan MRR Growth
Hobby $0 n/a
Developer $18,200 11%
Scale $41,900 19%
Full detail is available in the appendix & supporting spreadsheets.
```
the human-checked ideal
```
Q3 2026 Board Update
This update covers Q3 2026 performance across all product lines.
Highlights
Annual recurring revenue grew 14% quarter over quarter
Shipped the structured-output beta
Markdown mode
Element JSON mode
Support headcount increased to 6 engineers
Revenue by Plan
Plan MRR Growth
Hobby $0 n/a
Developer $18,200 11%
Scale $41,900 19%
Full detail is available in the appendix & supporting spreadsheets.
```
A .docx carries a real structural model, not pixels to guess at. Tika's OOXML parser has an easy time of it: the headings, the nested bullet list, and the table all come through.
S6 Launch Deck (PPTX)
`s6-launch-deck.pptx` · application/vnd.openxmlformats-officedocument.presentationml.presentation · left pane read via Apache Tika, **right pane: the human-checked expected.json**
raw parser output
```
Benchmarking the Quality Climb
txtfetch S6 — accuracy harness
Why it matters
Structured output needs proof, not just a demo
VLM tier cost only pays off if quality actually improves
What ships
Committed corpus across 10 categories
Reproducible CLI + committed report
```
the human-checked ideal
```
Benchmarking the Quality Climb
txtfetch S6 — accuracy harness
Why it matters
Structured output needs proof, not just a demo
VLM tier cost only pays off if quality actually improves
What ships
Committed corpus across 10 categories
Reproducible CLI + committed report
```
Tika reads one slide per page, title and bullets in order. A clean deck like this one needs nothing more than the standard tier.
FY2026 Budget Forecast (XLSX)
`fy2026-budget-forecast.xlsx` · application/vnd.openxmlformats-officedocument.spreadsheetml.sheet · left pane read via Apache Tika, **right pane: the human-checked expected.json**
raw parser output
```
Opex
Line item Q1 Q2
Cloud hosting 4200 4600
Payroll 61000 64500
Summary
Total 65200 69100
```
the human-checked ideal
```
Opex
Line item Q1 Q2
Cloud hosting 4200 4600
Payroll 61000 64500
Summary
Total 65200 69100
```
Two sheets, two real tables. Tika reads .xlsx's actual cell model rather than inferring rows and columns from visual spacing.
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.
```
A born-digital, single-column PDF with a real text layer. Reading order is already linear, so there's nothing here for a vision model to fix.
how to read this
Every document above is committed at `api/extract/bench/corpus//` in the txtfetch repo. It includes a recorded `tika.txt` (raw parser output), a human-checked `expected.json`, and, for the three hard PDFs, a recorded `vlm.md`. `make bench` re-runs the real extraction engine against the same fixtures and regenerates the committed report. It uses a dependency-injected mock Tika and mock VLM, with no JVM and no network calls.
For the full methodology, every metric definition, and the per-category numbers these documents feed into, see [/benchmarks](https://txtfetch.com/benchmarks). For how the whole pipeline fits together, see [/how-it-works](https://txtfetch.com/how-it-works).
faq
**Is this corpus proof that txtfetch is this accurate on my documents?**: No. It's ten small, synthetic documents: recorded fixtures standing in for real Tika and VLM responses. They demonstrate the shape of a handful of known failure modes, and their fix. They don't claim a population-level accuracy figure. Run your own file through /playground or the free file-to-text tool if you want evidence on your documents specifically.
**Why do the seven "easy" documents look almost identical on both sides?**: Because they mostly are. Office files, HTML, and email carry a real structural model. Tika's own OOXML/RFC822/HTML parser already reads them cleanly. There's very little left for a vision-language model to improve. The right pane for those seven is expected.json's human-checked ideal, not a recorded engine response. /benchmarks measures how close the standard tier actually lands: 100.0% text similarity on six of the seven categories, and 92.3% on the DOCX. That's the honest case for txtfetch: one endpoint that already gets these formats right, without you wiring up seven separate parsers to find that out.
**Does the premium tier really score 100% on the three hard PDFs?**: On these three recorded documents, yes, because the recorded vlm.md response is the human-checked ideal recovery for each one, not a general accuracy ceiling. Treat it as "the VLM read this page correctly," not "the VLM is always perfect." See /benchmarks for the full per-category numbers and their caveats.
**Where do these files actually live, and how do I regenerate the numbers?**: Every document is committed at api/extract/bench/corpus// in the txtfetch repo. It includes a recorded tika.txt (raw parser output), a human-checked expected.json, and, for the three hard PDFs, a recorded vlm.md. `make bench` re-runs the real extraction engine against the same fixtures and regenerates the committed report at api/extract/bench/REPORT.md. It uses a dependency-injected mock Tika and mock VLM, with no JVM and no network calls.
## Stop parsing. Start shipping.
Create an account and get an API key in minutes. The free Hobby plan needs no card.
[Get started →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/docs
# The whole API fits on this page.
One endpoint, two ways to call it, one response shape. If you can curl, you've already integrated.
Prefer to see it first? [Try the playground →](https://txtfetch.com/playground)
## Authenticate
Every request carries your API key in the `Authorization` header. [Create a key in your dashboard](https://app.txtfetch.com/signup).
```
export TXTFETCH_KEY="tf_live_..."
```
A missing key returns `401`. An invalid or revoked key returns `403`. Both come back as API Gateway's platform `{"message": "…"}` body, not the typed error shape below. See [the error reference](https://txtfetch.com/docs/errors) for the full auth-failure semantics.
## Extract a file
POST the bytes as multipart form data. Any of the [supported formats](https://txtfetch.com/formats) works. 615 of the 1,683 Tika detects have a real parser, [checked against the exact build we run](https://txtfetch.com/formats/coverage). txtfetch reads the real type from the bytes, not the extension.
curl
```curl
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@quarterly-report.pdf
```
Python
```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
```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
```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)
}
```
## Extract from a URL
Or skip the download: pass a `url` parameter and txtfetch fetches the document server-side.
curl
```curl
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/whitepaper.docx" \
-H "Authorization: Bearer $TXTFETCH_KEY"
```
Python
```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.docx"},
)
print(r.json()["extracted_text"])
```
JavaScript
```javascript
const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", "https://example.com/whitepaper.docx");
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
```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.docx")
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)
}
```
## Response
Success is always the same shape. Scanned documents and images go through OCR automatically. It's the same request and the same response.
```
{
"status": "success",
"extracted_text": "Q3 revenue grew 34% year over year, driven by...",
"metadata": {
"content_type": "application/pdf",
"bytes": 482913,
"chars": 812,
"ocr": false
}
}
```
## Response formats
Need structure instead of flat text? Pass `?format=markdown` for GFM output, tables included, or `?format=json` for a typed element tree. The endpoint, auth, and error shape stay the same. `format` defaults to `text` (the shape above) when omitted.
```
curl -s -X POST "https://api.txtfetch.com/v1/extract?format=markdown" \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@quarterly-report.pdf
```
```
{
"status": "success",
"markdown": "# Quarterly Report\n\n...\n\n| Region | Revenue |\n| --- | --- |\n| EMEA | 34% |\n",
"metadata": {
"content_type": "application/pdf",
"bytes": 482913,
"chars": 812,
"format": "markdown",
"tier": "standard",
"pages": 4,
"vlm": false
}
}
```
```
curl -s -X POST "https://api.txtfetch.com/v1/extract?format=json" \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@quarterly-report.pdf
```
```
{
"status": "success",
"elements": [
{ "type": "heading", "text": "Quarterly Report", "level": 1, "page": 1, "offset": 0, "bbox": null },
{
"type": "table",
"text": "Region\tRevenue\nEMEA\t34%",
"markdown": "| Region | Revenue |\n| --- | --- |\n| EMEA | 34% |",
"html": "
...
",
"cells": "[[…]] — one row per array entry, each cell carries text/colspan/rowspan/header",
"rows": 2,
"cols": 2,
"page": 1,
"offset": 1,
"bbox": null
}
],
"metadata": {
"content_type": "application/pdf",
"bytes": 482913,
"chars": 812,
"format": "json",
"tier": "standard",
"pages": 4,
"vlm": false
}
}
```
The full element schema is part of the published contract. It covers every type, required field, and the offset/page monotonicity guarantee. See the [OpenAPI spec →](https://txtfetch.com/openapi.json)
Deciding which of the three to request? [See the same document rendered all three ways →](https://txtfetch.com/output)
## Premium quality
Pass `?quality=premium` to run a vision-language model instead of Tika, for documents where layout matters more than raw text. Premium always routes async, even for a one-page PDF, since VLM latency can exceed the sync budget. Expect a `202` and a `job_id` to poll, same as above.
```
curl -s -X POST "https://api.txtfetch.com/v1/extract?quality=premium" \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@quarterly-report.pdf
```
```
{
"status": "success",
"extracted_text": "Q3 revenue grew 34% year over year, driven by...",
"metadata": {
"content_type": "application/pdf",
"bytes": 482913,
"chars": 812,
"format": "text",
"tier": "premium",
"pages": 4,
"vlm": true,
"usage": { "model": "claude-sonnet-5", "input_tokens": 2140, "output_tokens": 612 }
}
}
```
Premium never fails your request outright. If it hits a cost/timeout/size guardrail, it falls back safely to the same Tika baseline as the standard tier. The response is still a `success`, with `metadata.tier_downgraded: true` and a `metadata.downgrade_reason` explaining why.
```
{
"status": "success",
"extracted_text": "Q3 revenue grew 34% year over year, driven by...",
"metadata": {
"content_type": "application/pdf",
"bytes": 482913,
"chars": 812,
"format": "text",
"tier": "standard",
"pages": 4,
"vlm": false,
"tier_downgraded": true,
"downgrade_reason": "vlm_disabled"
}
}
```
There's no plan-level gate on `quality=premium`. Every plan can request it. Full contract, guardrails, and every `downgrade_reason`: [OpenAPI spec →](https://txtfetch.com/openapi.json)
Conceptual guide to both tiers, plus the async routing and downgrade caveat: [what comes back →](https://txtfetch.com/output)
## Errors
Failures are explicit and machine-readable. They're never a silent empty string. Failed extractions don't count against your quota. See the [complete error reference](https://txtfetch.com/docs/errors) for every code.
```
{
"status": "error",
"error": {
"code": "extraction_failed",
"message": "extraction produced no text"
}
}
```
## Limits and quotas
Every metered response carries `X-RateLimit-*`(per-minute) and `X-Quota-*` (monthly) headers. Going over either returns a typed `429`. `rate_limited` vs `quota_exceeded` is disambiguated by `error.code`. A `Retry-After` header tells you exactly when to retry. Failed extractions don't count against your monthly quota, only successful ones.
```
{
"status": "error",
"error": {
"code": "quota_exceeded",
"message": "monthly quota exceeded for this API key"
}
}
```
Full breakdown of both limits, the header contract, and the auth-failure statuses: [Error reference →](https://txtfetch.com/docs/errors)
## Works with everything
The response is plain JSON, so txtfetch drops into any stack. Pipe it straight into your chunker, embedder, or index:
```
curl -s -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@contract.pdf | jq -r .extracted_text
```
## Async jobs
Large uploads or documents are automatically routed to an async job path instead of blocking the request. Pass `?async=true` to force it for any request. Either way you get a `202` with a `job_id`. Poll `GET /v1/extract/{job_id}` until it returns the same success shape as above.
```
{
"status": "processing",
"job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
```
Full walkthrough, SDK helpers, and webhook delivery instead of polling: [Async & webhooks →](https://txtfetch.com/docs/async)
## Idempotency
Pass an `Idempotency-Key` header on `POST /v1/extract` so a retried request is never double-run. A repeat under the same key replays the original response instead of re-extracting. Details, TTL, and the SDKs' automatic retry behavior: [Idempotency →](https://txtfetch.com/docs/idempotency)
## Machine-readable spec
The full contract covers every field, error code, and status. It's published as [OpenAPI 3.1](https://txtfetch.com/openapi.json). Generate a typed client for whatever language you're in.
guides-and-sdks
## Past the first curl.
### [Quickstarts](https://txtfetch.com/docs/quickstarts)
Per-framework install + extract: SDK JS, SDK Python, LangChain (Python + JS), LlamaIndex.
install → extract
### [RAG recipe](https://txtfetch.com/docs/recipe)
Chunk → embed → index a document end-to-end with a loader, then query it.
chunk → embed → index
### [Async & webhooks](https://txtfetch.com/docs/async)
Job lifecycle, polling, webhook\_url delivery, and verifying the signature.
202 → poll or push
### [Idempotency](https://txtfetch.com/docs/idempotency)
Idempotency-Key semantics: replay, mismatch handling, and SDK auto-retry.
retry-safe by default
### [Error reference](https://txtfetch.com/docs/errors)
Every error code, its HTTP status, cause, and remediation, all in one table.
code → status → fix
### [Changelog](https://txtfetch.com/changelog)
A dated record of what shipped since launch, each entry linked to the page that proves it.
what shipped, when
## Watch it run, then take a key.
The playground replays a real recorded response for every sample document.
[Open the playground →](https://txtfetch.com/playground)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/docs/async
# Async jobs & webhooks.
Large inputs are routed off the request/response cycle automatically. Poll for the result, or have txtfetch push it to you.
job-lifecycle
`POST /v1/extract` returns `202` with a `job_id` whenever the input is too large or slow for a synchronous response, or whenever `?async=true` is set. Poll `GET /v1/extract/{job_id}`. It returns `{"status": "processing"}` while the job runs. Then it returns the exact same success (or error) shape a synchronous `200` would have returned.
Submit (async)
```submit
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/report.pdf&async=true" \
-H "Authorization: Bearer $TXTFETCH_KEY"
# {"status": "processing", "job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"}
```
Poll
```poll
curl "https://api.txtfetch.com/v1/extract/3fa85f64-5717-4562-b3fc-2c963f66afa6" \
-H "Authorization: Bearer $TXTFETCH_KEY"
# {"status": "processing", "job_id": "..."} while running, then the same
# {"status": "success", "extracted_text": "...", "metadata": {...}} shape
# POST /v1/extract would have returned synchronously.
```
sdk-async
Both official SDKs wrap this in a job handle: `extractAsync`/`waitFor`(JS) and `extract_async`/`wait_for` (Python). A plain `extract()`/`extract(...)` call already does this transparently. Reach for these only when you want the job handle without blocking on it.
JavaScript
```javascript
const job = await txtfetch.extractAsync({ url: "https://example.com/report.pdf" });
const result = await txtfetch.waitFor(job, { maxWaitMs: 5 * 60_000 });
```
Python
```python
job = client.extract_async(file="huge-report.pdf")
result = client.wait_for(job) # or: job.result()
```
webhook\_url
Pass `webhook_url` on the same `POST /v1/extract` call to have txtfetch push the finished result to your endpoint instead of (or in addition to) polling. The target must be an `http://`/`https://` URL. It's validated at accept time and again at delivery time. Private, internal, and link-local addresses are rejected. This is the same guard that `?url=` fetches go through.
curl
```curl
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/report.pdf" \
-H "Authorization: Bearer $TXTFETCH_KEY" \
--data-urlencode "webhook_url=https://example.com/webhooks/txtfetch"
```
Delivery is a `POST` of the job's result body with two headers: `X-Txtfetch-Signature: sha256=` and `X-Txtfetch-Timestamp: `. The signature is an HMAC-SHA256 (hex) of the string `"."`, keyed by your webhook signing secret. Delivery is retried up to 3 times with a short backoff between attempts. A delivery failure never fails the underlying job. The result is always still available by polling.
verify-the-signature
Both SDKs ship a verifier that reproduces this exactly, including a constant-time comparison and a timestamp-tolerance replay check. Verify against the _raw_ request body. Don't re-serialize parsed JSON, since that can change byte-for-byte and break the signature.
JavaScript
```javascript
import { verifyWebhook } from "@txtfetch/sdk";
app.post("/webhooks/txtfetch", (req, res) => {
verifyWebhook(req.rawBody, req.header("X-Txtfetch-Signature"), process.env.WEBHOOK_SECRET, {
timestamp: req.header("X-Txtfetch-Timestamp"),
toleranceSec: 300,
});
// ... handle req.body
});
```
Python
```python
from txtfetch import verify_webhook, WebhookVerificationError
try:
verify_webhook(
payload=request_body, # raw request body, str or bytes
signature=request.headers["X-Txtfetch-Signature"],
secret=WEBHOOK_SIGNING_SECRET,
timestamp=request.headers["X-Txtfetch-Timestamp"],
)
except WebhookVerificationError:
# reject the delivery
...
```
Raw HMAC
```raw-hmac
import { createHmac, timingSafeEqual } from "node:crypto";
// Reproduces exactly what api/extract/src/webhook.js signs: HMAC-SHA256 of
// ".", hex-encoded, "sha256=" prefixed.
function verify(rawBody, signatureHeader, timestampHeader, secret) {
const expectedHex = createHmac("sha256", secret)
.update(`${timestampHeader}.${rawBody}`)
.digest("hex");
const provided = Buffer.from(signatureHeader.replace(/^sha256=/, ""), "hex");
const expected = Buffer.from(expectedHex, "hex");
if (provided.length !== expected.length || !timingSafeEqual(provided, expected)) {
throw new Error("webhook signature mismatch");
}
}
```
see-also
Retrying a `POST /v1/extract` call (including one that also carries `webhook_url`)? Pair it with an [Idempotency-Key](https://txtfetch.com/docs/idempotency) so a retried submit never double-runs the extraction.
## Watch it run, then take a key.
The playground replays a real recorded response for every sample document.
[Open the playground →](https://txtfetch.com/playground)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/docs/errors
# Every error, one table.
status is always "error"; error.code is stable and safe to match on; error.message is for humans, not code. Derived from the published OpenAPI spec. See it match, byte for byte, at /openapi.json.
These are API errors: `status: "error"`, a rejected request. For a response that came back `status: "success"` with text that's still wrong (mojibake, empty, scrambled columns), see [fixes for broken extracted text](https://txtfetch.com/fixes) instead.
Every non-success response has the same shape:
```
{
"status": "error",
"error": {
"code": "too_large",
"message": "upload exceeds max size of 10485760 bytes; use ?url= for larger documents"
}
}
```
Match on `error.code`. It's stable across releases. `error.message` is human-readable detail and can change. Don't parse it.
| code | HTTP status | cause | remediation |
| --- | --- | --- | --- |
| `bad_request` | 400 | The request was malformed — no file part or ?url= was given, or a retried request under a live Idempotency-Key had a different body/target than the original. | Provide exactly one of a multipart file or ?url=. If you're reusing an Idempotency-Key, send the exact same request you sent the first time. |
| `fetch_failed` | 400 | The server-side ?url= fetch failed: a blocked/private address (SSRF-guarded), a connection error, a non-2xx response, or a timeout reaching the source. | Confirm the URL is publicly reachable over https:// and returns a 2xx. txtfetch refuses to fetch private/internal/link-local addresses by design. |
| `not_found` | 404 | The job\_id in GET /v1/extract/{job\_id} is malformed, unknown, or has expired. | Double-check the job\_id from the 202 response. If it's expired, re-submit the original extraction to get a new one. |
| `too_large` | 413 | The direct upload exceeds the maximum accepted size for the multipart file path. | Use ?url= instead of a direct upload for large documents — txtfetch fetches it server-side with a much higher size ceiling. |
| `unsupported_format` | 415 | The input's real, byte-detected type isn't a supported document format. | Check /formats for the current list. txtfetch sniffs the actual bytes, not the file extension, so a mislabeled file won't help. |
| `encrypted` | 422 | The document is encrypted or password-protected, so Tika can't open it. | Remove the password/encryption before submitting, or supply an already-decrypted copy. |
| `extraction_failed` | 422 | Extraction ran but produced no text — e.g. a blank scan, a corrupt file, or an image with no recoverable content. | Confirm the source actually contains extractable text or legible imagery. Failed extractions don't count against your quota, so retries are free. |
| `abuse_detected` | 429 | This API key's abuse circuit breaker has tripped: a burst of abusive outcomes — repeated malformed input, and especially oversized/body-bomb rejections — not raw request volume. Distinct from rate\_limited (per-minute volume) and quota\_exceeded (monthly successes); legitimate throttling and successes never contribute to this. | Stop sending the offending requests and back off until the Retry-After delta-seconds elapse. If this is unexpected, check your integration isn't retrying oversized/malformed payloads in a loop. |
| `quota_exceeded` | 429 | This API key's monthly document quota is exhausted for the current billing period. This counts only successful extractions and is distinct from the per-minute rate\_limited check — failed extractions don't count against it. | Wait for the monthly reset (X-Quota-Reset unix seconds, or the Retry-After delta-seconds), or upgrade to a larger plan in your dashboard (app.txtfetch.com). |
| `rate_limited` | 429 | This API key's per-minute request rate was exceeded. This counts every accepted request (even ones that go on to fail) and is unrelated to your monthly document quota (see quota\_exceeded). | Back off until the Retry-After delta-seconds elapse, or until X-RateLimit-Reset (unix seconds) passes, then retry. |
| `not_implemented` | 501 | The request asked for a valid but not-yet-shipped response mode — a format/quality combination other than the default text/standard. | Omit format/quality overrides for now; only the default text extraction mode is implemented. |
| `timeout` | 504 | Extraction did not complete within the processing budget (large/complex documents, slow OCR). | Retry the request, or force the async path with ?async=true so it isn't bound by the synchronous request timeout. |
rate\_limited-vs-quota
Both are `429`s, but they mean different things. `rate_limited` is your API key's per-minute request rate. It counts every accepted request, including ones that go on to fail. It resets every minute. It carries a `Retry-After` header (in seconds) plus `X-RateLimit-Limit`/`X-RateLimit-Remaining`/`X-RateLimit-Reset` headers, so you can back off precisely. `quota_exceeded` is your plan's monthly document quota. It counts only successful extractions. It's checked after the rate-limit gate, and it also carries `X-Quota-Limit`/`X-Quota-Remaining`/`X-Quota-Reset`. Wait for the reset or upgrade your plan.
`quota_exceeded` (`429`) is your plan's monthly document quota. It counts only _successful_ extractions, so failed requests never burn it. It resets once per UTC month. The response carries a `Retry-After` header (seconds until that reset) and `X-Quota-Limit`/`X-Quota-Remaining`/`X-Quota-Reset` headers. It also carries the `X-RateLimit-*` triad, since the rate check runs first. Those headers are always present too. Match on `error.code` to tell the two `429`s apart, not on the status alone. `X-Quota-*` headers also ride on every successful `200`/`202` response, so you can budget ahead of time instead of waiting to hit the limit.
authentication-failures
A **missing** `Authorization` header returns `401`; an **invalid or revoked** key returns `403`. Both come back with API Gateway's own `{"message": "…"}` body. This is deliberately not the typed `{"error": {"code": …}}` envelope above. That's a platform limit of the HTTP-API authorizer, not something this API controls. Integrators should match on HTTP status for auth failures, never on a `code` field. A revoked key can take up to 60 seconds to stop working (the authorizer caches its allow/deny decision for that long). Create a key in [your dashboard](https://app.txtfetch.com/signup).
typed-exceptions
Both official SDKs map every code above to its own exception class (for example, `TooLargeError`, `UnsupportedFormatError`). Each class derives from a common `TxtfetchError`. See the [quickstarts](https://txtfetch.com/docs/quickstarts) for the full class list.
machine-readable-spec
This table is generated at build time from the published [OpenAPI 3.1 spec](https://txtfetch.com/openapi.json)'s `ErrorCode` enum and per-code HTTP status. The build fails if this page's editorial guidance ever falls out of sync with the contract.
## Watch it run, then take a key.
The playground replays a real recorded response for every sample document.
[Open the playground →](https://txtfetch.com/playground)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/docs/idempotency
# Retries that can't double-run.
A client-side timeout or a dropped response shouldn't mean paying for the same extraction twice. Idempotency-Key makes a retried request safe.
how-it-works
Pass an `Idempotency-Key` header on any `POST /v1/extract`. txtfetch looks the key up (scoped to your API key, so two callers can't collide on the same value):
- **First time seen:** the request runs normally. If it finishes as a `2xx` or a `202` job-accepted, the response is stored against the key.
- **Repeated with the identical request** (same method, `url`/`async`/body/content-type): the stored response is replayed immediately. There's no re-extraction and no second charge against your quota.
- **Repeated with a different request** under the same key: rejected with `bad_request`. Reusing a key for a different request is almost always a bug. It fails loudly instead of silently returning the wrong cached result.
Only successful dispatches are cached. An error response is never stored, so retrying after a transient failure re-attempts the extraction (and caches it once it succeeds). Records expire after 1 day, the same lifecycle window async job results are kept for.
curl
```curl
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-H "Idempotency-Key: 3f29b6e4-9c1a-4b8e-9c2a-1e6f0a2d5b3c" \
-F file=@contract.pdf
```
JavaScript
```javascript
// The SDK auto-generates and reuses an Idempotency-Key across its own
// retry chain. Pass your own to control it explicitly:
await txtfetch.extract({ file: "./contract.pdf", idempotencyKey: "3f29b6e4-..." });
```
Python
```python
# The SDK auto-generates and reuses an Idempotency-Key across its own
# retry chain. Pass your own to control it explicitly:
client.extract(file="contract.pdf", idempotency_key="3f29b6e4-...")
```
sdk-behavior
Both official SDKs generate an `Idempotency-Key` automatically on every `POST`. They reuse the same one across a retry chain: a client-side timeout, a network error, or a bare `5xx`. An SDK-driven retry is never double-run, without you doing anything. Typed `4xx`/`5xx` error bodies are never retried. Pass your own key explicitly when you need to correlate a retry across process restarts or separate client instances.
see-also
Submitting a job that also carries `webhook_url`? See [async jobs & webhooks](https://txtfetch.com/docs/async) for the job lifecycle and delivery signature. A mismatched retry returns `bad_request`. The full list of codes is in the [error reference](https://txtfetch.com/docs/errors).
## Watch it run, then take a key.
The playground replays a real recorded response for every sample document.
[Open the playground →](https://txtfetch.com/playground)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/docs/quickstarts
# Pick your framework.
Official SDKs and RAG-framework loaders. Install, then extract, in under ten lines.
sdk-js
## @txtfetch/sdk (JavaScript / TypeScript)
Zero runtime dependencies. Node ≥ 20. Dual ESM/CJS with .d.ts.
Install
```install
npm install @txtfetch/sdk
```
Quickstart
```quickstart
import { Txtfetch } from "@txtfetch/sdk";
// apiKey defaults to process.env.TXTFETCH_KEY
const txtfetch = new Txtfetch();
const { extracted_text, metadata } = await txtfetch.extract({ file: "./whitepaper.pdf" });
console.log(extracted_text, metadata.chars);
const byUrl = await txtfetch.extract({ url: "https://example.com/report.docx" });
console.log(byUrl.extracted_text);
```
sdk-python
## txtfetch (Python)
Python 3.9+. The only runtime dependency is httpx.
Install
```install
pip install txtfetch
```
Quickstart
```quickstart
from txtfetch import Txtfetch
# api_key defaults to the TXTFETCH_KEY environment variable
client = Txtfetch(api_key="tf_live_...")
# Extract from a local file (path, bytes, or a file-like object all work)
result = client.extract(file="whitepaper.pdf")
print(result.extracted_text)
print(result.metadata.content_type, result.metadata.bytes, result.metadata.ocr)
# Extract from a URL — txtfetch fetches it server-side
result = client.extract(url="https://example.com/whitepaper.docx")
```
langchain-python
## langchain-txtfetch
A LangChain document loader: a thin adapter over the Python SDK. Each file/URL becomes one Document, ready for a text splitter.
Install
```install
pip install langchain-txtfetch
```
Quickstart
```quickstart
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_txtfetch import TxtfetchLoader
# api_key defaults to the TXTFETCH_KEY environment variable
loader = TxtfetchLoader(
files=["whitepaper.pdf"], # single path/URL or a list of them
urls=["https://example.com/spec.docx"],
)
documents = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
chunks = splitter.split_documents(documents)
```
langchain-js
## @txtfetch/langchain
A LangChain.js document loader: a thin adapter over @txtfetch/sdk. Requires @langchain/core as a peer dependency.
Install
```install
npm install @txtfetch/langchain @langchain/core
```
Quickstart
```quickstart
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { TxtfetchLoader } from "@txtfetch/langchain";
// apiKey defaults to the TXTFETCH_KEY environment variable
const loader = new TxtfetchLoader({
files: ["whitepaper.pdf"], // a single path/URL or an array of them
urls: ["https://example.com/spec.docx"],
});
const documents = await loader.load();
const splitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000, chunkOverlap: 100 });
const chunks = await splitter.splitDocuments(documents);
```
llamaindex-python
## llama-index-readers-txtfetch
A LlamaIndex reader: a thin adapter over the Python SDK. Each file/URL becomes one Document.
Install
```install
pip install llama-index-readers-txtfetch
```
Quickstart
```quickstart
from llama_index.core import VectorStoreIndex
from llama_index.readers.txtfetch import TxtfetchReader
# api_key defaults to the TXTFETCH_KEY environment variable
reader = TxtfetchReader()
documents = reader.load_data(
files=["whitepaper.pdf"], # single path/URL or a list of them
urls=["https://example.com/spec.docx"],
)
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
print(query_engine.query("What is this document about?"))
```
next
Turn any of these loaders into a full ingestion pipeline in the [chunk → embed → index recipe](https://txtfetch.com/docs/recipe). Or read the [error reference](https://txtfetch.com/docs/errors) to see how each SDK's typed exceptions map to the wire format. Writing Go, Java, or C#, languages with no official SDK yet? See [extract by language](https://txtfetch.com/for) for the whole HTTP client in each.
## Watch it run, then take a key.
The playground replays a real recorded response for every sample document.
[Open the playground →](https://txtfetch.com/playground)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/docs/recipe
# Chunk → embed → index, end to end.
txtfetch handles extraction; the loader hands the rest of the pipeline documents your framework already knows how to chunk, embed, and index.
langchain-python
Load with `TxtfetchLoader`, split with a LangChain text splitter, embed and index with any LangChain-supported embedding model and vector store, then query it. See [extract text into a vector store](https://txtfetch.com/ingest) for the same pipeline in plain Python, plus the constraints of five named stores. This extends the tested `examples/build_pipeline.py` from [the LangChain Python quickstart](https://txtfetch.com/docs/quickstarts). Only the embed/index/query steps are new.
LangChain (Python)
```python
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_txtfetch import TxtfetchLoader
# 1. Load — one Document per file/URL, extracted via txtfetch
loader = TxtfetchLoader(
files=["whitepaper.pdf"],
urls=["https://example.com/spec.docx"],
)
documents = loader.load()
# 2. Chunk
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
chunks = splitter.split_documents(documents)
# 3. Embed + index
vector_store = InMemoryVectorStore(OpenAIEmbeddings())
vector_store.add_documents(chunks)
# 4. Query
results = vector_store.similarity_search("What does the whitepaper conclude?", k=4)
for doc in results:
print(doc.metadata["source"], "->", doc.page_content[:80])
```
llamaindex-python
LlamaIndex folds chunking, embedding, and indexing into a single call: `VectorStoreIndex.from_documents(...)`. This runs once `TxtfetchReader` has loaded the source documents.
LlamaIndex (Python)
```python
from llama_index.core import VectorStoreIndex
from llama_index.readers.txtfetch import TxtfetchReader
reader = TxtfetchReader()
documents = reader.load_data(files=["whitepaper.pdf"], urls=["https://example.com/spec.docx"])
# Chunking, embedding, and indexing all happen inside from_documents(...)
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
print(query_engine.query("What does the whitepaper conclude?"))
```
notes
Both paths accept files and URLs in the same loader call. Mix a local upload with a hosted spec sheet in one pipeline. Large sources are handled the same way the API handles them directly. The loader's underlying SDK call transparently waits out the async job path. So `load()`/`load_data()` still return finished `Document`s either way.
Before picking a chunk size and overlap for the splitter step above, try the [chunk previewer](https://txtfetch.com/tools/chunk-preview). Paste extracted text into it and see the resulting chunk boundaries and token estimates. It also flags extraction-damage signals that work against your chunks. Everything runs in your browser, before the text ever reaches your embedding model.
## Watch it run, then take a key.
The playground replays a real recorded response for every sample document.
[Open the playground →](https://txtfetch.com/playground)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/dpa
# Data Processing Addendum
Version 1.0 — last updated 2026-07-16.
This page is print-friendly. Print to PDF for your records, or grab the plain-text copy directly.
Print / save as PDF
[Download dpa.txt](https://txtfetch.com/dpa.txt)
## 1\. Definitions
"Customer" means the organization or individual using the txtfetch API under an accepted plan. "txtfetch," "we," or "us" means the operator of the txtfetch service. "Data Processing Agreement" or "DPA" means this document. "Document Content" means the file bytes or URL-fetched content Customer submits to the API for text extraction. "Personal Data" has the meaning given under applicable data protection law (e.g. GDPR Art. 4(1)) to the extent it appears within Document Content.
## 2\. Roles
With respect to Document Content, Customer is the data controller and txtfetch is the data processor, acting only on Customer's documented instructions as expressed through API calls. With respect to Customer's own account and billing information, and to contact-form submissions, txtfetch is the controller — see the [privacy policy](https://txtfetch.com/privacy).
## 3\. Scope and nature of processing
txtfetch processes Document Content solely to perform text extraction (via Apache Tika, with Tesseract OCR for image-based documents) and to return the extracted text to Customer. Document Content is held in memory for the duration of the request and is not persisted to any datastore. Processing occurs in AWS us-west-2 unless otherwise agreed in writing.
## 4\. Sub-processors
txtfetch uses a defined set of infrastructure sub-processors to deliver the service — see the current list, with purpose, data handled, and region, at [/subprocessors](https://txtfetch.com/subprocessors). txtfetch will update that list before adding a new sub-processor that touches Document Content.
## 5\. Security measures
txtfetch's technical and organizational measures — what's enforced today and what's on the roadmap — are described in full at [/security](https://txtfetch.com/security), including TLS in transit, AWS-managed encryption at rest, in-memory-only processing of Document Content, and planned hardening (SSRF guards, abuse controls, automated patch scanning, SOC 2 Type II).
## 6\. Assistance with data-subject requests
Because Document Content is not retained, txtfetch generally has no stored Document Content to search, export, or delete in response to a data-subject request. Where Customer needs assistance responding to a request that implicates txtfetch's processing, contact us via [/contact](https://txtfetch.com/contact) and we will assist as reasonably required.
## 7\. Breach notification
If txtfetch becomes aware of a security incident affecting Customer's Document Content, we will notify Customer without undue delay after becoming aware, using the contact details on file or via [/contact](https://txtfetch.com/contact), and provide the information reasonably available at the time.
## 8\. Deletion and return
Because Document Content is not retained beyond the lifetime of a single request, there is no stored copy to return or delete at the end of a processing engagement. Any operational logs are handled per the patch and retention posture described at [/security](https://txtfetch.com/security).
## 9\. International transfers
Document Content is processed in AWS us-west-2 (United States). Where Customer is located outside the United States, Customer instructs txtfetch to process Document Content in the United States for the purpose of this DPA. Site analytics data is processed by Google Analytics per its own terms — see [/subprocessors](https://txtfetch.com/subprocessors).
## 10\. Liability
Each party's liability arising out of or related to this DPA is subject to the limitations and exclusions of liability set out in the agreement governing Customer's use of txtfetch. Nothing in this DPA expands either party's liability beyond that agreement.
## 11\. Contact
Questions about this DPA, or requests to execute a signed copy, should go through [/contact](https://txtfetch.com/contact).
---
# https://txtfetch.com/extract
# Extract text from anything, one format at a time.
Every format has its own gotchas. Pick yours below for the working curl, the covered extensions, and the answers people actually search for.
Prefer to start from your language instead of the file format? See [extract by language](https://txtfetch.com/for) for Python, JavaScript, Go, Java, and C#. Already extracted something and the text came back wrong instead? See [fixes for broken extracted text](https://txtfetch.com/fixes).
## PDF & scans
- **[.pdf (digital)](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.
- `.pdf`
Extract .pdf (digital) →
- **[.pdf (scanned)](https://txtfetch.com/extract/scanned-pdf)**
### 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, in the same request.
- `.pdf`
Extract .pdf (scanned) →
## Office
- **[.docx](https://txtfetch.com/extract/docx)**
### Every Office format, one endpoint.
Word, PowerPoint, Excel, RTF, OpenDocument. Per-format libraries and screenshot-reading VLMs both struggle to cover that breadth in one pipeline.
- `.docx`
- `.doc`
- `.pptx`
- `.ppt`
- `.xlsx`
- `.xls`
- `.odt`
- `.ods`
- `.odp`
- `.rtf`
Extract .docx →
- **[.pptx](https://txtfetch.com/extract/pptx)**
### Slide text and speaker notes, together.
Most extractors keep one or the other. txtfetch reads slide bodies and speaker notes from the same request.
- `.pptx`
- `.ppt`
- `.pptm`
- `.potx`
- `.odp`
Extract .pptx →
- **[.xlsx](https://txtfetch.com/extract/xlsx)**
### Spreadsheets, resolved not raw.
Cell values are indices into a shared string table. Formulas store their formula, not their answer. Tika resolves both before you ever see the response.
- `.xlsx`
- `.xls`
- `.xlsm`
- `.csv`
- `.ods`
Extract .xlsx →
- **[.doc](https://txtfetch.com/extract/legacy-office)**
### The 97-2003 binary formats, read properly.
OLE2 Compound File Binary Format isn't a smaller version of OOXML. It's a different filesystem-in-a-file, and .doc text isn't stored in reading order.
- `.doc`
- `.xls`
- `.ppt`
- `.pub`
- `.wpd`
Extract .doc →
- **[.rtf](https://txtfetch.com/extract/rtf)**
### RTF, parsed properly, not regex-stripped.
\\uN? Unicode escapes are followed by an ANSI fallback character on purpose. Strip backslashes with a regex, and you duplicate every non-ASCII character in the document.
- `.rtf`
Extract .rtf →
## Open formats
- **[.odt](https://txtfetch.com/extract/odf)**
### OpenDocument, zipped or flat.
content.xml plus its styles and metadata parts, a markup vocabulary of its own. And a tracked-changes block means struck-out text is still in the file you're about to extract.
- `.odt`
- `.ods`
- `.odp`
- `.fodt`
- `.otp`
- `.ott`
Extract .odt →
## Email
- **[.eml](https://txtfetch.com/extract/email)**
### Email, headers to attachments, one call.
From/To/Subject headers, the message body, and attached documents all come out together. You don't orchestrate separate steps.
- `.eml`
- `.msg`
- `.mbox`
Extract .eml →
- **[.msg](https://txtfetch.com/extract/msg)**
### Outlook .msg isn't email, structurally.
MAPI property streams inside an OLE2 container. Nested .msg-in-.msg attachments. Whole mailbox databases. None of it looks like an .eml file.
- `.msg`
- `.eml`
- `.mbox`
- `.pst`
- `.ost`
Extract .msg →
## Web & data
- **[.html](https://txtfetch.com/extract/html)**
### Any web page, fetched and reduced to clean text.
Pass a URL, not a file. txtfetch fetches the page server-side and hands back clean text, with the markup, scripts, and styles gone. Deciding what's chrome (nav, ads, cookie banners) versus article is a caller-side call. See the free tool below to check yours.
- `.html`
- `.htm`
- `.xhtml`
Extract .html →
- **[.csv](https://txtfetch.com/extract/csv)**
### CSV, decoded from the bytes.
Comma, semicolon, tab, or pipe. UTF-8, Windows-1252, or UTF-16 with a BOM. txtfetch settles the encoding question from the file itself, and hands back the rows exactly as written.
- `.csv`
- `.tsv`
- `.psv`
- `.txt`
Extract .csv →
- **[.xml](https://txtfetch.com/extract/xml)**
### Markup stripped, order preserved.
DITA, DocBook, JATS, XBRL filings. The content is real prose buried in tags, entities, and attributes, in document order, not alphabetical or arbitrary order.
- `.xml`
- `.json`
- `.yaml`
- `.html`
- `.xhtml`
- `.svg`
Extract .xml →
## Images & OCR
- **[.png](https://txtfetch.com/extract/image)**
### OCR that's just another curl request.
Scanned receipts, faxed contracts, photographed whiteboards. Tesseract OCR runs automatically, with no separate OCR pipeline to stand up.
- `.png`
- `.jpg`
- `.jpeg`
- `.tiff`
- `.tif`
- `.bmp`
- `.gif`
Extract .png →
- **[.tiff](https://txtfetch.com/extract/tiff)**
### Every page of the TIFF, not just the first.
A multi-page fax TIFF is a chain of image directories, not one picture. Read the file the way most image loaders do and you silently lose every page after the first.
- `.tiff`
- `.tif`
- `.jpg`
- `.png`
- `.bmp`
- `.webp`
- `.gif`
Extract .tiff →
## Books & archives
- **[.epub](https://txtfetch.com/extract/epub)**
### Ebooks in, chapter text out.
EPUB's zipped-XHTML internals are exactly the kind of format archaeology txtfetch exists to hide from you.
- `.epub`
Extract .epub →
- **[.zip](https://txtfetch.com/extract/zip)**
### One ZIP, every file's text back.
Skip the unzip-then-loop script. Point txtfetch at an archive and get every contained document's text back from one request.
- `.zip`
Extract .zip →
## Audio & video
- **[media captions](https://txtfetch.com/extract/captions)**
### Where the text in a video or audio file actually lives.
Not every media file has text in it. What you get back depends entirely on the container. This page maps a caption sidecar, a tagged MP3, and a plain WAV to what each one actually returns. That includes the cases where the honest answer is nothing.
- `.srt`
- `.vtt`
- `.ttml`
- `.mp4`
- `.mov`
- `.m4a`
- `.mp3`
- `.ogg`
- `.flac`
Extract media captions →
## Send a real file through it.
One HTTP call returns the text. Read one in your browser first, for free.
[Get an API key →](https://app.txtfetch.com/signup)
[Open the free file reader →](https://txtfetch.com/tools/file-to-text)
---
# https://txtfetch.com/extract/captions
# Where the text in a video or audio file actually lives.
Not every media file has text in it. What you get back depends entirely on the container. This page maps a caption sidecar, a tagged MP3, and a plain WAV to what each one actually returns. That includes the cases where the honest answer is nothing.
the-problem
"Extract text from video" usually bundles two different jobs into one request. One is pulling whatever text is already sitting in the file. The other is listening to the audio and writing down what's said. Almost nothing documents the difference. Every container handles the first job differently, and none of them do the second at all. A caption sidecar (.srt/.vtt/.ttml) carries a real transcript, but wrapped in cue numbers and timestamps that a naive read prints right alongside the words. An .mp3 or .mp4 carries whatever tag data its encoder wrote: a title, an artist, sometimes nothing. That tag data is the only text in the file, unless a caption track was muxed in beside it. Some containers have no text-bearing parser registered at all, checked against the exact Tika 3.3.1 build this API runs. They detect fine off their bytes, and then hand back nothing.
one-request-solution
txtfetch reads whatever text genuinely exists in the container, and says so plainly. A caption sidecar comes back close to verbatim, cue numbers, timestamps, and all. Cleaning that up is a separate job (see the free tool below). A tagged .mp3, .mp4, .ogg, or .flac comes back as a few bare lines of tag data, not a transcript. A file with nothing parseable, a plain .wav, an .mkv, a .webm, returns an explicit error instead of a silent empty success. The free in-browser subtitle tool goes further than this API in one respect. It opens an .mp4 or .mkv itself and reads the embedded caption track, client-side. That's a genuinely different capability, not the same thing described twice. txtfetch does not transcribe audio or video. If a file's words only ever existed as sound, run it through speech recognition first, or pull the platform's own caption track. Then send the resulting text through txtfetch like any other document.
curl
```curl
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@team-standup.mp4
```
Python
```python
import os
import requests
with open("team-standup.mp4", "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
```javascript
import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("team-standup.mp4")]);
const form = new FormData();
form.append("file", file, "team-standup.mp4");
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
```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("team-standup.mp4")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "team-standup.mp4")
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
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/webinar-captions.vtt" \
-H "Authorization: Bearer $TXTFETCH_KEY"
```
Python
```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/webinar-captions.vtt"},
)
print(r.json()["extracted_text"])
```
JavaScript
```javascript
const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", "https://example.com/webinar-captions.vtt");
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
```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/webinar-captions.vtt")
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
The corpus behind [/diff](https://txtfetch.com/diff) has no recorded media captions document yet, so we have nothing honest to show you here. Run one of your own instead. The [free converter](https://txtfetch.com/tools/file-to-text) reads the file in your browser, and nothing is uploaded.
formats-covered
- `.srt`
- `.vtt`
- `.ttml`
- `.mp4`
- `.mov`
- `.m4a`
- `.mp3`
- `.ogg`
- `.flac`
response-options
Need JSON instead of plain text? ?format=json wraps the same extracted\_text in the typed envelope every route uses. For media files that means the same tag lines or the same error, not new fields this parser doesn't produce.
faq
**What comes back for an .srt or .vtt caption file?**: Close to what's already in the file: the plain-text body, cue numbers and timestamps included. This route doesn't clean anything up. It hands back the sidecar's text as-is, which is exactly the raw material the free subtitle-to-text tool strips down into a real transcript.
**Is a .ttml or .dfxp caption file different?**: Yes, in a way worth knowing about. TTML stores its timing in XML attributes, not in the text itself. Once the markup is parsed away, what's left is just the caption lines: no cue numbers, no timestamps left to strip. An .srt or .vtt of the identical dialogue comes back messier than the .ttml version of the same thing.
**What does an .mp3, .mp4, .ogg, or .flac file return if it has no caption track?**: Whatever tag data the encoder wrote, and nothing else. That's a title, an artist, sometimes an album, run together as a few bare lines with no field names attached. It's real text pulled from the file, not a transcript of anything spoken in it. The same file with no tags at all, and no caption track either, returns an error instead of an empty success.
**Does a .wav file's title/artist tags come back the same way?**: No, and this is the one genuine surprise checking against a real build turned up. A .wav can carry the same kind of title/artist tags an .mp3 does. But the pinned engine's WAV reader only reports technical properties, sample rate, channel count, bit depth, and never those tag values. A tagged .wav with no other text in it returns the same extraction error as a file with no tags at all.
**What happens with an .mkv, .webm, or .avi file?**: An explicit error, not an empty success. The pinned Tika 3.3.1 build recognizes all three from their bytes, but has no registered parser behind any of them. That was confirmed by sending one through the exact request this API makes. It's a real difference from a container returning a blank string: you find out immediately, instead of getting a silent, empty result.
**Does txtfetch transcribe speech from audio or video?**: No. Every answer on this page is about text that already existed in the file, a caption sidecar, a container tag, never audio converted into words. If a file's content only ever existed as sound, run it through a speech-to-text tool first. Whisper, a hosted ASR API, or a platform's own auto-captions all work. Then send the resulting transcript back through txtfetch like any other text file.
go-further
- [Read the media captions guide →](https://txtfetch.com/blog/text-from-video-and-audio)
- [Clean captions into a real transcript, free →](https://txtfetch.com/tools/subtitles-to-text)
- [Check what's genuinely parsed vs. only detected →](https://txtfetch.com/formats/coverage)
- [Markup stripped a different way, the HTML/XML guide →](https://txtfetch.com/extract/html)
- [API quickstart →](https://txtfetch.com/docs)
- [How the pipeline works →](https://txtfetch.com/how-it-works)
- [Extract it from your language →](https://txtfetch.com/for)
- [Get an API key →](https://app.txtfetch.com/signup)
more-formats
- [All formats →](https://txtfetch.com/extract)
## Send a real caption file through it.
One HTTP call returns the text. Read one in your browser first, for free.
[Get an API key →](https://app.txtfetch.com/signup)
[Open the free caption file reader →](https://txtfetch.com/tools/subtitles-to-text)
---
# https://txtfetch.com/extract/csv
# CSV, decoded from the bytes.
Comma, semicolon, tab, or pipe. UTF-8, Windows-1252, or UTF-16 with a BOM. txtfetch settles the encoding question from the file itself, and hands back the rows exactly as written.
the-problem
A .csv extension promises very little about what's actually inside. The delimiter might be a comma, a semicolon (common in locales where comma is the decimal separator), a tab, or a pipe. The character encoding might be UTF-8, or Windows-1252, where a stray byte turns a curly quote into mojibake. It might also be UTF-16 with a byte-order-mark, which most parsers don't expect on a 'text' file. And quoted fields can legally contain embedded newlines and the delimiter character itself. A naive line-by-line reader splits those rows in the wrong place and shifts every column after it.
one-request-solution
txtfetch identifies the file as text from the actual bytes, not the extension. It detects the character encoding, UTF-8, Windows-1252, or UTF-16 with or without a byte-order-mark, before decoding it. That is where the mojibake bugs live, and it's the part you can't do reliably yourself without reading the file first. What comes back is the delimited content, decoded and complete: every row, every quoted field, with embedded newlines and separator characters intact. txtfetch deliberately doesn't guess at a delimiter and re-shape your data. Your own CSV reader already knows the separator. It works whether the file arrived as an upload, behind a ?url= fetch, or as an entry inside a .zip.
curl
```curl
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@customer-export.csv
```
Python
```python
import os
import requests
with open("customer-export.csv", "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
```javascript
import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("customer-export.csv")]);
const form = new FormData();
form.append("file", file, "customer-export.csv");
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
```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("customer-export.csv")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "customer-export.csv")
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
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/data/transactions-2024.csv" \
-H "Authorization: Bearer $TXTFETCH_KEY"
```
Python
```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/data/transactions-2024.csv"},
)
print(r.json()["extracted_text"])
```
JavaScript
```javascript
const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", "https://example.com/data/transactions-2024.csv");
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
```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/data/transactions-2024.csv")
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
The corpus behind [/diff](https://txtfetch.com/diff) has no recorded .csv document yet, so we have nothing honest to show you here. Run one of your own instead. The [free converter](https://txtfetch.com/tools/file-to-text) reads the file in your browser, and nothing is uploaded.
formats-covered
- `.csv`
- `.tsv`
- `.psv`
- `.txt`
response-options
Want real rows and columns rather than delimited text? That structure has to exist in the file to begin with. ?format=markdown renders a markdown table for spreadsheet formats, where there are actual cells to work from. See the .xlsx page. A raw .csv has no cell model to recover, so it comes back as text.
faq
**Does it handle semicolon- or tab-delimited files, not just commas?**: Yes, because the delimiter isn't something txtfetch has to get right. The file is decoded and returned as text with its separators intact. A semicolon-, tab-, or pipe-delimited export comes back complete, and your own CSV reader splits it on the separator it already expects.
**Why do my exported CSVs show garbled characters (mojibake)?**: That's a charset mismatch: a file saved as Windows-1252 and read as UTF-8, or a UTF-16 export read one byte at a time. txtfetch detects the encoding from the byte patterns rather than assuming UTF-8. It also strips the byte-order-mark that trips up readers treating .csv as plain ASCII.
**What happens to quoted fields containing commas or line breaks?**: Nothing is reflowed. A quoted value containing the separator character or an embedded newline comes back exactly as it appears in the file. It still parses as a single field downstream, because txtfetch doesn't split rows or columns on your behalf.
**Is TSV (tab-separated) or PSV (pipe-separated) treated differently from CSV?**: No. .tsv, .psv, and .txt take the same path as .csv. txtfetch recognises the file as text from the bytes, detects the encoding, and returns the content. The extension isn't what decides.
go-further
- [Read the .csv guide →](https://txtfetch.com/blog/extract-tables-for-rag)
- [Real cells and markdown tables live on the spreadsheet side (.xlsx/.ods) →](https://txtfetch.com/extract/xlsx)
- [Drop a real .csv/.tsv and see the decoded rows — free, in your browser →](https://txtfetch.com/tools/file-to-text)
- [API quickstart →](https://txtfetch.com/docs)
- [How the pipeline works →](https://txtfetch.com/how-it-works)
- [Extract it from your language →](https://txtfetch.com/for)
- [Get an API key →](https://app.txtfetch.com/signup)
Web & data
- [`.html`](https://txtfetch.com/extract/html)
- [`.xml`](https://txtfetch.com/extract/xml)
- [All formats →](https://txtfetch.com/extract)
## Send a real .csv through it.
One HTTP call returns the text. Read one in your browser first, for free.
[Get an API key →](https://app.txtfetch.com/signup)
[Open the free .csv reader →](https://txtfetch.com/tools/file-to-text)
---
# https://txtfetch.com/extract/docx
# Every Office format, one endpoint.
Word, PowerPoint, Excel, RTF, OpenDocument. Per-format libraries and screenshot-reading VLMs both struggle to cover that breadth in one pipeline.
the-problem
Office documents aren't one format. They're a family: .docx and the legacy .doc binary format, .pptx and .ppt, .xlsx and .xls, plus the OpenDocument siblings (.odt/.ods/.odp) and .rtf. A 'parse Office docs' feature usually means five or six separate libraries, each with its own quirks, versioning, and failure modes. Vision-based extractors treat every page as an image. That loses slide speaker notes, spreadsheet formulas, and the structural distinction between a table and a wall of text. Breadth across office formats is exactly where they're weakest.
one-request-solution
txtfetch wraps Apache Tika's Office parsers behind one request. POST any file in the family, or point it at a URL. You get the same { status, extracted\_text } response, with slide notes, sheet contents, and body text included.
curl
```curl
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@board-deck.pptx
```
Python
```python
import os
import requests
with open("board-deck.pptx", "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
```javascript
import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("board-deck.pptx")]);
const form = new FormData();
form.append("file", file, "board-deck.pptx");
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
```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("board-deck.pptx")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "board-deck.pptx")
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
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/sales-workbook.xlsx" \
-H "Authorization: Bearer $TXTFETCH_KEY"
```
Python
```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/sales-workbook.xlsx"},
)
print(r.json()["extracted_text"])
```
JavaScript
```javascript
const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", "https://example.com/sales-workbook.xlsx");
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
```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/sales-workbook.xlsx")
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 .docx 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 Board Update (DOCX)
`board-update-q3-2026.docx` · application/vnd.openxmlformats-officedocument.wordprocessingml.document · left pane read via Apache Tika, **right pane: the human-checked expected.json**
raw parser output
```
Q3 2026 Board Update
This update covers Q3 2026 performance across all product lines.
Highlights
Annual recurring revenue grew 14% quarter over quarter
Shipped the structured-output beta
Markdown mode
Element JSON mode
Support headcount increased to 6 engineers
Revenue by Plan
Plan MRR Growth
Hobby $0 n/a
Developer $18,200 11%
Scale $41,900 19%
Full detail is available in the appendix & supporting spreadsheets.
```
the human-checked ideal
```
Q3 2026 Board Update
This update covers Q3 2026 performance across all product lines.
Highlights
Annual recurring revenue grew 14% quarter over quarter
Shipped the structured-output beta
Markdown mode
Element JSON mode
Support headcount increased to 6 engineers
Revenue by Plan
Plan MRR Growth
Hobby $0 n/a
Developer $18,200 11%
Scale $41,900 19%
Full detail is available in the appendix & supporting spreadsheets.
```
[See all ten documents, with the full explanation →](https://txtfetch.com/diff)
formats-covered
- `.docx`
- `.doc`
- `.pptx`
- `.ppt`
- `.xlsx`
- `.xls`
- `.odt`
- `.ods`
- `.odp`
- `.rtf`
faq
**How do I extract text from a .docx file?**: POST it as multipart form data to https://api.txtfetch.com/v1/extract with your API key in the Authorization header. The response is { "status": "success", "extracted_text": "..." }.
**Can it read PowerPoint speaker notes, not just slide text?**: Yes. Apache Tika extracts slide body text and speaker notes from .pptx/.ppt files. Both are included in the returned text.
**Does it handle the legacy .doc and .xls binary formats too?**: Yes. txtfetch covers both the modern XML-based Office formats and their legacy binary predecessors through the same endpoint.
**What about OpenDocument files (.odt, .ods, .odp)?**: Supported the same way. POST the file or its URL, and get back the same JSON response shape as any other format.
go-further
- [Read the .docx guide →](https://txtfetch.com/blog/parse-office-docs-docx-pptx-xlsx-for-llms)
- [Drop a real .docx and see the exact text it extracts to, free →](https://txtfetch.com/tools/docx-to-text)
- [Need the .doc / .xls / .ppt 97-2003 story specifically? →](https://txtfetch.com/extract/legacy-office)
- [API quickstart →](https://txtfetch.com/docs)
- [How the pipeline works →](https://txtfetch.com/how-it-works)
- [Extract it from your language →](https://txtfetch.com/for)
- [Get an API key →](https://app.txtfetch.com/signup)
Office
- [`.pptx`](https://txtfetch.com/extract/pptx)
- [`.xlsx`](https://txtfetch.com/extract/xlsx)
- [`.doc`](https://txtfetch.com/extract/legacy-office)
- [`.rtf`](https://txtfetch.com/extract/rtf)
- [All formats →](https://txtfetch.com/extract)
## Send a real .docx through it.
One HTTP call returns the text. Read one in your browser first, for free.
[Get an API key →](https://app.txtfetch.com/signup)
[Open the free .docx reader →](https://txtfetch.com/tools/docx-to-text)
---
# https://txtfetch.com/extract/email
# Email, headers to attachments, one call.
From/To/Subject headers, the message body, and attached documents all come out together. You don't orchestrate separate steps.
the-problem
Email is a container format wearing a text format's clothes. It has headers, a MIME-encoded body (often both plain-text and HTML parts), and arbitrary attached documents that themselves need extracting. Most extraction tools have no email code path at all. That's especially true for VLM-based tools built around single images or pages. Teams end up hand-rolling header parsing and MIME decoding just to get the text they actually want.
one-request-solution
txtfetch treats email as a first-class format. POST an .eml, Outlook .msg, or .mbox archive, and Apache Tika extracts headers, body, and attachment text together into one plain-text response. The request shape is the same as every other format.
curl
```curl
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@support-thread.eml
```
Python
```python
import os
import requests
with open("support-thread.eml", "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
```javascript
import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("support-thread.eml")]);
const form = new FormData();
form.append("file", file, "support-thread.eml");
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
```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("support-thread.eml")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "support-thread.eml")
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
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/archive/2024-q1.mbox" \
-H "Authorization: Bearer $TXTFETCH_KEY"
```
Python
```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/archive/2024-q1.mbox"},
)
print(r.json()["extracted_text"])
```
JavaScript
```javascript
const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", "https://example.com/archive/2024-q1.mbox");
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
```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/archive/2024-q1.mbox")
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 .eml 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.
Invoice Follow-up (EML)
`invoice-followup.eml` · message/rfc822 · left pane read via Apache Tika, **right pane: the human-checked expected.json**
raw parser output
```
From: Priya Shah
To: Billing
Subject: Re: Invoice #7734
Hi team,
Following up on the invoice below — could you confirm receipt?
Description Qty Amount
Consulting hours 12 $1,800.00
Travel 1 $340.00
Invoice total: $2,140.00
Due date: 2026-08-15
Thanks,
Priya
```
the human-checked ideal
```
From: Priya Shah
To: Billing
Subject: Re: Invoice #7734
Hi team,
Following up on the invoice below — could you confirm receipt?
Description Qty Amount
Consulting hours 12 $1,800.00
Travel 1 $340.00
Invoice total: $2,140.00
Due date: 2026-08-15
Thanks, Priya
```
[See all ten documents, with the full explanation →](https://txtfetch.com/diff)
formats-covered
- `.eml`
- `.msg`
- `.mbox`
faq
**Can I parse a .eml email for an LLM?**: Yes. POST the .eml file to https://api.txtfetch.com/v1/extract and get back headers, body, and any attachment text as one plain-text response. It's ready to feed into a prompt or embedding step.
**Does it support Outlook .msg files?**: Yes, through the same endpoint, with the same response shape. Under the hood, .msg is a completely different container: OLE2 with MAPI property streams, not RFC-822. Read the dedicated .msg page if you're debugging attachment or nesting behavior.
**Can it process a whole .mbox archive?**: Yes. Apache Tika reads .mbox archives and extracts the contained messages' text.
**Are email attachments included in the extracted text?**: Yes. Attached documents are parsed, and their text is folded into the response alongside the message body.
go-further
- [Outlook .msg internals, .pst/.ost, and nested attachments →](https://txtfetch.com/extract/msg)
- [Drop a real .eml or .msg and see the extracted text, free →](https://txtfetch.com/tools/email-to-text)
- [API quickstart →](https://txtfetch.com/docs)
- [How the pipeline works →](https://txtfetch.com/how-it-works)
- [Extract it from your language →](https://txtfetch.com/for)
- [Get an API key →](https://app.txtfetch.com/signup)
Email
- [`.msg`](https://txtfetch.com/extract/msg)
- [All formats →](https://txtfetch.com/extract)
## Send a real .eml through it.
One HTTP call returns the text. Read one in your browser first, for free.
[Get an API key →](https://app.txtfetch.com/signup)
[Open the free .eml reader →](https://txtfetch.com/tools/email-to-text)
---
# https://txtfetch.com/extract/epub
# Ebooks in, chapter text out.
EPUB's zipped-XHTML internals are exactly the kind of format archaeology txtfetch exists to hide from you.
the-problem
An EPUB is a zip archive of XHTML files, a manifest, and a spine that defines reading order. None of that is something you want to parse by hand just to get a book's text out. Generic text extractors expect a single flat document. They don't know what to do with a container format like this, and most teams don't have an EPUB parser sitting around.
one-request-solution
POST the .epub to txtfetch, and Apache Tika walks the manifest and spine for you. It returns the book's text in reading order as plain text, with the same request shape and response shape as every other format.
curl
```curl
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@field-guide.epub
```
Python
```python
import os
import requests
with open("field-guide.epub", "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
```javascript
import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("field-guide.epub")]);
const form = new FormData();
form.append("file", file, "field-guide.epub");
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
```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("field-guide.epub")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "field-guide.epub")
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
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/library/field-guide.epub" \
-H "Authorization: Bearer $TXTFETCH_KEY"
```
Python
```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/library/field-guide.epub"},
)
print(r.json()["extracted_text"])
```
JavaScript
```javascript
const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", "https://example.com/library/field-guide.epub");
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
```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/library/field-guide.epub")
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
The corpus behind [/diff](https://txtfetch.com/diff) has no recorded .epub document yet, so we have nothing honest to show you here. Run one of your own instead. The [free converter](https://txtfetch.com/tools/file-to-text) reads the file in your browser, and nothing is uploaded.
formats-covered
- `.epub`
faq
**How do I extract text from an EPUB file?**: POST the .epub as multipart form data to https://api.txtfetch.com/v1/extract with your API key in the Authorization header. You get back { "status": "success", "extracted_text": "..." }, with the book's text in reading order.
**What about older Kindle formats like .mobi or .azw?**: txtfetch's core support targets EPUB. .mobi and .azw run through the same endpoint and often extract cleanly via Tika's fallback parsers, but EPUB is the best-tested path today.
**Does it preserve chapter order?**: Yes. Tika reads the EPUB's spine (its defined reading order) rather than the arbitrary order files happen to be zipped in.
go-further
- [Drop a real .epub and see the extracted text, chapter by chapter, free →](https://txtfetch.com/tools/epub-to-text)
- [API quickstart →](https://txtfetch.com/docs)
- [How the pipeline works →](https://txtfetch.com/how-it-works)
- [Extract it from your language →](https://txtfetch.com/for)
- [Get an API key →](https://app.txtfetch.com/signup)
Books & archives
- [`.zip`](https://txtfetch.com/extract/zip)
- [All formats →](https://txtfetch.com/extract)
## Send a real .epub through it.
One HTTP call returns the text. Read one in your browser first, for free.
[Get an API key →](https://app.txtfetch.com/signup)
[Open the free .epub reader →](https://txtfetch.com/tools/epub-to-text)
---
# https://txtfetch.com/extract/html
# Any web page, fetched and reduced to clean text.
Pass a URL, not a file. txtfetch fetches the page server-side and hands back clean text, with the markup, scripts, and styles gone. Deciding what's chrome (nav, ads, cookie banners) versus article is a caller-side call. See the free tool below to check yours.
the-problem
Turning a web page into usable text usually means standing up a scraper. You fetch the HTML yourself, strip scripts and styles, and guess at which
",
"cells": [
[{ "text": "Region", "colspan": 1, "rowspan": 1, "header": true }, { "text": "Revenue", "colspan": 1, "rowspan": 1, "header": true }],
[{ "text": "North America", "colspan": 1, "rowspan": 1, "header": false }, { "text": "$4.2M", "colspan": 1, "rowspan": 1, "header": false }],
[{ "text": "EMEA", "colspan": 1, "rowspan": 1, "header": false }, { "text": "$3.1M", "colspan": 1, "rowspan": 1, "header": false }]
],
"rows": 3,
"cols": 2,
"page": 1,
"offset": 84,
"bbox": null
}
],
"metadata": {
"content_type": "application/pdf",
"bytes": 482913,
"chars": 812,
"ocr": false,
"format": "json",
"tier": "standard",
"pages": 4,
"vlm": false
}
}
```
## which-shape-do-i-want
| format= | Best for | Learn more |
| --- | --- | --- |
| `text` | Search indexing, classification, and plain prompting all just want the words. A flat extracted\_text string is the smallest, fastest response to parse. It's also the one every existing integration already handles. This is the default: omit format entirely and you get this shape. | [Response shape in the docs](https://txtfetch.com/docs#response) |
| `markdown` | RAG chunking needs headings and tables to survive the splitter. A structure-aware or recursive chunker keys off Markdown's headings and blank-line paragraph breaks. A flat text dump doesn't carry those breaks. Tables render as real GFM pipe tables when every cell is a simple 1×1 cell. | [See it chunk in the previewer](https://txtfetch.com/tools/chunk-preview), [Chunking strategies for RAG](https://txtfetch.com/blog/chunking-strategies-for-rag) |
| `json` | Layout-aware pipelines need per-element page, offset, and type data. A typed elements array lets you filter to just tables, walk headings for a table of contents, or chunk per element. You don't need to re-derive structure from a Markdown string. | [Full element schema in the docs](https://txtfetch.com/docs#response-formats), [OpenAPI spec](https://txtfetch.com/openapi.json) |
## tables-survive
A flattened text dump loses column alignment once it's copied out of a table. Reconstructing it downstream means guessing where one column ends and the next begins. `format=markdown` keeps the grid:
Before: format=text
```
Region Revenue
North America $4.2M
EMEA $3.1M
```
After: format=markdown
```
| Region | Revenue |
| --- | --- |
| North America | $4.2M |
| EMEA | $3.1M |
```
Merged cells are the honest exception. GFM has no way to express a `colspan`/`rowspan` greater than 1. A table with a merged cell renders instead as a sanitized inline `
`, with only `table`/`thead`/`tbody`/`tr`/`th`/`td` tags, numeric spans, and escaped text. It's still valid GFM, since GFM allows raw HTML blocks. A table with no merged cells always renders as a plain pipe table.
## quality-modes
| quality= | Best for |
| --- | --- |
| `standard` | The default for every request. Apache Tika parses the document's real structure; Tesseract OCR runs automatically on pages with no text layer. Fast enough to stay synchronous for the vast majority of documents. |
| `premium` | Documents where layout matters more than raw text, like dense multi-column tables or scanned pages, need this tier. Tika's structural recovery is weakest here. This tier always routes async, even for a one-page PDF, since VLM latency can exceed the sync budget. Expect a 202 and a job\_id to poll. |
`quality` is independent of `format`. Pick any response shape with either tier. **standard** (Tika) is the default and stays synchronous. **premium** (a vision-language model) always routes async, even for a one-page PDF. VLM latency can exceed the sync request budget. Expect a `202` and a `job_id` to poll. Premium degrades rather than failing. On a cost, timeout, or size guardrail breach, or a VLM error, it falls back to the standard Tika baseline. It still returns a `success`, with `metadata.tier_downgraded: true` and a `metadata.downgrade_reason` explaining why. The one exception is a document the Tika baseline can't extract either. With no successful result to fall back to, you get the normal extraction error instead of a downgraded success.
Submit (premium)
```submit
curl -X POST "https://api.txtfetch.com/v1/extract?quality=premium" \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@quarterly-report.pdf
# {"status": "processing", "job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"}
# quality=premium always routes async, even for a one-page PDF.
```
Premium result
```premium-success
{
"status": "success",
"extracted_text": "Quarterly Report\n\nQ3 revenue grew 34% year over year, driven by expansion in EMEA.\n\nRegion\tRevenue\nNorth America\t$4.2M\nEMEA\t$3.1M\n",
"metadata": {
"content_type": "application/pdf",
"bytes": 482913,
"chars": 812,
"ocr": false,
"format": "text",
"tier": "premium",
"pages": 4,
"vlm": true,
"usage": { "model": "claude-sonnet-5", "input_tokens": 2140, "output_tokens": 612 }
}
}
```
Guardrail downgrade
```downgraded
{
"status": "success",
"extracted_text": "Quarterly Report\n\nQ3 revenue grew 34% year over year, driven by expansion in EMEA.\n\nRegion\tRevenue\nNorth America\t$4.2M\nEMEA\t$3.1M\n",
"metadata": {
"content_type": "application/pdf",
"bytes": 482913,
"chars": 812,
"ocr": false,
"format": "text",
"tier": "standard",
"pages": 4,
"vlm": false,
"tier_downgraded": true,
"downgrade_reason": "max_cost"
}
}
```
| downgrade\_reason | Meaning |
| --- | --- |
| `vlm_disabled` | the VLM\_ENABLED ops flag is off |
| `max_bytes` | a pre-flight size guardrail breach, so the VLM was never called |
| `max_pages` | a pre-flight page-count guardrail breach, so the VLM was never called |
| `max_cost` | a pre-flight cost guardrail breach, so the VLM was never called |
| `vlm_error` | an HTTP/timeout/parse failure calling the VLM |
| `truncated` | the VLM hit its own max\_tokens before finishing |
There's no plan-level gate on `quality=premium`. Every plan can request it, and pricing is unchanged either way (see [pricing](https://txtfetch.com/pricing)). Full guardrails and the measured accuracy climb between tiers: [benchmarks →](https://txtfetch.com/benchmarks) and [the docs →](https://txtfetch.com/docs#premium-quality).
## honest-limits
- **No schema-defined field extraction.** Structured Markdown and element JSON ship today; pulling typed fields per your own schema (invoice number, total, dates) is on the roadmap, not shipped.
- **Structured text, not a visual layout reconstruction.** Element JSON and Markdown preserve reading order and block structure. They don't reproduce the document's visual page layout.
- **OCR accuracy tracks scan quality.** A clean scan OCRs well; a low-resolution or skewed scan degrades like any OCR pipeline's would.
- **`bbox` is always `null` on the standard path.** The field is reserved for a future VLM-populated version. It isn't coordinates today, on either tier.
- **Premium is async-only.** There's no synchronous VLM path, regardless of document size.
## faq
**Does txtfetch return Markdown, not just plain text?**: Yes. Pass ?format=markdown and the response carries a markdown field instead of extracted_text. Headings, nested lists, and paragraphs keep reading order, joined by blank lines. Tables render as GFM pipe tables. It ships today, using the same endpoint and auth as the default text shape.
**Can I get structured JSON with per-element position info?**: Yes. ?format=json returns an elements array of typed heading/paragraph/list/table/image/code nodes. Each one carries a page, offset, and bbox value. bbox is always null on the standard Tika path. It's reserved for the premium VLM path, which doesn't populate it yet either. See the element JSON schema in the docs for the full field list.
**Do merged table cells survive in Markdown?**: GFM has no way to express a merged cell. A table with any colspan or rowspan greater than 1 renders as a sanitized inline
instead of a pipe table. The sanitized table keeps only table/thead/tbody/tr/th/td tags, numeric colspan/rowspan values, and HTML-escaped text. It's still valid GFM, since GFM allows raw HTML blocks. A table with no merged cells always renders as a plain pipe table.
**Is quality=premium synchronous, like the standard tier?**: No. Premium always routes async, even for a one-page PDF. Vision-language model latency can exceed the synchronous request budget. Expect a 202 and a job_id to poll, the same job lifecycle as any other async request.
**What happens if the premium tier fails or costs too much?**: A VLM failure never fails your request. It degrades instead. On a cost, timeout, or size guardrail breach, or a VLM error, premium falls back to the standard Tika baseline. It still returns status: success, with metadata.tier_downgraded: true and a metadata.downgrade_reason explaining why. The one exception is a document the Tika baseline couldn't extract either. Then there's no successful result to fall back to, so you get the normal extraction error instead.
**Can I request typed fields — invoice number, total, dates — out of a document?**: Not yet. Structured Markdown and element-JSON document output ship today. Schema-defined field extraction, pulling typed fields per your own schema, is on the roadmap. It hasn't shipped yet.
## Stop parsing. Start shipping.
Create an account and get an API key in minutes. The free Hobby plan needs no card.
[Get started →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/playground
# Run txtfetch on a real document.
Pick a sample. Watch the exact request, response, and extracted text txtfetch returns for it. Or drop your own file below and watch this page read it on the spot. Nothing is uploaded either way.
Multi-column layouts and dense financial tables collapse into unreadable strings in naive PDF parsers. Tika reads the underlying document structure instead of guessing from pixel positions.
[See the .pdf guide →](https://txtfetch.com/extract/pdf)
curl
```curl
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@quarterly-report.pdf
```
Python
```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
```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
```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)
}
```
Slide body text and speaker notes live in separate parts of a .pptx archive. Most extractors keep only one. Tika returns both together.
[See the .pptx guide →](https://txtfetch.com/extract/pptx)
curl
```curl
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@quarterly-board-deck.pptx
```
Python
```python
import os
import requests
with open("quarterly-board-deck.pptx", "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
```javascript
import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("quarterly-board-deck.pptx")]);
const form = new FormData();
form.append("file", file, "quarterly-board-deck.pptx");
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
```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-board-deck.pptx")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "quarterly-board-deck.pptx")
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)
}
```
A scan has no text layer at all. Tesseract OCR runs automatically behind the same endpoint, with no separate OCR pipeline to stand up.
[See the .png guide →](https://txtfetch.com/extract/image)
curl
```curl
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@scanned-invoice.png
```
Python
```python
import os
import requests
with open("scanned-invoice.png", "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
```javascript
import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("scanned-invoice.png")]);
const form = new FormData();
form.append("file", file, "scanned-invoice.png");
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
```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("scanned-invoice.png")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "scanned-invoice.png")
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)
}
```
Email is a container format: headers, a MIME body, and attached documents that themselves need extracting. Most tools have no code path for it at all.
[See the .eml guide →](https://txtfetch.com/extract/email)
curl
```curl
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@support-thread.eml
```
Python
```python
import os
import requests
with open("support-thread.eml", "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
```javascript
import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("support-thread.eml")]);
const form = new FormData();
form.append("file", file, "support-thread.eml");
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
```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("support-thread.eml")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "support-thread.eml")
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)
}
```
Spreadsheets encode meaning in formulas and sheet structure, not just cell text. Tika resolves formulas to their computed values.
[See the .xlsx guide →](https://txtfetch.com/extract/xlsx)
curl
```curl
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/reports/regional-sales.xlsx" \
-H "Authorization: Bearer $TXTFETCH_KEY"
```
Python
```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/reports/regional-sales.xlsx"},
)
print(r.json()["extracted_text"])
```
JavaScript
```javascript
const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", "https://example.com/reports/regional-sales.xlsx");
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
```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/reports/regional-sales.xlsx")
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)
}
```
A URL isn't a file. txtfetch fetches the page server-side and returns clean text with markup, scripts, and styles gone. Nav and cookie-banner text stay in the response. Picking out the article is a caller-side step (see /tools/html-to-text).
[See the .html guide →](https://txtfetch.com/extract/html)
curl
```curl
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/blog/2024-annual-report" \
-H "Authorization: Bearer $TXTFETCH_KEY"
```
Python
```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/blog/2024-annual-report"},
)
print(r.json()["extracted_text"])
```
JavaScript
```javascript
const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", "https://example.com/blog/2024-annual-report");
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
```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/blog/2024-annual-report")
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)
}
```
A zipped batch means one extraction request instead of unzip-then-loop-per-file orchestration code.
[See the .zip guide →](https://txtfetch.com/extract/zip)
curl
```curl
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@batch-export.zip
```
Python
```python
import os
import requests
with open("batch-export.zip", "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
```javascript
import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("batch-export.zip")]);
const form = new FormData();
form.append("file", file, "batch-export.zip");
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
```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("batch-export.zip")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "batch-export.zip")
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 drop your own file
The samples above are a recorded run — real output, but not from your document. Drop a file here and this page will fingerprint it from its bytes and read it live, right in this tab.
## faq
**Does dropping my own file upload it anywhere?**: No. Your file is read entirely in your browser, using the same engine behind /tools/file-to-text. It never touches the network. There's nothing to upload and nothing to wait on.
**Why doesn't this page call the real txtfetch API?**: Because that would expose the extraction endpoint with no login and no rate limits. That's an open door for abuse. The samples above are a genuine recording of that same API. Your own file runs through the identical readers, just locally instead of over HTTP. You get real output, without a public, unauthenticated endpoint.
**What happens if I drop a scan, a photo, or a video file?**: You get an honest verdict instead of invented text. A scanned PDF page reports itself as a scan that needs OCR. An image gets an OCR-readiness check, not fabricated text. Audio and video only return text if the file already has a caption track. This tool never transcribes speech. Neither does the real API.
## Stop parsing. Start shipping.
Create an account and get an API key in minutes. The free Hobby plan needs no card.
[Get started →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/pricing
# Priced per document, not per surprise.
Metered on one honest unit: a document extracted. No per-page arithmetic, no per-megabyte fine print.
## Hobby
$0/month
Free, permanently — for side projects and trying it out.
- 500 documents / month
- 10 MB max file size
- All 615 supported formats
- Community support
[Get started](https://app.txtfetch.com/signup)
most popular
## Developer
$19/month
For production pipelines that stay lean.
- 10,000 documents / month
- 50 MB max file size
- OCR for scans & images
- Priority support
[Get started](https://app.txtfetch.com/signup)
## Scale
$99/month
For ingestion at serious volume.
- 100,000 documents / month
- 200 MB max file size
- Priority OCR queue
- Uptime SLA (planned)
[Talk to us](https://txtfetch.com/contact?topic=scale)
**Billing:** Hobby is free, forever, with no card required. Developer and Scale are billed monthly via Stripe. Upgrade, downgrade, or cancel anytime from your dashboard.
Coming from a per-page vendor? [See how per-document pricing changes the math](https://txtfetch.com/compare/per-page-pricing).
Every plan includes every response mode and quality tier: `format=markdown`, `format=json`, and `quality=premium`. A document is one document, whatever shape it comes back in. See [what comes back →](https://txtfetch.com/output).
faq
## Questions we'd ask too.
**What counts as a document?**: One extraction request: one URL fetched or one file uploaded. A 300-page PDF is still one document.
**Which file formats are supported?**: Apache Tika detects 1,683 media types and has a real parser behind 615 of them, checked against the exact build txtfetch runs (see txtfetch.com/formats/coverage). Supported formats include PDF, DOCX, PPTX, XLSX, ODF, HTML, EPUB, EML/MSG email, RTF, and plain-text families. Scanned documents and images go through OCR.
**Do you keep my documents?**: No. Documents are processed in memory and discarded when the response is returned. We store request metadata (timestamps, sizes, status) for metering. We never store content.
**How do I get an API key?**: Sign up at app.txtfetch.com. The free Hobby plan gets you a key instantly, no card required.
**What happens if extraction fails?**: You get an explicit error response with a machine-readable reason (unsupported format, encrypted file, fetch failure). It's never a silent empty string. Failed extractions don't count against your quota.
**What happens when I hit a rate limit or my monthly quota?**: You get a typed 429. It's rate_limited if you've exceeded your API key's per-minute request rate, or quota_exceeded if you've used up your plan's monthly document allowance. Both codes are disambiguated by error.code, and both carry a Retry-After header telling you exactly when to retry. Failed extractions never count against your monthly quota, only successful ones. Need a larger plan? Upgrade in your dashboard.
## Stop parsing. Start shipping.
Create an account and get an API key in minutes. The free Hobby plan needs no card.
[Get started →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/privacy
# Privacy policy
Last updated 2026-07-16.
This page describes what txtfetch processes when you use the API or the website, who controls that data, and how long it's kept. It's written to be read, not just filed — if anything here is unclear, ask us via [/contact](https://txtfetch.com/contact).
## What we process
**Document content.** When you send a file or a URL to the extraction API, txtfetch reads the bytes into memory, runs them through the extraction engine, and returns the resulting text. That content is processed in memory and discarded — we do not persist document content to any datastore. We are in the process of removing a request-logging path that could otherwise capture raw request data in operational logs (see [/security](https://txtfetch.com/security#disclosure) for status); until that lands, treat "not retained" as our enforced target rather than a fully audited guarantee.
**Contact-form submissions.** If you submit [/contact](https://txtfetch.com/contact), we process the name, email address, and message you provide, and relay it by email via AWS SES. We keep these messages to answer you and to operate support — not for marketing.
**Site analytics.** The marketing site uses Google Analytics to see which pages get traffic. Google Analytics sets cookies and is governed by Google's own privacy policy. See [sub-processors](https://txtfetch.com/subprocessors) for the current list of analytics providers in use.
## Controller and processor roles
For document content sent through the API, you (or your organization) are the data controller and txtfetch is the data processor acting on your instructions. For contact-form submissions and site analytics, txtfetch is the controller. See the [Data Processing Addendum](https://txtfetch.com/dpa) for the contractual detail behind the processor relationship.
## Retention
- **Document content:** not retained. Processed in memory for the duration of a single request and discarded.
- **Contact-form messages:** retained for as long as needed to respond and provide support, then deleted.
- **Analytics:** Google Analytics retention follows Google's own Analytics data retention settings.
## Sub-processors
txtfetch runs on a small set of infrastructure providers — AWS (compute, CDN, storage, email delivery) and Google Analytics. The full list, with purpose, data handled, and region, is on the [sub-processors page](https://txtfetch.com/subprocessors).
## Your rights and requests
To exercise a data-subject request (access, deletion, correction) for information we hold as controller, or to report a security concern, reach us through [/contact](https://txtfetch.com/contact) — there's no separate email address to look up. We'll acknowledge requests within a reasonable time and follow up with next steps.
## Changes to this policy
We'll update the "last updated" date above whenever this policy changes materially, and note significant changes here rather than silently editing history.
---
# https://txtfetch.com/security
# What's actually enforced, and what's next.
txtfetch is pre-launch and hardening in public. Here's the honest split between what's shipped today and what's on the roadmap. No claim outruns the code.
in-place-today
### In-memory processing, no content store
Every request is held in memory for the duration of the extraction and never written to a datastore. See the data handling note below for the exact posture.
### TLS in transit
CloudFront and the Lambda function URL terminate TLS with AWS-managed certificates (ACM). Every hop between you and txtfetch is encrypted.
### AWS-managed encryption at rest
Anything txtfetch stores in AWS (S3 assets, CloudWatch logs) is encrypted at rest using AWS-managed keys (S3 SSE).
### Google Analytics
Google Analytics is the site's analytics provider. It sets cookies and is governed by Google's own policy. See /subprocessors.
### Least-privilege intent
The extraction Lambda's IAM role is scoped to what the function needs to run. That means compute and the artifacts it reads, nothing broader.
### Single region
All compute and storage run in a single AWS region (us-west-2). There's no unnecessary data replication across geographies.
on-the-roadmap
planned
### SSRF-guarded URL fetch
Server-side URL fetching (?url=) will be restricted with an allowlist and IP/metadata-endpoint guards, closing off internal-network targets.
planned
### Abuse controls beyond rate limiting
Additional request-shape and reputation checks are planned on top of today's basic rate limiting.
planned
### Automated dependency & runtime scanning
A published patch SLA, plus CI-driven dependency and base-image scanning. See the target windows in the data handling note below.
planned
### SOC 2 Type II
A formal audit is on the roadmap, not completed. Details on scope and timeline live on the /compliance page.
[Read more →](https://txtfetch.com/compliance)
data-handling
## No content store, and we're closing the last gap.
Document bytes are processed in memory and discarded. We do not persist document content to any datastore. We are actively closing out a request-logging path that could otherwise capture raw request data in operational logs. Until that work lands, treat this as our target posture, not a fully audited guarantee. Full detail is on the [privacy page](https://txtfetch.com/privacy).
Patch targets: critical vulnerabilities within 7 days, high-severity within 30 days. Today that's a manual process; automated scanning is on the roadmap above.
vulnerability-disclosure
## Found a problem? Tell us.
**Scope:** txtfetch.com, the extraction API, and any subdomain of txtfetch.com. Third-party services we depend on (AWS, Google Analytics) are out of scope. Report those directly to the provider.
**Safe harbor:** make a good-faith effort to find and report a vulnerability. Avoid data destruction, privacy violations, and service disruption. Do that, and we will not pursue legal action against you for that research.
**How to report:** send details through [/contact](https://txtfetch.com/contact). There's no email address to guess at. Include what you found, how to reproduce it, and its potential impact.
**Response window:** expect an acknowledgment within 3 business days. We'll follow up with a timeline once we've triaged the report.
Machine-readable policy: [/.well-known/security.txt](https://txtfetch.com/.well-known/security.txt)
more
- [Privacy policy](https://txtfetch.com/privacy)
- [Sub-processors](https://txtfetch.com/subprocessors)
- [Data Processing Addendum](https://txtfetch.com/dpa)
- [Compliance roadmap](https://txtfetch.com/compliance)
Last updated 2026-07-16.
## Stop parsing. Start shipping.
Create an account and get an API key in minutes. The free Hobby plan needs no card.
[Get started →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/solutions
# Text extraction, by what you're building.
One API, whatever you are building. Pick your use case below to see where the extraction step fits.
**[RAG & LLM ingestion](https://txtfetch.com/solutions/rag-ingestion)**
One extraction step before your chunker, embedder, and vector store.
explore →
**[Search indexing](https://txtfetch.com/solutions/search-indexing)**
One ingestion path for your whole document estate, OCR included.
explore →
**[Document workflows & automation](https://txtfetch.com/solutions/document-workflows)**
Receive a document, get text back, act on it. OCR and webhooks included.
explore →
**[Invoice & receipt processing](https://txtfetch.com/solutions/invoice-and-receipt-processing)**
One extraction step before your AP rules or LLM parser touches the invoice.
explore →
**[Contract & legal review](https://txtfetch.com/solutions/contract-and-legal-review)**
One reader for contracts, email threads, and the scans stapled inside them.
explore →
**[Resume & CV parsing](https://txtfetch.com/solutions/resume-and-cv-parsing)**
One reader for every résumé format a candidate might upload.
explore →
**[Research & academic papers](https://txtfetch.com/solutions/research-and-academic-papers)**
One reader for PDF, EPUB, and the scanned TIFF an old archive hands you.
explore →
## Start on the free plan.
Run your own documents through it before you commit. The Hobby plan needs no card.
[Get started →](https://app.txtfetch.com/signup)
[See the pricing →](https://txtfetch.com/pricing)
---
# https://txtfetch.com/solutions/contract-and-legal-review
# Read a whole contract, attachments and all.
Legal teams review old files: .doc contracts, .msg and .eml threads with attachments, and scanned signature pages inside a digital PDF. txtfetch reads all of them.
the-problem
Legal operations and e-discovery deal with an old corpus. A contract might be a .doc file from 2009, or a .msg thread with three attachments. A signature page is often a scan glued inside an otherwise digital PDF. Generic extraction breaks the reading order on multi-column exhibits and loses the thread on nested email attachments.
how-txtfetch-solves-it
txtfetch reads .doc, .msg, and .eml threads, attachments included, plus scanned signature pages inside a digital PDF. It extracts in reading order for most PDFs, and /fixes/columns-out-of-order covers the multi-column exhibits that still need a check. It does not classify clauses and it does not redact text. Pricing is per document, not per page, which matters when a contract runs to 300 pages.
- One endpoint reads .doc, .msg, and .eml threads with attachments.
- Multi-column exhibits can still interleave, and /fixes/columns-out-of-order covers how to check.
- OCR runs automatically on scanned pages inside an otherwise digital-native PDF.
- Per-document pricing means a 300-page contract costs the same as a one-pager.
- Async job and webhook mode handles large discovery batches.
- The response text keeps attachment boundaries clear, so one email thread never blends into the next.
- The same endpoint reads a native .docx exhibit and a scanned signature page with no format check on your side.
curl
```curl
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@contract.doc
```
Python
```python
import os
import requests
with open("contract.doc", "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
```javascript
import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("contract.doc")]);
const form = new FormData();
form.append("file", file, "contract.doc");
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
```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("contract.doc")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "contract.doc")
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)
}
```
```
{
"status": "success",
"extracted_text": "..."
}
```
faq
**Does txtfetch classify contract clauses?**: No. txtfetch returns the contract as plain text. Clause classification and redaction are up to your own tooling.
**Can txtfetch read an .msg or .eml email thread with attachments?**: Yes. It extracts the message body, headers, and each attachment's text in one call.
**How does per-document pricing help with long contracts?**: A 300-page contract costs the same as a one-page NDA, so long exhibits don't multiply your bill.
related-reading
- [Per-document vs per-page pricing →](https://txtfetch.com/blog/per-document-vs-per-page-pricing)
- [Extract text from legacy .doc/.xls/.ppt →](https://txtfetch.com/extract/legacy-office)
- [Extract text from .msg email →](https://txtfetch.com/extract/msg)
- [Extract text from .eml email →](https://txtfetch.com/extract/email)
- [Extract text from a scanned PDF →](https://txtfetch.com/extract/scanned-pdf)
- [Fix: columns out of order →](https://txtfetch.com/fixes/columns-out-of-order)
- [See how txtfetch compares →](https://txtfetch.com/compare/per-page-pricing)
- [Get an API key →](https://app.txtfetch.com/signup)
other-solutions
- [RAG & LLM ingestion →](https://txtfetch.com/solutions/rag-ingestion)
- [Search indexing →](https://txtfetch.com/solutions/search-indexing)
- [Document workflows & automation →](https://txtfetch.com/solutions/document-workflows)
- [Invoice & receipt processing →](https://txtfetch.com/solutions/invoice-and-receipt-processing)
- [Resume & CV parsing →](https://txtfetch.com/solutions/resume-and-cv-parsing)
- [Research & academic papers →](https://txtfetch.com/solutions/research-and-academic-papers)
## Start on the free plan.
Run your own documents through it before you commit. The Hobby plan needs no card.
[Get started →](https://app.txtfetch.com/signup)
[See the pricing →](https://txtfetch.com/pricing)
---
# https://txtfetch.com/solutions/document-workflows
# Turn incoming documents into structured action.
Invoices, inbound email, and scanned forms all start the same way. Extract the text first, then classify, route, or act on it downstream.
the-problem
Document-driven workflows share the same first step: turn an incoming file into text before any business logic runs. That step is usually the least reliable part of the pipeline. The inputs are the least controlled: a photographed form, a forwarded email thread, a scan from a fax gateway. Stitching together OCR, a mail parser, and a document library often breaks on the one case nobody tested.
how-txtfetch-solves-it
txtfetch is the extraction step, not a workflow engine. POST the incoming file, or its URL, and get back plain text, whatever the source. Async mode with a webhook\_url means the workflow never blocks on OCR. The callback fires when the text is ready, and your automation, a queue worker, or a Lambda, picks up from there.
- OCR runs automatically on scanned forms and photographed documents, with no separate vision service to wire in.
- Email extraction (.eml/.msg/.mbox) includes headers and attachment text, useful for routing rules.
- Webhook callbacks (webhook\_url) mean a long-running OCR job never holds a workflow connection open.
- Webhook payloads are HMAC-signed and verifiable with the SDK's verifyWebhook or verify\_webhook helper.
- Idempotency-Key support stops a retried webhook delivery from reprocessing the same document twice.
- The same endpoint handles a PDF, a scanned form, and an email thread with no branch in your workflow code.
- A queue worker, a Lambda function, or a Zapier automation can each pick up the extracted text the same way.
curl
```curl
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@scanned-form.png
```
Python
```python
import os
import requests
with open("scanned-form.png", "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
```javascript
import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("scanned-form.png")]);
const form = new FormData();
form.append("file", file, "scanned-form.png");
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
```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("scanned-form.png")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "scanned-form.png")
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)
}
```
```
{
"status": "success",
"extracted_text": "..."
}
```
faq
**Can txtfetch OCR a photographed form for an automation pipeline?**: Yes. POST the image (PNG/JPG/TIFF) to /v1/extract, and Tesseract OCR runs automatically. The response is the same { status, extracted_text } shape as any other format.
**How do I avoid blocking a workflow on a slow OCR job?**: Pass async=true and supply a webhook_url. txtfetch calls it back with the result when extraction finishes, so the workflow can move on.
**Are webhook deliveries verifiable, so I know they came from txtfetch?**: Yes. Each webhook is HMAC-SHA256 signed. Verify it with the SDK's verifyWebhook (JS) or verify_webhook (Python) helper. You can also reproduce the raw HMAC check documented at /docs/async.
related-reading
- [OCR scanned documents through one API →](https://txtfetch.com/blog/ocr-scanned-documents-api)
- [Parsing Office docs into clean text →](https://txtfetch.com/blog/parse-office-docs-docx-pptx-xlsx-for-llms)
- [Batch and large-document ingestion →](https://txtfetch.com/blog/batch-and-large-document-ingestion)
- [Async jobs & webhooks →](https://txtfetch.com/docs/async)
- [Error reference →](https://txtfetch.com/docs/errors)
- [Wire this into a Zap with no txtfetch app →](https://txtfetch.com/integrations/zapier)
- [Get an API key →](https://app.txtfetch.com/signup)
other-solutions
- [RAG & LLM ingestion →](https://txtfetch.com/solutions/rag-ingestion)
- [Search indexing →](https://txtfetch.com/solutions/search-indexing)
- [Invoice & receipt processing →](https://txtfetch.com/solutions/invoice-and-receipt-processing)
- [Contract & legal review →](https://txtfetch.com/solutions/contract-and-legal-review)
- [Resume & CV parsing →](https://txtfetch.com/solutions/resume-and-cv-parsing)
- [Research & academic papers →](https://txtfetch.com/solutions/research-and-academic-papers)
## Start on the free plan.
Run your own documents through it before you commit. The Hobby plan needs no card.
[Get started →](https://app.txtfetch.com/signup)
[See the pricing →](https://txtfetch.com/pricing)
---
# https://txtfetch.com/solutions/invoice-and-receipt-processing
# Turn invoices and receipts into text your AP tools can use.
Inbound invoices arrive as PDFs, phone photos, scans, spreadsheets, and email attachments. txtfetch turns each one into text and table rows for your own rules or an LLM to parse.
the-problem
Accounts payable teams receive invoices in every shape. A vendor emails a PDF, a contractor texts a photo, a supplier uploads a scan to a portal. Each format needs its own reader before any approval logic can run. Teams that only handle PDFs miss the photos and scans, and those invoices sit in a manual queue.
how-txtfetch-solves-it
txtfetch reads the invoice, whatever the format, and returns the text and the table rows. It does not return a typed total field or a typed vendor field. Pair it with your own rules or an LLM to pull those values out. Pass format=markdown on the same request to keep the line-item table intact for that step.
- OCR runs automatically on photographed and scanned invoices, with no separate vision service.
- format=markdown keeps the line-item table intact for your parser or LLM prompt.
- One endpoint reads a vendor's PDF, a phone photo of a receipt, a spreadsheet, and an email attachment.
- Async job and webhook mode keeps large batch runs from blocking approval queues.
- Idempotency-Key support stops a retried upload from double-processing an invoice.
- The response includes the text and the table rows, ready for your own totals logic.
curl
```curl
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@invoice.pdf
```
Python
```python
import os
import requests
with open("invoice.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
```javascript
import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("invoice.pdf")]);
const form = new FormData();
form.append("file", file, "invoice.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
```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("invoice.pdf")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "invoice.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)
}
```
```
{
"status": "success",
"extracted_text": "..."
}
```
faq
**Does txtfetch return a total or vendor field from an invoice?**: No. It returns the invoice as text and table rows. Pull a typed total or vendor field with your own rules or an LLM.
**Can txtfetch read a photographed invoice from a phone?**: Yes. OCR runs automatically on the photo, and the response has the same shape as a digital-native PDF.
**How do I keep the line-item table readable for my parser?**: Add format=markdown to the request. The table rows stay intact instead of flattening into one block of text.
related-reading
- [Extract tables for RAG →](https://txtfetch.com/blog/extract-tables-for-rag)
- [OCR scanned documents through one API →](https://txtfetch.com/blog/ocr-scanned-documents-api)
- [Extract text from PDF →](https://txtfetch.com/extract/pdf)
- [Extract text from a scanned PDF →](https://txtfetch.com/extract/scanned-pdf)
- [OCR a photographed or scanned image →](https://txtfetch.com/extract/image)
- [Text vs Markdown vs element JSON, compared →](https://txtfetch.com/output)
- [Get an API key →](https://app.txtfetch.com/signup)
other-solutions
- [RAG & LLM ingestion →](https://txtfetch.com/solutions/rag-ingestion)
- [Search indexing →](https://txtfetch.com/solutions/search-indexing)
- [Document workflows & automation →](https://txtfetch.com/solutions/document-workflows)
- [Contract & legal review →](https://txtfetch.com/solutions/contract-and-legal-review)
- [Resume & CV parsing →](https://txtfetch.com/solutions/resume-and-cv-parsing)
- [Research & academic papers →](https://txtfetch.com/solutions/research-and-academic-papers)
## Start on the free plan.
Run your own documents through it before you commit. The Hobby plan needs no card.
[Get started →](https://app.txtfetch.com/signup)
[See the pricing →](https://txtfetch.com/pricing)
---
# https://txtfetch.com/solutions/rag-ingestion
# Feed your RAG pipeline clean text, not parser output.
Users upload PDFs, DOCX files, and scanned contracts. txtfetch turns each one into plain text your chunker and embedding model can use right away.
the-problem
RAG pipelines live or die on what enters the vector store. Most ingestion code spends more time on format detection than on chunking. A production knowledge base needs a PDF library, an Office parser, and an OCR fallback for scans. Each new upload format is one more parser to maintain, and one more way retrieval quality can degrade.
how-txtfetch-solves-it
txtfetch collapses that into one API call. PDF, Office file, scanned image, or a URL: the response always has the same shape. Pass that string straight to your chunker. Text-layer pages route through Apache Tika. Pages with no text layer route through Tesseract OCR automatically, in the same request. A batch of mixed digital and scanned documents needs no branching logic on your side. Add format=markdown to the same request when headings and tables need to survive the splitter.
- Every source format returns the same response shape, so no per-parser branch runs before chunking.
- OCR runs automatically on scanned pages inside an otherwise-digital PDF batch.
- Official LangChain and LlamaIndex loaders drop into an existing splitter and embedder pipeline.
- Pass a URL instead of downloading first, and txtfetch fetches the document server-side.
- Async job and webhook mode keeps large batches from blocking on slow OCR.
- Idempotency-Key support stops a retried ingestion job from re-embedding the same document.
- Add format=markdown to the same request to keep headings and tables intact for the splitter.
curl
```curl
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@whitepaper.pdf
```
Python
```python
import os
import requests
with open("whitepaper.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
```javascript
import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("whitepaper.pdf")]);
const form = new FormData();
form.append("file", file, "whitepaper.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
```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("whitepaper.pdf")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "whitepaper.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)
}
```
```
{
"status": "success",
"extracted_text": "..."
}
```
faq
**Does txtfetch work with LangChain or LlamaIndex?**: Yes. langchain-txtfetch (Python) and @txtfetch/langchain (JS) are official document loaders, and llama-index-readers-txtfetch is an official LlamaIndex reader. Each wraps the extract API and returns Document objects ready for your text splitter.
**What does txtfetch return for a scanned PDF in a RAG pipeline?**: The same { status, extracted_text } shape as a digital-native PDF. Pages with no text layer are OCR'd via Tesseract automatically, so your chunker doesn't need to know which pages were scanned.
**Can I ingest a document directly from a URL instead of downloading it first?**: Yes. Pass a url parameter and txtfetch fetches the document server-side, the same code path the LangChain and LlamaIndex loaders use for their urls= argument.
related-reading
- [Extract text from a PDF for RAG →](https://txtfetch.com/blog/extract-text-from-pdf-for-rag)
- [Chunking strategies for RAG →](https://txtfetch.com/blog/chunking-strategies-for-rag)
- [Extract tables for RAG →](https://txtfetch.com/blog/extract-tables-for-rag)
- [The LangChain & LlamaIndex document loader →](https://txtfetch.com/blog/langchain-llamaindex-document-loader)
- [RAG recipe: chunk → embed → index →](https://txtfetch.com/docs/recipe)
- [SDK & framework quickstarts →](https://txtfetch.com/docs/quickstarts)
- [Extract text into a vector store →](https://txtfetch.com/ingest)
- [Chunk previewer: test your own extracted text →](https://txtfetch.com/tools/chunk-preview)
- [Text vs Markdown vs element JSON, compared →](https://txtfetch.com/output)
- [See how txtfetch compares →](https://txtfetch.com/compare)
- [Get an API key →](https://app.txtfetch.com/signup)
other-solutions
- [Search indexing →](https://txtfetch.com/solutions/search-indexing)
- [Document workflows & automation →](https://txtfetch.com/solutions/document-workflows)
- [Invoice & receipt processing →](https://txtfetch.com/solutions/invoice-and-receipt-processing)
- [Contract & legal review →](https://txtfetch.com/solutions/contract-and-legal-review)
- [Resume & CV parsing →](https://txtfetch.com/solutions/resume-and-cv-parsing)
- [Research & academic papers →](https://txtfetch.com/solutions/research-and-academic-papers)
## Start on the free plan.
Run your own documents through it before you commit. The Hobby plan needs no card.
[Get started →](https://app.txtfetch.com/signup)
[See the pricing →](https://txtfetch.com/pricing)
---
# https://txtfetch.com/solutions/research-and-academic-papers
# Extract text from papers, EPUBs, and old archive scans.
Research corpora run PDF-heavy, with EPUB references and scanned TIFF from older archives. txtfetch reads all three, and two-column layouts still need a second look.
the-problem
Literature review tools and archive digitisation projects live on PDF. Older archives add EPUB and scanned TIFF, often the only surviving copy of a paper. A two-column layout reads out of order when a parser flattens it into one text stream. Ligatures and hyphenated line breaks turn a common word into two broken tokens.
how-txtfetch-solves-it
txtfetch reads PDF, EPUB, and scanned TIFF through the same endpoint. It returns text, not typed citation or metadata fields. OCR runs automatically on TIFF pages and on PDF pages with no text layer. Two-column layouts, ligatures, and hyphenated line breaks are real problems. /fixes covers how to check for and correct each one.
- One endpoint reads PDF, EPUB, and scanned TIFF.
- OCR runs automatically on TIFF pages and on PDF pages with no text layer.
- Two-column layouts can come back interleaved, and /fixes/columns-out-of-order covers the check.
- /fixes documents ligature and hyphenated-line-break repair for this corpus.
- Async job and webhook mode covers digitising a large archive.
- A batch of mixed PDF, EPUB, and scanned TIFF files needs no branching logic on your side.
curl
```curl
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@paper.pdf
```
Python
```python
import os
import requests
with open("paper.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
```javascript
import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("paper.pdf")]);
const form = new FormData();
form.append("file", file, "paper.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
```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("paper.pdf")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "paper.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)
}
```
```
{
"status": "success",
"extracted_text": "..."
}
```
faq
**Does txtfetch fix two-column reading order automatically?**: It extracts in reading order for most PDFs. /fixes/columns-out-of-order covers the cases that still need a check.
**Can txtfetch read a scanned TIFF from an old archive?**: Yes. OCR runs automatically on TIFF pages, the same as a scanned PDF page.
**Does txtfetch handle EPUB files?**: Yes. EPUB is one of the formats read through the same endpoint as PDF and TIFF.
related-reading
- [Extract text from PDF →](https://txtfetch.com/extract/pdf)
- [Extract text from EPUB →](https://txtfetch.com/extract/epub)
- [Extract text from TIFF →](https://txtfetch.com/extract/tiff)
- [Fix: columns out of order →](https://txtfetch.com/fixes/columns-out-of-order)
- [Fix: ligatures and smart punctuation →](https://txtfetch.com/fixes/ligatures-and-smart-punctuation)
- [Fix: hyphenated line breaks →](https://txtfetch.com/fixes/hyphenated-line-breaks)
- [Glossary: reading order →](https://txtfetch.com/glossary/reading-order)
- [Get an API key →](https://app.txtfetch.com/signup)
other-solutions
- [RAG & LLM ingestion →](https://txtfetch.com/solutions/rag-ingestion)
- [Search indexing →](https://txtfetch.com/solutions/search-indexing)
- [Document workflows & automation →](https://txtfetch.com/solutions/document-workflows)
- [Invoice & receipt processing →](https://txtfetch.com/solutions/invoice-and-receipt-processing)
- [Contract & legal review →](https://txtfetch.com/solutions/contract-and-legal-review)
- [Resume & CV parsing →](https://txtfetch.com/solutions/resume-and-cv-parsing)
## Start on the free plan.
Run your own documents through it before you commit. The Hobby plan needs no card.
[Get started →](https://app.txtfetch.com/signup)
[See the pricing →](https://txtfetch.com/pricing)
---
# https://txtfetch.com/solutions/resume-and-cv-parsing
# Turn any résumé format into text your ATS can search.
Candidates upload whatever they have: .docx, .pdf, .doc, .odt, .rtf, and sometimes a design-tool PDF with no text layer. txtfetch turns every one of them into plain text.
the-problem
Applicant tracking systems accept whatever a candidate uploads. Most resumes arrive as .docx or .pdf, but some come as .doc, .odt, or .rtf. A design-tool export sometimes has no text layer at all, just an image of the page. A parser tuned for one format silently drops every candidate who used another.
how-txtfetch-solves-it
txtfetch returns the résumé as plain text, whatever the source format. It does not return a typed name, skill, or date field. Feed that text to your own parser or an LLM to pull structured fields out. A design-tool PDF with no text layer still works, because OCR runs automatically.
- One endpoint reads .docx, .pdf, .doc, .odt, and .rtf.
- OCR runs automatically on a design-tool PDF with no text layer.
- The response is plain text, ready for your own parser or LLM to structure.
- Async job and webhook mode covers bulk résumé imports.
- Idempotency-Key support stops a retried upload from reprocessing the same résumé.
- A batch of mixed .docx, .pdf, and scanned résumés needs no branching logic on your side.
curl
```curl
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@resume.docx
```
Python
```python
import os
import requests
with open("resume.docx", "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
```javascript
import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("resume.docx")]);
const form = new FormData();
form.append("file", file, "resume.docx");
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
```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("resume.docx")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "resume.docx")
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)
}
```
```
{
"status": "success",
"extracted_text": "..."
}
```
faq
**Does txtfetch extract structured fields like name or skills from a résumé?**: No. It returns the résumé as plain text. Pull structured fields with your own parser or an LLM.
**What happens with a résumé exported from a design tool with no text layer?**: OCR runs automatically, the same as any scanned page, so you still get text back.
**Which resume file formats does txtfetch read?**: docx, pdf, doc, odt, and rtf, all through the same endpoint.
related-reading
- [Extract text from Word & Office →](https://txtfetch.com/extract/docx)
- [Extract text from legacy .doc →](https://txtfetch.com/extract/legacy-office)
- [Extract text from RTF →](https://txtfetch.com/extract/rtf)
- [Extract text from OpenDocument (.odt) →](https://txtfetch.com/extract/odf)
- [Any file to text, in the browser →](https://txtfetch.com/tools/file-to-text)
- [Fix: Office text missing →](https://txtfetch.com/fixes/office-text-missing)
- [Get an API key →](https://app.txtfetch.com/signup)
other-solutions
- [RAG & LLM ingestion →](https://txtfetch.com/solutions/rag-ingestion)
- [Search indexing →](https://txtfetch.com/solutions/search-indexing)
- [Document workflows & automation →](https://txtfetch.com/solutions/document-workflows)
- [Invoice & receipt processing →](https://txtfetch.com/solutions/invoice-and-receipt-processing)
- [Contract & legal review →](https://txtfetch.com/solutions/contract-and-legal-review)
- [Research & academic papers →](https://txtfetch.com/solutions/research-and-academic-papers)
## Start on the free plan.
Run your own documents through it before you commit. The Hobby plan needs no card.
[Get started →](https://app.txtfetch.com/signup)
[See the pricing →](https://txtfetch.com/pricing)
---
# https://txtfetch.com/solutions/search-indexing
# Every file in your document estate, made searchable.
Contracts, decks, spreadsheets, and scanned forms all land in the same folder. One extraction call turns each into indexable plain text.
the-problem
A search index is only as complete as the pipeline that feeds it. Document estates are never one format. A shared drive holds PDFs, Word docs, Excel exports, and scanned paperwork someone photographed on a phone. Teams often build separate handling for each format. Files outside that set never reach the index, and they become a blind spot.
how-txtfetch-solves-it
txtfetch gives every file type the same code path. POST it, or point at its URL, and get back plain text. Hand that text to your indexer's bulk API: Elasticsearch, OpenSearch, Algolia, or Meilisearch. Scanned pages route through OCR automatically. A folder of PDFs, Office files, and photographed forms indexes through one loop instead of three.
- One extraction call covers every source format, so no per-format indexer feed is needed.
- OCR runs automatically on scanned and photographed documents, in the same request shape as any other format.
- Pass a URL to index a linked or attached document without a separate download step.
- Async job mode with a webhook callback indexes large batches without holding a connection open.
- Idempotency-Key support stops a retried indexing job from double-indexing a document.
- The response text drops straight into Elasticsearch, OpenSearch, Algolia, or Meilisearch.
- The response is plain JSON, easy to script into a nightly re-index job.
curl
```curl
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/report.docx" \
-H "Authorization: Bearer $TXTFETCH_KEY"
```
Python
```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/report.docx"},
)
print(r.json()["extracted_text"])
```
JavaScript
```javascript
const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", "https://example.com/report.docx");
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
```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/report.docx")
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": "..."
}
```
faq
**Can txtfetch feed an Elasticsearch or OpenSearch index?**: Yes. POST each document, or its URL, to /v1/extract. Hand the returned extracted_text to your indexer's bulk API. txtfetch doesn't talk to the index directly; it just gives you clean text to index.
**What happens to scanned documents in a search-indexing pipeline?**: They're OCR'd automatically via Tesseract, same request and response shape as a digital-native file, so scanned paperwork ends up searchable alongside everything else.
**How do I index a large batch without blocking on slow OCR jobs?**: Pass async=true, or use the SDK's extractAsync/extract_async. Poll the job, or supply a webhook_url. The batch submits right away, and results arrive as they complete.
related-reading
- [Parsing Office docs into clean text →](https://txtfetch.com/blog/parse-office-docs-docx-pptx-xlsx-for-llms)
- [OCR scanned documents through one API →](https://txtfetch.com/blog/ocr-scanned-documents-api)
- [Batch and large-document ingestion →](https://txtfetch.com/blog/batch-and-large-document-ingestion)
- [API quickstart →](https://txtfetch.com/docs)
- [Async jobs & webhooks →](https://txtfetch.com/docs/async)
- [Get an API key →](https://app.txtfetch.com/signup)
other-solutions
- [RAG & LLM ingestion →](https://txtfetch.com/solutions/rag-ingestion)
- [Document workflows & automation →](https://txtfetch.com/solutions/document-workflows)
- [Invoice & receipt processing →](https://txtfetch.com/solutions/invoice-and-receipt-processing)
- [Contract & legal review →](https://txtfetch.com/solutions/contract-and-legal-review)
- [Resume & CV parsing →](https://txtfetch.com/solutions/resume-and-cv-parsing)
- [Research & academic papers →](https://txtfetch.com/solutions/research-and-academic-papers)
## Start on the free plan.
Run your own documents through it before you commit. The Hobby plan needs no card.
[Get started →](https://app.txtfetch.com/signup)
[See the pricing →](https://txtfetch.com/pricing)
---
# https://txtfetch.com/sources
# Your documents don't live on a local disk. Here's how txtfetch reaches them.
Every store below answers one question. Can it hand txtfetch a URL with no Authorization header, or do you send the bytes yourself?
## One fact settles every row
txtfetch's `?url=` parameter fetches a document server-side, but it sends no custom headers of its own. A store that can mint a URL needing no `Authorization` header lets `?url=` reach it directly. A store that can't means you fetch the bytes with your own credentials, then POST them to txtfetch instead.
Nothing below is a benchmark of these stores. It's a map of one narrow, practical question: which path gets a document from each one into txtfetch.
the decision table
| Store | Pre-authenticated URL? | txtfetch path | Link lifetime | The trap |
| --- | --- | --- | --- | --- |
| Google Drive | No — Drive requires a Bearer header on every download | POST the bytes | N/A — no public link ever exists | A native Google Doc, Sheet, or Slide has no raw bytes. Export it first, and the export tops out at 10 MB. |
| SharePoint & OneDrive | Yes — @microsoft.graph.downloadUrl needs no header | ?url= | Minutes — Microsoft warns it can expire fast | The URL might expire within minutes. Fetch it right before the txtfetch call, never from a queue or a retry. |
| Amazon S3 | Yes — a presigned GET URL | ?url= | Your choice, set when you sign it | Set the expiry longer than the async job can take, or a slow OCR pass outlives the link. |
| Dropbox | Yes — the returned temporary link | ?url= | About four hours, then 410 Gone | Refreshing the access token that generated the link can cut a live link short. |
| Confluence | No — none is documented | POST the bytes | Not documented, and not a signed expiry | The download path has already moved once, from the v1 \_links.download field to v2's downloadLink. Check the current API reference before you depend on either. |
| Email inbox | No — a mailbox has no download URL at all | POST the bytes | N/A | The Gmail API's raw field is base64url-encoded text, not a file. Decode it before you write the .eml file, or txtfetch reads garbage. |
Full citations, dated, live on each spoke below. See [the error reference](https://txtfetch.com/docs/errors) for what a failed fetch actually returns.
## What `?url=` can and can't do
- It sends no custom headers. There's no way to pass an `Authorization` header on the fetch itself.
- It rejects credentials written into the URL, like `https://user:pass@host`. A blocked address fails the same way.
- It only fetches over `http` and `https`, on the default web ports.
- It follows a redirect chain, re-checking every hop against the same rules, so a redirect can't smuggle past a header or scheme limit.
- It runs under a fixed time and size budget. A source that's slow or huge fails the same way a broken link does, with a typed `fetch_failed` error.
See [the error reference](https://txtfetch.com/docs/errors) for every code `?url=` can return, and [async jobs & webhooks](https://txtfetch.com/docs/async) for how a slow source stays inside that budget.
## No connector ships for any of them
txtfetch ships no plugin or connector for Google Drive, SharePoint, OneDrive, S3, Dropbox, Confluence, or a mailbox. Each spoke below shows the whole path: fetch the document with the store's own API, then hand it to txtfetch. Already running on a no-code platform? See [integrations](https://txtfetch.com/integrations) for the platform side of the same wiring. For where the extracted text goes next, see [ingest into a vector store](https://txtfetch.com/ingest).
pick a source
**[Google Drive](https://txtfetch.com/sources/google-drive)**
A file that needs a header on every download, and an export step for anything native.
POST the bytes
**[SharePoint & OneDrive](https://txtfetch.com/sources/sharepoint-onedrive)**
The download URL that needs no Authorization header at all.
?url= works
**[Amazon S3](https://txtfetch.com/sources/amazon-s3)**
Credentials that ride in the query string, not blocked by the SSRF guard.
?url= works
**[Dropbox](https://txtfetch.com/sources/dropbox)**
A four-hour link, minted by a token that never leaves your side.
?url= works
**[Confluence](https://txtfetch.com/sources/confluence)**
A download link Atlassian never promises will work without your credentials.
POST the bytes
**[Email inbox](https://txtfetch.com/sources/email-inbox)**
The whole message, not a link, since a private inbox has no download URL.
POST the bytes
frequently asked questions
**Does txtfetch connect directly to Google Drive, SharePoint, OneDrive, S3, Dropbox, Confluence, or a mailbox?**: No. txtfetch ships no connector or plugin for any of the six. Fetch the document yourself, using the store's own API, then send it to txtfetch as a URL or a file.
**How do I know if a store needs ?url= or a file upload?**: Check whether the store can hand you a link with no Authorization header. SharePoint, OneDrive, S3, and Dropbox can. Google Drive, Confluence, and a mailbox can't, so you POST the bytes instead.
**Why does ?url= work for some stores and not others?**: ?url= sends no custom headers of its own. A store that needs an Authorization header on every download can't be reached that way, no matter how the link looks.
**Does txtfetch benchmark accuracy or speed for any of these sources?**: No. None of these six is a text-extraction service, so there's no such comparison to make here. See /benchmarks for txtfetch's own measured extraction numbers.
## Point it at your source files.
Pass a signed URL and the text comes back. There is no connector to install.
[Get an API key →](https://app.txtfetch.com/signup)
[See the URL docs →](https://txtfetch.com/docs)
---
# https://txtfetch.com/sources/amazon-s3
# Extract text from Amazon S3
Credentials that ride in the query string, not blocked by the SSRF guard. Here's how a document actually gets from Amazon S3 to a txtfetch response.
## The problem
S3 objects are private by default, so a plain object URL returns Access Denied. A presigned URL grants time-limited access instead, using the credentials of whoever generated it. No password ever appears in the URL itself.
?url=, and why
| Pre-authenticated URL? | Yes — a presigned GET URL |
| --- | --- |
| txtfetch path | ?url= |
| Link lifetime | Your choice, set when you sign it |
| The trap | Set the expiry longer than the async job can take, or a slow OCR pass outlives the link. |
A presigned GET URL carries its credentials as query parameters. txtfetch's SSRF guard blocks credentials in a URL's userinfo, not its query string, so ?url= reaches it.
## How it works
Generate a presigned GET URL for the object, with an expiry longer than the extraction can take. Pass that URL straight to txtfetch's ?url= parameter, the same as any other link. The S3-and-Lambda integration guide below wires the same call to an object-created event, so an upload triggers the extraction on its own.
the script
A plain HTTP call against Amazon S3's own REST API, then a plain call to txtfetch. No vendor SDK either side.
Python
```python
import os
import requests
# Generate this with your own AWS credentials — see /integrations/aws-s3-lambda
# for the event-driven version. Set the expiry longer than the job can take.
presigned_url = "https://my-bucket.s3.amazonaws.com/reports/q3.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=3600&X-Amz-Signature=REPLACE_ME"
r = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
params={"url": presigned_url},
)
print(r.json()["extracted_text"])
```
JavaScript
```javascript
// Generate this with your own AWS credentials — see /integrations/aws-s3-lambda
// for the event-driven version. Set the expiry longer than the job can take.
const presignedUrl =
"https://my-bucket.s3.amazonaws.com/reports/q3.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=3600&X-Amz-Signature=REPLACE_ME";
const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", presignedUrl);
const res = await fetch(endpoint, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` },
});
const { extracted_text } = await res.json();
console.log(extracted_text);
```
txtfetch ships no connector, plugin, or client for Amazon S3. The script above is the whole integration. Fetch the document with Amazon S3's own API, then hand it to txtfetch, the same as any other source of text.
frequently asked questions
**Does txtfetch have an S3 connector?**: No. txtfetch ships no S3 connector or Lambda layer. A presigned URL is already a plain link, so ?url= reaches the object with no extra wiring.
**Isn't a credential in the URL a security risk?**: txtfetch's guard blocks credentials in a URL's userinfo, like https://user:pass@host. A presigned URL's signature lives in the query string instead, which the guard allows.
**What happens if the presigned URL expires mid-request?**: The fetch fails with fetch_failed, the same as any other broken link. Sign the URL for longer than the extraction, especially before an OCR-heavy scan.
sources
- [AWS: Share objects with presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html) Accessed 2026-09
## Related
- [Extract text from S3 uploads with Lambda](https://txtfetch.com/integrations/aws-s3-lambda), the same presigned-URL call, triggered by an object-created event.
- [Integrations](https://txtfetch.com/integrations), for wiring this into a no-code automation platform instead of a script.
- [Ingest into a vector store](https://txtfetch.com/ingest), for where the extracted text goes next.
- [Async jobs & webhooks](https://txtfetch.com/docs/async), for a source that's large or slow to fetch.
- [Error reference](https://txtfetch.com/docs/errors), for what a failed `?url=` fetch returns.
other sources
- [Extract text from Google Drive →](https://txtfetch.com/sources/google-drive)
- [Extract text from SharePoint & OneDrive →](https://txtfetch.com/sources/sharepoint-onedrive)
- [Extract text from Dropbox →](https://txtfetch.com/sources/dropbox)
- [Extract text from Confluence →](https://txtfetch.com/sources/confluence)
- [Extract text from Email inbox →](https://txtfetch.com/sources/email-inbox)
- [All sources →](https://txtfetch.com/sources)
## Point it at your Amazon S3 files.
Pass a signed URL and the text comes back. There is no connector to install.
[Get an API key →](https://app.txtfetch.com/signup)
[See the URL docs →](https://txtfetch.com/docs)
---
# https://txtfetch.com/sources/confluence
# Extract text from Confluence
A download link Atlassian never promises will work without your credentials. Here's how a document actually gets from Confluence to a txtfetch response.
## The problem
Confluence's v2 API returns an attachment's downloadLink alongside its metadata. Atlassian never documents that link as pre-authenticated, unlike Microsoft Graph's download URL. Absent that promise, assume the link wants your credentials. Fetch the bytes yourself and send those to txtfetch.
POST the bytes, and why
| Pre-authenticated URL? | No — none is documented |
| --- | --- |
| txtfetch path | POST the bytes |
| Link lifetime | Not documented, and not a signed expiry |
| The trap | The download path has already moved once, from the v1 \_links.download field to v2's downloadLink. Check the current API reference before you depend on either. |
Atlassian documents no pre-authenticated download URL for an attachment. Microsoft and Dropbox both promise one in writing; Confluence does not, so treat the link as needing your credentials and POST the bytes.
## How it works
Request the attachment with GET /wiki/api/v2/attachments/{id} and your own API token. Read downloadLink from the response. Confluence Cloud returns it as a relative path today, so resolve it against your site's /wiki base. Fetch it with the same token, then POST the bytes to txtfetch as a normal file upload.
the script
A plain HTTP call against Confluence's own REST API, then a plain call to txtfetch. No vendor SDK either side.
Python
```python
import os
import requests
BASE_URL = "https://your-domain.atlassian.net/wiki"
ATTACHMENT_ID = "att123456789"
auth = (os.environ["CONFLUENCE_EMAIL"], os.environ["CONFLUENCE_API_TOKEN"])
attachment = requests.get(f"{BASE_URL}/api/v2/attachments/{ATTACHMENT_ID}", auth=auth)
attachment.raise_for_status()
download_link = attachment.json()["downloadLink"]
# Confluence Cloud returns downloadLink as a relative path today. Resolve it
# against the /wiki base, and pass it through if it ever arrives absolute.
file_url = download_link if download_link.startswith("http") else f"{BASE_URL}{download_link}"
file_res = requests.get(file_url, auth=auth)
file_res.raise_for_status()
r = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
files={"file": (attachment.json()["title"], file_res.content)},
)
print(r.json()["extracted_text"])
```
JavaScript
```javascript
const baseUrl = "https://your-domain.atlassian.net/wiki";
const attachmentId = "att123456789";
const auth = "Basic " + Buffer.from(`${process.env.CONFLUENCE_EMAIL}:${process.env.CONFLUENCE_API_TOKEN}`).toString("base64");
const attachmentRes = await fetch(`${baseUrl}/api/v2/attachments/${attachmentId}`, {
headers: { Authorization: auth },
});
const { title, downloadLink } = await attachmentRes.json();
// Confluence Cloud returns downloadLink as a relative path today. Resolve it
// against the /wiki base, and pass it through if it ever arrives absolute.
const fileUrl = downloadLink.startsWith("http") ? downloadLink : `${baseUrl}${downloadLink}`;
const fileRes = await fetch(fileUrl, { headers: { Authorization: auth } });
const bytes = await fileRes.arrayBuffer();
const form = new FormData();
form.append("file", new Blob([bytes]), title);
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);
```
txtfetch ships no connector, plugin, or client for Confluence. The script above is the whole integration. Fetch the document with Confluence's own API, then hand it to txtfetch, the same as any other source of text.
frequently asked questions
**Does txtfetch have a Confluence connector?**: No. txtfetch ships no Confluence connector. Fetch the attachment yourself with your own API token, then POST the bytes to txtfetch.
**Can I pass downloadLink straight to ?url=?**: Don't rely on it. Atlassian documents no pre-authenticated download URL for an attachment, so the link may well want your credentials. txtfetch's fetch carries none, and it sends no Authorization header.
**Why does this spoke hedge where the others don't?**: Microsoft and Dropbox both document their link's auth behaviour and lifetime. Atlassian documents neither, and the download path already changed between v1 and v2. POSTing the bytes works no matter how that settles.
sources
- [Confluence Cloud REST API v2: Get attachment by id (downloadLink)](https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-attachment/#api-attachments-id-get) Accessed 2026-09
- [Confluence Cloud REST API v1: Content — attachments (the older \_links.download shape)](https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-content---attachments/) Accessed 2026-09 directional
## Related
- [Integrations](https://txtfetch.com/integrations), for wiring this into a no-code automation platform instead of a script.
- [Ingest into a vector store](https://txtfetch.com/ingest), for where the extracted text goes next.
- [Async jobs & webhooks](https://txtfetch.com/docs/async), for a source that's large or slow to fetch.
- [Error reference](https://txtfetch.com/docs/errors), for what a failed `?url=` fetch returns.
other sources
- [Extract text from Google Drive →](https://txtfetch.com/sources/google-drive)
- [Extract text from SharePoint & OneDrive →](https://txtfetch.com/sources/sharepoint-onedrive)
- [Extract text from Amazon S3 →](https://txtfetch.com/sources/amazon-s3)
- [Extract text from Dropbox →](https://txtfetch.com/sources/dropbox)
- [Extract text from Email inbox →](https://txtfetch.com/sources/email-inbox)
- [All sources →](https://txtfetch.com/sources)
## Point it at your Confluence files.
Pass a signed URL and the text comes back. There is no connector to install.
[Get an API key →](https://app.txtfetch.com/signup)
[See the URL docs →](https://txtfetch.com/docs)
---
# https://txtfetch.com/sources/dropbox
# Extract text from Dropbox
A four-hour link, minted by a token that never leaves your side. Here's how a document actually gets from Dropbox to a txtfetch response.
## The problem
A file inside Dropbox sits behind your own account, the same as any private store. get\_temporary\_link trades that private access for a plain, unauthenticated URL, good for a few hours. That trade is what lets ?url= reach it at all.
?url=, and why
| Pre-authenticated URL? | Yes — the returned temporary link |
| --- | --- |
| txtfetch path | ?url= |
| Link lifetime | About four hours, then 410 Gone |
| The trap | Refreshing the access token that generated the link can cut a live link short. |
get\_temporary\_link returns a direct link that needs no login. Generating it needs your own access token, but only the returned URL goes to txtfetch.
## How it works
Call /2/files/get\_temporary\_link with your own access token and the file's path. The response carries a link field with a direct URL. Pass that URL to txtfetch's ?url= parameter right away, since it stops working after about four hours and then returns 410 Gone.
the script
A plain HTTP call against Dropbox's own REST API, then a plain call to txtfetch. No vendor SDK either side.
Python
```python
import os
import requests
headers = {"Authorization": f"Bearer {os.environ['DROPBOX_ACCESS_TOKEN']}"}
link = requests.post(
"https://api.dropboxapi.com/2/files/get_temporary_link",
headers={**headers, "Content-Type": "application/json"},
json={"path": "/reports/q3.pdf"},
)
link.raise_for_status()
temporary_url = link.json()["link"]
r = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
params={"url": temporary_url},
)
print(r.json()["extracted_text"])
```
JavaScript
```javascript
const dbxRes = await fetch("https://api.dropboxapi.com/2/files/get_temporary_link", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.DROPBOX_ACCESS_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ path: "/reports/q3.pdf" }),
});
const { link: temporaryUrl } = await dbxRes.json();
const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", temporaryUrl);
const res = await fetch(endpoint, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` },
});
const { extracted_text } = await res.json();
console.log(extracted_text);
```
txtfetch ships no connector, plugin, or client for Dropbox. The script above is the whole integration. Fetch the document with Dropbox's own API, then hand it to txtfetch, the same as any other source of text.
frequently asked questions
**Does txtfetch have a Dropbox connector?**: No. txtfetch ships no Dropbox connector. get_temporary_link already returns a plain URL, so ?url= reaches it with no extra step.
**Does txtfetch ever see my Dropbox access token?**: No. Your own code calls get_temporary_link and keeps the access token. Only the returned, unauthenticated link goes to txtfetch.
**Why did a link that worked an hour ago suddenly fail?**: It likely passed the four-hour mark and now returns 410 Gone. Generate a fresh link right before each txtfetch call instead of reusing an old one.
sources
- [Dropbox API: get\_temporary\_link](https://www.dropbox.com/developers/documentation/http/documentation#files-get_temporary_link) Accessed 2026-09 directional
## Related
- [Integrations](https://txtfetch.com/integrations), for wiring this into a no-code automation platform instead of a script.
- [Ingest into a vector store](https://txtfetch.com/ingest), for where the extracted text goes next.
- [Async jobs & webhooks](https://txtfetch.com/docs/async), for a source that's large or slow to fetch.
- [Error reference](https://txtfetch.com/docs/errors), for what a failed `?url=` fetch returns.
other sources
- [Extract text from Google Drive →](https://txtfetch.com/sources/google-drive)
- [Extract text from SharePoint & OneDrive →](https://txtfetch.com/sources/sharepoint-onedrive)
- [Extract text from Amazon S3 →](https://txtfetch.com/sources/amazon-s3)
- [Extract text from Confluence →](https://txtfetch.com/sources/confluence)
- [Extract text from Email inbox →](https://txtfetch.com/sources/email-inbox)
- [All sources →](https://txtfetch.com/sources)
## Point it at your Dropbox files.
Pass a signed URL and the text comes back. There is no connector to install.
[Get an API key →](https://app.txtfetch.com/signup)
[See the URL docs →](https://txtfetch.com/docs)
---
# https://txtfetch.com/sources/email-inbox
# Extract text from Email inbox
The whole message, not a link, since a private inbox has no download URL. Here's how a document actually gets from Email inbox to a txtfetch response.
## The problem
An email message lives inside a private inbox, with no equivalent of a shareable file link. Reaching it always means calling a mail API or protocol with your own credentials first. ?url= has nothing to point at until you do.
POST the bytes, and why
| Pre-authenticated URL? | No — a mailbox has no download URL at all |
| --- | --- |
| txtfetch path | POST the bytes |
| Link lifetime | N/A |
| The trap | The Gmail API's raw field is base64url-encoded text, not a file. Decode it before you write the .eml file, or txtfetch reads garbage. |
A mailbox has no public download URL for any message. Fetch the raw message yourself, then POST it to txtfetch as a file.
## How it works
Call the Gmail API's users.messages.get with format=raw, or run an IMAP FETCH BODY\[\] over a logged-in connection. Either path returns the message as a complete RFC 822 document, headers and attachments included. Decode the Gmail API's base64url raw field into bytes, then POST that .eml file to txtfetch. txtfetch parses the message body and every attachment in the same call.
the script
A plain HTTP call against Email inbox's own REST API, then a plain call to txtfetch. No vendor SDK either side.
Python
```python
import base64
import os
import requests
MESSAGE_ID = "18c8f2a1b3d4e5f6"
headers = {"Authorization": f"Bearer {os.environ['GMAIL_ACCESS_TOKEN']}"}
message = requests.get(
f"https://gmail.googleapis.com/gmail/v1/users/me/messages/{MESSAGE_ID}",
headers=headers,
params={"format": "raw"},
)
message.raise_for_status()
raw_bytes = base64.urlsafe_b64decode(message.json()["raw"])
r = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
files={"file": ("message.eml", raw_bytes)},
)
print(r.json()["extracted_text"])
```
JavaScript
```javascript
const messageId = "18c8f2a1b3d4e5f6";
const gmailRes = await fetch(
`https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}?format=raw`,
{ headers: { Authorization: `Bearer ${process.env.GMAIL_ACCESS_TOKEN}` } },
);
const { raw } = await gmailRes.json();
const rawBytes = Buffer.from(raw, "base64url");
const form = new FormData();
form.append("file", new Blob([rawBytes]), "message.eml");
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);
```
txtfetch ships no connector, plugin, or client for Email inbox. The script above is the whole integration. Fetch the document with Email inbox's own API, then hand it to txtfetch, the same as any other source of text.
frequently asked questions
**Does txtfetch have a Gmail or Outlook connector?**: No. txtfetch ships no mailbox connector for any provider. Fetch the raw message with the provider's own API, then POST it to txtfetch as a file.
**Can txtfetch read my inbox directly?**: No. txtfetch never holds mailbox credentials of any kind. Your own code authenticates, fetches the message, and sends only that message's bytes.
**Does txtfetch see the message's attachments too?**: Yes. An RFC 822 .eml file carries its attachments inline, and txtfetch parses the message body and every attachment in the same request.
sources
- [Gmail API: users.messages.get](https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.messages/get) Accessed 2026-09 directional
## Related
- [Extract text from email](https://txtfetch.com/extract/email), what txtfetch pulls out of an .eml, .msg, or .mbox file.
- [Free message-to-text converter](https://txtfetch.com/tools/email-to-text), drop a saved message in your browser, with nothing uploaded.
- [Integrations](https://txtfetch.com/integrations), for wiring this into a no-code automation platform instead of a script.
- [Ingest into a vector store](https://txtfetch.com/ingest), for where the extracted text goes next.
- [Async jobs & webhooks](https://txtfetch.com/docs/async), for a source that's large or slow to fetch.
- [Error reference](https://txtfetch.com/docs/errors), for what a failed `?url=` fetch returns.
other sources
- [Extract text from Google Drive →](https://txtfetch.com/sources/google-drive)
- [Extract text from SharePoint & OneDrive →](https://txtfetch.com/sources/sharepoint-onedrive)
- [Extract text from Amazon S3 →](https://txtfetch.com/sources/amazon-s3)
- [Extract text from Dropbox →](https://txtfetch.com/sources/dropbox)
- [Extract text from Confluence →](https://txtfetch.com/sources/confluence)
- [All sources →](https://txtfetch.com/sources)
## Point it at your Email inbox files.
Pass a signed URL and the text comes back. There is no connector to install.
[Get an API key →](https://app.txtfetch.com/signup)
[See the URL docs →](https://txtfetch.com/docs)
---
# https://txtfetch.com/sources/google-drive
# Extract text from Google Drive
A file that needs a header on every download, and an export step for anything native. Here's how a document actually gets from Google Drive to a txtfetch response.
## The problem
Google Drive checks an Authorization header on every download, whether the file is a PDF you uploaded or a Doc you typed. txtfetch's ?url= fetch sends no custom headers, so it can never call files.get on your behalf. Fetch the bytes yourself first, then send them to txtfetch.
POST the bytes, and why
| Pre-authenticated URL? | No — Drive requires a Bearer header on every download |
| --- | --- |
| txtfetch path | POST the bytes |
| Link lifetime | N/A — no public link ever exists |
| The trap | A native Google Doc, Sheet, or Slide has no raw bytes. Export it first, and the export tops out at 10 MB. |
files.get needs an Authorization header on every download. Drive publishes no public download URL, so ?url= has nothing to reach.
## How it works
Call files.get with alt=media and your own Bearer token to download an uploaded file's raw bytes. A native Google Doc, Sheet, or Slide isn't a file in that sense. Call files.export instead, and pick a format txtfetch already parses, like DOCX, XLSX, PPTX, or PDF. Export output stops at 10 MB, so a very long native Doc may need trimming first. Either way, POST the resulting bytes to txtfetch as a normal file upload.
the script
A plain HTTP call against Google Drive's own REST API, then a plain call to txtfetch. No vendor SDK either side.
Python
```python
import os
import requests
FILE_ID = "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms"
headers = {"Authorization": f"Bearer {os.environ['DRIVE_ACCESS_TOKEN']}"}
# A native Google Doc has no raw bytes of its own — export it first.
export = requests.get(
f"https://www.googleapis.com/drive/v3/files/{FILE_ID}/export",
headers=headers,
params={"mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
)
export.raise_for_status()
r = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
files={"file": ("doc.docx", export.content)},
)
print(r.json()["extracted_text"])
```
JavaScript
```javascript
const fileId = "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms";
const headers = { Authorization: `Bearer ${process.env.DRIVE_ACCESS_TOKEN}` };
// A native Google Doc has no raw bytes of its own — export it first.
const exportUrl = new URL(`https://www.googleapis.com/drive/v3/files/${fileId}/export`);
exportUrl.searchParams.set("mimeType", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
const exported = await fetch(exportUrl, { headers });
const bytes = await exported.arrayBuffer();
const form = new FormData();
form.append("file", new Blob([bytes]), "doc.docx");
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);
```
txtfetch ships no connector, plugin, or client for Google Drive. The script above is the whole integration. Fetch the document with Google Drive's own API, then hand it to txtfetch, the same as any other source of text.
frequently asked questions
**Does txtfetch have a Google Drive connector?**: No. txtfetch ships no Drive connector or plugin. Fetch the file yourself with the Drive API, then POST the bytes to txtfetch, the same as any other upload.
**Can I just paste a Drive share link into ?url=?**: No. A share link opens a Google sign-in page in a browser. txtfetch's server-side fetch has no browser session and no way to send the Authorization header Drive requires.
**What if the file is a native Google Doc, not an upload?**: Call files.export first, in a format txtfetch parses, like DOCX or PDF. files.get only works on files with real bytes behind them, and a native Doc has none.
sources
- [Google Drive API: Download and export files](https://developers.google.com/workspace/drive/api/guides/manage-downloads) Accessed 2026-09
## Related
- [Integrations](https://txtfetch.com/integrations), for wiring this into a no-code automation platform instead of a script.
- [Ingest into a vector store](https://txtfetch.com/ingest), for where the extracted text goes next.
- [Async jobs & webhooks](https://txtfetch.com/docs/async), for a source that's large or slow to fetch.
- [Error reference](https://txtfetch.com/docs/errors), for what a failed `?url=` fetch returns.
other sources
- [Extract text from SharePoint & OneDrive →](https://txtfetch.com/sources/sharepoint-onedrive)
- [Extract text from Amazon S3 →](https://txtfetch.com/sources/amazon-s3)
- [Extract text from Dropbox →](https://txtfetch.com/sources/dropbox)
- [Extract text from Confluence →](https://txtfetch.com/sources/confluence)
- [Extract text from Email inbox →](https://txtfetch.com/sources/email-inbox)
- [All sources →](https://txtfetch.com/sources)
## Point it at your Google Drive files.
Pass a signed URL and the text comes back. There is no connector to install.
[Get an API key →](https://app.txtfetch.com/signup)
[See the URL docs →](https://txtfetch.com/docs)
---
# https://txtfetch.com/sources/sharepoint-onedrive
# Extract text from SharePoint & OneDrive
The download URL that needs no Authorization header at all. Here's how a document actually gets from SharePoint & OneDrive to a txtfetch response.
## The problem
SharePoint and OneDrive both sit behind Microsoft Graph. Graph already solved the header problem: its download URL needs no Authorization header of its own. The catch is how briefly that URL stays valid.
?url=, and why
| Pre-authenticated URL? | Yes — @microsoft.graph.downloadUrl needs no header |
| --- | --- |
| txtfetch path | ?url= |
| Link lifetime | Minutes — Microsoft warns it can expire fast |
| The trap | The URL might expire within minutes. Fetch it right before the txtfetch call, never from a queue or a retry. |
Microsoft Graph's @microsoft.graph.downloadUrl is pre-authenticated. It carries its own short-lived access, so ?url= reaches it with no header.
## How it works
Request a driveItem with $select=@microsoft.graph.downloadUrl, and Graph returns that URL directly in the JSON body. Pass it straight to txtfetch's ?url= parameter. GET .../content works too — Graph replies with a 302 redirect to the same URL, and txtfetch follows redirects automatically. Use the JSON form for a browser-side call, since a browser blocks that redirect with a CORS preflight. A server-side call can use either form.
the script
A plain HTTP call against SharePoint & OneDrive's own REST API, then a plain call to txtfetch. No vendor SDK either side.
Python
```python
import os
import requests
ITEM_ID = "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD4NX"
headers = {"Authorization": f"Bearer {os.environ['GRAPH_ACCESS_TOKEN']}"}
item = requests.get(
f"https://graph.microsoft.com/v1.0/me/drive/items/{ITEM_ID}",
headers=headers,
params={"select": "@microsoft.graph.downloadUrl"},
)
item.raise_for_status()
download_url = item.json()["@microsoft.graph.downloadUrl"]
r = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
params={"url": download_url},
)
print(r.json()["extracted_text"])
```
JavaScript
```javascript
const itemId = "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD4NX";
const graphRes = await fetch(
`https://graph.microsoft.com/v1.0/me/drive/items/${itemId}?select=@microsoft.graph.downloadUrl`,
{ headers: { Authorization: `Bearer ${process.env.GRAPH_ACCESS_TOKEN}` } },
);
const { "@microsoft.graph.downloadUrl": downloadUrl } = await graphRes.json();
const endpoint = new URL("https://api.txtfetch.com/v1/extract");
endpoint.searchParams.set("url", downloadUrl);
const res = await fetch(endpoint, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` },
});
const { extracted_text } = await res.json();
console.log(extracted_text);
```
txtfetch ships no connector, plugin, or client for SharePoint & OneDrive. The script above is the whole integration. Fetch the document with SharePoint & OneDrive's own API, then hand it to txtfetch, the same as any other source of text.
frequently asked questions
**Does txtfetch have a SharePoint or OneDrive connector?**: No. txtfetch ships no connector for either. The Graph download URL already needs no header, so ?url= reaches it directly with no extra plumbing.
**Why does the download URL expire so fast?**: Microsoft designs it that way on purpose. It's meant for an immediate download, not for storing and reusing later, so fetch it right before you call txtfetch.
**Can I call the download URL from a browser instead of a server?**: Not through GET .../content — a browser blocks its 302 redirect with a CORS preflight. Request @microsoft.graph.downloadUrl directly and fetch that URL instead.
sources
- [Microsoft Graph: Download driveItem content](https://learn.microsoft.com/en-us/graph/api/driveitem-get-content) Accessed 2026-09
## Related
- [Integrations](https://txtfetch.com/integrations), for wiring this into a no-code automation platform instead of a script.
- [Ingest into a vector store](https://txtfetch.com/ingest), for where the extracted text goes next.
- [Async jobs & webhooks](https://txtfetch.com/docs/async), for a source that's large or slow to fetch.
- [Error reference](https://txtfetch.com/docs/errors), for what a failed `?url=` fetch returns.
other sources
- [Extract text from Google Drive →](https://txtfetch.com/sources/google-drive)
- [Extract text from Amazon S3 →](https://txtfetch.com/sources/amazon-s3)
- [Extract text from Dropbox →](https://txtfetch.com/sources/dropbox)
- [Extract text from Confluence →](https://txtfetch.com/sources/confluence)
- [Extract text from Email inbox →](https://txtfetch.com/sources/email-inbox)
- [All sources →](https://txtfetch.com/sources)
## Point it at your SharePoint & OneDrive files.
Pass a signed URL and the text comes back. There is no connector to install.
[Get an API key →](https://app.txtfetch.com/signup)
[See the URL docs →](https://txtfetch.com/docs)
---
# https://txtfetch.com/status
# Status
Current status and uptime history for the txtfetch API. It updates automatically. No login is required.
Operational
All systems operational.
as of 2026-07-16T00:00:00Z
24h
100.00%
7d
100.00%
30d
99.85%
90d
99.94%
90-day uptime
components
- Extraction API Operational
- OCR pipeline Operational
- Status page Operational
Uptime is measured by automated checks against the extraction API every few minutes. Questions about an incident? [Get in touch](https://txtfetch.com/contact). For what changed, not just whether it's up, see the [changelog](https://txtfetch.com/changelog).
---
# https://txtfetch.com/subprocessors
# Sub-processors
The infrastructure and service providers txtfetch relies on to run the API and the site — nothing more.
**AWS Lambda**: us-west-2 (Oregon) · Runs the extraction engine (Tika/Tesseract) and the API itself · data: Document bytes, in memory only, for the duration of a single request
**AWS CloudFront**: Global edge network · CDN and TLS termination for the site and API edge · data: Request metadata (no document content)
**AWS S3**: us-west-2 (Oregon) · Transient large-file upload buffers, plus static site assets · data: Document bytes for large uploads (transient); compiled site files
**AWS SES**: us-west-2 (Oregon) · Delivers /contact form submissions by email · data: Name, email address, and message text you submit via /contact
**AWS ACM / CloudWatch**: us-west-2 (Oregon) · TLS certificate management and operational logging/metrics · data: Certificates; operational logs and metrics (no document content by design)
**Google Analytics**: Global (Google infrastructure) · Website traffic analytics for txtfetch.com and app.txtfetch.com · data: Cookies and per-visitor identifiers, per Google's standard Analytics data collection — see Google's privacy policy
Last updated 2026-07-16. This list is referenced by the [Data Processing Addendum](https://txtfetch.com/dpa) — any addition will be reflected here first.
---
# https://txtfetch.com/text
# Every page here is also plain text.
We turn documents into clean plain text for a living. It would be embarrassing to make a machine parse 90 KB of HTML full of widgets just to read about that. So this page is plain text too.
the convention
## Append `.md` to any path.
Every rendered page has a plain-text twin at the same path plus `.md`. There's no separate host, no API key, no rate limit. It's a static file served from the same CDN as the HTML.
```
$ curl https://txtfetch.com/pricing.md
> Source: https://txtfetch.com/pricing
> Plain-text twin — every page on txtfetch.com has one. https://txtfetch.com/text
---
# Priced per document, not per surprise.
No per-page arithmetic, no per-megabyte fine print — Hobby is free, forever.
...
```
The two lines up top name the canonical source URL and point back here. Everything after the rule is the page's own content. It's converted straight from the same HTML a browser gets.
the whole site in one file
## Two files if a whole site is easier than eighty.
- [`/llms-full.txt`](https://txtfetch.com/llms-full.txt): every page's markdown, concatenated into one document. Each page is marked with its source URL. The whole corpus, in one request.
- [`/llms.txt`](https://txtfetch.com/llms.txt): a curated index of what txtfetch does, who it's for, and links to every page on the site, grouped by section. It's built from the same record set that feeds the ⌘K search palette. It can't go stale as pages are added.
what's in the markdown
## What survives the conversion, and what doesn't.
The markdown keeps headings, GFM tables, and fenced code blocks. It labels code blocks with the language, where we know it. It also keeps links, always absolute and never root-relative. This way a twin reads correctly on its own, with no base URL to resolve. Some things don't survive: navigation chrome, terminal animations, and anything that needs JavaScript to run. Examples include a drop zone, a live calculator, and a tabbed code switcher. Those get replaced with a one-line note naming the tool and linking to the real thing, instead of dead markup or silence.
why we bother
## Agents and answer engines read our docs too.
Developer tools increasingly get found through LLM answer engines and coding agents, not just search results. These tools read markdown far more reliably than they read a page built for a browser. txtfetch sells clean plain text extracted from documents. Serving our own site the same way is the same product, turned on itself.
faq
**Do I need an API key to read the plain-text twins?**: No. Every .md twin, llms.txt, and llms-full.txt is a static file served from the same CDN as the HTML. Curl them like any other URL. There's no auth and no rate limit.
**How current is a twin?**: As current as the HTML it's generated from. Twins are rebuilt from the live page content on every deploy. They are not hand-maintained, so they can't drift out of sync the way a hand-typed llms.txt used to.
**What happens to interactive tools like the PDF checker or the chunk previewer?**: Those tools run client-side in your browser. They have no plain-text equivalent. The twin replaces each one with a one-line note that names the tool and links to it. This avoids leaving blank space where a widget used to be.
**Why not just serve Markdown for every request instead of HTML?**: Most visitors are people using a browser, not agents. HTML gets the styling, the interactive tools, and the navigation. The .md twin is an addition for readers who want the content without any of that. It's not a replacement.
## Stop parsing. Start shipping.
Create an account and get an API key in minutes. The free Hobby plan needs no card.
[Get started →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/tools
# Free tools, nothing uploaded.
Small, in-browser utilities for the questions that come up before you ever call the API. What's actually in this file? Can txtfetch get text out of it?
Already extracted something and the text came back wrong? See [fixes for broken extracted text](https://txtfetch.com/fixes).
**[File to text — any format](https://txtfetch.com/tools/file-to-text)**
Drop one file, many files, a whole folder, or a .zip. This page works out what each one is from the bytes, then reads it. It's the front door to every tool below. No OCR, no speech transcription. Both get an honest verdict instead.
open →
**[PDF to text](https://txtfetch.com/tools/pdf-to-text)**
Drop a real PDF and see its plain text, in reading order. Multi-column layouts read correctly, and scanned pages get flagged honestly. Nothing uploaded.
open →
**[Image OCR-readiness check](https://txtfetch.com/tools/image-ocr-check)**
Drop a PNG, JPG, TIFF, BMP, GIF, WebP, or HEIC and find out whether it will OCR cleanly. Resolution, blur, skew, and inversion are measured from the pixels. Nothing uploaded.
open →
**[PDF text-layer check](https://txtfetch.com/tools/pdf-text-check)**
Drop a PDF and find out whether it has a real text layer, is a scan that needs OCR, or is a mixed document. Nothing uploaded.
open →
**[File type inspector](https://txtfetch.com/formats)**
Drop any file and see what txtfetch actually detects from its bytes, including when the extension is lying about the real format.
open →
**[Format coverage, checked](https://txtfetch.com/formats/coverage)**
Look up any extension or media type and see whether tika-server-standard actually parses it. That's checked against the exact build we run, not assumed from a list.
open →
**[Chunk previewer](https://txtfetch.com/tools/chunk-preview)**
Paste extracted text and see chunk boundaries, token estimates, and the extraction-damage signals wrecking them, for fixed, recursive, and structure-aware chunking.
open →
**[Clean up extracted text](https://txtfetch.com/tools/clean-extracted-text)**
Paste extracted text and see every damage signal, named and linked to its fix. Then repair what can honestly be repaired: mojibake, hyphenated line breaks, repeated headers, and more.
open →
**[DOCX to text](https://txtfetch.com/tools/docx-to-text)**
Drop a real Word document and see the exact plain text it extracts to. Tables and tracked-change deletions are handled right. Nothing uploaded.
open →
**[XLSX to text](https://txtfetch.com/tools/xlsx-to-text)**
Drop a real Excel workbook and see every sheet's text, in its real order. Formulas come back as cached values, and hidden sheets get flagged. Nothing uploaded.
open →
**[PPTX to text](https://txtfetch.com/tools/pptx-to-text)**
Drop a real PowerPoint deck and see every slide's text and speaker notes, in the deck's real order. Nothing uploaded.
open →
**[DOC to text](https://txtfetch.com/tools/doc-to-text)**
Drop a real Word 97-2003 .doc file and see the exact plain text it extracts to. Its piece table gets reassembled in the right order. Nothing uploaded.
open →
**[XLS to text](https://txtfetch.com/tools/xls-to-text)**
Drop a real Excel 97-2003 .xls workbook and see every sheet's text, in its real order. Shared strings get resolved, and hidden sheets get flagged. Nothing uploaded.
open →
**[PPT to text](https://txtfetch.com/tools/ppt-to-text)**
Drop a real PowerPoint 97-2003 .ppt deck and see its slide text and speaker notes, separated and labelled. Nothing uploaded.
open →
**[Email to text](https://txtfetch.com/tools/email-to-text)**
Drop a real .eml or Outlook .msg and see the headers, body, and attachment text it extracts to. Attachments get recursed through, too. Nothing uploaded.
open →
**[HTML to text](https://txtfetch.com/tools/html-to-text)**
Drop an .html file, paste HTML source, or paste a copied web page. See chrome (nav, footer, cookie banners) separated from the actual content. Nothing uploaded.
open →
**[Subtitles to text](https://txtfetch.com/tools/subtitles-to-text)**
Drop an .mp4/.mkv/.mp3/.wav or a .srt/.vtt/.ass/.ttml file and get a clean transcript. Cue numbers and timestamps are gone, and rolling captions are collapsed. No caption track? You get the honest verdict, not a guess. Nothing uploaded.
open →
**[EPUB to text](https://txtfetch.com/tools/epub-to-text)**
Drop a real .epub and see its plain text, chapter by chapter, in the book's own spine order, not zip order. Nothing uploaded.
open →
**[ODT to text](https://txtfetch.com/tools/odt-to-text)**
Drop a real OpenDocument text file (zipped .odt or flat .fodt) and see the exact plain text it extracts to. Nothing uploaded.
open →
**[ODS to text](https://txtfetch.com/tools/ods-to-text)**
Drop a real OpenDocument spreadsheet (zipped .ods or flat .fods) and see every sheet's text, with repeated and merged cells kept aligned. Nothing uploaded.
open →
**[ODP to text](https://txtfetch.com/tools/odp-to-text)**
Drop a real OpenDocument presentation (zipped .odp or flat .fodp) and see every slide's text and speaker notes, labelled. Nothing uploaded.
open →
## These tools read one file. The API does the queue.
Every tool here runs in your browser. The API reads a folder of them.
[Read the quickstart →](https://txtfetch.com/docs)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/tools/chunk-preview
# See how your text will actually chunk.
Paste text you've already extracted, or load a sample, and watch chunk boundaries, token estimates, and extraction damage happen on real content. It runs entirely in your browser. Nothing is uploaded.
Paste text you've already extracted, or load a sample below, to see how it chunks. It runs entirely in your browser. Nothing is uploaded.
## what-this-does
[Chunking strategies for RAG](https://txtfetch.com/blog/chunking-strategies-for-rag) argues that extraction quality bounds chunk quality. No chunker recovers a reading order that a mangled extraction already destroyed. This tool turns that argument into something you can prove on your own text. Paste it, pick a strategy, and see exactly where the boundaries land. It shows how many estimated tokens each chunk costs, and which specific extraction-damage signals are wrecking those boundaries. Nothing you paste or load ever leaves your browser. New to [chunking](https://txtfetch.com/glossary/chunking) or [tokens](https://txtfetch.com/glossary/token) as concepts? The [glossary](https://txtfetch.com/glossary) defines both plainly.
## the-three-strategies
- **Fixed size.** A hard window of N estimated tokens with some percentage overlap, cut without regard for sentence or paragraph boundaries. It gives simple, predictable chunk counts. It's also the strategy most likely to cut a chunk off mid-sentence. That's deliberate, so you can see the cost of ignoring structure.
- **Recursive.** Splits on paragraph, then line, then sentence, then word. It greedily packs units up to the target size. It only falls back to a smaller unit when the current one is still too big. A hard character cut only happens when a single word overflows the target on its own.
- **Structure-aware.** Detects heading-ish lines: markdown headings, short ALL-CAPS lines, numbered clauses like `4.2 Termination` or `Chapter N`. It sections the document on them. It recursively splits any section that's still oversized. It merges tiny adjacent sections back up toward the target, so you don't get one chunk per one-line heading.
All three are implemented exactly as described in [the chunking strategies guide](https://txtfetch.com/blog/chunking-strategies-for-rag). The tool and the writing can't contradict each other, because they share the same logic.
## overlap-and-why
Overlap re-includes a slice of the previous chunk's trailing content at the start of the next chunk. That way, a sentence or idea that straddles a boundary still appears in full in at least one chunk. It isn't split with no full copy anywhere. Every chunk below shows its overlap. It's shown both as a highlighted span in the chunk text and as an exact character count. That way you can see precisely what's duplicated and how much. Too little overlap risks losing context at a boundary. Too much means embedding and storing the same content repeatedly, for no retrieval benefit past a certain point.
## token-estimates-are-estimates
Every token count on this page is an **estimate**, not a real tokenizer result. The heuristic counts whitespace-delimited words. It splits each into its letter/digit run(s) and punctuation run(s). It charges roughly 4 characters per token, with a floor of one token per run. It is not a BPE tokenizer. Real token counts are model-specific. The same text can tokenize to meaningfully different counts across embedding models. The exact character count shown next to every estimate is the one number on this page that isn't an estimate. Treat the token figure as a sizing guide, not a ground truth.
## extraction-damage-signals
Every finding below is a **signal, not proof**. A structural scan over plain text can be wrong about intent, so read the sample it shows before acting on it. Here's what each one does to chunking, and where it typically gets fixed. Each links to its own page under [fixes for broken extracted text](https://txtfetch.com/fixes), with the real cause and an honest DIY remedy:
| Signal | What it does to chunking | Where it's fixed |
| --- | --- | --- |
| Hyphenated line breaks | A chunk boundary can land right at the hyphen, splitting one word into two fragments. | [Fix hyphenated line breaks](https://txtfetch.com/fixes/hyphenated-line-breaks) |
| Hard-wrapped lines | Chunking packs by line instead of by sentence or paragraph, ending chunks mid-thought. | [Fix hard-wrapped lines](https://txtfetch.com/fixes/hard-wrapped-lines) |
| Repeated header/footer lines | Boilerplate gets pulled into every chunk that lines up with a page boundary. | [Fix repeated headers and footers](https://txtfetch.com/fixes/headers-and-footers-in-every-chunk) |
| Page-number lines | A bare number can get packed as content, or misread as a heading in structure mode. | [Fix bare page-number lines](https://txtfetch.com/fixes/headers-and-footers-in-every-chunk) |
| Mojibake | Garbled characters break sentence- and paragraph-boundary detection near them. | [Fix mojibake and wrong-encoding text](https://txtfetch.com/fixes/mojibake-wrong-encoding) |
| Replacement characters | Marks a spot the original extraction couldn't decode at all — that text is already lost. | [Fix mojibake and wrong-encoding text](https://txtfetch.com/fixes/mojibake-wrong-encoding) |
| Unexpanded ligatures | Reads as one unrecognized character to word- and token-boundary logic. | [Fix ligatures and smart punctuation](https://txtfetch.com/fixes/ligatures-and-smart-punctuation) |
| Missing spaces between words | Two fused words are tokenized and chunked as a single unrecognizable unit. | [Fix missing spaces between words](https://txtfetch.com/fixes/missing-spaces-between-words) |
| Collapsed long lines | Usually two interleaved columns read as one — any chunker packs both in the wrong order. | [Fix columns out of order](https://txtfetch.com/fixes/columns-out-of-order) |
| Space-run table columns | Table rows collapse into plain text with no structural marker; column order scrambles. | [Fix tables losing structure](https://txtfetch.com/fixes/tables-lose-structure) |
| No paragraph breaks | Recursive and structure-aware chunking both degrade to splitting on sentences or lines. | [Fix missing paragraph breaks](https://txtfetch.com/fixes/no-paragraph-breaks) |
The tool's headline verdict reports on two separate axes, and it always ranks them the same way: **extraction damage outranks chunk sizing.** A chunk running past your selected embedding limit is a sizing problem, one slider away from fixed. So it reads `chunk sizing needs work`, never "fix extraction first." There is nothing wrong with that document's extraction in that case. Damage signals, by contrast, are not fixable at chunk time at all. So any of them present reads `fix extraction first`, and the summary names which axis drove the call. Text carrying damage signals is never reported as clean.
## same-document-two-extractions
The two "load sample" buttons above are the same source page, extracted two different ways. One is a clean extraction with real headings, paragraphs, and a small table. The other is a naive PDF text dump of the exact same content. It carries hyphenated line breaks, a running header, page numbers, mojibake, and a collapsed table. Load each one and compare: same words, differently extracted, visibly worse chunks. That gap is the whole argument for [extracting well in the first place](https://txtfetch.com/extract/pdf), made on identical content instead of asserted in prose.
## what-to-do-next
Once your own extracted text chunks the way you expect, see the extraction call that produced it. It's the same request shown in the tool above: `POST /v1/extract`, a file or a URL. Back comes `extracted_text`. Feed that straight into whichever chunker you use downstream. See the [chunk → embed → index recipe](https://txtfetch.com/docs/recipe) for the LangChain and LlamaIndex versions of the same pipeline.
## faq
**What chunk size should I use for RAG?**: Recursive chunking with a target of 300–500 estimated tokens and 10–15% overlap is a reasonable default for most prose-heavy documents. It's the same default the chunking-strategies guide recommends. Reach for structure-aware chunking when the source has reliable headings, like technical docs or contracts with numbered clauses. Use fixed-size chunking only when you need predictable chunk counts more than clean boundaries.
**How much overlap should chunks have?**: 10–15% is a reasonable starting point. That's enough for a sentence straddling a boundary to still appear in full in at least one chunk. It isn't so much that you pay to embed and store the same text repeatedly. Overlap matters more for fixed-size chunking, which ignores sentence boundaries entirely. Recursive and structure-aware chunking mostly avoid cutting mid-sentence in the first place, so overlap matters less for them.
**Why are my chunks cut off mid-sentence?**: Either you're using fixed-size chunking, which cuts at a hard character window regardless of what it lands on. Or a single sentence in your source text is longer than your target chunk size. Recursive and structure-aware chunking both fall back to a word-level split when even one sentence alone overflows the target. There's no smaller boundary above a hard character cut. This tool flags every chunk that starts or ends mid-sentence, so you can see exactly where and why.
**Does this tool upload my text anywhere?**: No. Chunking and the extraction-quality scan both run entirely in your browser. Nothing you paste, load, or drop is sent over the network. The only thing that leaves your browser is the JSON report, if you choose to copy it.
**How many tokens is my document, really?**: This tool can only estimate. It counts letter/digit runs and punctuation runs and applies a documented ~4-characters-per-token heuristic, not a real BPE tokenizer. Real token counts are model-specific. They can differ from this estimate by a meaningful margin, especially on non-English text or heavy punctuation. The exact character count shown alongside every estimate is the one number here that isn't an estimate.
**Should I chunk before or after extraction?**: After, always. Chunking operates on whatever text extraction handed it. No chunking strategy recovers a reading order that extraction already destroyed. Paste your own extracted text above, or load the naive-PDF-text-dump sample. You'll see the same source content chunk visibly worse once extraction quality drops.
**Why does a page number or header show up in every chunk?**: A naive text extraction keeps whatever a page's layout put on it as ordinary lines of text, indistinguishable from content. That includes a running header, a footer, or a bare page number. Any chunker packs those lines in wherever they fall. So a repeated header can end up duplicated across dozens of chunks. The extraction-damage scan above flags repeated lines and bare page-number lines specifically, so you can catch this before it reaches your vector store.
**What's the actual difference between the three strategies?**: Fixed-size chunking cuts a hard window of N estimated tokens with some overlap, and ignores every boundary. Recursive chunking splits on paragraph, then line, then sentence, then word. It greedily packs units up to the target and only falls back to a smaller unit when the current one overflows. Structure-aware chunking sections the document on detected headings first. It then recursively splits any oversized section, and merges tiny adjacent sections back up toward the target.
## That was one file. The API does the queue.
This page read your text on your own machine. The API reads a folder of them.
[Read the quickstart →](https://txtfetch.com/docs)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/tools/clean-extracted-text
# Your extracted text has damage. Here's exactly what, and what can honestly be fixed.
Paste it below and this names every signal, links each one to its fix, and repairs what it safely can. It runs entirely in your browser. Nothing is uploaded.
Paste text you've already extracted, or drop a local `.txt` file. This names every kind of damage in it, links each one to the page that explains it, and repairs what it honestly can. It runs entirely in your browser. Nothing is uploaded.
## what-this-tool-does
This scan runs the same detector behind the [chunk previewer](https://txtfetch.com/tools/chunk-preview). It names 11 kinds of extraction damage. Mojibake, hyphenated line breaks, and hard-wrapped lines are three of them. Repeated headers, bare page numbers, and stray replacement characters are three more. Unexpanded ligatures, glued words, space-run columns, collapsed long lines, and missing paragraph breaks round out the list. Every finding links to the [page](https://txtfetch.com/fixes) that explains it in full.
Naming the damage is only half the job. This page repairs what it honestly can. Eight repairs are listed below. Each one is a heuristic, unless its own note says exact. Every repair reports how many edits it made.
## the-eight-repairs
Two repairs are exact and lossless. The rest are heuristics. Each one fixes the common case, and says so plainly:
- **Join hyphenated line breaks.** Joins `word-\nword` back into one word. It can also join a real compound word that broke at the same spot.
- **Unwrap hard-wrapped lines.** Joins a line into the next line. It only joins when this line does not end a sentence, and the next line starts lowercase. That is the shape a hard-wrapped paragraph leaves behind.
- **Remove repeated headers and footers.** Drops any short line that repeats three or more times. That is the same floor the detector itself uses.
- **Remove page-number lines.** Drops bare lines like "3" or "Page 3 of 12".
- **Expand ligatures.** Exact and lossless. Turns fi, fl, and five other ligatures back into plain letters.
- **Remove undecodable characters.** Drops the replacement character (�) and stray control bytes. This deletes damage. It does not restore what was already lost.
- **Collapse space-run columns.** Off by default, because it's lossy. Collapses table-column spacing down to one space. This destroys the column alignment. It does not fix reading order.
- **Fix mojibake.** See the next section. This one is exact, but only some of the time.
## how-the-mojibake-fix-works
Mojibake happens when UTF-8 bytes get decoded as Windows-1252 instead. This is almost always a mistake somewhere upstream. The original bytes stay intact. Only the encoding label is wrong. So the fix maps each character back to its Windows-1252 byte. Then it decodes those bytes as UTF-8 again. When the source really was UTF-8 read as Windows-1252, this recovers the original text exactly.
This tool never guesses. It applies the fix only when two checks both pass. First, the re-decode must produce no `U+FFFD` character. Second, a fresh scan of the result must find zero mojibake findings. Genuine accented text, like café or crème, usually fails this check. That text was never mojibake in the first place. When the checks fail, this page reports "not applied". It never silently corrupts text that was not broken.
## two-kinds-of-damage-this-cannot-fix
Two signals get no repair, on purpose. **Glued words**, like `thequickbrownfox`, need a dictionary to split safely. A wrong split invents a word boundary that might not be real. That is worse than leaving the words fused. **Lost paragraph structure** is the second case. It shows up as one giant block, or as two columns collapsed into one line. This structure cannot be invented after the fact. The source document either encoded it, or it did not. Once it is gone, nothing in the plain text can rebuild it. Unwrapping hard-wrapped lines sometimes restores a paragraph-like shape as a side effect. The before-and-after counts on this page prove that when it happens, instead of promising it upfront.
## what-to-do-next
Got a whole document to check, not just a snippet? The [fixes hub](https://txtfetch.com/fixes) covers every symptom this scan can name. Each page explains why it happens, and how to fix it yourself. Chunking this text next? The [chunk previewer](https://txtfetch.com/tools/chunk-preview) shows exactly where a boundary would land. It also shows whether these same signals are why a chunk ends mid-thought. Want to see real damage next to the corrected text? See [the extracted text, side by side](https://txtfetch.com/diff).
[Get your key →](https://app.txtfetch.com/signup)
## faq
**Why does extracted PDF text lose the spaces between words?**: PDF text is a sequence of positioned glyph runs, not words with real boundaries. A producer draws each run at a coordinate, and relies on visual spacing to imply a gap. An extractor that just joins runs in order can fuse two words with no space at all. See /fixes/missing-spaces-between-words.
**Is it safe to de-hyphenate extracted text automatically?**: Mostly, yes. The join only fires when both letters around the break are lowercase. That is the shape a print-layout line wrap leaves. It can still merge a genuine compound word that broke at the same spot, so spot-check the result before you trust it at scale.
**What does ’ mean, and can it be recovered?**: It's mojibake: UTF-8 bytes read back with the wrong encoding, almost always Windows-1252. The original bytes are intact, so it's usually fully recoverable by re-decoding correctly. This tool does that automatically, but only after checking the reversal is exact. It refuses rather than guess. See /fixes/mojibake-wrong-encoding.
**Does this tool upload my text anywhere?**: No. Every scan and every repair runs in your browser, in plain JavaScript. Nothing you paste or drop ever leaves the page.
**Why can't glued words like thequickbrownfox be split back apart automatically?**: Splitting a fused run needs a dictionary. Even then, a wrong split is worse than the original: it invents a word boundary that might not be real. This tool names the signal, and links to the fix. It does not guess at where the space belongs.
**Can this tool restore paragraph breaks that extraction lost?**: Not directly. A paragraph break is a blank line the source document had to encode in the first place. If that line is gone, nothing left in the text can recover it. Unwrapping hard-wrapped lines sometimes restores the same shape as a side effect. The after-count on this page proves that when it happens, instead of promising it upfront.
**What does txtfetch do differently so I don't need this tool at all?**: txtfetch extracts through Apache Tika. Tika detects source encoding and reflows paragraphs as part of parsing. So mojibake and hard-wrapped lines show up far less than in a raw parser dump. This page exists for text you already have, from whatever produced it. It doesn't replace checking your own extraction pipeline first.
## That was one file. The API does the queue.
This page read your text on your own machine. The API reads a folder of them.
[Read the quickstart →](https://txtfetch.com/docs)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/tools/doc-to-text
# Your old .doc file, as text. No code required.
Drop a real Word 97-2003 document below. Watch it become plain text right here, in your browser. No Word install, no conversion service, nothing uploaded.
Drop a legacy Word (.doc), Excel (.xls), or PowerPoint (.ppt) file below to see the actual text it extracts to. These 97-2003 binary formats get read right here in your browser. Nothing is uploaded.
## whats-hard-about-doc
A .doc isn't a smaller version of .docx. It's a different container entirely, OLE2/Compound File Binary. Even once you're inside it, the document's text doesn't sit in reading order. Word stores it as a piece table: a list of byte ranges, called pieces, in the WordDocument stream. Each piece is either plain 8-bit text or UTF-16, and they have to be stitched together in a specific order to come out readable. Read the stream start to finish and you get a scrambled, out-of-order mess. Not garbage exactly, just wrong.
Fields compound the problem. A page number, a table of contents entry, or a hyperlink is stored as an instruction: what to compute. It's followed by a result: what Word last computed. Both sit inline in the same character stream with no visual separator. Show the instruction and a hyperlink field looks like "HYPERLINK \\"http://example.com\\"" glued into the middle of a sentence. Skip both and you lose the link text entirely. This reader keeps the result and drops the instruction, the same distinction the file itself makes.
Some content isn't the main body: footnotes, headers and footers, comments, endnotes, text boxes. All of it lives in the SAME character stream as the main text, one after another. The boundaries are recorded only in the file's header, as character counts, not markers you can search for. Treat the whole stream as one document and a footnote ends up dumped mid-paragraph wherever it happens to fall in the byte layout. This tool cuts each of those out using the real counts, and labels them. They show up as their own sections instead of interrupting the body.
## what-to-do-next
Got the text out and want the API call for it directly? The panel above already has it, with your file's real name. Extracting many documents, or files bigger than this browser tool's 25 MB cap? [See the full legacy-Office extraction guide (.doc, .xls, .ppt) →](https://txtfetch.com/extract/legacy-office)
## faq
**Does this tool upload my .doc file anywhere?**: No. The whole read happens in your browser. The file's bytes never leave your machine. Only the finished text ever leaves, and only if you choose to copy or download it.
**Why does a .doc opened with a plain byte scraper come out scrambled?**: Because the text isn't stored in reading order. Word 97-2003 keeps a piece table, a list of byte ranges scattered through the file. A tool that just reads the stream start to finish gets those ranges out of sequence. This reader reassembles them using the same piece table Word itself relies on.
**What happens to fields like page numbers, a table of contents, or hyperlinks?**: You get the result Word last computed, the link text or the cached page number, not the underlying field instruction. That matches what you'd see reading the document normally, rather than a raw field code sitting mid-sentence.
**Are footnotes, comments, and headers included?**: Yes. Each is pulled out into its own labelled section below the main text. It isn't left wherever it happens to fall in the file's internal byte layout.
**Do tables keep their rows and columns?**: Yes. Cells come out tab-separated and each row on its own line, so a table extracts as a readable grid. That's less obvious than it sounds in this format. Word ends every cell with the same marker byte. It ends the row with one more of them, so a row break is really two cell markers in a row. Miss that and every row separator turns into another tab. The whole table then arrives as one unbroken line. That's exactly the shape that wrecks it downstream in a search index or a RAG chunker.
**My old .doc extracts as ÊîìïàíèÿÀ-style gibberish — what's happening?**: That's a Word 6.0/95 file written in a non-Western codepage. Those older files store raw bytes in whatever Windows codepage the machine that wrote them used: Cyrillic, Greek, Central European, Japanese. The file records that codepage nowhere, so any reader has to guess, and this one assumes Western European. When the result comes back dense in accented characters, this tool says plainly the text is likely garbled. It doesn't present it as the document. txtfetch's API resolves the real codepage.
**What about tracked changes — are deletions removed like the .docx tool does?**: Not in this reader. Telling a tracked deletion apart from kept text means walking each run's character formatting. This tool doesn't do that. So struck-out text can show up in the output. The .docx tool does filter it, since that format marks deletions more directly.
**Does it handle an encrypted or password-protected .doc?**: No. It reports the file as encrypted and stops, rather than attempting to guess a password or return garbled output. txtfetch's API will reject it too, unless it's decrypted first.
**Is there a file size limit?**: This tool reads up to 25 MB entirely in your browser. Larger files, or a batch you want to automate, go through the same extraction via the API, which has no such limit.
## That was one file. The API does the queue.
This page read your DOC on your own machine. The API reads a folder of them.
[Read the quickstart →](https://txtfetch.com/docs)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/tools/docx-to-text
# Your .docx, as text. No code required.
Drop a real Word document below. Watch it become plain text right here, in your browser. Nothing is uploaded. It's the same read txtfetch's API gives you, just local.
Drop a Word, Excel, PowerPoint, or OpenDocument (.odt/.ods/.odp, zipped or flat) file below to see the actual text it extracts to. It runs entirely in your browser, and nothing is uploaded.
## whats-hard-about-docx
A .docx is a zip archive of XML. The document's text lives in word/document.xml as a tree of paragraphs () and runs (), not a flat string. Tables are their own nested structure (//), and each cell contains its own paragraphs. Just grabbing the text nodes either loses the table's row and column shape, or runs every cell together with no separator at all.
Track changes make this harder in a way that's easy to miss. A deleted sentence doesn't disappear from the XML when a reviewer accepts the edit view. It's marked with and is still sitting in the file. A naive text dump includes text nobody wants in the final document. This tool reads the same tracked-change markers txtfetch's API does, and leaves deletions out.
Text boxes and pull quotes add a second layer. Their content lives in a nested element. Word's own compatibility markup () can wrap that content twice, once for older readers and once for newer ones. Read both branches and every text box's text is duplicated in the output for no reason a reader would expect.
## what-to-do-next
Got the text out and want the API call for it directly? The panel above already has it, with your file's real name. Extracting many documents, or files bigger than this browser tool's 25 MB cap? [See the full Office-family extraction guide (DOCX, PPTX, XLSX) →](https://txtfetch.com/extract/docx)
## faq
**Does this tool upload my .docx anywhere?**: No. Everything happens in your browser using the Web Platform's own DecompressionStream API. The file is read locally and never sent over the network. Only the finished text ever leaves your machine, and only if you choose to copy or download it.
**Does it include text from tables?**: Yes. Table cells are joined with tabs, and rows with line breaks. So a table extracts as a readable tab-separated grid, not one run-together wall of text.
**What happens to tracked changes?**: Deleted text, Word's markup, is excluded from the output, the same way txtfetch's API handles it. You get the document as it reads today, not a mix of current and struck-out text. Comments, footnotes, and headers/footers aren't included either. They live in separate parts of the file this in-browser tool doesn't read yet.
**Why would I use this instead of just opening the file in Word?**: This shows you exactly what a program gets when it extracts text from your file, which is usually less than what you see on screen. It's the fastest way to check, before you write any integration code, whether your actual documents will extract cleanly.
**Does it work on .doc (the older binary format), not just .docx?**: No. This in-browser tool only reads the modern, zip-based OOXML format (.docx). Legacy .doc is a completely different binary container (OLE2) that needs a real parser, not a browser API. txtfetch's API handles .doc directly; see the legacy-Office extraction guide.
**Is there a file size limit?**: This tool reads up to 25 MB entirely in your browser. Larger files, or a batch you want to automate, go through the same extraction via the API, which has no such limit.
## That was one file. The API does the queue.
This page read your DOCX on your own machine. The API reads a folder of them.
[Read the quickstart →](https://txtfetch.com/docs)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/tools/email-to-text
# Your email, as text. Attachments included.
Drop a real .eml or Outlook .msg below. Watch the headers, body, and every attachment's text come out right here, in your browser. Nothing is uploaded. It's the same read txtfetch's API gives you, just local.
Drop a real .eml or Outlook .msg file below to see the headers, body, and attachment text it extracts to. Attachments are recursed through, too. It runs entirely in your browser, and nothing is uploaded.
## whats-hard-about-email
An .eml is RFC-822 headers over a MIME body. The body is rarely just "the text." It's usually a tree of multipart/alternative and multipart/mixed parts, where the plain-text and HTML renditions of the SAME content sit side by side. Only one of them should end up in your output. Read both and the text doubles. Read the wrong one and you inherit HTML markup you now have to strip. Encoded-word headers (=?utf-8?Q?...?=) need their own decode step. So do per-part charsets and transfer encodings, like quoted-printable and base64, before any of that text is readable at all.
Outlook's .msg is a completely different problem. It is not RFC-822 or MIME at all. It's an OLE2 compound-file container, the same binary-filesystem-in-a-file format as legacy .doc/.xls, holding MAPI property streams named things like \_\_substg1.0\_1000001F for the body. There's no header block and no boundary strings. A parser built for RFC-822 fails on a .msg outright, because it's looking for a structure that simply isn't there. This tool ships its own from-scratch OLE2/CFB reader, FAT, mini-FAT, and all, because there's no other way to get inside one in a browser.
And here is the actual point of this page: attachments aren't opaque blobs. A Word document attached to an email is itself a document that needs extracting. A PDF attachment might be a scan with no text layer. A forwarded email shows up as a message nested inside a message. Most "email to text" tools stop at the body and hand you attachments as base64, or as filenames with no content. This one recurses into each attachment with the SAME in-browser extractors the other tools on this page use. A .msg with a Word attachment gets you the message text AND the attachment's text, in one drop.
## what-to-do-next
Got the text out and want the API call for it directly? The panel above already has it, with your file's real name. Working with a whole .mbox archive, a .pst/.ost mailbox, or messages bigger than this browser tool's 25 MB cap? [See the full email extraction guide (.eml, .msg, .mbox) →](https://txtfetch.com/extract/email) covers .eml and .mbox end to end; [the .msg/.pst/.ost internals guide](https://txtfetch.com/extract/msg) covers Outlook's own container format specifically.
## faq
**Does this tool upload my email anywhere?**: No. Everything happens in your browser. The .eml or .msg bytes are parsed locally with a hand-rolled MIME/OLE2 reader, and never sent over the network. Only the finished text ever leaves your machine, and only if you choose to copy or download it. That matters more here than for most file types, since real email is often the most sensitive thing anyone drops into a converter.
**Does it handle Outlook .msg files, not just .eml?**: Yes. This tool includes its own from-scratch CFB/OLE2 reader. It walks a .msg file's MAPI property streams directly, including the mini-FAT, where most of a real .msg's small properties live. Subject, body, sender, recipients, and attachments all come from the same reader the API's Tika pipeline effectively mirrors.
**Are attachments extracted, or just listed?**: Extracted, recursively. A Word, Excel, or PowerPoint attachment gets its actual text pulled out with the same in-browser extractor the /tools/docx-to-text page uses. A PDF attachment gets a text-layer verdict, not the text itself; see the FAQ below. An embedded .msg or .eml gets expanded the same way, up to three levels deep.
**Does a PDF attachment's text get extracted too?**: No. For a PDF attachment, this browser tool only reports whether it HAS a text layer, the same structural check as /tools/pdf-text-check, not the text itself. Extracting PDF text is a heavier job than this quick client-side check does. txtfetch's API returns the actual text for a PDF attachment, in the same request.
**What about images, scanned attachments, or a .msg's forwarded .doc file?**: Named honestly, not silently dropped or guessed at. Images are listed as images; OCR only happens server-side, never in your browser. A legacy .doc/.xls/.ppt attachment, a different binary format from .msg despite sharing the OLE2 container, is named as such rather than misread. Nothing here is dressed up as more than it is.
**Does it work on a whole .mbox archive, or a .pst/.ost mailbox file?**: Not a multi-message archive. This tool reads one message at a time. An .mbox holding several messages, or a .pst/.ost, gets an honest message pointing at the right page. A .pst/.ost is a whole mailbox database. Either way, that's better than a guess at which message you meant. One exception is still a single message. A file saved by mutt, pine, or Unix mail carries a "From " envelope line in front of the message. That gets unwrapped and read normally. txtfetch's API handles full .mbox, .pst, and .ost archives directly.
**Is there a file size limit?**: This tool reads up to 25 MB entirely in your browser. It has its own caps on attachment count, recursion depth, and total output. So a hostile or malformed file can't hang the tab. Larger messages, or a batch you want to automate, go through the same extraction via the API, which has no such limit.
## That was one file. The API does the queue.
This page read your email on your own machine. The API reads a folder of them.
[Read the quickstart →](https://txtfetch.com/docs)
[Get an API key →](https://app.txtfetch.com/signup)
---
# https://txtfetch.com/tools/epub-to-text
# Your EPUB, as text. In the book's real reading order.
Drop a real .epub below. Watch it become plain text right here, in your browser. It follows the order the book actually defines, not the order the zip happens to store it in. Nothing is uploaded. It's the same read txtfetch's API gives you, just local.
Drop a real .epub below to see its plain text, chapter by chapter, in the book's actual reading order. It runs entirely in your browser, and nothing is uploaded.
## whats-hard-about-epub
An EPUB is a zip archive of XHTML files. Reading order lives in exactly one place: the OPF's , a list of manifest items by id. It has nothing to do with the order those files happen to sit in the zip. Unzip an EPUB and read the files in whatever order the archive lists them, and you get real problems. Chapters come out shuffled. The navigation document ends up mixed in with the story. The cover page and copyright boilerplate land wherever the packaging tool happened to write them.
Every book also carries a table of contents, but which one depends on when it was made. EPUB 3 books ship a real XHTML nav document with a marked-up