# 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
is the actual content versus navigation and ads. You also handle redirects and encoding along the way. That's infrastructure most teams don't want to own just to get plain text into a RAG index. one-request-solution Skip the fetch-and-strip pipeline. Pass a url parameter to txtfetch, and it retrieves the page server-side, runs it through Apache Tika's HTML parser, and returns clean plain text. No headless browser, no boilerplate code to maintain. curl ```curl curl -X POST https://api.txtfetch.com/v1/extract \ -H "Authorization: Bearer $TXTFETCH_KEY" \ -F file=@landing-page.html ``` Python ```python import os import requests with open("landing-page.html", "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("landing-page.html")]); const form = new FormData(); form.append("file", file, "landing-page.html"); 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("landing-page.html") if err != nil { panic(err) } defer f.Close() var body bytes.Buffer writer := multipart.NewWriter(&body) part, err := writer.CreateFormFile("file", "landing-page.html") 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/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) } ``` ``` { "status": "success", "extracted_text": "..." } ``` what-comes-back That is the shape. This is the text. A real .html 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. 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. ``` [See all ten documents, with the full explanation →](https://txtfetch.com/diff) formats-covered - `.html` - `.htm` - `.xhtml` faq **How do I extract text from a web page URL?**: POST to https://api.txtfetch.com/v1/extract?url= with your API key in the Authorization header. txtfetch fetches the page server-side and returns { "status": "success", "extracted_text": "..." }. **Does it strip navigation, ads, and boilerplate?**: It strips markup, not boilerplate. Tika's HTML parser removes scripts, styles, and tags. But nav, footer, and cookie-banner text is still text, so it comes back in the response like any other paragraph. Deciding what's chrome versus article is a deliberate, caller-side step. See /tools/html-to-text for a free tool that shows you exactly what your pages' chrome looks like before you index them. **Can I upload a local .html file instead of a URL?**: Yes. The same endpoint accepts a multipart file upload for local HTML files, same as any other format. go-further - [Drop an .html file, paste HTML, or paste a copied page — free, in your browser →](https://txtfetch.com/tools/html-to-text) - [Delimited data lives on the CSV side →](https://txtfetch.com/extract/csv) - [Structured/config markup lives on the XML side →](https://txtfetch.com/extract/xml) - [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 - [`.csv`](https://txtfetch.com/extract/csv) - [`.xml`](https://txtfetch.com/extract/xml) - [All formats →](https://txtfetch.com/extract) ## Send a real .html 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 .html reader →](https://txtfetch.com/tools/html-to-text) --- # 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. the-problem Scans and photos aren't 'documents' to most extraction stacks. They're a special case. Handling them usually means a separate OCR service, with its own SDK and its own error handling. It also means a branch in your ingestion code, to detect when a file needs OCR instead of ordinary parsing. Standing that up, and keeping it patched, is its own project. one-request-solution In txtfetch, OCR isn't a separate code path. It's just what happens when pixels arrive. POST a PNG, JPG, or TIFF (or a scanned PDF), and Tesseract OCR runs automatically behind the same /v1/extract endpoint. You get back the same { status, extracted\_text } shape as any other format. 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) } ``` 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/uploads/receipt-photo.jpg" \ -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/uploads/receipt-photo.jpg"}, ) print(r.json()["extracted_text"]) ``` JavaScript ```javascript const endpoint = new URL("https://api.txtfetch.com/v1/extract"); endpoint.searchParams.set("url", "https://example.com/uploads/receipt-photo.jpg"); 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/uploads/receipt-photo.jpg") 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 .png 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 - `.png` - `.jpg` - `.jpeg` - `.tiff` - `.tif` - `.bmp` - `.gif` faq **How do I OCR a scanned document via API?**: POST the image as multipart form data to https://api.txtfetch.com/v1/extract. Tesseract OCR runs automatically. The response is { "status": "success", "extracted_text": "..." }, with no OCR-specific parameters needed. **Which image formats are supported for OCR?**: PNG, JPG/JPEG, TIFF, BMP, and GIF are all routed through Tesseract OCR automatically, the same as scanned PDF pages with no text layer. **Do I need to tell txtfetch that a file needs OCR?**: No. Format detection is automatic. Any file with no extractable text layer, image or PDF, is OCR'd without any extra flag or parameter. go-further - [Read the .png guide →](https://txtfetch.com/blog/ocr-scanned-documents-api) - [Multi-page TIFF faxes need a different read →](https://txtfetch.com/extract/tiff) - [Not sure a scan will OCR cleanly? Check it free, in your browser →](https://txtfetch.com/tools/image-ocr-check) - [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) Images & OCR - [`.tiff`](https://txtfetch.com/extract/tiff) - [All formats →](https://txtfetch.com/extract) ## Send a real .png 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 .png reader →](https://txtfetch.com/tools/image-ocr-check) --- # 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. the-problem Before OOXML, Office files were OLE2, the Compound File Binary Format. That's essentially a tiny FAT filesystem embedded in one file, streams and all. It shares nothing structurally with the zip-of-XML that .docx/.xlsx/.pptx use. So a parser built for the modern formats throws outright on a legacy one, rather than degrading gracefully. Word's .doc format compounds this. The document text sits in a WordDocument stream as a piece table. That's a list of byte ranges that must be reassembled in a specific order to produce readable text. Read the stream's raw bytes start to finish and you get scrambled, out-of-order text. It's not garbage exactly, but it's wrong. There's also a classic naming trap. Plenty of files with a .doc extension are actually RTF or a renamed OOXML file underneath. Extension-based routing gets those wrong before extraction even starts. one-request-solution txtfetch detects OLE2's compound-file signature from the file's actual bytes, not its extension. It routes the file to Tika's POI-backed legacy parsers: HWPF for .doc, HSSF for .xls, HSLF for .ppt. Those parsers understand the piece table and reassemble .doc text in the correct reading order. Because detection is byte-based, a file named .doc that's really RTF or renamed OOXML still gets parsed correctly instead of misrouted. curl ```curl curl -X POST https://api.txtfetch.com/v1/extract \ -H "Authorization: Bearer $TXTFETCH_KEY" \ -F file=@1998-contract-template.doc ``` Python ```python import os import requests with open("1998-contract-template.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("1998-contract-template.doc")]); const form = new FormData(); form.append("file", file, "1998-contract-template.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("1998-contract-template.doc") if err != nil { panic(err) } defer f.Close() var body bytes.Buffer writer := multipart.NewWriter(&body) part, err := writer.CreateFormFile("file", "1998-contract-template.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) } ``` 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/legacy-invoice.xls" \ -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/legacy-invoice.xls"}, ) 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/legacy-invoice.xls"); 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/legacy-invoice.xls") 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 .doc 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 - `.doc` - `.xls` - `.ppt` - `.pub` - `.wpd` faq **Why does my .doc file come out as scrambled or out-of-order gibberish?**: The legacy .doc format stores text as a piece table: byte ranges in the WordDocument stream that aren't laid out in reading order. A byte-scraping extractor that reads the stream start-to-finish gets the pieces in the wrong order. Tika's HWPF parser reassembles them correctly. **Do modern, OOXML-only libraries support .doc and .xls at all?**: Usually not. Many libraries built only for .docx/.xlsx/.pptx throw an error on an OLE2 file rather than degrading. txtfetch routes OLE2 signatures to dedicated legacy parsers instead: POI's HWPF, HSSF, and HSLF. **What if a file is named .doc but is actually RTF or a renamed OOXML file?**: txtfetch detects the real format from the file's byte signature, not its extension, so a mislabeled file still routes to the correct parser. **Are .pub (Publisher) and .wpd (WordPerfect) files supported too?**: Yes. Both are legacy binary formats handled through the same Apache Tika pipeline as .doc/.xls/.ppt. go-further - [Read the .doc guide →](https://txtfetch.com/blog/parse-office-docs-docx-pptx-xlsx-for-llms) - [Drop a real .doc and see the exact text it extracts to, free →](https://txtfetch.com/tools/doc-to-text) - [Drop a real .xls and see every sheet's text, free →](https://txtfetch.com/tools/xls-to-text) - [Drop a real .ppt and see its slide text and speaker notes, free →](https://txtfetch.com/tools/ppt-to-text) - [The modern OOXML formats (.docx/.pptx/.xlsx) →](https://txtfetch.com/extract/docx) - [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 - [`.docx`](https://txtfetch.com/extract/docx) - [`.pptx`](https://txtfetch.com/extract/pptx) - [`.xlsx`](https://txtfetch.com/extract/xlsx) - [`.rtf`](https://txtfetch.com/extract/rtf) - [All formats →](https://txtfetch.com/extract) ## Send a real .doc 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 .doc reader →](https://txtfetch.com/tools/doc-to-text) --- # 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. the-problem It's easy to assume .msg is just email in a different wrapper. Structurally, though, it has nothing to do with .eml. A .msg file is an OLE2 compound-file container, the same binary-filesystem-in-a-file format as legacy .doc/.xls. It holds MAPI property streams named things like \_\_substg1.0\_37010102 for the body, or \_\_substg1.0\_1000001E for plain text. There's no MIME and no RFC-822 headers to parse. Every .eml/RFC-822 parser fails on a .msg outright, because it isn't looking at the right binary shape at all. Attachments are embedded OLE storages inside the same container. A forwarded or embedded email shows up as a .msg nested inside a .msg. Zoom out further, and .pst/.ost files aren't single messages at all. They're whole mailbox databases, potentially thousands of messages in one binary file. one-request-solution txtfetch detects the OLE2 signature and routes .msg through Tika's MAPI-aware parser. That parser reads the property streams directly, body, headers-as-properties, and attachments, rather than expecting MIME. Nested .msg-in-.msg attachments are walked recursively. And .pst/.ost mailbox files are unpacked message by message. All of it goes through the same /v1/extract endpoint and response shape as a plain .eml. curl ```curl curl -X POST https://api.txtfetch.com/v1/extract \ -H "Authorization: Bearer $TXTFETCH_KEY" \ -F file=@forwarded-thread.msg ``` Python ```python import os import requests with open("forwarded-thread.msg", "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("forwarded-thread.msg")]); const form = new FormData(); form.append("file", file, "forwarded-thread.msg"); 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("forwarded-thread.msg") if err != nil { panic(err) } defer f.Close() var body bytes.Buffer writer := multipart.NewWriter(&body) part, err := writer.CreateFormFile("file", "forwarded-thread.msg") 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/exports/mailbox-2024.pst" \ -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/exports/mailbox-2024.pst"}, ) print(r.json()["extracted_text"]) ``` JavaScript ```javascript const endpoint = new URL("https://api.txtfetch.com/v1/extract"); endpoint.searchParams.set("url", "https://example.com/exports/mailbox-2024.pst"); 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/exports/mailbox-2024.pst") 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 .msg 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. 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 ``` [See all ten documents, with the full explanation →](https://txtfetch.com/diff) formats-covered - `.msg` - `.eml` - `.mbox` - `.pst` - `.ost` faq **Why does my .eml parser fail on a .msg file?**: Because .msg isn't RFC-822/MIME at all. It's an OLE2 compound-file container with MAPI property streams. Any parser built around parsing email headers and MIME parts is looking for a structure that simply isn't there in a .msg file. **How is .msg different from the .eml page on this site?**: /extract/email covers RFC-822 .eml and .mbox: text-based, MIME-encoded formats. This page covers .msg, .pst, and .ost instead: OLE2-based Outlook formats with a completely different binary structure. The end result, message text, looks the same in the API response either way. **Can a .msg file contain another .msg file?**: Yes. A forwarded or embedded message shows up as a nested OLE2 storage inside the parent .msg. txtfetch walks these recursively rather than stopping at the first level. **Can I extract a whole .pst or .ost mailbox in one request?**: Yes. .pst and .ost are whole mailbox databases rather than single messages. txtfetch unpacks and extracts the contained messages' text through the same endpoint. go-further - [RFC-822 .eml and .mbox (a different format, despite the overlap) →](https://txtfetch.com/extract/email) - [Drop a real .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 - [`.eml`](https://txtfetch.com/extract/email) - [All formats →](https://txtfetch.com/extract) ## Send a real .msg 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 .msg reader →](https://txtfetch.com/tools/email-to-text) --- # 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. the-problem OpenDocument files are a zip archive containing content.xml, the actual document text and structure, plus separate styles.xml and meta.xml parts. Text lives inside paragraph and run elements. That's comparable to OOXML's structure, but with its own vocabulary. Not every ODF file is even zipped. The 'flat' variants, .fodt and similarly flat ODS/ODP, are plain single-file XML with no zip container at all. That breaks any extractor that assumes 'ODF means unzip first'. And then there's tracked changes, the trap nobody expects. Deleting a paragraph in LibreOffice with change tracking on doesn't remove it from the file. Instead it moves into a block near the top of . The text is still there, so 'the text of this document' stops being a question with one answer. one-request-solution txtfetch reads content.xml's paragraph and span structure directly. It handles the flat single-file XML variants without assuming a zip container is present. So a .fodt goes through the same endpoint as a zipped .odt. On tracked changes it does the honest thing rather than the convenient one. The document's full text content comes back, deletions included, because that text is genuinely in the file. This is worth knowing before you index legal or HR documents. If you only want accepted text, resolve the changes in LibreOffice, or strip from content.xml, before extracting. curl ```curl curl -X POST https://api.txtfetch.com/v1/extract \ -H "Authorization: Bearer $TXTFETCH_KEY" \ -F file=@meeting-minutes.odt ``` Python ```python import os import requests with open("meeting-minutes.odt", "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("meeting-minutes.odt")]); const form = new FormData(); form.append("file", file, "meeting-minutes.odt"); 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("meeting-minutes.odt") if err != nil { panic(err) } defer f.Close() var body bytes.Buffer writer := multipart.NewWriter(&body) part, err := writer.CreateFormFile("file", "meeting-minutes.odt") 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/docs/project-charter.odt" \ -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/docs/project-charter.odt"}, ) print(r.json()["extracted_text"]) ``` JavaScript ```javascript const endpoint = new URL("https://api.txtfetch.com/v1/extract"); endpoint.searchParams.set("url", "https://example.com/docs/project-charter.odt"); 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/docs/project-charter.odt") 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 .odt 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 - `.odt` - `.ods` - `.odp` - `.fodt` - `.otp` - `.ott` faq **Does extracted text include content that was deleted via tracked changes?**: Yes, and it's worth planning for. OpenDocument keeps deleted text in a block instead of removing it from the file. Extraction returns the document's full text content, deletions included. Because that block sits near the start of , a struck-out paragraph can even appear ahead of the text that's actually in the document. If your index must hold only accepted text, accept or reject the changes in LibreOffice first, or filter out of content.xml yourself. **Are the flat XML variants like .fodt supported?**: Yes. Flat ODF files are single-file XML with no zip container, unlike regular .odt/.ods/.odp. txtfetch's parser handles both the zipped and flat forms. **How is this different from a .docx or .pptx?**: Same general shape: a container with a structured markup part. But it's a completely different vocabulary and packaging, produced by LibreOffice/OpenOffice/Google Docs' ODF export rather than Microsoft's OOXML. **Are .ott and .otp templates supported the same way?**: Yes. Templates use the same content.xml structure as their non-template counterparts and go through the same endpoint. go-further - [Read the .odt guide →](https://txtfetch.com/blog/parse-office-docs-docx-pptx-xlsx-for-llms) - [Drop a real .odt (or flat .fodt) and see the exact text it extracts to, free →](https://txtfetch.com/tools/odt-to-text) - [Drop a real .ods (or flat .fods) and see every sheet's text, free →](https://txtfetch.com/tools/ods-to-text) - [Drop a real .odp (or flat .fodp) and see its slide text and speaker notes, free →](https://txtfetch.com/tools/odp-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) more-formats - [All formats →](https://txtfetch.com/extract) ## Send a real .odt 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 .odt reader →](https://txtfetch.com/tools/odt-to-text) --- # https://txtfetch.com/extract/pdf # Extract text from any PDF, one request. Multi-column academic papers. Dense financial tables. Scanned contracts. One endpoint returns all of them in the same response shape, with no per-layout tuning. the-problem PDF is the least standardized 'standard' in document formats. Multi-column layouts confuse naive text extraction. Tables collapse into unreadable strings. Some pages are scans with no text layer at all. Most teams bolt together a PDF library for the easy cases and a separate OCR pipeline for the scanned ones. That means two code paths to maintain and two sets of edge cases to debug. VLM-based extractors that read pages as images do fine on a single clean scan. They struggle with mixed batches, like a folder of digital-native reports next to faxed scans. That breaks the assumption that every page is a picture. one-request-solution txtfetch takes any PDF, digital-native or scanned, and always gives back the same JSON shape. Text-layer pages go straight through Apache Tika. Pages with no text layer route through Tesseract OCR automatically, in the same request. That's one code path for every PDF in your pipeline. curl ```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) } ``` 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.pdf" \ -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.pdf"}, ) 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.pdf"); const res = await fetch(endpoint, { method: "POST", headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` }, }); const { extracted_text } = await res.json(); console.log(extracted_text); ``` Go ```go package main import ( "encoding/json" "fmt" "net/http" "net/url" "os" ) type extractResponse struct { Status string `json:"status"` ExtractedText string `json:"extracted_text"` } func main() { endpoint, err := url.Parse("https://api.txtfetch.com/v1/extract") if err != nil { panic(err) } q := endpoint.Query() q.Set("url", "https://example.com/whitepaper.pdf") endpoint.RawQuery = q.Encode() req, err := http.NewRequest("POST", endpoint.String(), nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result extractResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Println(result.ExtractedText) } ``` ``` { "status": "success", "extracted_text": "..." } ``` what-comes-back That is the shape. This is the text. A real .pdf from the benchmark corpus, word-diffed against the raw parser output. Nothing here was written for this page. Every character comes from a committed fixture. Q3 2026 Regional Performance Report (2-column PDF) `q3-2026-regional-performance.pdf` · application/pdf · left pane read via Apache Tika, **right pane at premium (VLM)** raw parser output ``` Q3 2026 Regional Performance Report Q3 was a strong quarter across every region, driven Support tickets rose 8% quarter over quarter, concentrated by the launch of the structured-output beta and continued in the API-key rotation flow; a fix shipped mid-quarter expansion in EMEA. Renewal rates held above 92% despite reduced volume by half within two weeks. the price increase that took effect in July. Region Revenue Growth North America $5.1M 11% EMEA $3.4M 24% APAC $1.8M 15% ``` what txtfetch returns ``` # Q3 2026 Regional Performance Report Q3 was a strong quarter across every region, driven by the launch of the structured-output beta and continued expansion in EMEA. Renewal rates held above 92% despite the price increase that took effect in July. Support tickets rose 8% quarter over quarter, concentrated in the API-key rotation flow; a fix shipped mid-quarter reduced volume by half within two weeks. | Region | Revenue | Growth | | --- | --- | --- | | North America | $5.1M | 11% | | EMEA | $3.4M | 24% | | APAC | $1.8M | 15% | ``` [Columns out of order →](https://txtfetch.com/fixes/columns-out-of-order) **Show 2 more .pdf documents** Q3 Product Catalog (5-column pricing table) `q3-product-catalog.pdf` · application/pdf · left pane read via Apache Tika, **right pane at premium (VLM)** raw parser output ``` Q3 Product Catalog Pricing reflects the July rate card; stock levels are warehouse totals as of quarter-end. Product Monthly Annual Stock WD-100 Widget Pro $29 $290 120 WD-200 Widget Mini $9 $90 340 WD-300 Widget Max $59 $590 45 ``` what txtfetch returns ``` # Q3 Product Catalog Pricing reflects the July rate card; stock levels are warehouse totals as of quarter-end. | SKU | Product | Monthly | Annual | Stock | | --- | --- | --- | --- | --- | | WD-100 | Widget Pro | $29 | $290 | 120 | | WD-200 | Widget Mini | $9 | $90 | 340 | | WD-300 | Widget Max | $59 | $590 | 45 | | WD-400 | Widget Nano | $5 | $50 | 610 | ``` [Tables losing structure →](https://txtfetch.com/fixes/tables-lose-structure) Security Brief (single-column PDF) `security-brief.pdf` · application/pdf · left pane read via Apache Tika, **right pane: the human-checked expected.json** raw parser output ``` Security Brief This document describes the security posture of the txtfetch extraction pipeline. Every document is treated as hostile until parsed: zip-bomb guards, SSRF-guarded fetches, and a hard extraction budget all run before Tika ever sees the bytes. Threat Model Untrusted input arrives as arbitrary bytes from a URL fetch or a direct upload. Secrets and document content are never logged; only allowlisted metadata fields reach stdout. ``` the human-checked ideal ``` Security Brief This document describes the security posture of the txtfetch extraction pipeline. Every document is treated as hostile until parsed: zip-bomb guards, SSRF-guarded fetches, and a hard extraction budget all run before Tika ever sees the bytes. Threat Model Untrusted input arrives as arbitrary bytes from a URL fetch or a direct upload. Secrets and document content are never logged; only allowlisted metadata fields reach stdout. ``` [See all ten documents, with the full explanation →](https://txtfetch.com/diff) formats-covered - `.pdf` response-options Need Markdown with real tables instead of flattened text? Or a typed element JSON tree with page and offset per block? ?format=markdown and ?format=json both cover PDFs. See /output for the same document rendered all three ways. faq **How do I extract text from a PDF?**: POST the file as multipart form data to https://api.txtfetch.com/v1/extract. Or pass a url parameter, and txtfetch fetches it server-side. Either way you get back { "status": "success", "extracted_text": "..." }. **Does it handle scanned PDFs, not just digital-native ones?**: Yes. Pages with no text layer route through Tesseract OCR automatically, in the same request, with the same response shape. You don't need to detect or flag scanned pages yourself. **What about multi-column layouts and tables?**: Apache Tika parses the underlying PDF structure rather than guessing from pixel positions. That means multi-column academic papers and tabular financial reports come out as readable, ordered text. **Is there a page limit?**: No. One extraction request is one document, regardless of length. A 300-page PDF still counts as a single request. go-further - [Read the .pdf guide →](https://txtfetch.com/blog/extract-text-from-pdf-for-rag) - [See your PDF's text right now, free →](https://txtfetch.com/tools/pdf-to-text) - [No text layer at all? See the scanned-PDF page →](https://txtfetch.com/extract/scanned-pdf) - [Not sure if your PDF has a text layer? Check it free →](https://txtfetch.com/tools/pdf-text-check) - [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) PDF & scans - [`.pdf (scanned)`](https://txtfetch.com/extract/scanned-pdf) - [All formats →](https://txtfetch.com/extract) ## Send a real .pdf (digital) through it. One HTTP call returns the text. Read one in your browser first, for free. [Get an API key →](https://app.txtfetch.com/signup) [Open the free .pdf (digital) reader →](https://txtfetch.com/tools/pdf-to-text) --- # 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. the-problem A .pptx is an OOXML zip archive. Each slide's visible text lives in ppt/slides/slideN.xml. But speaker notes are a separate part entirely, ppt/notesSlides/, with its own XML files and its own relationship IDs back to the slide they annotate. Extractors that read slideN.xml and stop miss the notes silently. Extractors built around the notes miss the body instead. Text also hides in grouped shapes, nested inside other shapes rather than top-level. It hides in SmartArt diagrams too: their text sits in diagrams/data\*.xml, disconnected from the visible diagram layout. Tables can hide text as well, and so can text baked into slide images as pixels rather than markup. Slide order compounds all of this. It's defined by relationships in presentation.xml, not by the numeric order slide files happen to be zipped in. one-request-solution txtfetch's Office parser reads both parts of a .pptx in one pass: slide body text from ppt/slides/, and speaker notes from ppt/notesSlides/. It also pulls grouped-shape and SmartArt text out of their nested XML. All of it folds into one extracted\_text response, in the deck's real slide order. 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) } ``` 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/decks/product-roadmap.pptx" \ -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/decks/product-roadmap.pptx"}, ) print(r.json()["extracted_text"]) ``` JavaScript ```javascript const endpoint = new URL("https://api.txtfetch.com/v1/extract"); endpoint.searchParams.set("url", "https://example.com/decks/product-roadmap.pptx"); 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/decks/product-roadmap.pptx") 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 .pptx 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. 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 ``` [See all ten documents, with the full explanation →](https://txtfetch.com/diff) formats-covered - `.pptx` - `.ppt` - `.pptm` - `.potx` - `.odp` response-options Presenting slide structure to an LLM? ?format=markdown returns headings and bullet hierarchy per slide instead of one flat block of text. faq **Does extraction include PowerPoint speaker notes, or just slide text?**: Both. Speaker notes live in a separate ppt/notesSlides/ part of the .pptx archive, apart from the slide body. txtfetch reads both parts and includes them together in the response. **What about text inside grouped shapes or SmartArt diagrams?**: Yes. Grouped shapes nest inside other shapes rather than sitting at the top level, and SmartArt text lives in its own diagrams/data*.xml part. Both are walked and included, not just top-level slide text boxes. **Does it preserve the actual slide order?**: Yes. Slide order comes from presentation.xml's relationship list, not from the filename order of the slideN.xml parts inside the zip. That filename order doesn't reliably match reading order. **Does it handle the legacy .ppt binary format too?**: Yes, and .pptm (macro-enabled) and .potx (template) too. All four route through the same endpoint and return the same response shape. go-further - [Read the .pptx guide →](https://txtfetch.com/blog/parse-office-docs-docx-pptx-xlsx-for-llms) - [Drop a real .pptx and see the exact text (and speaker notes) it extracts to, free →](https://txtfetch.com/tools/pptx-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) Office - [`.docx`](https://txtfetch.com/extract/docx) - [`.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 .pptx 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 .pptx reader →](https://txtfetch.com/tools/pptx-to-text) --- # 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. the-problem RTF looks like it should be easy to strip. It's plain ASCII text with backslash control words. That's exactly why so many home-grown parsers get it wrong. Non-ASCII characters are hex-escaped as \\'hh, a byte in the document's code page, set by an \\ansicpg control word like \\ansicpg1252 earlier in the file. Or they're written as \\uN?, a Unicode code point immediately followed by an ANSI fallback character meant for readers that don't support \\u. A regex that just deletes backslash sequences leaves that fallback character behind. So every accented letter, curly quote, or em dash gets duplicated in the output. Embedded objects and images are stored as long hex blobs inline in the control-word stream. A naive stripper will happily interpret those blobs as more 'text' if it isn't specifically built to recognize and skip them. one-request-solution txtfetch runs RTF through Apache Tika's actual RTF parser. It tracks the code page from \\ansicpg, and resolves \\'hh hex escapes and \\uN? Unicode-plus-fallback pairs correctly, keeping the Unicode character and dropping the fallback. It also recognizes embedded-object hex blobs as binary data rather than text. The response is clean prose, not a document with every special character doubled. curl ```curl curl -X POST https://api.txtfetch.com/v1/extract \ -H "Authorization: Bearer $TXTFETCH_KEY" \ -F file=@signed-agreement.rtf ``` Python ```python import os import requests with open("signed-agreement.rtf", "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("signed-agreement.rtf")]); const form = new FormData(); form.append("file", file, "signed-agreement.rtf"); 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("signed-agreement.rtf") if err != nil { panic(err) } defer f.Close() var body bytes.Buffer writer := multipart.NewWriter(&body) part, err := writer.CreateFormFile("file", "signed-agreement.rtf") 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/legal/terms-v3.rtf" \ -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/legal/terms-v3.rtf"}, ) print(r.json()["extracted_text"]) ``` JavaScript ```javascript const endpoint = new URL("https://api.txtfetch.com/v1/extract"); endpoint.searchParams.set("url", "https://example.com/legal/terms-v3.rtf"); 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/legal/terms-v3.rtf") 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 .rtf 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 - `.rtf` faq **Why do accented characters or curly quotes appear twice in my extracted RTF text?**: That's the classic symptom of naive backslash-stripping. RTF's \uN? Unicode escape is followed by a plain-ASCII fallback character by design. A regex that just deletes backslash sequences leaves the fallback character behind next to the real one. A real RTF parser resolves the pair correctly instead. **Does character encoding vary between RTF files?**: Yes. The code page for \'hh hex escapes is set per-document by a \ansicpg control word, commonly \ansicpg1252 for Windows-1252. So the same hex byte can mean a different character in different RTF files. Tika reads the declared code page rather than assuming one. **What happens to embedded images or OLE objects in an RTF file?**: They're stored as hex-encoded binary blobs inline in the RTF stream. Tika's parser recognizes and skips them as binary data rather than attempting to read them as text. go-further - [Drop a real .rtf and see the parsed text — 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) Office - [`.docx`](https://txtfetch.com/extract/docx) - [`.pptx`](https://txtfetch.com/extract/pptx) - [`.xlsx`](https://txtfetch.com/extract/xlsx) - [`.doc`](https://txtfetch.com/extract/legacy-office) - [All formats →](https://txtfetch.com/extract) ## Send a real .rtf 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 .rtf reader →](https://txtfetch.com/tools/file-to-text) --- # 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. the-problem A scanned PDF has no text layer at all. It's a picture of a document wearing PDF packaging. A naive text-extraction pass on it reads back nothing but whitespace. Real scans add their own damage on top. There's skew and rotation from the sheet feeder. Low-DPI captures under ~200 dpi blur character edges past recognition. And fax-style CCITT Group 4 compression hits bitonal pages. The sharpest trap is the mixed document: a digitally-created cover page stapled ahead of a faxed, scanned body. A page-one text pass on that document comes back non-empty. So a system that only checks 'did we get any text' never learns that the rest of the file is a picture. one-request-solution txtfetch always tries the fast path first: a direct text-layer extraction. Only when the \*entire\* document comes back whitespace does it retry the whole file. That retry runs through Tika's ocr\_only PDF strategy with Tesseract underneath. It's the same request and the same { status, extracted\_text } shape, with no ocr=true flag to set. That escalation is deliberately whole-document. So the mixed cover-page-plus-scanned-body case above won't auto-escalate, because the first pass isn't blank. For those documents, or anywhere page-by-page fidelity matters more than the extra latency, pass ?quality=premium. Every page then routes through a vision-capable model instead of the text/OCR fork. So a digital cover page and a scanned body both get read consistently. curl ```curl curl -X POST https://api.txtfetch.com/v1/extract \ -H "Authorization: Bearer $TXTFETCH_KEY" \ -F file=@faxed-inspection-report.pdf ``` Python ```python import os import requests with open("faxed-inspection-report.pdf", "rb") as f: r = requests.post( "https://api.txtfetch.com/v1/extract", headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"}, files={"file": f}, ) print(r.json()["extracted_text"]) ``` JavaScript ```javascript import { readFile } from "node:fs/promises"; const file = new Blob([await readFile("faxed-inspection-report.pdf")]); const form = new FormData(); form.append("file", file, "faxed-inspection-report.pdf"); const res = await fetch("https://api.txtfetch.com/v1/extract", { method: "POST", headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` }, body: form, }); const { extracted_text } = await res.json(); console.log(extracted_text); ``` Go ```go package main import ( "bytes" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "os" ) type extractResponse struct { Status string `json:"status"` ExtractedText string `json:"extracted_text"` } func main() { f, err := os.Open("faxed-inspection-report.pdf") if err != nil { panic(err) } defer f.Close() var body bytes.Buffer writer := multipart.NewWriter(&body) part, err := writer.CreateFormFile("file", "faxed-inspection-report.pdf") if err != nil { panic(err) } if _, err := io.Copy(part, f); err != nil { panic(err) } writer.Close() req, err := http.NewRequest("POST", "https://api.txtfetch.com/v1/extract", &body) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY")) req.Header.Set("Content-Type", writer.FormDataContentType()) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result extractResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Println(result.ExtractedText) } ``` Or skip the download. Pass a `url` parameter and txtfetch fetches the document server-side: curl ```curl curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/scans/site-survey.pdf" \ -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/scans/site-survey.pdf"}, ) print(r.json()["extracted_text"]) ``` JavaScript ```javascript const endpoint = new URL("https://api.txtfetch.com/v1/extract"); endpoint.searchParams.set("url", "https://example.com/scans/site-survey.pdf"); const res = await fetch(endpoint, { method: "POST", headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` }, }); const { extracted_text } = await res.json(); console.log(extracted_text); ``` Go ```go package main import ( "encoding/json" "fmt" "net/http" "net/url" "os" ) type extractResponse struct { Status string `json:"status"` ExtractedText string `json:"extracted_text"` } func main() { endpoint, err := url.Parse("https://api.txtfetch.com/v1/extract") if err != nil { panic(err) } q := endpoint.Query() q.Set("url", "https://example.com/scans/site-survey.pdf") endpoint.RawQuery = q.Encode() req, err := http.NewRequest("POST", endpoint.String(), nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result extractResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Println(result.ExtractedText) } ``` ``` { "status": "success", "extracted_text": "..." } ``` what-comes-back That is the shape. This is the text. A real .pdf from the benchmark corpus, word-diffed against the raw parser output. Nothing here was written for this page. Every character comes from a committed fixture. 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 ``` [PDF extracts no text (the OCR escalation path) →](https://txtfetch.com/fixes/pdf-extracts-no-text) [See all ten documents, with the full explanation →](https://txtfetch.com/diff) formats-covered - `.pdf` response-options Mixed text-and-scan PDF? ?quality=premium reads every page through a vision model instead of the whole-document text/OCR fork. A digital cover page and a scanned body both come back accurately. faq **Why is my scanned PDF coming back empty or blank?**: If Tesseract still finds no readable text after the automatic OCR retry, the request returns an extraction_failed error rather than guessing. That can happen with an extremely low-resolution scan, a blank page, or a corrupted image stream. Try ?quality=premium for a second read via a vision model. **Do I need to tell txtfetch a PDF is scanned?**: No, there's no ocr=true parameter for PDFs. txtfetch runs a text pass first. It only escalates to OCR automatically when that pass comes back whitespace across the whole document. **What about a PDF with a digital cover page and scanned pages after it?**: That's the one case whole-document escalation misses. The first pass isn't blank, so OCR never kicks in for the rest. Pass ?quality=premium for these mixed documents. It reads every page through a vision model instead of relying on the text-or-OCR fork. **Does OCR handle skewed or low-quality fax scans?**: Tesseract corrects moderate skew and reads CCITT Group 4 fax-compressed bitonal pages. Accuracy drops, though, on very low-DPI (under ~150 dpi) source scans. See /benchmarks for measured accuracy by document category. go-further - [Read the .pdf guide →](https://txtfetch.com/blog/ocr-scanned-documents-api) - [Digital-native PDFs and the text-layer fast path →](https://txtfetch.com/extract/pdf) - [See exactly which of your PDF's pages have real text — free, page by page →](https://txtfetch.com/tools/pdf-to-text) - [Multi-page TIFF faxes need a different read →](https://txtfetch.com/extract/tiff) - [Measured OCR accuracy by document category →](https://txtfetch.com/benchmarks) - [Is your PDF scanned, mixed, or text — find out free →](https://txtfetch.com/tools/pdf-text-check) - [Not sure a scan will OCR cleanly? Check it free, in your browser →](https://txtfetch.com/tools/image-ocr-check) - [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) PDF & scans - [`.pdf (digital)`](https://txtfetch.com/extract/pdf) - [All formats →](https://txtfetch.com/extract) ## Send a real .pdf (scanned) 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 .pdf (scanned) reader →](https://txtfetch.com/tools/pdf-to-text) --- # 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. the-problem Unlike PNG or JPEG, TIFF is a container format. It's a chain of Image File Directories (IFDs), each pointing to the next, and each holding one page's worth of image data. A generic image-loading library built around 'one file, one image' reads the first IFD, produces a picture, and stops. The rest of a multi-page fax or scanned document simply never gets read, with no error to signal that pages are missing. Fax-originated TIFFs add compression variety on top. Their pages are bitonal (1-bit) and compressed with CCITT Group 3 or Group 4 fax encoding. Resolutions are often asymmetric too, like 200×100 dpi, twice the horizontal detail of vertical. A generic OCR pipeline tuned for square-pixel photos doesn't expect that. Other TIFFs use LZW compression, or embed a full JPEG per page instead. All of these encodings still need to decode correctly before OCR ever runs. one-request-solution txtfetch's image path walks the full IFD chain, decoding every page: CCITT G3/G4 fax-compressed, LZW, or JPEG-in-TIFF. It runs OCR on each one, not just the first. Because detection classifies TIFF as an image, OCR always runs automatically. There's no flag to set to make multi-page decoding or OCR happen. curl ```curl curl -X POST https://api.txtfetch.com/v1/extract \ -H "Authorization: Bearer $TXTFETCH_KEY" \ -F file=@faxed-purchase-order.tiff ``` Python ```python import os import requests with open("faxed-purchase-order.tiff", "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("faxed-purchase-order.tiff")]); const form = new FormData(); form.append("file", file, "faxed-purchase-order.tiff"); 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("faxed-purchase-order.tiff") if err != nil { panic(err) } defer f.Close() var body bytes.Buffer writer := multipart.NewWriter(&body) part, err := writer.CreateFormFile("file", "faxed-purchase-order.tiff") 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/scans/multi-page-fax.tif" \ -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/scans/multi-page-fax.tif"}, ) print(r.json()["extracted_text"]) ``` JavaScript ```javascript const endpoint = new URL("https://api.txtfetch.com/v1/extract"); endpoint.searchParams.set("url", "https://example.com/scans/multi-page-fax.tif"); 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/scans/multi-page-fax.tif") 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 .tiff 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 - `.tiff` - `.tif` - `.jpg` - `.png` - `.bmp` - `.webp` - `.gif` faq **Why am I only getting the first page of a multi-page TIFF?**: TIFF stores multiple pages as a chain of Image File Directories (IFDs). Many generic image loaders are built around 'one file, one image' and only read the first IFD. txtfetch walks the full chain and OCRs every page. **Does it handle CCITT fax-compressed TIFFs, not just plain images?**: Yes. Bitonal fax scans compressed with CCITT Group 3 or Group 4 encoding decode and OCR correctly. That includes asymmetric resolutions like 200×100 dpi, common on faxed documents. **Do I need to set an OCR flag for TIFF files?**: No. Detection classifies any TIFF as an image, and images always run through Tesseract OCR automatically. There's no ocr parameter to set. **What about LZW-compressed or JPEG-in-TIFF files?**: Both are supported. The parser decodes whichever compression scheme the file actually uses, CCITT, LZW, or embedded JPEG per page, before handing pixels to OCR. go-further - [Read the .tiff guide →](https://txtfetch.com/blog/ocr-scanned-documents-api) - [Scanned PDFs use a different OCR-escalation path →](https://txtfetch.com/extract/scanned-pdf) - [Not sure a scan will OCR cleanly? Check it free, in your browser →](https://txtfetch.com/tools/image-ocr-check) - [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) Images & OCR - [`.png`](https://txtfetch.com/extract/image) - [All formats →](https://txtfetch.com/extract) ## Send a real .tiff 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 .tiff reader →](https://txtfetch.com/tools/image-ocr-check) --- # 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. the-problem A spreadsheet's cells don't hold their own text. In .xlsx, string cell values are indices into xl/sharedStrings.xml, a dedup table shared across the whole workbook, not inline text. So reading cells in isolation just gets you numbers pointing at other numbers. Formula cells store the formula itself, like =SUM(A1:A10). The number a human sees is a separately cached result value, and plenty of naive readers grab the wrong one, or the formula string, instead. Number formats add another layer of indirection. A cell can be stored as 0.42 and displayed as 42%. Or it can be stored as a serial integer and displayed as a date. So 'the value' and 'what's on screen' genuinely differ. Hidden sheets, hidden rows, and merged cells still carry meaning that a flat text dump can drop entirely. one-request-solution txtfetch resolves the indirection for you. Shared-string references become their actual text, and formula cells return Tika's cached computed value rather than the =SUM() source. That means the numbers in your response match what a person looking at the sheet would see. For table structure, with rows and columns kept intact instead of flattened into a wall of numbers, request ?format=markdown. curl ```curl curl -X POST https://api.txtfetch.com/v1/extract \ -H "Authorization: Bearer $TXTFETCH_KEY" \ -F file=@q1-revenue-workbook.xlsx ``` Python ```python import os import requests with open("q1-revenue-workbook.xlsx", "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("q1-revenue-workbook.xlsx")]); const form = new FormData(); form.append("file", file, "q1-revenue-workbook.xlsx"); 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("q1-revenue-workbook.xlsx") if err != nil { panic(err) } defer f.Close() var body bytes.Buffer writer := multipart.NewWriter(&body) part, err := writer.CreateFormFile("file", "q1-revenue-workbook.xlsx") 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/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) } ``` ``` { "status": "success", "extracted_text": "..." } ``` what-comes-back That is the shape. This is the text. A real .xlsx 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. 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 ``` [See all ten documents, with the full explanation →](https://txtfetch.com/diff) formats-covered - `.xlsx` - `.xls` - `.xlsm` - `.csv` - `.ods` response-options Need rows and columns, not a flat wall of numbers? ?format=markdown returns each sheet as a proper markdown table. faq **Does extraction return the formula or the calculated value?**: The calculated value. Tika reads the cached formula result that Excel stores alongside the formula, so you get 18200 instead of the literal string =SUM(A1:A10). **Do percentages and dates come back as raw numbers?**: No. The cell's number format is applied before the text is returned. A cell stored as 0.42 with a percent format comes back as 42%. A date stored as the serial number 46229 comes back as 2026-07-26. A cell with no format applied returns its stored value unchanged, which is what you want for a plain number. **Are hidden sheets and hidden rows included?**: Yes. txtfetch extracts hidden sheets and rows along with visible ones, since hiding a sheet in Excel doesn't remove its data. **Can I get real table structure instead of a flat text dump?**: Yes. Pass ?format=markdown, and rows and columns come back as a markdown table per sheet instead of space-separated cell values. go-further - [Read the .xlsx guide →](https://txtfetch.com/blog/extract-tables-for-rag) - [Drop a real .xlsx and see the exact text every sheet extracts to, free →](https://txtfetch.com/tools/xlsx-to-text) - [The wider Office-family parsing story (DOCX/PPTX/XLSX) →](https://txtfetch.com/blog/parse-office-docs-docx-pptx-xlsx-for-llms) - [Charset detection for plain .csv and other delimited exports →](https://txtfetch.com/extract/csv) - [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 - [`.docx`](https://txtfetch.com/extract/docx) - [`.pptx`](https://txtfetch.com/extract/pptx) - [`.doc`](https://txtfetch.com/extract/legacy-office) - [`.rtf`](https://txtfetch.com/extract/rtf) - [All formats →](https://txtfetch.com/extract) ## Send a real .xlsx 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 .xlsx reader →](https://txtfetch.com/tools/xlsx-to-text) --- # 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. the-problem Markup formats put real content in places a simple tag-stripper doesn't look. HTML entities like ’ or & need resolving back to the character they represent, not left as literal text or double-decoded. CDATA sections hold raw, unescaped content that a naive parser can either skip entirely or fail to close correctly. XML namespaces mean the 'same' tag name can mean two different things in two different parts of a document. And in plenty of real-world XML dialects, meaningful content lives in attribute values, not just element text. Think DITA and DocBook technical docs, JATS scientific articles, or XBRL financial filings. A text-node-only extractor misses that completely. Getting this right means resolving entities. It means walking the tree in document order, rather than the order attributes happen to be declared. And it means knowing which attributes are content, versus which are just structural scaffolding. one-request-solution txtfetch parses the actual XML tree rather than regex-stripping tags. Entities resolve to their real characters, CDATA content is read correctly, and element text comes back in document order with the tag scaffolding removed. For a typed, structured view of the same document, with element names and hierarchy preserved rather than flattened to prose, request ?format=json. curl ```curl curl -X POST https://api.txtfetch.com/v1/extract \ -H "Authorization: Bearer $TXTFETCH_KEY" \ -F file=@product-catalog.xml ``` Python ```python import os import requests with open("product-catalog.xml", "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("product-catalog.xml")]); const form = new FormData(); form.append("file", file, "product-catalog.xml"); 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("product-catalog.xml") if err != nil { panic(err) } defer f.Close() var body bytes.Buffer writer := multipart.NewWriter(&body) part, err := writer.CreateFormFile("file", "product-catalog.xml") 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/feeds/press-release.xml" \ -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/feeds/press-release.xml"}, ) print(r.json()["extracted_text"]) ``` JavaScript ```javascript const endpoint = new URL("https://api.txtfetch.com/v1/extract"); endpoint.searchParams.set("url", "https://example.com/feeds/press-release.xml"); 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/feeds/press-release.xml") 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 .xml 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 - `.xml` - `.json` - `.yaml` - `.html` - `.xhtml` - `.svg` response-options Need the element structure, not flattened prose? ?format=json returns a typed element tree instead of one text blob. faq **Does extraction resolve HTML/XML entities like & or ’?**: Yes. Entities resolve to the character they represent in the extracted text rather than being left as literal escape sequences. **Is content inside CDATA sections included?**: Yes. CDATA blocks are parsed as the raw content they contain, not skipped or double-escaped. **What about content stored in XML attributes rather than element text, like in DITA or XBRL files?**: Common technical and financial XML dialects (DITA, DocBook, JATS, XBRL) carry meaningful content in specific attributes, not just element text. txtfetch's XML handling accounts for this rather than only reading text nodes. **Can I get a structured element tree instead of flattened text?**: Yes. Pass ?format=json for a typed, hierarchical view of the document instead of prose-flattened text. go-further - [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) - [`.csv`](https://txtfetch.com/extract/csv) - [All formats →](https://txtfetch.com/extract) ## Send a real .xml 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 .xml reader →](https://txtfetch.com/tools/file-to-text) --- # 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. the-problem A folder of documents to ingest usually arrives zipped: an export from a CMS, a bulk upload, a batch of scanned forms. The typical pipeline is download, unzip, iterate the file list, call an extractor per file, and stitch the results back together. That's orchestration code, and it has nothing to do with the actual extraction problem. one-request-solution POST the .zip directly to txtfetch, and Apache Tika walks the archive. It extracts every contained file's text and folds it into one response. No unzip step, no per-file loop to write. 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 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/exports/batch-2024-06.zip" \ -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/exports/batch-2024-06.zip"}, ) print(r.json()["extracted_text"]) ``` JavaScript ```javascript const endpoint = new URL("https://api.txtfetch.com/v1/extract"); endpoint.searchParams.set("url", "https://example.com/exports/batch-2024-06.zip"); 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/exports/batch-2024-06.zip") 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 .zip 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 - `.zip` faq **Can I extract text from every file in a ZIP archive at once?**: Yes. POST the .zip to https://api.txtfetch.com/v1/extract, and txtfetch walks the archive. It returns the extracted text of every contained file in one { "status": "success", "extracted_text": "..." } response. **What file types can be inside the ZIP?**: Any format txtfetch supports: PDFs, Office documents, HTML, images, email, mixed within a single archive. **Do nested folders inside the ZIP matter?**: No. Tika walks the archive structure recursively, so nested directories inside the ZIP are handled the same as top-level files. go-further - [See what's inside a ZIP — free, in your browser (file list only) →](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) Books & archives - [`.epub`](https://txtfetch.com/extract/epub) - [All formats →](https://txtfetch.com/extract) ## Send a real .zip 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 .zip reader →](https://txtfetch.com/tools/file-to-text) --- # https://txtfetch.com/fixes # Your extraction succeeded. The text is still wrong. "status": "success" doesn't mean the text is usable. Mojibake, scrambled columns, fused words, and a dozen other quality failures all return a clean success. Find your symptom, see why it happens, and fix it. These are extraction-_quality_ problems. The response came back `success` and the text is still broken. Getting an actual API error instead? See the [error reference](https://txtfetch.com/docs/errors). Not sure your file is even the format you think it is? The [file type inspector](https://txtfetch.com/formats) fingerprints it from its bytes. New to some of the vocabulary below, like text layer, mojibake, or reading order? The [glossary of extraction terms](https://txtfetch.com/glossary) defines each one plainly. ## PDF - **[extraction returns empty text from a PDF](https://txtfetch.com/fixes/pdf-extracts-no-text)** ``` >>> extracted_text '' ``` See why, and how to fix it → - **[extraction returns nonsense glyphs from a PDF that clearly has real text](https://txtfetch.com/fixes/garbled-pdf-text)** ``` Q3 (cid:415)(cid:286)(cid:448)(cid:286)(cid:374)(cid:437)(cid:286) grew 34% year over year ``` See why, and how to fix it → ## Characters & encoding - **[extracted text is full of mojibake or replacement characters](https://txtfetch.com/fixes/mojibake-wrong-encoding)** ``` It won’t behave the way you expect—and “smart quotes” turn into this. ``` See why, and how to fix it → - **[missing spaces between words in extracted text](https://txtfetch.com/fixes/missing-spaces-between-words)** ``` Revenue grew 34% driven byEMEA demand.The board approved a newpricing tier. ``` See why, and how to fix it → - **[hyphenated line breaks left in extracted text](https://txtfetch.com/fixes/hyphenated-line-breaks)** ``` The report covers the inter- national deployment process in detail. ``` See why, and how to fix it → - **[unexpanded ligatures and curly punctuation in extracted text](https://txtfetch.com/fixes/ligatures-and-smart-punctuation)** ``` The office workflow relies on efficient filing — but ligature glyphs like fi and fl don't always expand back to plain letters. ``` See why, and how to fix it → ## Layout & structure - **[two-column PDF text comes out interleaved and scrambled](https://txtfetch.com/fixes/columns-out-of-order)** ``` Revenue grew 34% year over year, driven by The board approved a new enterprise pricing tier and a hiring plan targeting engineering and sales ``` See why, and how to fix it → - **[PDF or scanned tables flatten into unreadable space-separated text](https://txtfetch.com/fixes/tables-lose-structure)** ``` Name Amount Date Alice 120 2026-01-04 Bob 75 2026-01-11 Carol 410 2026-01-19 ``` See why, and how to fix it → - **[the same header, footer, or page number shows up in every chunk](https://txtfetch.com/fixes/headers-and-footers-in-every-chunk)** ``` Confidential — Acme Corp Internal Use Only ...page content... Confidential — Acme Corp Internal Use Only 3 ...more content... ``` See why, and how to fix it → - **[extracted text is hard-wrapped at the print page's line width](https://txtfetch.com/fixes/hard-wrapped-lines)** ``` This is a long paragraph that has been hard-wrapped by a naive text extraction pipeline instead of being reflowed into one continuous line the way a real paragraph normally would be represented in clean extracted text. ``` See why, and how to fix it → - **[extracted text is one giant undifferentiated blob with no paragraph breaks](https://txtfetch.com/fixes/no-paragraph-breaks)** ``` Sentence one keeps this block going. Sentence two keeps this block going without any blank line anywhere in it. Sentence three keeps this block going the exact same way, and so does every sentence after it for the rest of the document. ``` See why, and how to fix it → ## Office & file types - **[text I know is in a DOCX, PPTX, or XLSX file never shows up in the extracted output](https://txtfetch.com/fixes/office-text-missing)** ``` extracted_text: "Slide 4\n\nQ3 roadmap overview" // the deck's speaker notes on slide 4 — the actual talking points — never appear anywhere in the response. ``` See why, and how to fix it → ## Fix the text you already have. The free cleaner repairs this damage in your browser. Nothing leaves the page. [Clean up your text →](https://txtfetch.com/tools/clean-extracted-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/fixes/columns-out-of-order # My two-column PDF extracted with both columns mixed together. Every word from the page is usually still present, just not in the order a human reader would encounter it. That's a reading-order failure, and it's worse than missing text because it reads as plausible. looks-like ``` Revenue grew 34% year over year, driven by The board approved a new enterprise pricing tier and a hiring plan targeting engineering and sales ``` Two unrelated sentences from the left and right columns, spliced together mid-thought because both sit at the same height on the page. why-it-happens A two-column PDF page is drawn as two separate vertical blocks of text. But the underlying content stream doesn't necessarily record "block" as a concept. It records individual text-showing operations at (x, y) positions. An extractor that reads those operations in file order can interleave line one of the left column with line one of the right column. So can one that sorts primarily by vertical position with nothing reliable to break ties. That happens because both lines sit at roughly the same height on the page. The result reads as one long line that's actually two unrelated sentences spliced together mid-thought. This is genuinely worse than getting no text at all, because it looks plausible enough to pass a casual review before it's embedded. This is fundamentally a reading-order problem, not a missing-data problem. Every word from the page is usually present in the output somewhere, just not in the order a human reader would encounter them. confirm-it - Paste extracted text into the chunk previewer. A single line over 300 characters with no sentence-ending punctuation is flagged as a likely interleaved-column collapse. [Scan for collapsed long lines](https://txtfetch.com/tools/chunk-preview) fix-it-yourself Re-extract with layout-aware column detection pdftotext's -layout mode detects column boundaries from horizontal gaps. It preserves left-to-right, top-to-bottom reading order within each detected column. bash ``` pdftotext -layout input.pdf output.txt ``` Bucket words into columns yourself for persistent cases pdfplumber exposes each text object's bounding box directly. You can group words by horizontal position into columns, then sort each column top to bottom, when the built-in reflow still gets it wrong. python ``` import pdfplumber with pdfplumber.open("input.pdf") as pdf: for page in pdf.pages: words = page.extract_words() # bucket by word["x0"] into columns, then sort each column by word["top"] ``` what-txtfetch-does The default extraction runs Apache Tika's PDF parser (PDFBox underneath), which does layout-aware text extraction rather than raw stream order. That recovers correct reading order on most ordinary multi-column documents. But it isn't guaranteed on an unusual or irregular column structure. When it does get a document's columns wrong, pass ?quality=premium. Every page then routes through a vision-language model instead of Tika's text pass. That model reads the page the way a person does, rather than inferring order from glyph coordinates. Two caveats are worth knowing up front, both real. Premium always routes async, even for a one-page PDF, so expect a 202 response and a job\_id to poll rather than a synchronous body. If a cost or size guardrail trips, or the model call fails, the request safely falls back to the Tika baseline instead of erroring. That fallback is flagged as metadata.tier\_downgraded: true, with a downgrade\_reason. The gap between the two is measured, not asserted, though on a deliberately small slice. On the multi-column PDF in our benchmark corpus, reading-order fidelity goes from 50.0% at the standard tier to 100% at the premium tier. That premium figure covers only the 3 hardest documents in the corpus, not the whole 10-document set. See /benchmarks for the full methodology, the per-category breakdown, and the corpus caveats. what-it-costs-you-downstream Two columns spliced into one line get packed into the same chunk in the wrong order. A chunker has no way to know they're unrelated, so the resulting embedding represents neither column's actual meaning. An answer synthesized from that chunk risks blending two unrelated claims into one confident-sounding but wrong sentence. faq **Is column-scrambled text a missing-data problem?**: Usually not. Every word is typically still present in the output, just in the wrong order. The extractor read across columns instead of down one column at a time. That distinction matters for how you fix it: reordering, not re-extracting from scratch, is usually all that's needed. **Does layout-aware extraction always get column order right?**: It gets most ordinary two- and three-column layouts right by detecting horizontal gaps between columns. Unusual layouts, like overlapping text boxes, sidebars, or pull quotes breaking the column grid, can still confuse it. That's when a library like pdfplumber that exposes raw word positions becomes worth reaching for. **Can I pass a parameter to txtfetch to fix column order?**: Yes. Pass ?quality=premium and every page routes through a vision-language model instead of Tika's coordinate-based text pass. That's the case multi-column layouts benefit from most. Premium always routes async, so you get a 202 response and a job_id to poll instead of a synchronous response. It falls back to the Tika baseline (flagged metadata.tier_downgraded: true) if a guardrail trips. There's no plan-level gate; every plan can request it. **Should I use ?format=markdown or ?format=json for a multi-column document?**: They solve a different problem. Format controls the SHAPE of the response: GFM markdown, or a typed element tree with per-element page and offset. Quality controls which engine reads the page instead. Both ship on the standard tier. If the underlying reading order is wrong, a different output shape won't fix it. Reach for ?quality=premium for that, optionally alongside the format you want. related-reading - [Docs: the premium quality tier →](https://txtfetch.com/docs#premium-quality) - [Measured reading-order accuracy →](https://txtfetch.com/benchmarks) - [Tables losing structure →](https://txtfetch.com/fixes/tables-lose-structure) - [Chunk previewer →](https://txtfetch.com/tools/chunk-preview) - [See the actual text: a two-column report, raw vs. corrected →](https://txtfetch.com/diff#pdf-complex-financial-report) Layout & structure - [PDF or scanned tables flatten into unreadable space-separated text →](https://txtfetch.com/fixes/tables-lose-structure) - [the same header, footer, or page number shows up in every chunk →](https://txtfetch.com/fixes/headers-and-footers-in-every-chunk) - [extracted text is hard-wrapped at the print page's line width →](https://txtfetch.com/fixes/hard-wrapped-lines) - [extracted text is one giant undifferentiated blob with no paragraph breaks →](https://txtfetch.com/fixes/no-paragraph-breaks) - [All fixes →](https://txtfetch.com/fixes) ## Fix the text you already have. The free cleaner repairs this damage in your browser. Nothing leaves the page. [Clean up your text →](https://txtfetch.com/tools/clean-extracted-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/fixes/garbled-pdf-text # My PDF renders fine but extracts as gibberish. The page looks perfect on screen. The glyph shapes are all there. Reading the text back out uses a separate table entirely. When it's missing, extraction has nothing reliable to translate character codes into. looks-like ``` Q3 (cid:415)(cid:286)(cid:448)(cid:286)(cid:374)(cid:437)(cid:286) grew 34% year over year ``` Poppler-based tools (pdftotext) surface this literally as (cid:N) glyph-index tokens when they give up. Tika/PDFBox, what txtfetch runs, more often substitutes a replacement character or drops the run silently instead. Different libraries fail differently, but the root cause is the same. why-it-happens The page renders perfectly because rendering only needs the font's outline data, not a mapping back to what character each glyph represents. Reading the text back out needs a second table entirely: /ToUnicode, a CMap recording which Unicode codepoint each glyph stands for. When it's missing or wrong, an extractor has nothing reliable to translate character codes into. It matters most for composite /Type0 fonts using /Identity-H encoding. These map character codes straight to glyph indexes inside the embedded font program. There's no encoding-derived fallback meaning, only whatever /ToUnicode says. That's the one font shape this is certain about. Other font types generally aren't affected the same way. Ordinary simple fonts (/Type1, /TrueType) without a /ToUnicode map are usually fine. Their own encoding already establishes what each code means: a standard name like /WinAnsiEncoding, or the base-14 fonts' built-in StandardEncoding. This is specific to the /Type0 + /Identity-H + no-/ToUnicode combination. That's why it hits some PDFs from a producer and not others. A common source is a font subsetted down to only the glyphs actually used, without regenerating the CMap alongside it. Or a producer optimized for print fidelity and never considered anyone would try to read the text back out. confirm-it - The PDF text-layer checker's structural scan flags exactly this font shape: Type0/Identity-H with no ToUnicode. It's the one case it can be certain about from structure alone. [Check the PDF's font encoding](https://txtfetch.com/tools/pdf-text-check) - Paste a sample of the extracted text into the chunk previewer. Mojibake and stray replacement characters both surface there as named, counted signals if that's the shape your garbling takes. [Scan your extracted text](https://txtfetch.com/tools/chunk-preview) fix-it-yourself Regenerate the PDF with a font that embeds ToUnicode There's no reading-side fix. A missing CMap can't be reconstructed from glyph shapes alone. If you control the source, such as a report generator or a print-to-PDF step: switch to a library or driver that writes ToUnicode. Or re-export from the original document. OCR the affected pages instead of extracting their text Render the page to an image and OCR the rendering directly. OCR reads glyph shapes, and never touches the broken CMap that trips up text extraction. bash ``` pdftoppm -png -r 300 input.pdf page tesseract page-1.png output ``` Detect the failure before you trust the text A cheap heuristic: count literal (cid:N)-shaped tokens or out-of-range characters in the extraction. A spike is a strong signal this page hit the CMap problem. python ``` import re suspicious = len(re.findall(r"\(cid:\d+\)", text)) if suspicious > 5: print("likely missing ToUnicode CMap — consider OCR for this page") ``` what-txtfetch-does txtfetch runs Apache Tika (PDFBox underneath), which resolves /ToUnicode the same way any PDFBox-based reader does. When the CMap is missing on a Type0/Identity-H font, that run of text either won't decode to meaningful Unicode or comes back as replacement characters. It does this honestly, with no silent guessing at what it might have said. Automatic Tesseract escalation won't rescue this on its own. It only fires when the whole document's first pass comes back blank, and a broken-CMap page returns text, just wrong text (see /fixes/pdf-extracts-no-text). Since the page isn't blank, nothing about the default path detects the problem. What does work is ?quality=premium. It routes every page through a vision-language model reading the rendered page, which never touches the broken /ToUnicode table that defeats text extraction. That's the same reason the render-then-OCR route above works, without you running it yourself. Premium always routes async: a 202 response plus a job\_id to poll. It falls back to the Tika baseline with metadata.tier\_downgraded: true if a guardrail trips. Keep the DIY route above as your check on a document where the text matters and you want to compare. what-it-costs-you-downstream Garbled runs break sentence- and paragraph-boundary detection right where they occur, so a chunker draws boundaries blind through the damage. Every embedding computed over gibberish tokens pollutes the vector space around genuinely related, cleanly-extracted content nearby. faq **Why does my PDF render perfectly but extract as garbage?**: Rendering only needs the font's glyph outlines. Reading text back out needs a separate table, /ToUnicode, mapping each glyph to a Unicode character. That table is often missing or broken on a subsetted Type0/Identity-H font. When it is, extraction has nothing to translate character codes into, even though the page looks completely normal. **Is this the same thing as mojibake?**: No. Mojibake is a decoding bug: the right bytes, the wrong encoding applied. It's reversible by re-decoding correctly. A missing ToUnicode CMap means there's no mapping to reverse. Reading the rendered page instead of its character codes is the practical fix, not a re-decode. See /fixes/mojibake-wrong-encoding for the encoding case. **Can I fix this after the fact, in the text I already extracted?**: No. Once the character codes have been read without a valid ToUnicode map, the original glyph identity generally can't be recovered from the output text alone. You have to re-read the page rather than repair the string. Use either ?quality=premium, which routes the page through a vision-language model, or the DIY render-to-image-then-OCR route above. related-reading - [PDF text-layer checker →](https://txtfetch.com/tools/pdf-text-check) - [Image OCR-readiness checker →](https://txtfetch.com/tools/image-ocr-check) - [Mojibake / wrong encoding (a different cause) →](https://txtfetch.com/fixes/mojibake-wrong-encoding) - [Docs: the premium quality tier →](https://txtfetch.com/docs#premium-quality) - [Measured extraction accuracy by document category →](https://txtfetch.com/benchmarks) PDF - [extraction returns empty text from a PDF →](https://txtfetch.com/fixes/pdf-extracts-no-text) - [All fixes →](https://txtfetch.com/fixes) ## Fix the text you already have. The free cleaner repairs this damage in your browser. Nothing leaves the page. [Clean up your text →](https://txtfetch.com/tools/clean-extracted-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/fixes/hard-wrapped-lines # Every line in my extracted text ends at the same width, mid-sentence. Some extraction paths preserve every visual line break from the source layout literally. It's readable to a human, but each print-width line becomes its own unit to a chunker. looks-like ``` This is a long paragraph that has been hard-wrapped by a naive text extraction pipeline instead of being reflowed into one continuous line the way a real paragraph normally would be represented in clean extracted text. ``` Visually readable line by line. That's exactly why it's easy to miss until a chunker treats every line as its own unit. why-it-happens Some extraction paths preserve every visual line break from the source layout literally. Each line the page happened to wrap at roughly 60-80 characters becomes its own line in the output, with a real newline character. It isn't reflowed into the single logical paragraph a human reader experiences it as. This is common output from plain-text renderings of fixed-width layouts. Certain OCR post-processing steps also emit one line per detected text line, as do PDF extractors running without a reflow or de-hyphenation pass. It's visually readable to a human. The paragraph still makes sense line by line. That's exactly why it's easy to miss until a chunker treats every hard-wrapped line as its own unit. confirm-it - Paste the text into the chunk previewer. Three or more lines that read like prose, don't end a sentence, and continue lowercase on the next line trip this signal. [Scan for hard-wrapped lines](https://txtfetch.com/tools/chunk-preview) fix-it-yourself Reflow with a single-newline-to-space join Joins a line into the previous one only when the previous line doesn't end a sentence. Real paragraph breaks (blank lines) are left alone. python ``` import re def reflow(text): lines = text.split("\n") out = [] for line in lines: if out and out[-1] and not re.search(r"[.!?:;]\s*$", out[-1]) and line and line[0].islower(): out[-1] = out[-1] + " " + line else: out.append(line) return "\n".join(out) ``` Better, when blank lines already mark the paragraphs If the source kept its blank lines between paragraphs, don't guess at sentence boundaries at all. Split on the blank lines first, then collapse every newline inside each block. The paragraph edges are explicit, so this can't merge two paragraphs the way a sentence-boundary heuristic can. That heuristic can misfire when a paragraph ends without punctuation, like a heading, a list item, or a table cell. Stdlib only. python ``` import re def reflow_paragraphs(text): blocks = re.split(r"\n\s*\n", text) return "\n\n".join(re.sub(r"\s*\n\s*", " ", b).strip() for b in blocks) ``` what-txtfetch-does This is a caller-side post-processing concern, not something the extraction step decides on your behalf. txtfetch returns the text Tika produces, and Tika generally reflows PDF and Office paragraphs rather than preserving literal print-line breaks. So hard-wrapping shows up far less than in a raw fixed-width text dump or a naive OCR-to-text pipeline. It does happen most often on plain-text or heavily fixed-width sources, where there's no paragraph model to reflow from in the first place. The fix above is intentionally something you own. "Is this a real line break or a print-width wrap" is a judgment call about the source document. It's not a fact the extraction step can always know for certain. what-it-costs-you-downstream Every hard-wrapped line reads as its own short unit to recursive and structure-aware chunking, which packs chunks by line instead of by sentence or paragraph. Chunks end mid-thought exactly where the printed page happened to run out of width, not where the idea actually ended. faq **Is hard-wrapped text actually damaged, or just formatted differently?**: The words themselves are intact. Nothing is lost, unlike mojibake or a missing ToUnicode CMap. The damage is structural: a chunker that reads line by line, instead of by paragraph, ends chunks wherever the print page ran out of width. It doesn't end them where the idea actually ended. **How do I reflow hard-wrapped text without merging real paragraph breaks?**: Only join a line into the previous one when the previous line doesn't already end with sentence-ending punctuation. A real paragraph break (a blank line) is a different pattern entirely and should be left alone. See the fix-it-yourself snippet above. **Does txtfetch hard-wrap its output?**: No. Tika's PDF and Office parsing generally reflows paragraphs rather than preserving literal print-line breaks. Hard-wrapping mostly shows up on plain-text or heavily fixed-width sources where there's no paragraph structure to reflow from in the first place. related-reading - [No paragraph breaks →](https://txtfetch.com/fixes/no-paragraph-breaks) - [Hyphenated line breaks →](https://txtfetch.com/fixes/hyphenated-line-breaks) - [Chunk previewer →](https://txtfetch.com/tools/chunk-preview) Layout & structure - [two-column PDF text comes out interleaved and scrambled →](https://txtfetch.com/fixes/columns-out-of-order) - [PDF or scanned tables flatten into unreadable space-separated text →](https://txtfetch.com/fixes/tables-lose-structure) - [the same header, footer, or page number shows up in every chunk →](https://txtfetch.com/fixes/headers-and-footers-in-every-chunk) - [extracted text is one giant undifferentiated blob with no paragraph breaks →](https://txtfetch.com/fixes/no-paragraph-breaks) - [All fixes →](https://txtfetch.com/fixes) ## Fix the text you already have. The free cleaner repairs this damage in your browser. Nothing leaves the page. [Clean up your text →](https://txtfetch.com/tools/clean-extracted-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/fixes/headers-and-footers-in-every-chunk # The same header shows up at the top of every single chunk. A running header or footer is drawn on every page as part of the layout. Extraction has no concept of "this line is chrome, not content", so it reads it as body text, every time, on every page. looks-like ``` Confidential — Acme Corp Internal Use Only ...page content... Confidential — Acme Corp Internal Use Only 3 ...more content... ``` The confidentiality stamp and the bare page number both extract as ordinary text, identical to the surrounding content, on every page. why-it-happens A running header or footer, such as a document title, a confidentiality stamp, or a page number, is drawn on every page. It's part of the page's own layout, at the same position each time. A layout-naive text extractor has no concept of "this line is chrome, not content". It reads every page's text top to bottom, including whatever repeats identically at the top or bottom, and folds it straight into the body. A bare page number is a related but distinct case. "3" or "Page 3 of 42" on its own line is real, extractable text. It's just not content. In structure-aware chunking it can even get misread as a heading, since it's a short, isolated line much like a real one. confirm-it - Paste extracted text into the chunk previewer. A line repeating three or more times, or two or more bare page-number-shaped lines, both trip named signals. [Scan for repeated lines and page numbers](https://txtfetch.com/tools/chunk-preview) fix-it-yourself Strip lines that repeat across pages A frequency filter over line-level text removes boilerplate that shows up far more often than any real sentence would. It does this without needing to know the document's layout in advance. python ``` from collections import Counter lines = text.split("\n") counts = Counter(l.strip() for l in lines if 3 <= len(l.strip()) <= 80) boilerplate = {l for l, c in counts.items() if c >= 3} cleaned = "\n".join(l for l in lines if l.strip() not in boilerplate) ``` Drop bare page-number lines with a regex Matches "Page 3", "Page 3 of 42", a bare "3", or "- 3 -" on their own line. These are the common shapes a page-number footer takes once extracted as text. python ``` import re PAGE_NUM_RE = re.compile(r"^(?:page\s+)?\d{1,4}(?:\s*(?:of|/)\s*\d{1,4})?$|^-\s*\d{1,4}\s*-$", re.I | re.M) cleaned = "\n".join(l for l in text.split("\n") if not PAGE_NUM_RE.match(l.strip())) ``` what-txtfetch-does txtfetch returns exactly what Tika reads from the document's content stream. It doesn't run a header/footer-stripping heuristic on the default text response. "Is this line chrome or content" is a document-layout judgment call. It's best made with the frequency-across-pages signal above, on your side, where you control how aggressive to be. This is deliberately a caller-side post-processing step, not something the extraction API decides for you. It's the same reasoning /fixes/hard-wrapped-lines and /fixes/no-paragraph-breaks apply to their own post-processing concerns. what-it-costs-you-downstream A repeated header gets pulled into every chunk that lines up with a page boundary. This wastes embedding budget on the same few words, dozens of times over, instead of unique content. A bare page number can get packed into a chunk as if it carried meaning, or worse, mistaken for a section heading in structure-aware chunking. faq **Why doesn't txtfetch just strip repeated headers automatically?**: Deciding what's boilerplate versus real repeated content is a judgment call specific to your document, not a fact the extraction step can know for certain. A short line that repeats could be a header, or it could be a genuinely repeated piece of content. txtfetch returns exactly what Tika reads; stripping is a deliberate caller-side step. **How many repeats does the chunk previewer need to flag a line as a header?**: Three or more identical occurrences for the repeated-line signal, or two or more bare page-number-shaped lines for the page-number signal. This is tuned so a coincidental one-off repeated phrase doesn't trigger a false positive. **Can a page number get misread as a heading?**: Yes, specifically in structure-aware chunking. A bare short line like "3" resembles the shape of an all-caps or numbered-clause heading. Left unfiltered, it can seed a spurious section boundary. related-reading - [Chunk previewer →](https://txtfetch.com/tools/chunk-preview) - [No paragraph breaks →](https://txtfetch.com/fixes/no-paragraph-breaks) Layout & structure - [two-column PDF text comes out interleaved and scrambled →](https://txtfetch.com/fixes/columns-out-of-order) - [PDF or scanned tables flatten into unreadable space-separated text →](https://txtfetch.com/fixes/tables-lose-structure) - [extracted text is hard-wrapped at the print page's line width →](https://txtfetch.com/fixes/hard-wrapped-lines) - [extracted text is one giant undifferentiated blob with no paragraph breaks →](https://txtfetch.com/fixes/no-paragraph-breaks) - [All fixes →](https://txtfetch.com/fixes) ## Fix the text you already have. The free cleaner repairs this damage in your browser. Nothing leaves the page. [Clean up your text →](https://txtfetch.com/tools/clean-extracted-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/fixes/hyphenated-line-breaks # My extracted text still has inter-\\nnational split across a line break. A justified print layout breaks a long word at the end of a line and marks it with a hyphen. A naive extractor preserves that hyphen and the newline literally. The source word never had either. looks-like ``` The report covers the inter- national deployment process in detail. ``` "international" was never meant to contain a hyphen or a line break. It only exists because the printed page wrapped there. why-it-happens A justified or hyphenated print layout breaks a long word across the end of one visual line and the start of the next. It marks the break with a hyphen: "inter-" ends one line, "national" starts the next. A naive extractor preserves the source's original line breaks literally. So the hyphen and the newline both survive into the output exactly where the printed page happened to wrap the word. This is a print-layout artifact, not a real hyphen. The source document's word "international" was never meant to contain either character. It exists purely because the page has a fixed width and the word didn't fit on one line. confirm-it - Paste the text into the chunk previewer. It flags every letter-hyphen-newline-lowercase break specifically, with a live count. [Scan for hyphenated line breaks](https://txtfetch.com/tools/chunk-preview) fix-it-yourself Re-join with a de-hyphenation regex Only joins when the character after the break is lowercase. A genuine end-of-sentence hyphen followed by a capitalized new sentence is left alone. python ``` import re text = re.sub(r"([A-Za-z])-\n([a-z])", r"\1\2", text) ``` Extract with reflow instead of raw line preservation pdftotext without -layout already reflows most paragraphs. This avoids introducing the hyphenated break in the first place rather than fixing it after the fact. bash ``` pdftotext input.pdf output.txt ``` what-txtfetch-does Tika's PDF extraction reflows paragraph text rather than preserving the source's literal line-wrap positions. So hyphenated line-break artifacts show up substantially less than in a raw layout dump. Reflow heuristics aren't perfect on every producer, particularly dense multi-column layouts. It's worth spot-checking on documents where retrieval quality matters, using the de-hyphenation regex above as a safety net. what-it-costs-you-downstream A chunk boundary can land exactly at the hyphen, turning one word into two separate chunk-boundary fragments. "inter-" ends up at the end of one chunk and "national" at the start of the next. Neither one embeds or matches a search for "international". faq **Is the hyphen in inter-\nnational a real hyphen from the source document?**: No. It's a print-layout artifact from where the page happened to wrap the word across two lines. The source word is "international", with no hyphen at all. A naive extractor just preserves the visual line break literally. **Will the de-hyphenation regex ever join text it shouldn't?**: It's written to only fire when the character right after the break is lowercase. So a genuine hyphenated compound followed by a new, capitalized sentence is left alone. It can still occasionally misfire on an intentional hyphenated word that happens to wrap at a line break, so spot-check the output. **Does txtfetch fix this automatically?**: Mostly. Tika's PDF parsing reflows paragraphs rather than preserving literal print-line positions, so this shows up far less than in a raw layout-preserving extraction. It's a reflow heuristic, not a guarantee on every document, so the fix above is worth keeping as a backstop. related-reading - [Missing spaces between words →](https://txtfetch.com/fixes/missing-spaces-between-words) - [Hard-wrapped lines →](https://txtfetch.com/fixes/hard-wrapped-lines) - [Chunk previewer →](https://txtfetch.com/tools/chunk-preview) Characters & encoding - [extracted text is full of mojibake or replacement characters →](https://txtfetch.com/fixes/mojibake-wrong-encoding) - [missing spaces between words in extracted text →](https://txtfetch.com/fixes/missing-spaces-between-words) - [unexpanded ligatures and curly punctuation in extracted text →](https://txtfetch.com/fixes/ligatures-and-smart-punctuation) - [All fixes →](https://txtfetch.com/fixes) ## Fix the text you already have. The free cleaner repairs this damage in your browser. Nothing leaves the page. [Clean up your text →](https://txtfetch.com/tools/clean-extracted-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/fixes/ligatures-and-smart-punctuation # My extracted text has fi and fl instead of plain letters. A ligature is one glyph standing in for a letter pair, encoded as its own character. Smart quotes and non-breaking spaces are similar, deliberate substitutions. They're correct in the source, but different from what a naive string match expects. looks-like ``` The office workflow relies on efficient filing — but ligature glyphs like fi and fl don't always expand back to plain letters. ``` office and workflow each carry a single ligature glyph (ffi, U+FB03; fl, U+FB02) where a search or tokenizer expects two plain letters. why-it-happens Typography renders certain letter pairs, fi, fl, ff, ffi, ffl, as one combined glyph for visual quality, particularly in serif fonts. The PDF's font can encode that combined shape as its own character code, distinct from separate "f" and "i" codes. So an extractor reading character codes literally gets back one ligature character (fi, U+FB01) rather than the two ASCII letters a search or tokenizer expects. Smart, or "curly", punctuation is a related but separate substitution. Word processors and typesetting systems replace straight quotes and hyphens with their typographic equivalents (', ', ", ", –, —) as you type. This is correct and intentional in the source, but a different codepoint than the plain ASCII a naive string match expects. Non-breaking spaces (U+00A0) and soft hyphens (U+00AD) are two more invisible-looking codepoints that behave differently from what they appear to be. A non-breaking space looks like a space but doesn't always split the same way under whitespace-based tokenization. A soft hyphen is invisible unless a line actually breaks there, but it survives into extracted text as a stray character regardless. confirm-it - Paste the text into the chunk previewer. Unexpanded ligature glyphs are a named, counted signal in its extraction-quality scan. [Scan for unexpanded ligatures](https://txtfetch.com/tools/chunk-preview) fix-it-yourself Expand ligatures with NFKC normalization NFKC (compatibility) normalization is the right tool for ligatures specifically: fi becomes fi, ffi becomes ffi. It also folds a non-breaking space to a plain space and an ellipsis character to three dots. What it does NOT touch is smart punctuation. Curly quotes and en/em dashes aren't compatibility characters, so they survive NFKC unchanged, and a soft hyphen does too. Those need the explicit map below. python ``` import unicodedata text = unicodedata.normalize("NFKC", text) ``` Fold smart punctuation with an explicit map This is the half NFKC leaves alone: curly quotes, en/em dashes, and the soft hyphen. A translate() map is better than chained replace() calls here, because it's one pass and the intent is legible. Add or drop a codepoint as your downstream matching requires. Keep the NBSP and soft-hyphen entries even if you also ran NFKC. An NBSP that survives silently breaks whitespace-based tokenizers. A soft hyphen is invisible, right up until it splits a word for you. python ``` PUNCT = { 0x2018: "'", 0x2019: "'", # ' ' curly single quotes 0x201c: '"', 0x201d: '"', # " " curly double quotes 0x2013: "-", 0x2014: "-", # – — en/em dash 0x00a0: " ", # non-breaking space 0x00ad: "", # soft hyphen — delete outright } text = text.translate(PUNCT) ``` what-txtfetch-does Apache Tika expands most common ligatures during PDF and Office text extraction as part of its normal glyph-to-Unicode mapping. So this shows up far less than in a raw PDF-library dump. It isn't guaranteed for every font and producer combination, though. Smart punctuation is passed through as-is, deliberately. Curly quotes and em dashes are the correct rendering of the source document, not an extraction defect, so txtfetch doesn't rewrite them. Fold them on your side if your downstream matching needs plain ASCII. Note that NFKC alone won't do it, since those characters aren't compatibility characters. The explicit translate() map above is what handles them. what-it-costs-you-downstream An unexpanded ligature reads as one unrecognized character to word- and token-boundary logic. "office" doesn't tokenize as, embed as, or lexically match "office". This quietly shifts both token counts and retrieval recall near every occurrence. faq **Should I normalize curly quotes to straight quotes before embedding?**: Only if your downstream matching (exact-string search, a keyword filter) needs plain ASCII. Curly quotes are the source document's correct, intentional typography, not a defect. Semantic embedding models generally handle both forms fine on their own. **Does NFKC normalization handle curly quotes and em dashes too?**: No, and this is the most common wrong assumption about NFKC. It expands ligatures (fi to fi), folds a non-breaking space to a plain space, and turns an ellipsis character into three dots. But curly quotes, en dashes, em dashes, and soft hyphens are not compatibility characters. NFKC leaves every one of them exactly as it found them. Use an explicit translate() map for those; there's one in the fix-it-yourself section above. **Does NFKC normalization ever change meaning, not just form?**: It can. NFKC folds some distinct-looking characters toward a shared compatibility form, which is what you want for ligatures. But it occasionally collapses characters that were meaningfully distinct in specialized text. Some mathematical notation is one example. East Asian full-width forms folding to their half-width equivalents are another. For ordinary English prose extraction it's a safe default. Spot-check it if your corpus is mathematical or CJK. **Why do only some fonts produce ligature characters in extracted text?**: It depends on whether the font encodes fi/fl/ff as their own glyph codes, or as separate letter codes rendered close together. The former is common in serif fonts optimized for print. Tika expands the common cases, but isn't guaranteed across every font and producer. related-reading - [Mojibake / wrong encoding →](https://txtfetch.com/fixes/mojibake-wrong-encoding) - [Chunk previewer →](https://txtfetch.com/tools/chunk-preview) Characters & encoding - [extracted text is full of mojibake or replacement characters →](https://txtfetch.com/fixes/mojibake-wrong-encoding) - [missing spaces between words in extracted text →](https://txtfetch.com/fixes/missing-spaces-between-words) - [hyphenated line breaks left in extracted text →](https://txtfetch.com/fixes/hyphenated-line-breaks) - [All fixes →](https://txtfetch.com/fixes) ## Fix the text you already have. The free cleaner repairs this damage in your browser. Nothing leaves the page. [Clean up your text →](https://txtfetch.com/tools/clean-extracted-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/fixes/missing-spaces-between-words # Two words fused together with no space, over and over. PDF text is a sequence of positioned glyph runs, not words. There's often no space CHARACTER between them at all, only a visual gap an extractor has to reason about. looks-like ``` Revenue grew 34% driven byEMEA demand.The board approved a newpricing tier. ``` Three fusions, and only two of them leave a detectable trace. byEMEA has a lowercase-to-uppercase boundary, and demand.The has a period followed by a letter. But newpricing is all lowercase mid-sentence, so no pattern can see it. why-it-happens PDF text is stored as a sequence of positioned glyph runs, not words. A producer draws each run at an (x, y) coordinate and relies on visual spacing to imply a word boundary. There's no space character between "quick" and "brown" unless the layout explicitly encoded one. An extractor that concatenates runs in draw order without reasoning about the horizontal gap between them fuses adjacent words whose gap happens to be small. This shows up most in PDFs generated by layout engines that letter-space or kern aggressively, or that split one visual word across multiple glyph runs. That's common with justified text or certain font-subsetting tools. Each run individually has no space character, and none was ever encoded for the extractor to find. confirm-it - Paste the text into the chunk previewer. Five or more fused-word occurrences trips the missing-spaces signal specifically. [Scan for missing spaces](https://txtfetch.com/tools/chunk-preview) fix-it-yourself Re-run extraction with layout preserved pdftotext's -layout flag reasons about column positions and horizontal gaps rather than raw run order. This recovers spaces a naive concatenation drops. bash ``` pdftotext -layout input.pdf output.txt ``` Insert spaces heuristically as a post-process These two patterns, a lowercase-to-uppercase boundary and a period followed immediately by a letter, are exactly the two the chunk previewer counts for this signal. So what the tool flags is what this repairs. Two honest limits apply. It will occasionally split a genuine CamelCase identifier or an acronym. And it cannot see an all-lowercase fusion mid-sentence, like the newpricing case above, because nothing in the text marks where the boundary was. python ``` import re text = re.sub(r"([a-z])([A-Z])", r"\1 \2", text) text = re.sub(r"\.([A-Za-z])", r". \1", text) ``` what-txtfetch-does Apache Tika's PDF text extraction reasons about glyph position, not just draw order. So it reconstructs word and line spacing far more reliably than a naive run-concatenation extractor. This is the same class of layout-aware reading pdftotext -layout does. It isn't perfect on every producer. A PDF that genuinely encodes zero space characters between visually-adjacent runs, relying entirely on gap detection, can still fuse words in txtfetch's output. That's because there's no space in the underlying content stream for any extractor to find. what-it-costs-you-downstream A fused run like "newpricing" tokenizes and chunks as one unrecognizable unit. It won't match a lexical search for either of its component words. It embeds as noise rather than as the concept those words actually represent. faq **Why does PDF extraction sometimes fuse two words together?**: PDF text is positioned glyph runs, not words with explicit boundaries. The space between two words is a visual gap, not always an encoded character. An extractor that reads runs in order without reasoning about that gap can fuse adjacent words whose horizontal spacing was tight. **Will layout-aware extraction always fix this?**: It fixes most cases, since it reasons about horizontal gaps rather than raw draw order. It can't recover a case where the PDF genuinely has zero space in its underlying content stream. Such a case relies entirely on visual alignment, which an extractor has to infer. Those cases need a post-process regex as a backstop. **Will the camelCase regex fix ever break real text?**: Occasionally. A genuine CamelCase identifier or acronym followed by a capital letter can get an unwanted space inserted. It's a heuristic, not a certainty. Spot-check a sample of the output before trusting it at scale. related-reading - [Hyphenated line breaks →](https://txtfetch.com/fixes/hyphenated-line-breaks) - [Chunk previewer →](https://txtfetch.com/tools/chunk-preview) - [Extract text from a PDF →](https://txtfetch.com/extract/pdf) Characters & encoding - [extracted text is full of mojibake or replacement characters →](https://txtfetch.com/fixes/mojibake-wrong-encoding) - [hyphenated line breaks left in extracted text →](https://txtfetch.com/fixes/hyphenated-line-breaks) - [unexpanded ligatures and curly punctuation in extracted text →](https://txtfetch.com/fixes/ligatures-and-smart-punctuation) - [All fixes →](https://txtfetch.com/fixes) ## Fix the text you already have. The free cleaner repairs this damage in your browser. Nothing leaves the page. [Clean up your text →](https://txtfetch.com/tools/clean-extracted-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/fixes/mojibake-wrong-encoding # My extracted text is full of ’ and “. Mojibake is a decoding bug, not lost data. The right bytes get read with the wrong encoding. A replacement character (�) is the other, worse case: bytes that couldn't be decoded at all. looks-like ``` It won’t behave the way you expect—and “smart quotes” turn into this. ``` Classic UTF-8-decoded-as-Windows-1252 mojibake. The right single quote in "won't" is three UTF-8 bytes. Reinterpreting them one byte at a time turns that one character into the three wrong ones you see. The closing quote at the end looks like only two. Its third byte, 0x9D, is undefined in Windows-1252 and lands on an invisible control character. That same 0x9D is what breaks the naive round-trip fix below. why-it-happens Mojibake happens when bytes are decoded with the wrong character encoding, almost always UTF-8 bytes read as Windows-1252 (or the reverse). A UTF-8-encoded right single quote is three bytes: E2 80 99. Reinterpreted one byte at a time as Windows-1252, it renders as ’: three separate "wrong" characters standing in for the one correct one. It's a decoding bug, not a content problem. The original bytes are intact and correct. Only the encoding label applied when turning them into text is wrong. That's why re-decoding with the correct encoding, or round-tripping through the wrong one, recovers the original text exactly. No OCR or reconstruction is needed. The one wrinkle is mechanical rather than lossy. Windows-1252 has five undefined byte values, so a strict round trip needs to pass those through explicitly. The fix-it-yourself section below shows how. The U+FFFD replacement character (�) is a different, worse failure. A decoder hit a byte sequence it couldn't map to any character in the encoding it assumed, and substituted the generic "unknown" glyph. Unlike mojibake, this is not reversible. The original byte value is gone, replaced permanently, so no re-decode recovers it. confirm-it - Paste the extracted text into the chunk previewer. Both mojibake and stray replacement characters are named, counted signals in its extraction-quality scan. [Scan for mojibake and replacement characters](https://txtfetch.com/tools/chunk-preview) fix-it-yourself Fix it in Python with ftfy ftfy specifically targets "the text looks like mojibake, guess and undo the wrong decode". It's built for exactly this failure mode and handles cases beyond the simple UTF-8/Latin-1 round trip. Pass uncurl\_quotes=False: by default fix\_text also rewrites curly quotes to straight ASCII ones, which is a second, separate change you probably didn't ask for. See /fixes/ligatures-and-smart-punctuation on why that punctuation is usually correct as-is. bash ``` pip install ftfy python -c "import ftfy; print(ftfy.fix_text(open('extracted.txt').read(), uncurl_quotes=False))" ``` Re-decode manually when you know the source encodings If you know the source was UTF-8 read as Windows-1252 (the most common case), the classic round trip recovers it directly. Read the next remedy before relying on this one, though. It raises UnicodeEncodeError on a large class of real input, including the sample at the top of this page. python ``` fixed = broken.encode("windows-1252").decode("utf-8") ``` …and the variant that survives cp1252's undefined bytes Windows-1252 leaves five byte values undefined: 0x81, 0x8D, 0x8F, 0x90, 0x9D. So strict .encode("windows-1252") raises UnicodeEncodeError the moment the mojibake contains one. That is not an edge case. A mojibaked right double quote (" is E2 80 9D in UTF-8) ends in the undefined 0x9D. That's exactly why the one-liner above dies on this page's own sample. Encoding character by character, and passing those five through by codepoint, recovers the original text in full, curly punctuation intact, with no dependency. If you'd rather not hand-roll it, ftfy ships the same idea as a codec: import ftfy.bad\_codecs, then broken.encode("sloppy-windows-1252").decode("utf-8"). python ``` def undo_cp1252_mojibake(s): out = bytearray() for ch in s: try: out += ch.encode("windows-1252") except UnicodeEncodeError: out.append(ord(ch)) # 0x81 0x8d 0x8f 0x90 0x9d — undefined in cp1252 return out.decode("utf-8") ``` Detect the encoding before you decode, upstream If you're extracting bytes yourself rather than relying on a fixed pipeline, detect the encoding instead of assuming UTF-8 or Latin-1 in the first place. python ``` from charset_normalizer import from_bytes result = from_bytes(raw_bytes).best() text = str(result) ``` what-txtfetch-does txtfetch's extraction runs through Apache Tika, which handles source-encoding detection as part of parsing rather than assuming UTF-8. The double-decode failure mode above is largely avoided for documents Tika parses directly. That isn't a guarantee against every source. A document's text can already be mojibake'd before it ever reaches Tika. The damage, baked in by an earlier, unrelated conversion step, comes back exactly as damaged as the source. There's no way to distinguish "wrong on purpose" from "wrong by an upstream mistake" after the fact. what-it-costs-you-downstream Garbled runs break sentence- and paragraph-boundary detection right where they occur, so recursive and structure-aware chunking can't reliably find clean edges around them. The corrupted tokens themselves add embedding noise exactly where real, meaningful words used to be. faq **Can I always fix mojibake after the fact?**: Usually, yes. Mojibake preserves the original bytes, just decoded with the wrong encoding. So re-decoding correctly, or using a tool like ftfy, recovers the original text exactly. Mind one mechanical trap: the naive broken.encode("windows-1252") round trip raises UnicodeEncodeError whenever the damage includes one of cp1252's five undefined bytes. A mojibaked curly double quote always does. The fix-it-yourself section above has the version that handles it. A replacement character (�) is the genuinely unrecoverable case: the byte was lost during decoding, and no amount of re-decoding brings it back. **How do I tell mojibake apart from a replacement character?**: Mojibake reads as a run of plausible-looking wrong letters, like ’ or é. A replacement character is the single glyph � standing alone, or the diamond-question-mark you sometimes see instead. The chunk previewer's quality scan reports them as two separate, named signals for exactly this reason. **Does txtfetch produce mojibake on documents it extracts?**: Rarely, since Tika detects source encoding as part of parsing rather than assuming UTF-8. It can still happen if the mojibake was already present in the source document's text before txtfetch ever saw it. There's no way to tell 'broken on purpose' from 'broken upstream' from the bytes alone. related-reading - [Garbled PDF text (a different cause) →](https://txtfetch.com/fixes/garbled-pdf-text) - [Chunk previewer →](https://txtfetch.com/tools/chunk-preview) - [PDF text-layer checker →](https://txtfetch.com/tools/pdf-text-check) Characters & encoding - [missing spaces between words in extracted text →](https://txtfetch.com/fixes/missing-spaces-between-words) - [hyphenated line breaks left in extracted text →](https://txtfetch.com/fixes/hyphenated-line-breaks) - [unexpanded ligatures and curly punctuation in extracted text →](https://txtfetch.com/fixes/ligatures-and-smart-punctuation) - [All fixes →](https://txtfetch.com/fixes) ## Fix the text you already have. The free cleaner repairs this damage in your browser. Nothing leaves the page. [Clean up your text →](https://txtfetch.com/tools/clean-extracted-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/fixes/no-paragraph-breaks # My extracted text is one giant block with no paragraph breaks anywhere. A paragraph break is really just a whitespace pattern, a blank line. It only survives if the source document encoded one in the first place. Some sources genuinely don't have one. looks-like ``` Sentence one keeps this block going. Sentence two keeps this block going without any blank line anywhere in it. Sentence three keeps this block going the exact same way, and so does every sentence after it for the rest of the document. ``` Sentence boundaries survive, since a period is still a period. But the higher-level grouping a real paragraph represents is gone. why-it-happens A paragraph break in extracted text is really just a specific whitespace pattern: a blank line, or two consecutive newlines. That pattern only survives if the source document encoded paragraphs as a real structural break in the first place. The extractor also has to preserve it. Some sources genuinely don't have one. A single long text block pasted into a form field is one example. So is a PDF built from a layout engine that treats the whole page as one flow, with no paragraph-level markup. An aggressive reflow step can also strip blank lines along with the print-line breaks it was trying to fix. Without that signal, there's nothing left in the text itself to mark where one idea ends and the next begins. Sentence boundaries survive, since a period is still a period. But the higher-level grouping a real paragraph represents is gone. confirm-it - Paste the text into the chunk previewer. A document longer than 2,000 characters with zero blank-line breaks anywhere trips this signal. [Scan for missing paragraph breaks](https://txtfetch.com/tools/chunk-preview) fix-it-yourself Fall back to sentence-boundary splitting explicitly When there's no paragraph signal to key off, group a fixed number of sentences together as a paragraph substitute. This is better than letting a chunker degrade silently to line-level splitting. python ``` import re sentences = re.split(r"(?<=[.!?])\s+", text) paragraphs = [" ".join(sentences[i:i+5]) for i in range(0, len(sentences), 5)] text_with_breaks = "\n\n".join(paragraphs) ``` Use a real sentence tokenizer for cleaner boundaries A proper sentence segmenter handles abbreviations, decimals, and quoted sentences far more reliably than a punctuation regex. That matters once you're relying on it to define chunk-worthy groups. The model is a separate download from the package. Without it, spacy.load() raises OSError: \[E050\] Can't find model 'en\_core\_web\_sm'. python ``` # pip install spacy # python -m spacy download en_core_web_sm import spacy nlp = spacy.load("en_core_web_sm") sentences = [s.text for s in nlp(text).sents] ``` what-txtfetch-does txtfetch preserves whatever paragraph structure the source document actually encoded. It doesn't invent breaks that weren't there, and it doesn't strip real ones. If the source genuinely has no paragraph-level structure, the extracted text won't either. A single flowed text block, a plain .txt file authored that way, and a form-field export are all examples. There's nothing there to recover. This is a property of the input, not something the extraction step can fix without guessing at document structure the source never provided. what-it-costs-you-downstream With no paragraph breaks to key off, recursive and structure-aware chunking both fall back to splitting on sentences or lines. This effectively degrades to the same boundary quality as fixed-size chunking, the one strategy explicitly designed to ignore structure. faq **Can extraction add paragraph breaks that aren't in the source?**: No, and it shouldn't try to. Inventing a break where the source document didn't encode one is a guess about document structure that's just as likely to be wrong as right. txtfetch returns exactly the paragraph structure (or lack of it) the source actually has. **What should I use instead of paragraph breaks for chunking this kind of text?**: Sentence-boundary splitting, grouped into fixed-size batches, is the practical substitute. See the fix-it-yourself snippets above. It won't recover real semantic paragraph groupings that were never there, but it avoids degrading all the way to line-level or fixed-character chunking. **Is this the same problem as hard-wrapped lines?**: Related but distinct. Hard-wrapped text has too MANY line breaks (one per print line, no paragraph grouping). This is text with too FEW (none at all). See /fixes/hard-wrapped-lines for the other shape. related-reading - [Hard-wrapped lines →](https://txtfetch.com/fixes/hard-wrapped-lines) - [Chunking strategies for RAG →](https://txtfetch.com/blog/chunking-strategies-for-rag) - [Chunk previewer →](https://txtfetch.com/tools/chunk-preview) Layout & structure - [two-column PDF text comes out interleaved and scrambled →](https://txtfetch.com/fixes/columns-out-of-order) - [PDF or scanned tables flatten into unreadable space-separated text →](https://txtfetch.com/fixes/tables-lose-structure) - [the same header, footer, or page number shows up in every chunk →](https://txtfetch.com/fixes/headers-and-footers-in-every-chunk) - [extracted text is hard-wrapped at the print page's line width →](https://txtfetch.com/fixes/hard-wrapped-lines) - [All fixes →](https://txtfetch.com/fixes) ## Fix the text you already have. The free cleaner repairs this damage in your browser. Nothing leaves the page. [Clean up your text →](https://txtfetch.com/tools/clean-extracted-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/fixes/office-text-missing # The text is definitely in the file. It's just not in extracted\_text. Office documents are a zip archive of separate XML parts, not one flat stream. A parser that reads the visible body and stops silently drops whatever lives in the other parts. looks-like ``` extracted_text: "Slide 4\n\nQ3 roadmap overview" // the deck's speaker notes on slide 4 — the actual talking points — never appear anywhere in the response. ``` The visible body text extracts fine. A specific part of the file's own structure just never made it into extracted\_text. why-it-happens Office documents aren't one flat stream of text. They're a zip archive of separate XML parts, each holding a different piece of the document's content. Each part carries its own relationships back to where it's used. A .pptx keeps each slide's visible text in ppt/slides/slideN.xml. Speaker notes live entirely separately in ppt/notesSlides/, with their own files and their own relationship IDs. A parser that reads slideN.xml and stops silently drops the notes. That's not because the notes were empty, but because it never looked there. The same shape repeats across the Office family. Grouped or nested shapes hide their text an extra level down inside the shape tree rather than at the top level. SmartArt diagram labels live in a separate diagrams/data\*.xml part, disconnected from the diagram's visible layout XML. A .docx's tracked changes and comments are stored in their own parts (w:ins/w:del runs, comments.xml) rather than inline in the main body flow. And a .xlsx's string cell values are indices into a shared xl/sharedStrings.xml table, not inline text. A reader that doesn't resolve that indirection gets numbers pointing at other numbers instead of the words themselves. None of this shows up as garbled or damaged text. The extraction genuinely just doesn't include it. The response can look completely clean and successful while quietly leaving out an entire category of content. confirm-it - Every Office file is a zip archive. List its parts and grep for the text you expect to confirm it's actually there before assuming a parser bug. - Open the file in the native app (Word, PowerPoint, Excel, or LibreOffice). Check the specific view you suspect is missing: the notes pane, the comments pane, or a specific SmartArt diagram. Compare it against what extracted\_text actually contains. fix-it-yourself Inspect the raw XML parts yourself Confirms exactly what's inside the file before you go looking for a parser fix. Every Office file is just a zip you can unzip and grep. bash ``` unzip -o deck.pptx -d deck_unzipped grep -r "speaker note text" deck_unzipped/ppt/notesSlides/ ``` Use a part-aware library for the specific content you need python-pptx, python-docx, and openpyxl expose comments, tracked changes, speaker notes, and SmartArt text as distinct properties rather than flattening everything (or nothing) into one string. python ``` from pptx import Presentation prs = Presentation("deck.pptx") for slide in prs.slides: if slide.has_notes_slide: print(slide.notes_slide.notes_text_frame.text) ``` what-txtfetch-does txtfetch's Office parsing, via Apache Tika's POI-based parsers, reads slide body text AND speaker notes for .pptx, folding both into the response. See /extract/pptx for the specifics of what's covered part by part. Grouped-shape and SmartArt text extraction depends on Tika's own POI support for that underlying part. That support is broad, but not exhaustive, for every nested or unusual shape structure. Tracked-changes and comment text are similarly parsed where Tika's OOXML support covers that part. A construct outside what Tika's parser resolves comes back the same way it would for any Tika-based pipeline: absent, not garbled. If you depend on comments, tracked changes, or a specific diagram's text being present, verify it against a real extraction call on your actual file. Don't assume coverage. See /extract/docx, /extract/pptx, and /extract/xlsx for what each format covers. what-it-costs-you-downstream Missing speaker notes or comment text isn't a formatting nuisance in a RAG pipeline. It's silent data loss. The exact context, such as a slide's talking points or a reviewer's comment explaining a decision, never enters the vector store at all. Nothing about a successful-looking extraction call tells you it's gone. faq **Does txtfetch extract PowerPoint speaker notes?**: Yes. Tika reads both the slide body (ppt/slides/) and the speaker notes (ppt/notesSlides/) and includes them together in extracted_text. See /extract/pptx. **Why would text be missing from extraction without any error?**: Office documents store different kinds of content in separate XML parts inside the zip archive: body text, speaker notes, comments, tracked changes, and SmartArt labels. A parser that doesn't resolve a specific part returns a perfectly successful response that simply never included that part's text. Nothing about the response signals the gap. **How do I check what's actually inside my Office file before assuming a bug?**: Every .docx/.pptx/.xlsx is a zip archive. `unzip -l file.pptx` lists every part. Grep the relevant XML files (ppt/notesSlides/, word/comments.xml, xl/sharedStrings.xml) directly to confirm the text you expect is actually present in the file. related-reading - [Extract text from Word (.docx) →](https://txtfetch.com/extract/docx) - [Extract text from PowerPoint (.pptx) →](https://txtfetch.com/extract/pptx) - [Extract text and tables from Excel (.xlsx) →](https://txtfetch.com/extract/xlsx) - [Parsing Office docs into clean text →](https://txtfetch.com/blog/parse-office-docs-docx-pptx-xlsx-for-llms) ## Fix the text you already have. The free cleaner repairs this damage in your browser. Nothing leaves the page. [Clean up your text →](https://txtfetch.com/tools/clean-extracted-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/fixes/pdf-extracts-no-text # Why did my PDF come back with no text at all? Almost always because there's no text layer to read. It's not a bug in whatever you're using to extract it. Here's how to tell for certain, and the one shape of PDF that fools a naive check. looks-like ``` >>> extracted_text '' ``` Your extraction call succeeds and returns nothing. Or, with txtfetch specifically, the call itself fails with a typed extraction\_failed error instead of a silent empty string. why-it-happens A PDF page is one of two fundamentally different things. It has real text, drawn with font and character-code operators. Or it's a picture of a page, with no text operators at all. pdftotext, pypdf, pdfminer.six, and Apache Tika all read the first case correctly. They all correctly return nothing for the second case. An empty result isn't a parsing failure. It's the honest answer to "is there text here?" That's the answer when the file is a scanned image, a fax, or a flatbed scan saved straight to PDF. You can run this check yourself: open the PDF and try to select a word with your cursor. If nothing highlights, there's no text layer on that page. No library finds one either, because none exists. The trickier case is a mixed PDF. Some pages are born-digital and some are scanned. A common example is a digital cover page or coversheet stapled ahead of a faxed body. The document isn't blank overall, so a check that only asks "did we get text back at all" passes. The scanned pages behind the cover contribute nothing, and nobody notices. confirm-it - Drop the file into the PDF text-layer checker. It reads each page individually, so it catches the mixed-document case a whole-document check misses. [Run the PDF text-layer check](https://txtfetch.com/tools/pdf-text-check) - Or just try selecting text in your PDF viewer. No highlight on a given page means no text layer on that page. fix-it-yourself OCR it yourself ocrmypdf adds an invisible, searchable text layer over the existing scanned page images. It's the fastest way to get real text out of a scan without discarding the original page images. bash ``` ocrmypdf input.pdf output.pdf pdftotext output.pdf output.txt ``` Confirm it's genuinely empty before reaching for OCR A quick byte-count sanity check on the plain-text extraction. It's cheaper than assuming and running OCR on a file that already had text. bash ``` pdftotext -layout input.pdf - | wc -c ``` what-txtfetch-does txtfetch runs the plain-text extraction pass first. It then automatically escalates the whole document to Tesseract OCR, but only when that first pass comes back completely blank. A fully scanned PDF gets OCR'd automatically, in the same request. metadata.ocr: true in the response confirms it happened. Know the mixed-document case before you rely on that. Tesseract escalation only fires when the entire first pass is blank. A digital cover page ahead of a scanned body means the document isn't blank overall. So the scanned pages behind it do not trigger OCR on their own. For those documents, pass ?quality=premium. Every page then routes through a vision-language model instead of the text-or-OCR fork, so a digital cover page and a scanned body both get read. Premium always routes async: you get a 202 response plus a job\_id to poll. It also safely falls back to the Tika baseline, flagged metadata.tier\_downgraded: true, if a cost or size guardrail trips. Run the PDF text-layer check above first on anything you suspect is mixed, since it reports per page rather than per document. There's a second safety net on the async path specifically. A request that did NOT pin ?quality= explicitly gets a premium retry automatically. This happens when the Tika baseline for a PDF either needed OCR (metadata.ocr: true) or came back extraction\_failed. So an unpinned async request gets a vision-model second read before it gives up. Pinning ?quality=standard opts out of that. A small synchronous upload doesn't take this path at all. If the document genuinely has no recoverable text after those attempts, txtfetch returns a typed extraction\_failed error, not a silent empty success. Failed extractions never count against your monthly quota. what-it-costs-you-downstream An empty or partially-empty extracted\_text produces a chunk with nothing to embed, silently shrinking that document's presence in the vector store. Or, worse, an ingestion job reports success while a whole document, or the scanned half of one, has quietly never entered the index at all. faq **Is an empty PDF extraction always a bug in the library I'm using?**: Almost never. It's the correct answer when the PDF has no text layer at all: a scan, a fax, or a photographed page saved as a PDF. Try selecting text in the PDF viewer itself. If nothing highlights, there's genuinely nothing there for any library to find. **Why did my mixed PDF come back with SOME text but I know pages are missing?**: Automatic Tesseract escalation, in txtfetch and in most extractors that offer it, only fires when the whole document's first-pass text comes back blank. A digital cover page ahead of a scanned body means the first pass isn't blank. So the scanned pages behind it never trigger OCR on their own. Pass ?quality=premium for these documents. It reads every page through a vision-language model instead of the text-or-OCR fork. Check with the PDF text-layer checker first if you're unsure, since it reports per page instead of per document. **Does txtfetch charge me for a PDF that comes back with no text?**: No. A whitespace-only result returns a typed extraction_failed error. This applies after the OCR retry and, on an unpinned async request, the automatic premium retry. Failed extractions don't count against your monthly document quota. related-reading - [PDF text-layer checker →](https://txtfetch.com/tools/pdf-text-check) - [Image OCR-readiness checker →](https://txtfetch.com/tools/image-ocr-check) - [Scanned PDF extraction →](https://txtfetch.com/extract/scanned-pdf) - [Docs: the premium quality tier for mixed PDFs →](https://txtfetch.com/docs#premium-quality) - [OCR scanned documents through one API call →](https://txtfetch.com/blog/ocr-scanned-documents-api) - [Error reference: extraction\_failed →](https://txtfetch.com/docs/errors) - [See the actual OCR output: a scanned invoice, raw vs. corrected →](https://txtfetch.com/diff#pdf-scanned-invoice-scan) PDF - [extraction returns nonsense glyphs from a PDF that clearly has real text →](https://txtfetch.com/fixes/garbled-pdf-text) - [All fixes →](https://txtfetch.com/fixes) ## Fix the text you already have. The free cleaner repairs this damage in your browser. Nothing leaves the page. [Clean up your text →](https://txtfetch.com/tools/clean-extracted-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/fixes/tables-lose-structure # My PDF table extracted as a wall of misaligned text. A table in a PDF usually has no structural markup at all, just a grid of text positioned to look aligned. Extraction sees the positions, not the grid. looks-like ``` Name Amount Date Alice 120 2026-01-04 Bob 75 2026-01-11 Carol 410 2026-01-19 ``` Looks like a table to a human skimming it. To a chunker or downstream parser it's indistinguishable from a run-on sentence with irregular spacing. why-it-happens A table in a PDF or a scanned document has no structural markup in most extraction paths. Text extraction sees a grid of text positioned at specific coordinates. Absent a real table model, it falls back to describing what it sees. That description is runs of text, separated by however many spaces happened to align the columns visually. The row/column relationship, which number belongs to which header, exists only in that visual alignment. Any wrapping, chunking, or whitespace-normalization step downstream is free to destroy it without ever knowing it was destroying anything. confirm-it - Paste extracted text into the chunk previewer. Two or more space-run "column" lines in a row trips this signal specifically. [Scan for space-run table columns](https://txtfetch.com/tools/chunk-preview) fix-it-yourself Extract tables directly with a table-aware library pdfplumber and camelot both detect table regions and return real row/column structures instead of a flattened text dump. Reach for them specifically on pages you know contain tables. python ``` import pdfplumber with pdfplumber.open("input.pdf") as pdf: for page in pdf.pages: for table in page.extract_tables(): print(table) # list of rows, each a list of cells ``` Render tables as markdown for a chunker to key off Turning each detected table into a markdown table gives your chunker an explicit structural boundary to split on instead of guessing from whitespace. python ``` def to_markdown(rows): header, *body = rows out = ["| " + " | ".join(header) + " |", "|" + "|".join(["---"] * len(header)) + "|"] out += ["| " + " | ".join(r) + " |" for r in body] return "\n".join(out) ``` what-txtfetch-does The important distinction is the response shape you ask for, not just the file type. The default ?format=text response flattens a table into space-separated cell values for every format. That's the shape this page is about. Pass ?format=markdown and each table comes back as a real GFM markdown table. Pass ?format=json instead, and you get a typed element tree, where a table element carries its own cells, rows, and cols. That tree also carries markdown and html renderings of the same table. Both ship on the standard tier. For structured formats, .xlsx/.ods spreadsheets and DOCX/HTML tables, the underlying cell model genuinely exists in the file. So Tika reads real cells (resolving .xlsx shared strings and cached formula values) rather than inferring them from visual spacing. Asking for ?format=markdown or ?format=json is what carries that structure through into the response instead of flattening it. See /extract/xlsx. For a table embedded in a PDF or a scan, there is no cell model in the file to read. Structure has to be recovered from the page. Tika's structured extraction does attempt that. On the hardest documents, it's where Tika struggles most. On the table-heavy PDF in our benchmark corpus, table cell-F1 is 0.0% at the standard tier. It reaches 100% at the premium tier (?quality=premium, a vision-language model reading the page). That premium figure covers only the 3 hardest documents in the corpus, not the whole 10-document set. Premium always routes async: a 202 response plus a job\_id to poll. It falls back to the Tika baseline, flagged metadata.tier\_downgraded: true, if a guardrail trips. See /benchmarks for the full methodology and caveats. what-it-costs-you-downstream A flattened table row reads to a chunker as an ordinary sentence with odd spacing. Column order can scramble, and a header can end up chunked away from the rows it labels. A numeric answer synthesized from that chunk loses the row/column context that made the number meaningful in the first place. faq **Does txtfetch preserve table structure for spreadsheets the same way it fails to for PDFs?**: A spreadsheet has a genuine advantage: its cells are a real, addressable data model. There's structure in the file to read, rather than infer from visual spacing. But you still have to ask for it. The default ?format=text response flattens a sheet into space-separated cell values. ?format=markdown (or ?format=json) is what returns real rows and columns instead. The harder problem described on this page is a table embedded in a PDF or a scan. There, no cell model exists in the file at all. **Is there a call-time option to get real table structure from a PDF?**: Yes, two options that compose. ?format=markdown or ?format=json changes the response shape, so a recovered table comes back as a real table (cells, rows, cols) rather than flattened text. Both ship on the standard tier. ?quality=premium changes which engine reads the page instead, routing it through a vision-language model. That's the one that matters most for a hard PDF table. Our benchmark corpus measures standard-tier table cell-F1 at 0.0%, against premium's 100%, on the 3 hardest documents. See /benchmarks. **Why does a chunker make table damage worse instead of just passing it through?**: A chunk boundary can land mid-row or mid-table, splitting a header from the rows it labels, or splitting one row across two chunks. Once that happens, no downstream step can reassemble which number belonged to which column. The structural information is gone, not just displayed oddly. related-reading - [Docs: format=markdown and format=json →](https://txtfetch.com/docs#response-formats) - [Docs: the premium quality tier →](https://txtfetch.com/docs#premium-quality) - [Measured table-fidelity accuracy →](https://txtfetch.com/benchmarks) - [Extract tables from spreadsheets →](https://txtfetch.com/extract/xlsx) - [Extracting tables for RAG →](https://txtfetch.com/blog/extract-tables-for-rag) - [See the actual text: a 5-column pricing table, raw vs. corrected →](https://txtfetch.com/diff#table-heavy-product-catalog) Layout & structure - [two-column PDF text comes out interleaved and scrambled →](https://txtfetch.com/fixes/columns-out-of-order) - [the same header, footer, or page number shows up in every chunk →](https://txtfetch.com/fixes/headers-and-footers-in-every-chunk) - [extracted text is hard-wrapped at the print page's line width →](https://txtfetch.com/fixes/hard-wrapped-lines) - [extracted text is one giant undifferentiated blob with no paragraph breaks →](https://txtfetch.com/fixes/no-paragraph-breaks) - [All fixes →](https://txtfetch.com/fixes) ## Fix the text you already have. The free cleaner repairs this damage in your browser. Nothing leaves the page. [Clean up your text →](https://txtfetch.com/tools/clean-extracted-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/for # Extract text in your language. Every ecosystem has its own parser zoo. A PDF library here, an Office reader there, an OCR wrapper bolted on the side. Pick yours below for the honest rundown and the one request that replaces it. These pages exist because "extract text from a PDF in Python" and "parse a DOCX in Node.js" are real, distinct searches. The honest answer is different for every language, because every ecosystem's libraries stop in a different place. Each page below names the real libraries developers already reach for, what they cover well, and exactly where they stop. Then it shows the same job as one `txtfetch` request. See also [extract by format](https://txtfetch.com/extract) for the format-first version of the same idea. Or see [SDK & framework quickstarts](https://txtfetch.com/docs/quickstarts) for official clients and RAG-framework loaders, and [solutions](https://txtfetch.com/solutions) for the use-case view. - **[Python](https://txtfetch.com/for/python)** Python 3.9+ · requests or httpx ## Python's parser zoo, replaced by one POST. pypdf, python-docx, openpyxl, extract-msg, and pytesseract. One POST replaces them all. - `pypdf` - `python-docx` - `openpyxl` - `extract-msg` - `pytesseract` Read the Python guide → - **[JavaScript](https://txtfetch.com/for/javascript)** Node.js 18+ · built-in fetch ## Four npm packages, one fetch() call. pdf-parse, mammoth, xlsx, and tesseract.js. One fetch() call replaces the whole stack. - `pdf-parse` - `mammoth` - `xlsx` - `tesseract.js` Read the JavaScript guide → - **[Go](https://txtfetch.com/for/go)** Go 1.21+ · net/http, standard library only ## The library Go's ecosystem doesn't have. No mainstream Office parser. OCR needs cgo. One net/http call sidesteps both. - `ledongthuc/pdf` - `unipdf (UniDoc)` - `gosseract` - `(community OOXML readers)` Read the Go guide → - **[Java](https://txtfetch.com/for/java)** Java 11+ · java.net.http, standard library only ## The libraries you'd reach for are already inside Tika. PDFBox, POI, and tess4j are Tika's own building blocks. You would be re-deriving the glue. - `Apache PDFBox` - `Apache POI (HSSF/XSSF)` - `Apache POI (HWPF/XWPF)` - `tess4j` Read the Java guide → - **[C# / .NET](https://txtfetch.com/for/csharp)** .NET 6+ · System.Net.Http, standard library only ## Three NuGet packages, one HttpClient call. PdfPig or iText, plus the Open XML SDK, plus a Tesseract wrapper. Or just one HttpClient call. - `PdfPig` - `iText` - `Open XML SDK` - `Tesseract (NuGet wrapper)` Read the C# / .NET guide → ## Pick your language and paste it in. Every guide here carries a snippet that runs as written. Add your key and it works. [Get an API key →](https://app.txtfetch.com/signup) [More SDK quickstarts →](https://txtfetch.com/docs/quickstarts) --- # https://txtfetch.com/for/csharp # Three NuGet packages, one HttpClient call. PdfPig or iText, the Open XML SDK, and a Tesseract wrapper each do one job well. None of the three talk to each other, and none of them do OCR without native binaries. the-parser-zoo .NET's document story means picking a library per format, and living with each one's specific trade-off. On PDF, PdfPig is MIT-licensed and capable for text extraction, but it has no OCR of its own. iText is more full-featured, but it is dual-licensed. It is free under AGPLv3, which requires open-sourcing anything that links it. In most commercial contexts, that is a non-starter. The alternative is a paid commercial license. The Open XML SDK reads .docx, .xlsx, and .pptx directly against the OOXML schema. But its object model is low-level by design. Pulling plain text out of a paragraph means walking Run and Text elements manually. It also has nothing at all for the legacy .doc, .xls, and .ppt binary formats, which need a separate library entirely. OCR means a native Tesseract wrapper, commonly the Tesseract NuGet package, itself a wrapper over the C++ library. It ships platform-specific native binaries (win-x64, linux-x64, osx-arm64, and more) as part of your deployment. Picking the wrong runtime identifier for a target container is a common source of a DllNotFoundException. That error only shows up once deployed, not at build time. | library | covers | stops at | | --- | --- | --- | | `PdfPig` | PDF text extraction, MIT-licensed | No OCR of its own — scanned pages with no text layer return nothing | | `iText` | Full-featured PDF reading and writing | Free under AGPLv3 (viral copyleft) or a paid commercial license — a real decision for closed-source products | | `Open XML SDK` | Direct, schema-accurate .docx/.xlsx/.pptx access | Low-level object model (manual Run/Text traversal); no legacy .doc/.xls/.ppt support at all | | `Tesseract (NuGet wrapper)` | OCR via a native Tesseract binary per platform | Ships platform-specific native binaries; a wrong runtime identifier fails only after deployment | one-request txtfetch replaces the three-library split with one call. POST a file, or pass ?url=, to https://api.txtfetch.com/v1/extract using System.Net.Http and System.Text.Json. Both are already in the BCL, so this needs zero NuGet packages. There is no AGPL-vs-commercial licensing decision for PDF, and no manual Run/Text traversal for Office files. There is also no native Tesseract runtime identifier to get right for OCR. The response is the same { "status": "success", "extracted\_text": "..." } across every format. C# ```csharp using System.Net.Http.Headers; using System.Text.Json; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("TXTFETCH_KEY")); using var content = new MultipartFormDataContent(); var fileBytes = await File.ReadAllBytesAsync("report.pdf"); content.Add(new ByteArrayContent(fileBytes), "file", "report.pdf"); var response = await client.PostAsync("https://api.txtfetch.com/v1/extract", content); var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement; Console.WriteLine(json.GetProperty("extracted_text").GetString()); ``` ``` { "status": "success", "extracted_text": "..." } ``` from-a-url Skip the download entirely. Pass a `url` parameter and txtfetch fetches the document server-side: C# ```csharp using System.Net.Http.Headers; using System.Text.Json; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("TXTFETCH_KEY")); var url = "https://api.txtfetch.com/v1/extract?url=" + Uri.EscapeDataString("https://example.com/report.pdf"); var response = await client.PostAsync(url, null); var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement; Console.WriteLine(json.GetProperty("extracted_text").GetString()); ``` errors Every non-success response carries a stable `error.code`. Match on that, not on `error.message`. See the full [error reference](https://txtfetch.com/docs/errors) for every code and HTTP status txtfetch can return. C# ```csharp using System.Net; using System.Net.Http.Headers; using System.Text.Json; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("TXTFETCH_KEY")); using var content = new MultipartFormDataContent(); var fileBytes = await File.ReadAllBytesAsync("report.pdf"); content.Add(new ByteArrayContent(fileBytes), "file", "report.pdf"); var response = await client.PostAsync("https://api.txtfetch.com/v1/extract", content); var body = await response.Content.ReadAsStringAsync(); var json = JsonDocument.Parse(body).RootElement; if (response.IsSuccessStatusCode) { Console.WriteLine(json.GetProperty("extracted_text").GetString()); } else if (response.StatusCode == (HttpStatusCode)429) { var code = json.GetProperty("error").GetProperty("code").GetString(); var retryAfter = response.Headers.RetryAfter?.Delta?.TotalSeconds; Console.WriteLine($"back off: {code}, retry after {retryAfter}s"); } else { var error = json.GetProperty("error"); Console.WriteLine($"extraction failed: {error.GetProperty("code").GetString()} — {error.GetProperty("message").GetString()}"); } ``` big-files-and-batches Large uploads or slow documents are routed to an async job automatically. That returns a `202` plus a `job_id` to poll, and `?async=true` forces that path for any request. Direct upload size ceilings by plan: Hobby 10 MB, Developer 50 MB, Scale 200 MB. Use `?url=` for anything larger. Server-side fetches aren't held to the upload ceiling. See [async jobs & webhooks](https://txtfetch.com/docs/async) for the full lifecycle, including webhook delivery instead of polling. C# ```csharp using System.Net.Http.Headers; using System.Text.Json; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("TXTFETCH_KEY")); var submitUrl = "https://api.txtfetch.com/v1/extract?url=" + Uri.EscapeDataString("https://example.com/report.pdf") + "&async=true"; var submit = await client.PostAsync(submitUrl, null); var submitJson = JsonDocument.Parse(await submit.Content.ReadAsStringAsync()).RootElement; var jobId = submitJson.GetProperty("job_id").GetString(); JsonElement result; while (true) { await Task.Delay(2000); var poll = await client.GetAsync($"https://api.txtfetch.com/v1/extract/{jobId}"); result = JsonDocument.Parse(await poll.Content.ReadAsStringAsync()).RootElement; if (result.GetProperty("status").GetString() != "processing") break; } Console.WriteLine(result.GetProperty("extracted_text").GetString()); ``` gotchas - iText's AGPLv3 free tier requires open-sourcing software that links it, in most distribution models. Many commercial .NET teams end up on the paid commercial license specifically to avoid that. That license cost is more than most PDF-reading tasks need to carry. - The Open XML SDK's object model mirrors the OOXML schema closely. That is powerful for editing, but it means even "give me the plain text" requires walking Paragraph → Run → Text nodes yourself. There's no ExtractText() convenience method. - Open XML SDK is OOXML-only. A legacy .doc or .xls, still common in older enterprise archives, needs an entirely separate library. The SDK doesn't parse the OLE2 binary format at all. - The Tesseract NuGet wrapper's native binaries are runtime-identifier-specific (win-x64, linux-x64, linux-arm64, and more). A container built for the wrong RID compiles fine, then throws DllNotFoundException the first time OCR actually runs. formats - [PDF](https://txtfetch.com/extract/pdf) - [Word / PowerPoint / Excel](https://txtfetch.com/extract/docx) - [Legacy .doc / .xls / .ppt](https://txtfetch.com/extract/legacy-office) - [Scans & images (OCR)](https://txtfetch.com/extract/image) faq **How do I extract text from a PDF in C# without iText's AGPL license?**: POST the PDF (or pass ?url=) to https://api.txtfetch.com/v1/extract with System.Net.Http. There is no PDF library to choose, and so no AGPLv3-vs-commercial-license decision to make at all. The response is { "status": "success", "extracted_text": "..." }. **Does the Open XML SDK have a simple way to get plain text from a .docx?**: Not directly. Its object model mirrors the OOXML schema, so plain text means walking Paragraph, Run, and Text elements yourself. txtfetch returns extracted_text directly for .docx, .pptx, and .xlsx. It also reads the legacy .doc, .xls, and .ppt formats, which the Open XML SDK doesn't read at all. **How do I OCR a scanned document in .NET without shipping native Tesseract binaries?**: POST the scan to txtfetch. OCR runs server-side, so there's no Tesseract NuGet package, no per-runtime-identifier native binary, and no DllNotFoundException risk in your deployment. **Is there an official .NET SDK for txtfetch?**: Not yet. But there's nothing to install in the meantime. It's a single HttpClient POST using System.Net.Http and System.Text.Json, both already part of the .NET base class library, shown above. go-further - [How to extract text from a PDF for RAG →](https://txtfetch.com/blog/extract-text-from-pdf-for-rag) - [Parsing DOCX, PPTX, and XLSX for LLM ingestion →](https://txtfetch.com/blog/parse-office-docs-docx-pptx-xlsx-for-llms) - [Document workflows & automation →](https://txtfetch.com/solutions/document-workflows) - [API quickstart →](https://txtfetch.com/docs) - [How the pipeline works →](https://txtfetch.com/how-it-works) - [Get an API key →](https://app.txtfetch.com/signup) other-languages - [`Python`](https://txtfetch.com/for/python) - [`JavaScript`](https://txtfetch.com/for/javascript) - [`Go`](https://txtfetch.com/for/go) - [`Java`](https://txtfetch.com/for/java) - [All languages →](https://txtfetch.com/for) ## Paste it into your project. The C# / .NET snippet above runs as written. Add your key and it works. [Get an API key →](https://app.txtfetch.com/signup) [More SDK quickstarts →](https://txtfetch.com/docs/quickstarts) --- # https://txtfetch.com/for/go # The library Go's ecosystem doesn't have. Python and Node both have an imperfect but usable Office parser. Go doesn't. Its OCR story means cgo, and cgo breaks the static, cross-compiled binary Go programs are supposed to be. the-parser-zoo Go's document-parsing ecosystem is thin compared to Python's or Node's. The gap is structural, not just a missing package. PDF text extraction has options. ledongthuc/pdf covers basic digital-native text. UniDoc/unipdf is capable, but it is commercially licensed for anything beyond noncommercial use. Neither is a python-docx or mammoth equivalent with meaningful adoption for reading .docx, .pptx, or .xlsx as plain text. Teams either shell out to a converter, or hand-parse the OOXML zip-of-XML themselves. Searching for a golang docx parser turns up low-adoption repos and wrappers around non-Go tooling, not a python-docx equivalent. OCR is the sharper problem. The practical option is gosseract, a cgo binding to the system libtesseract. That means CGO\_ENABLED=1, a matching Tesseract install on every build and deploy target, and no more single static binary. It also means giving up the easy cross-compilation Go is usually chosen for. A Lambda or scratch-container Go binary that needs OCR faces a choice. Either bundle a Tesseract shared library manually, or give up the zero-dependency deploy story entirely. | library | covers | stops at | | --- | --- | --- | | `ledongthuc/pdf` | Digital-native PDF text extraction | No OCR, no Office formats; struggles with complex layouts | | `unipdf (UniDoc)` | PDF text, forms, and more, capably | Commercial license required beyond noncommercial/eval use | | `gosseract` | OCR via cgo bindings to libtesseract | Requires CGO\_ENABLED=1 and a matching native Tesseract install on every build/deploy target | | `(community OOXML readers)` | Fragmented, low-adoption .docx/.xlsx readers | No mammoth/python-docx-level standard; most teams hand-parse the zip themselves | one-request txtfetch replaces the whole gap with one request. POST a file, or pass ?url=, to https://api.txtfetch.com/v1/extract using net/http from the standard library. There is no cgo, no CGO\_ENABLED=1, and no Tesseract shared library to vendor alongside your binary. The response is the same { "status": "success", "extracted\_text": "..." }, whether the source was a PDF, an Office file, or a scan. A Go service stays a single static binary, regardless of what it's asked to extract. 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("report.pdf") if err != nil { panic(err) } defer f.Close() var body bytes.Buffer writer := multipart.NewWriter(&body) part, err := writer.CreateFormFile("file", "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) } ``` ``` { "status": "success", "extracted_text": "..." } ``` from-a-url Skip the download entirely. Pass a `url` parameter and txtfetch fetches the document server-side: 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.pdf") endpoint.RawQuery = q.Encode() req, err := http.NewRequest("POST", endpoint.String(), nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result extractResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Println(result.ExtractedText) } ``` errors Every non-success response carries a stable `error.code`. Match on that, not on `error.message`. See the full [error reference](https://txtfetch.com/docs/errors) for every code and HTTP status txtfetch can return. Go ```go package main import ( "bytes" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "os" ) type extractError struct { Code string `json:"code"` Message string `json:"message"` } type extractResponse struct { Status string `json:"status"` ExtractedText string `json:"extracted_text"` Error *extractError `json:"error"` } func main() { f, err := os.Open("report.pdf") if err != nil { panic(err) } defer f.Close() var body bytes.Buffer writer := multipart.NewWriter(&body) part, err := writer.CreateFormFile("file", "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) } switch { case resp.StatusCode == 200: fmt.Println(result.ExtractedText) case resp.StatusCode == 429: fmt.Printf("back off: %s, retry after %ss\n", result.Error.Code, resp.Header.Get("Retry-After")) default: fmt.Printf("extraction failed: %s — %s\n", result.Error.Code, result.Error.Message) } } ``` big-files-and-batches Large uploads or slow documents are routed to an async job automatically. That returns a `202` plus a `job_id` to poll, and `?async=true` forces that path for any request. Direct upload size ceilings by plan: Hobby 10 MB, Developer 50 MB, Scale 200 MB. Use `?url=` for anything larger. Server-side fetches aren't held to the upload ceiling. See [async jobs & webhooks](https://txtfetch.com/docs/async) for the full lifecycle, including webhook delivery instead of polling. Go ```go package main import ( "encoding/json" "fmt" "net/http" "net/url" "os" "time" ) type jobResponse struct { Status string `json:"status"` JobID string `json:"job_id"` ExtractedText string `json:"extracted_text"` } func poll(req *http.Request) (*jobResponse, error) { resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var result jobResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } return &result, nil } func main() { token := "Bearer " + os.Getenv("TXTFETCH_KEY") endpoint, _ := url.Parse("https://api.txtfetch.com/v1/extract") q := endpoint.Query() q.Set("url", "https://example.com/report.pdf") q.Set("async", "true") endpoint.RawQuery = q.Encode() submitReq, _ := http.NewRequest("POST", endpoint.String(), nil) submitReq.Header.Set("Authorization", token) job, err := poll(submitReq) if err != nil { panic(err) } pollURL := "https://api.txtfetch.com/v1/extract/" + job.JobID var result *jobResponse for { pollReq, _ := http.NewRequest("GET", pollURL, nil) pollReq.Header.Set("Authorization", token) result, err = poll(pollReq) if err != nil { panic(err) } if result.Status != "processing" { break } time.Sleep(2 * time.Second) } fmt.Println(result.ExtractedText) } ``` gotchas - CGO\_ENABLED=0 builds are the common way to produce a truly static, scratch-container-friendly Go binary. gosseract's cgo dependency means those builds can't use it at all. - Cross-compiling a cgo-dependent binary (say, building for linux/arm64 from an amd64 CI runner) means cross-compiling libtesseract too, not just your Go code. That is a build-pipeline problem most Go teams would rather not own. - There's no single OOXML reader with the adoption level of Python's python-docx. Most Go codebases either shell out to LibreOffice or Pandoc for conversion, or hand-walk the .docx zip's document.xml. - unipdf's licensing (commercial past noncommercial/evaluation use) is worth checking before it ends up load-bearing in a shipped product. formats - [PDF](https://txtfetch.com/extract/pdf) - [Word / PowerPoint / Excel](https://txtfetch.com/extract/docx) - [Spreadsheets](https://txtfetch.com/extract/xlsx) - [Scans & images (OCR)](https://txtfetch.com/extract/image) faq **How do I extract text from a PDF in Go without cgo?**: POST the PDF (or pass ?url=) to https://api.txtfetch.com/v1/extract using net/http. There is no cgo, no CGO_ENABLED flag, and no native library to link. The response is { "status": "success", "extracted_text": "..." }, whether the PDF is digital-native or scanned. **Is there a golang library for reading .docx or .xlsx files?**: Nothing has the adoption level of Python's python-docx or openpyxl. Most Go teams shell out to a converter, or hand-parse the OOXML zip. txtfetch handles the whole Office family (.docx, .pptx, .xlsx, and the legacy .doc, .xls, .ppt) through the same endpoint as everything else. **How do I OCR a scanned document in Go without gosseract's cgo dependency?**: POST the scan to txtfetch. OCR runs server-side, so your Go binary never links libtesseract and never sets CGO_ENABLED=1. It stays cross-compilable to any target, the same way it was before you needed OCR. **Does txtfetch have an official Go SDK?**: No, and it doesn't need one. It's a single JSON-in, JSON-out HTTP request. The standard library's net/http and encoding/json cover the whole client, in well under fifty lines, shown above. go-further - [How to extract text from a PDF for RAG →](https://txtfetch.com/blog/extract-text-from-pdf-for-rag) - [Document workflows & automation →](https://txtfetch.com/solutions/document-workflows) - [Measured extraction accuracy by category →](https://txtfetch.com/benchmarks) - [API quickstart →](https://txtfetch.com/docs) - [How the pipeline works →](https://txtfetch.com/how-it-works) - [Get an API key →](https://app.txtfetch.com/signup) other-languages - [`Python`](https://txtfetch.com/for/python) - [`JavaScript`](https://txtfetch.com/for/javascript) - [`Java`](https://txtfetch.com/for/java) - [`C# / .NET`](https://txtfetch.com/for/csharp) - [All languages →](https://txtfetch.com/for) ## Paste it into your project. The Go snippet above runs as written. Add your key and it works. [Get an API key →](https://app.txtfetch.com/signup) [More SDK quickstarts →](https://txtfetch.com/docs/quickstarts) --- # https://txtfetch.com/for/java # The libraries you'd reach for are already inside Tika. PDFBox for PDF, POI for Office, tess4j for OCR: solid libraries individually. Wiring them into one pipeline, with consistent routing and error handling, is a smaller version of the project Apache Tika already finished. the-parser-zoo Java's document-parsing libraries are genuinely good. That makes the trap subtler than a missing library. It is closer to rebuilding Tika yourself. Apache PDFBox extracts PDF text and structure well. But text-only PDFBox has no OCR path of its own, so a scanned page still comes back blank. Apache POI covers the Office family, but its API is split down a historical fault line. HSSF handles legacy binary .xls, XSSF handles modern .xlsx, HWPF handles .doc, and XWPF handles .docx. That is four class hierarchies for what a business user calls "just Word and Excel." POI releases and Office file variants also carry version-compatibility issues that surface as opaque exceptions. OCR means tess4j, a JNA binding to native Tesseract. That binding pulls per-OS native library binaries (Windows, macOS, Linux, and increasingly per-CPU-architecture) into what was otherwise a portable JAR. A mismatch between the JNA version and the bundled native binary is a common source of runtime-only failures. Those failures never show up at compile time. Apache Tika itself is this exact stack: PDFBox, POI, and Tesseract via tess4j, wired together with format detection and routing. Assembling that wiring yourself, correctly, is a meaningfully sized project, not an afternoon's integration work. | library | covers | stops at | | --- | --- | --- | | `Apache PDFBox` | PDF text and structure extraction | No OCR of its own — a scanned page with no text layer returns nothing | | `Apache POI (HSSF/XSSF)` | Excel — legacy .xls (HSSF) and modern .xlsx (XSSF) | Two separate API families for one file type, plus version-compatibility quirks across POI releases | | `Apache POI (HWPF/XWPF)` | Word — legacy .doc (HWPF) and modern .docx (XWPF) | Same split-API problem as Excel; no unified read-any-Word-file call | | `tess4j` | OCR via JNA bindings to native Tesseract | Ships per-OS native binaries into your JAR; JNA/native-version mismatches fail only at runtime | one-request txtfetch runs that stack for you, behind one endpoint. POST a file, or pass ?url=, to https://api.txtfetch.com/v1/extract with java.net.http from the JDK's standard library. There is no PDFBox, POI, or tess4j version matrix to pin. There is no per-OS native Tesseract binary to bundle into your JAR or container image. The response is the same { "status": "success", "extracted\_text": "..." }, whether the source needed PDFBox's job, POI's, or tess4j's. Java ```java import java.io.ByteArrayOutputStream; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; class Main { public static void main(String[] args) throws Exception { byte[] fileBytes = Files.readAllBytes(Path.of("report.pdf")); String boundary = "txtfetch-" + System.nanoTime(); ByteArrayOutputStream body = new ByteArrayOutputStream(); body.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"; filename=\"report.pdf\"\r\nContent-Type: application/octet-stream\r\n\r\n").getBytes()); body.write(fileBytes); body.write(("\r\n--" + boundary + "--\r\n").getBytes()); HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.txtfetch.com/v1/extract")) .header("Authorization", "Bearer " + System.getenv("TXTFETCH_KEY")) .header("Content-Type", "multipart/form-data; boundary=" + boundary) .POST(HttpRequest.BodyPublishers.ofByteArray(body.toByteArray())) .build(); HttpResponse response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); // {"status": "success", "extracted_text": "...", "metadata": {...}} — // parse with your JSON library of choice (Jackson, org.json, Gson, ...). System.out.println(response.body()); } } ``` ``` { "status": "success", "extracted_text": "..." } ``` from-a-url Skip the download entirely. Pass a `url` parameter and txtfetch fetches the document server-side: Java ```java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; class Main { public static void main(String[] args) throws Exception { String url = "https://api.txtfetch.com/v1/extract?url=" + URLEncoder.encode("https://example.com/report.pdf", StandardCharsets.UTF_8); HttpRequest request = HttpRequest.newBuilder(URI.create(url)) .header("Authorization", "Bearer " + System.getenv("TXTFETCH_KEY")) .POST(HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); } } ``` errors Every non-success response carries a stable `error.code`. Match on that, not on `error.message`. See the full [error reference](https://txtfetch.com/docs/errors) for every code and HTTP status txtfetch can return. Java ```java import java.io.ByteArrayOutputStream; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; class Main { static String field(String json, String name) { String key = "\"" + name + "\":\""; int start = json.indexOf(key); if (start < 0) return null; start += key.length(); return json.substring(start, json.indexOf('"', start)); } public static void main(String[] args) throws Exception { byte[] fileBytes = Files.readAllBytes(Path.of("report.pdf")); String boundary = "txtfetch-" + System.nanoTime(); ByteArrayOutputStream body = new ByteArrayOutputStream(); body.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"; filename=\"report.pdf\"\r\nContent-Type: application/octet-stream\r\n\r\n").getBytes()); body.write(fileBytes); body.write(("\r\n--" + boundary + "--\r\n").getBytes()); HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.txtfetch.com/v1/extract")) .header("Authorization", "Bearer " + System.getenv("TXTFETCH_KEY")) .header("Content-Type", "multipart/form-data; boundary=" + boundary) .POST(HttpRequest.BodyPublishers.ofByteArray(body.toByteArray())) .build(); HttpResponse response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println(response.body()); } else if (response.statusCode() == 429) { String retryAfter = response.headers().firstValue("Retry-After").orElse("?"); System.out.println("back off: " + field(response.body(), "code") + ", retry after " + retryAfter + "s"); } else { System.out.println("extraction failed: " + field(response.body(), "code")); } } } ``` big-files-and-batches Large uploads or slow documents are routed to an async job automatically. That returns a `202` plus a `job_id` to poll, and `?async=true` forces that path for any request. Direct upload size ceilings by plan: Hobby 10 MB, Developer 50 MB, Scale 200 MB. Use `?url=` for anything larger. Server-side fetches aren't held to the upload ceiling. See [async jobs & webhooks](https://txtfetch.com/docs/async) for the full lifecycle, including webhook delivery instead of polling. Java ```java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; class Main { static String field(String json, String name) { String key = "\"" + name + "\":\""; int start = json.indexOf(key); if (start < 0) return null; start += key.length(); return json.substring(start, json.indexOf('"', start)); } public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); String auth = "Bearer " + System.getenv("TXTFETCH_KEY"); String submitUrl = "https://api.txtfetch.com/v1/extract?url=" + URLEncoder.encode("https://example.com/report.pdf", StandardCharsets.UTF_8) + "&async=true"; HttpRequest submitReq = HttpRequest.newBuilder(URI.create(submitUrl)) .header("Authorization", auth) .POST(HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse submitResp = client.send(submitReq, HttpResponse.BodyHandlers.ofString()); String jobId = field(submitResp.body(), "job_id"); String status; String body; do { Thread.sleep(2000); HttpRequest pollReq = HttpRequest.newBuilder(URI.create("https://api.txtfetch.com/v1/extract/" + jobId)) .header("Authorization", auth) .GET() .build(); body = client.send(pollReq, HttpResponse.BodyHandlers.ofString()).body(); status = field(body, "status"); } while ("processing".equals(status)); // Once the job leaves "processing", print the finished result — // same success/error shape a synchronous 200 would have returned. System.out.println(body); } } ``` gotchas - Apache Tika, the engine behind txtfetch, is PDFBox, POI, and Tesseract (via tess4j) wired together, with format detection and routing already solved. Assembling that stack yourself is closer to a rewrite of Tika than a quick integration. - POI's HSSF/XSSF and HWPF/XWPF split means "read this Excel file" and "read this Word file" are each two APIs, not one. Code that only handles XSSF silently mishandles a legacy .xls someone still has lying around. - tess4j's native Tesseract binaries are platform- and architecture-specific. A JAR built and tested on one OS can fail to load its native library entirely on another. The fix is to bundle every target you plan to deploy to. - PDFBox has no built-in OCR fallback. Detecting that a PDF page is a blank scan, and routing it to tess4j separately, is logic you have to write and maintain yourself. formats - [PDF](https://txtfetch.com/extract/pdf) - [Word / PowerPoint / Excel](https://txtfetch.com/extract/docx) - [Legacy .doc / .xls / .ppt](https://txtfetch.com/extract/legacy-office) - [Scans & images (OCR)](https://txtfetch.com/extract/image) faq **How do I extract text from a PDF in Java without Apache PDFBox?**: POST the PDF (or pass ?url=) to https://api.txtfetch.com/v1/extract using java.net.http. There is no PDFBox dependency, and no separate OCR step to add for scanned pages. txtfetch escalates to OCR automatically, when a document's text pass comes back blank. **Why does Apache POI need different classes for .xls and .xlsx?**: POI's HSSF/XSSF split reflects the two underlying file formats: OLE2 binary for .xls, OOXML zip-of-XML for .xlsx. These really are structurally different. txtfetch detects the actual format from the file's bytes, and returns the same response shape either way. Your calling code doesn't need to know which one it received. **Is Apache Tika the same thing as txtfetch?**: Tika is the open-source parsing engine txtfetch runs: PDFBox, POI, and Tesseract wired together with format detection and routing. txtfetch is that engine deployed as a hosted API, with metering, async job handling, and webhooks on top. You don't run and patch the JVM stack yourself. **Do I need to bundle native Tesseract binaries in my Java project?**: No. OCR runs server-side. If you were planning to use tess4j, txtfetch removes the need for its per-OS native Tesseract binaries and JNA version-matching entirely. go-further - [Parsing DOCX, PPTX, and XLSX for LLM ingestion →](https://txtfetch.com/blog/parse-office-docs-docx-pptx-xlsx-for-llms) - [OCR scanned documents through one API call →](https://txtfetch.com/blog/ocr-scanned-documents-api) - [Document workflows & automation →](https://txtfetch.com/solutions/document-workflows) - [API quickstart →](https://txtfetch.com/docs) - [How the pipeline works →](https://txtfetch.com/how-it-works) - [Get an API key →](https://app.txtfetch.com/signup) other-languages - [`Python`](https://txtfetch.com/for/python) - [`JavaScript`](https://txtfetch.com/for/javascript) - [`Go`](https://txtfetch.com/for/go) - [`C# / .NET`](https://txtfetch.com/for/csharp) - [All languages →](https://txtfetch.com/for) ## Paste it into your project. The Java snippet above runs as written. Add your key and it works. [Get an API key →](https://app.txtfetch.com/signup) [More SDK quickstarts →](https://txtfetch.com/docs/quickstarts) --- # https://txtfetch.com/for/javascript # Four npm packages, one fetch() call. pdf-parse, mammoth, xlsx, tesseract.js: each one solid at its single job. None of them knows the other three exist. the-parser-zoo Node's document-parsing story is a shelf of single-purpose packages, not one library. Each package has a real limitation worth knowing before you build around it. pdf-parse is a thin wrapper around Mozilla's pdf.js, and it has not seen much maintenance. It gets plain text out of a normal PDF. But it has no OCR path, and it has no graceful handling for encrypted or malformed files. A corrupt PDF throws an error instead of degrading gracefully. mammoth converts .docx to plain text or HTML, but only .docx. It has nothing for .doc, .pptx, or .xlsx, and it deliberately drops images rather than describing them. xlsx (the ubiquitous SheetJS-derived package) reads spreadsheet cells across a wide format range. But formula evaluation depends on the workbook already carrying a cached result, the same trap as Python's openpyxl. Its API surface has also shifted across versions in ways that break upgrades. tesseract.js runs real OCR through a WASM build of Tesseract, which is genuinely impressive for a browser. But on a server it means downloading language-model data at cold start, a real problem in serverless. It also means running CPU-bound recognition on Node's single main thread, unless you wire up worker threads yourself. | library | covers | stops at | | --- | --- | --- | | `pdf-parse` | Digital-native PDF text via pdf.js | No OCR; throws rather than degrading on encrypted or malformed PDFs | | `mammoth` | .docx → plain text or HTML, style-aware | docx only — no .doc, .pptx, or .xlsx; images are dropped, not described | | `xlsx` | Spreadsheet cells across .xlsx/.xls/.csv and more | Formula values depend on a pre-existing cached result, same as Excel's own file format quirk | | `tesseract.js` | In-process OCR via a WASM Tesseract build | Downloads language data at cold start; CPU-bound recognition needs manual worker-thread wiring to avoid blocking | one-request txtfetch collapses the four-package stack into one endpoint. POST a file, or pass ?url=, and get back { "status": "success", "extracted\_text": "..." }. That is true regardless of whether the source needed pdf-parse's job, mammoth's, xlsx's, or tesseract.js's. The official @txtfetch/sdk wraps the same request in a zero-dependency TypeScript client, with typed errors. There is no per-format if/else needed to route a file to the right package before extraction can even start. JavaScript ```javascript import { readFile } from "node:fs/promises"; const file = new Blob([await readFile("report.pdf")]); const form = new FormData(); form.append("file", file, "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); ``` ``` { "status": "success", "extracted_text": "..." } ``` @txtfetch/sdk (JS/TS) npm install @txtfetch/sdk. Zero runtime dependencies, dual ESM/CJS, and it ships its own .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); ``` from-a-url Skip the download entirely. Pass a `url` parameter and txtfetch fetches the document server-side: JavaScript ```javascript const endpoint = new URL("https://api.txtfetch.com/v1/extract"); endpoint.searchParams.set("url", "https://example.com/report.pdf"); const res = await fetch(endpoint, { method: "POST", headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` }, }); const { extracted_text } = await res.json(); console.log(extracted_text); ``` errors Every non-success response carries a stable `error.code`. Match on that, not on `error.message`. See the full [error reference](https://txtfetch.com/docs/errors) for every code and HTTP status txtfetch can return. JavaScript ```javascript import { readFile } from "node:fs/promises"; const file = new Blob([await readFile("report.pdf")]); const form = new FormData(); form.append("file", file, "report.pdf"); const res = await fetch("https://api.txtfetch.com/v1/extract", { method: "POST", headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` }, body: form, }); const body = await res.json(); if (res.ok) { console.log(body.extracted_text); } else if (res.status === 429) { console.log(`back off: ${body.error.code}, retry after ${res.headers.get("Retry-After")}s`); } else { console.log(`extraction failed: ${body.error.code} — ${body.error.message}`); } ``` big-files-and-batches Large uploads or slow documents are routed to an async job automatically. That returns a `202` plus a `job_id` to poll, and `?async=true` forces that path for any request. Direct upload size ceilings by plan: Hobby 10 MB, Developer 50 MB, Scale 200 MB. Use `?url=` for anything larger. Server-side fetches aren't held to the upload ceiling. See [async jobs & webhooks](https://txtfetch.com/docs/async) for the full lifecycle, including webhook delivery instead of polling. JavaScript ```javascript const headers = { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` }; const submitUrl = new URL("https://api.txtfetch.com/v1/extract"); submitUrl.searchParams.set("url", "https://example.com/report.pdf"); submitUrl.searchParams.set("async", "true"); const submit = await fetch(submitUrl, { method: "POST", headers }); const { job_id } = await submit.json(); let result; do { await new Promise((resolve) => setTimeout(resolve, 2000)); const poll = await fetch(`https://api.txtfetch.com/v1/extract/${job_id}`, { headers }); result = await poll.json(); } while (result.status === "processing"); console.log(result.extracted_text); ``` gotchas - tesseract.js's default setup fetches its language traineddata files over the network, on first use. A serverless function gets a fresh container on every cold start. It re-downloads the language files every time, unless you bundle them and point at a local path. - pdf-parse hasn't seen substantial maintenance in years. Encrypted PDFs, and some malformed ones, throw an unhandled exception rather than a typed error you can catch and branch on. - mammoth's plain-text mode discards inline images entirely. That is correct behavior for a text-extraction step. But it means image-embedded diagrams inside a .docx contribute nothing to the output, silently. - xlsx's own maintainers have shipped breaking API changes across major versions. Pin the version deliberately, rather than accepting ^ ranges, in a document-ingestion pipeline you don't want to babysit. formats - [PDF](https://txtfetch.com/extract/pdf) - [Word / PowerPoint / Excel](https://txtfetch.com/extract/docx) - [Spreadsheets](https://txtfetch.com/extract/xlsx) - [Scans & images (OCR)](https://txtfetch.com/extract/image) faq **How do I parse a DOCX file in Node.js without mammoth?**: POST the .docx to https://api.txtfetch.com/v1/extract, or call txtfetch.extract({ file: "..." }) with the @txtfetch/sdk. You get back { "status": "success", "extracted_text": "..." }. The same call also handles .pptx, .xlsx, and legacy .doc, which mammoth doesn't. **Does tesseract.js need any setup to avoid slow cold starts?**: With txtfetch, there's no tesseract.js in your own deployment at all. OCR runs server-side on request. There is no WASM bundle to ship, and no language-model download to warm up on a fresh serverless container. **Why does my PDF parsing code throw on some files but not others?**: pdf-parse throws on encrypted or malformed PDFs rather than returning a typed error. txtfetch returns a structured { "error": { "code": "encrypted", ... } } or unsupported_format response instead. You can catch and branch on error.code, without a bare try/catch around a library's internals. **Is there an official JavaScript or TypeScript SDK?**: Yes. npm install @txtfetch/sdk. It has zero runtime dependencies. It ships both ESM and CJS builds, with .d.ts types. It requires Node ≥ 20. **Can I use txtfetch as a LangChain.js document loader?**: Yes. @txtfetch/langchain wraps the SDK as a TxtfetchLoader. Each file or URL becomes one Document, ready for a text splitter. See /docs/quickstarts for the install and a ten-line example. go-further - [@txtfetch/sdk (JS/TS) quickstart →](https://txtfetch.com/docs/quickstarts) - [Using txtfetch as a LangChain and LlamaIndex document loader →](https://txtfetch.com/blog/langchain-llamaindex-document-loader) - [Ingesting large documents and big batches →](https://txtfetch.com/blog/batch-and-large-document-ingestion) - [Search indexing pipelines →](https://txtfetch.com/solutions/search-indexing) - [API quickstart →](https://txtfetch.com/docs) - [How the pipeline works →](https://txtfetch.com/how-it-works) - [Get an API key →](https://app.txtfetch.com/signup) other-languages - [`Python`](https://txtfetch.com/for/python) - [`Go`](https://txtfetch.com/for/go) - [`Java`](https://txtfetch.com/for/java) - [`C# / .NET`](https://txtfetch.com/for/csharp) - [All languages →](https://txtfetch.com/for) ## Paste it into your project. The JavaScript snippet above runs as written. Add your key and it works. [Get an API key →](https://app.txtfetch.com/signup) [More SDK quickstarts →](https://txtfetch.com/docs/quickstarts) --- # https://txtfetch.com/for/python # Python's parser zoo, replaced by one POST. pypdf handles PDFs. python-docx handles Word. openpyxl handles Excel. extract-msg handles Outlook. pytesseract handles scans. That's five libraries, five APIs, and five sets of edge cases. txtfetch is one. the-parser-zoo Python has the deepest document-parsing ecosystem of any language. That is also the problem. Covering PDF, Office, email, and scans means installing and maintaining five separate libraries. Each one has its own API and its own blind spot. pypdf reads digital-native PDF text cleanly, but it has no OCR path and struggles with multi-column layouts and dense tables. python-docx is OOXML-only. A .docx file works. But a .doc file from 2009 throws an error before you get a single character back. And python-docx has nothing at all for .pptx or .xlsx. openpyxl reads .xlsx cells directly. But formula cells need data\_only=True to get the calculated value instead of the =SUM() string. That cached value only exists if Excel itself last saved the file. A workbook produced by another tool can hand back None instead. pytesseract is not really an OCR library. It is a thin wrapper that shells out to a system tesseract binary. You have to install that binary yourself and keep it on PATH. You also need Pillow for images, and pdf2image (which itself needs poppler-utils) if the scan is a PDF rather than a bare image. | library | covers | stops at | | --- | --- | --- | | `pypdf` | Digital-native PDF text extraction | No OCR; struggles with multi-column layouts and dense tables | | `python-docx` | .docx paragraphs, tables, and headers | OOXML only — no legacy .doc, and nothing for .pptx or .xlsx | | `openpyxl` | .xlsx cell values, including shared strings | Formula cells need data\_only=True plus a prior Excel save — no live recalculation; no .xls | | `extract-msg` | Outlook .msg header and body parsing | Only .msg — no .pst/.ost mailbox archives, and nothing outside email | | `pytesseract` | OCR, once paired with Pillow and a system Tesseract install | You install, update, and package the tesseract binary yourself; PDFs need pdf2image + poppler on top | one-request txtfetch replaces the imports, not just the parsing. One POST does the job: send a multipart file, or pass ?url=. Either way you get back { "status": "success", "extracted\_text": "..." }. That is true whether the source was a pypdf-shaped PDF, a python-docx-shaped .docx, an openpyxl-shaped .xlsx, or a pytesseract-shaped scan. The official txtfetch Python SDK wraps the same call in a typed client, with exceptions per error code. A single try/except replaces the format-detection branching a hand-rolled pipeline needs before it can even pick which library to call. Python ```python import os import requests with open("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"]) ``` ``` { "status": "success", "extracted_text": "..." } ``` txtfetch (Python SDK) pip install txtfetch. One client class, with typed exceptions per error code, and no per-format branching. 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") ``` from-a-url Skip the download entirely. Pass a `url` parameter and txtfetch fetches the document server-side: 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.pdf"}, ) print(r.json()["extracted_text"]) ``` errors Every non-success response carries a stable `error.code`. Match on that, not on `error.message`. See the full [error reference](https://txtfetch.com/docs/errors) for every code and HTTP status txtfetch can return. Python ```python import os import requests with open("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}, ) if r.status_code == 200: print(r.json()["extracted_text"]) elif r.status_code == 429: code = r.json()["error"]["code"] # "rate_limited", "quota_exceeded", or "abuse_detected" print(f"back off: {code}, retry after {r.headers['Retry-After']}s") else: error = r.json()["error"] print(f"extraction failed: {error['code']} — {error['message']}") ``` big-files-and-batches Large uploads or slow documents are routed to an async job automatically. That returns a `202` plus a `job_id` to poll, and `?async=true` forces that path for any request. Direct upload size ceilings by plan: Hobby 10 MB, Developer 50 MB, Scale 200 MB. Use `?url=` for anything larger. Server-side fetches aren't held to the upload ceiling. See [async jobs & webhooks](https://txtfetch.com/docs/async) for the full lifecycle, including webhook delivery instead of polling. Python ```python import os import time import requests HEADERS = {"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"} submit = requests.post( "https://api.txtfetch.com/v1/extract", headers=HEADERS, params={"url": "https://example.com/report.pdf", "async": "true"}, ) job_id = submit.json()["job_id"] while True: poll = requests.get(f"https://api.txtfetch.com/v1/extract/{job_id}", headers=HEADERS) result = poll.json() if result["status"] != "processing": break time.sleep(2) print(result["extracted_text"]) ``` gotchas - pip install pytesseract installs a Python wrapper, not an OCR engine. It does not install the tesseract binary itself. Without that binary on PATH, every call fails at runtime, not at import time. The error reads "tesseract is not installed or it's not in your PATH". - openpyxl's data\_only=True reads Excel's last cached calculation, not a live recalculation. A workbook written by a script that never opened it in Excel can have an empty cache. A perfectly valid formula cell can then read back as None. - pandas.read\_csv guesses UTF-8 by default. A Windows-1252 export with curly quotes or em dashes mojibakes silently, unless you pass the correct encoding= yourself. - python-docx, python-pptx, and openpyxl are three separate PyPI packages. Each has its own object model, for what marketing calls "just Office files". No single import spans the whole family. formats - [PDF](https://txtfetch.com/extract/pdf) - [Word / PowerPoint / Excel](https://txtfetch.com/extract/docx) - [Excel formulas & tables](https://txtfetch.com/extract/xlsx) - [Scans & images (OCR)](https://txtfetch.com/extract/image) - [Outlook .msg](https://txtfetch.com/extract/msg) faq **How do I extract text from a PDF in Python without pypdf or pdfplumber?**: POST the file to https://api.txtfetch.com/v1/extract (multipart, or ?url= for a remote PDF), with your API key in the Authorization header. Or use the txtfetch Python SDK's client.extract(file="..."). Either way you get back { "status": "success", "extracted_text": "..." }, whether the PDF is digital-native or scanned. There is no pypdf or pdfplumber import, and no separate OCR branch to write. **Can I OCR a scanned document in Python without installing Tesseract myself?**: Yes. POST the scan (PNG, JPG, TIFF, or a scanned PDF) to txtfetch, and OCR runs server-side automatically. There's no tesseract binary to install, no PATH to configure, and no pytesseract/Pillow/pdf2image chain to keep working across OS upgrades. **How do I read an Outlook .msg file in Python?**: extract-msg works, but it only covers .msg. txtfetch's Python SDK handles .msg the same way as every other format, with client.extract(file="thread.msg"). It also covers whole .pst/.ost mailbox archives through the same call, which extract-msg doesn't. **Does openpyxl's data_only=True always give me the calculated cell value?**: Only if Excel itself last saved the workbook. data_only=True reads a cached result, and that cache can be empty for files produced by other tools. txtfetch returns the same cached calculated value, with no data_only= flag to remember and no separate code path per format. But txtfetch reads Excel's cache too. A workbook whose formulas were never calculated by a spreadsheet app has no stored result for either tool to find. **Is there an official Python SDK for txtfetch?**: Yes. pip install txtfetch. Its only runtime dependency is httpx, and it maps every API error code to its own typed exception class (see /docs/quickstarts). go-further - [txtfetch (Python SDK) quickstart →](https://txtfetch.com/docs/quickstarts) - [Using txtfetch as a LangChain and LlamaIndex document loader →](https://txtfetch.com/blog/langchain-llamaindex-document-loader) - [RAG & LLM ingestion pipelines →](https://txtfetch.com/solutions/rag-ingestion) - [Measured extraction accuracy by category →](https://txtfetch.com/benchmarks) - [API quickstart →](https://txtfetch.com/docs) - [How the pipeline works →](https://txtfetch.com/how-it-works) - [Get an API key →](https://app.txtfetch.com/signup) other-languages - [`JavaScript`](https://txtfetch.com/for/javascript) - [`Go`](https://txtfetch.com/for/go) - [`Java`](https://txtfetch.com/for/java) - [`C# / .NET`](https://txtfetch.com/for/csharp) - [All languages →](https://txtfetch.com/for) ## Paste it into your project. The Python snippet above runs as written. Add your key and it works. [Get an API key →](https://app.txtfetch.com/signup) [More SDK quickstarts →](https://txtfetch.com/docs/quickstarts) --- # https://txtfetch.com/formats # Every format, one endpoint. txtfetch detects the real type from the bytes, not the extension, and hands it to Apache Tika. Drop a file below and watch txtfetch fingerprint it from the bytes, or search the full list. Drop a file below to see what txtfetch detects — runs in your browser, nothing is uploaded. Filter supported formats Showing 65 of 65 listed formats, plus 615 more via Apache Tika. [Checked against the build we run →](https://txtfetch.com/formats/coverage) ## PDF & documents - `.pdf` Portable Document Format - `.rtf` Rich Text Format - `.txt` Plain text - `.md` Markdown - `.tex` LaTeX source - `.log` Log file [Deep dive: extract text from PDFs →](https://txtfetch.com/extract/pdf) ## Microsoft Office (modern) - `.docx` Word document - `.dotx` Word template - `.xlsx` Excel workbook - `.xlsm` Excel workbook (macro-enabled) - `.pptx` PowerPoint presentation - `.pptm` PowerPoint presentation (macro-enabled) - `.potx` PowerPoint template [Deep dive: extract text from modern Office files →](https://txtfetch.com/extract/docx) ## Microsoft Office (legacy) - `.doc` Word 97-2003 document - `.xls` Excel 97-2003 workbook - `.ppt` PowerPoint 97-2003 presentation - `.pub` Publisher document - `.vsd` Visio drawing - `.one` OneNote notebook - `.msg` Outlook message [Deep dive: extract text from legacy .doc, .xls and .ppt files →](https://txtfetch.com/extract/legacy-office) ## OpenDocument - `.odt` OpenDocument text - `.ods` OpenDocument spreadsheet - `.odp` OpenDocument presentation - `.odg` OpenDocument graphics - `.ott` OpenDocument text template - `.fodt` Flat OpenDocument text [Deep dive: extract text from OpenDocument files →](https://txtfetch.com/extract/odf) ## Apple iWork - `.pages` Pages document - `.numbers` Numbers spreadsheet - `.key` Keynote presentation ## Email & messaging - `.eml` Email message - `.msg` Outlook message - `.mbox` Mailbox archive - `.pst` Outlook data file - `.ost` Outlook offline data file - `.vcf` vCard contact [Deep dive: extract text from email messages →](https://txtfetch.com/extract/email) ## eBooks - `.epub` EPUB ebook - `.fb2` FictionBook [Deep dive: extract text from EPUB ebooks →](https://txtfetch.com/extract/epub) ## Web & markup - `.html` HTML page - `.htm` HTML page - `.xhtml` XHTML page - `.xml` XML document - `.rss` RSS feed - `.atom` Atom feed [Deep dive: extract text from web pages and HTML →](https://txtfetch.com/extract/html) ## Data & structured - `.csv` Comma-separated values - `.tsv` Tab-separated values - `.json` JSON document - `.yaml` YAML document [Deep dive: extract text from CSV and delimited data →](https://txtfetch.com/extract/csv) ## Images (OCR via Tesseract) No text layer? Tesseract OCR reads the pixels automatically — same request, same response. - `.png` PNG image - `.jpg` JPEG image - `.jpeg` JPEG image - `.tiff` TIFF image - `.bmp` Bitmap image - `.gif` GIF image - `.webp` WebP image [Deep dive: extract text from images and scans →](https://txtfetch.com/extract/image) ## Archives (recursed) Contained files are extracted and their text pulled out too, not just filenames. - `.zip` ZIP archive - `.tar` Tar archive - `.gz` Gzip archive - `.7z` 7-Zip archive [Deep dive: extract text from ZIP archives →](https://txtfetch.com/extract/zip) ## Audio & video (metadata only) Tika reads embedded metadata and tags here — title, artist, duration, codec — not spoken words. txtfetch does not transcribe audio or video. - `.mp3` MP3 audio (ID3 tags) - `.wav` WAV audio (metadata) - `.mp4` MP4 video (metadata) - `.mov` QuickTime video (metadata) - `.m4a` MPEG-4 audio (metadata) - `.ogg` Ogg audio/video (metadata) - `.flac` FLAC audio (metadata) [Try it: extract & clean up a video/audio file's caption track →](https://txtfetch.com/tools/subtitles-to-text) under-the-hood Every format above goes through the same pipeline. txtfetch detects the real type, extracts structurally with Apache Tika, and falls back to Tesseract OCR when there's no text layer. Then it returns clean JSON. See [how it works](https://txtfetch.com/how-it-works) end to end. Detection reads a file's [magic bytes](https://txtfetch.com/glossary/magic-bytes), not its extension or claimed [media type](https://txtfetch.com/glossary/media-type). See the [glossary](https://txtfetch.com/glossary) for what both terms mean. ## 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/formats/coverage # Every format, checked. “1,000+ formats” is true, and also not the number that matters. Here's what tika-server-standard actually parses, measured against the exact jar this build ships, not assumed from a list. ## the-measurement 1,683 media types detected 615 have a real parser (217 direct + 398 via supertype fallback) 601 file extensions reach a parser 80 distinct parser classes doing the work Detection and parsing are different claims. Tika's detector reads a file's magic bytes, container structure, or XML namespace, and names the media type correctly. That's the 1,000+ figure everyone, us included, quotes elsewhere on this site. Parsing is a separate question: does any registered parser actually turn that type into text or metadata? For **tika-server-standard 3.3.1**, the honest answer is 615 of the 1,683 types it detects. The rest come back correctly named and empty. **Provenance:** Tika version `3.3.1`, jar SHA-256 `755d252de43a1995151db3a25c825332d2f27371272c41459bb5b78e21b028bd`. This is the exact jar pinned in `terraform/lambda-layers/build/versions.env` and shipped in the extraction Lambda's layer. Re-run it yourself: download that jar, verify the checksum, then run `marketing/scripts/tika-coverage/build-coverage.mjs`. The repo's `docs/tika-coverage.md` has the exact commands. No API key or account needed; it's a standalone Java program against a public jar. Generated 2026-08-13. ## look-it-up Type a file extension (`.wpd`) or a media type (`image/png`). Get the verdict this exact build gives it: parsed and by which parser, detect-only, or unknown to Tika entirely. ## parsed Every one of the 601 file extensions that reach a real parser in this build, A to Z. Static markup: this table is here with JavaScript off. It's what a search crawler or an LLM reading [llms-full.txt](https://txtfetch.com/llms-full.txt) sees too. | Extension | Media type | Parser | What you get | | --- | --- | --- | --- | | `.3dml` | text/vnd.in3d.3dml | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.3g2` | video/3gpp2 | MP4Parser | the container's tag metadata (title, duration, codec) for MP4-family video and audio — never a transcript of spoken words. | | `.3gp` | video/3gpp | MP4Parser | the container's tag metadata (title, duration, codec) for MP4-family video and audio — never a transcript of spoken words. | | `.3mf` | application/vnd.ms-package.3dmanufacturing-3dmodel+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.4th` | text/x-forth | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.7z` | application/x-7z-compressed | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.a` | application/x-archive | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.aart` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.ac` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.accdb` | application/x-msaccess | JackcessParser | table and row data from a Microsoft Access database file, as text. | | `.acfm` | application/x-font-adobe-metric | AdobeFontMetricParser | the font's metadata (name, glyph widths) — a font file has no prose to extract. | | `.ad` | text/x-asciidoc | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.ada` | text/x-ada | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.adb` | text/x-ada | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.adoc` | text/x-asciidoc | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.ads` | text/x-ada | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.afm` | application/x-font-adobe-metric | AdobeFontMetricParser | the font's metadata (name, glyph widths) — a font file has no prose to extract. | | `.ai` | application/illustrator | PDFParser | the text layer, reading order reconstructed across columns and pages — if the PDF is a scan with no text layer, this parser returns nothing and Tesseract OCR fills in behind it. | | `.aif` | audio/x-aiff | AudioParser | container-level metadata (sample rate, channels, duration) for uncompressed/basic audio formats — never a transcript. | | `.aifc` | audio/x-aiff | AudioParser | container-level metadata (sample rate, channels, duration) for uncompressed/basic audio formats — never a transcript. | | `.aiff` | audio/x-aiff | AudioParser | container-level metadata (sample rate, channels, duration) for uncompressed/basic audio formats — never a transcript. | | `.air` | application/vnd.adobe.air-application-installer-package+zip | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.aj` | text/x-aspectj | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.al` | text/x-perl | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.am` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.amf` | application/x-amf | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.amfm` | application/x-font-adobe-metric | AdobeFontMetricParser | the font's metadata (name, glyph widths) — a font file has no prose to extract. | | `.anpa` | text/vnd.iptc.anpa | IptcAnpaParser | the article body text from this news-wire transmission format. | | `.apk` | application/vnd.android.package-archive | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.applescript` | text/x-applescript | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.apt` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.ar` | application/x-archive | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.arc` | application/x-internet-archive | WARCParser | each captured HTTP response unpacked and re-run through detection — a page crawled into a WARC comes back as that page's extracted text. | | `.arj` | application/x-arj | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.as` | text/x-actionscript | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.asciidoc` | text/x-asciidoc | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.asice` | application/vnd.etsi.asic-e+zip | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.asics` | application/vnd.etsi.asic-s+zip | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.asm` | text/x-assembly | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.asp` | text/asp | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.aspx` | text/aspdotnet | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.asx` | application/x-ms-asx | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.atom` | application/atom+xml | FeedParser | every entry's title, summary, and body text pulled out of the feed, not just the feed's own metadata. | | `.atomcat` | application/atomcat+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.atomsvc` | application/atomsvc+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.au` | audio/basic | AudioParser | container-level metadata (sample rate, channels, duration) for uncompressed/basic audio formats — never a transcript. | | `.awk` | text/x-awk | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.bas` | text/x-basic | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.bash` | application/x-sh | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.bat` | application/x-bat | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.bau` | application/vnd.openofficeorg.autotext | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.bib` | application/x-bibtex-text-file | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.bibtex` | application/x-bibtex-text-file | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.bmp` | image/bmp | ImageParser +OCR | dimensions and embedded EXIF/IPTC tags — no OCR from this parser alone. (Several of the mime types this covers get real OCR in production anyway — see the Tesseract note below.) | | `.boz` | application/x-bzip2 | CompressorParser | whatever the compressed stream contains, decompressed and then run back through detection — a .gz wrapping a PDF comes back as that PDF's text, not a description of gzip. | | `.bpg` | image/x-bpg | BPGParser | container metadata from this rare image format — not OCR. | | `.bpm` | application/bizagi-modeler | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.br` | application/x-brotli | CompressorParser | whatever the compressed stream contains, decompressed and then run back through detection — a .gz wrapping a PDF comes back as that PDF's text, not a description of gzip. | | `.brotli` | application/x-brotli | CompressorParser | whatever the compressed stream contains, decompressed and then run back through detection — a .gz wrapping a PDF comes back as that PDF's text, not a description of gzip. | | `.bsh` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.bz` | application/x-bzip | CompressorParser | whatever the compressed stream contains, decompressed and then run back through detection — a .gz wrapping a PDF comes back as that PDF's text, not a description of gzip. | | `.bz2` | application/x-bzip2 | CompressorParser | whatever the compressed stream contains, decompressed and then run back through detection — a .gz wrapping a PDF comes back as that PDF's text, not a description of gzip. | | `.c` | text/x-c++src | SourceCodeParser | the raw source as text, same as TextAndCSVParser, plus language-aware metadata for a handful of C-family languages. | | `.cbl` | text/x-cobol | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.cc` | text/x-c++src | SourceCodeParser | the raw source as text, same as TextAndCSVParser, plus language-aware metadata for a handful of C-family languages. | | `.ccxml` | application/ccxml+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.cdxml` | application/vnd.chemdraw+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.cfc` | text/x-coldfusion | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.cfg` | text/x-config | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.cfm` | text/x-coldfusion | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.cfml` | text/x-coldfusion | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.cgi` | text/x-cgi | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.chm` | application/vnd.ms-htmlhelp | ChmParser | the text of every HTML page bundled inside the .chm help file. | | `.cl` | text/x-common-lisp | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.class` | application/java-vm | ClassParser | structural metadata about the compiled class (version, source file name) — never source code, because a .class file doesn't contain any. | | `.classpath` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.clj` | text/x-clojure | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.cls` | text/x-vbasic | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.cmd` | application/x-bat | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.cnd` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.cob` | text/x-cobol | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.coffee` | text/x-coffeescript | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.com` | application/x-msdownload | ExecutableParser | structural metadata about the binary — architecture, linked libraries, sections — never readable prose, because compiled binaries don't contain any. | | `.conf` | text/x-config | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.config` | text/x-config | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.cpio` | application/x-cpio | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.cpp` | text/x-c++src | SourceCodeParser | the raw source as text, same as TextAndCSVParser, plus language-aware metadata for a handful of C-family languages. | | `.cr2` | image/x-canon-cr2 | TiffParser | dimensions and embedded EXIF tags — no OCR from this parser alone (TIFF gets real OCR in production; see the Tesseract note below). | | `.cr3` | image/x-canon-cr3 | MP4Parser | the container's tag metadata (title, duration, codec) for MP4-family video and audio — never a transcript of spoken words. | | `.cs` | text/x-csharp | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.css` | text/css | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.csv` | text/csv | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.curl` | text/vnd.curl | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.cwiki` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.cxx` | text/x-c++src | SourceCodeParser | the raw source as text, same as TextAndCSVParser, plus language-aware metadata for a handful of C-family languages. | | `.d` | text/x-d | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.data` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.davmount` | application/davmount+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.dbase` | application/x-dbf | DBFParser | every record's field values, as text — a flat-file database format still used by some GIS and legacy business tools. | | `.dbase3` | application/x-dbf | DBFParser | every record's field values, as text — a flat-file database format still used by some GIS and legacy business tools. | | `.dbf` | application/x-dbf | DBFParser | every record's field values, as text — a flat-file database format still used by some GIS and legacy business tools. | | `.dcl` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.dcurl` | text/vnd.curl.dcurl | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.dd2` | application/vnd.oma.dd2+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.deb` | application/x-debian-package | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.def` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.dib` | image/bmp | ImageParser +OCR | dimensions and embedded EXIF/IPTC tags — no OCR from this parser alone. (Several of the mime types this covers get real OCR in production anyway — see the Tesseract note below.) | | `.dif` | application/dif+xml | DIFParser | cell values from this old spreadsheet interchange format, as text. | | `.diff` | text/x-diff | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.dita` | application/dita+xml; format=topic | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.ditamap` | application/dita+xml; format=map | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.ditaval` | application/dita+xml; format=val | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.dll` | application/x-msdownload | ExecutableParser | structural metadata about the binary — architecture, linked libraries, sections — never readable prose, because compiled binaries don't contain any. | | `.doc` | application/msword | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.docm` | application/vnd.ms-word.document.macroenabled.12 | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.docx` | application/vnd.openxmlformats-officedocument.wordprocessingml.document | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.dot` | application/msword | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.dotm` | application/vnd.ms-word.template.macroenabled.12 | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.dotx` | application/vnd.openxmlformats-officedocument.wordprocessingml.template | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.dpr` | text/x-pascal | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.drc` | video/x-dirac | OggParser | the container's tag metadata (title, artist, encoder) — never a transcript of what's actually said or played. | | `.dsc` | text/prs.lines.tag | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.dsp` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.dsw` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.dtb` | application/x-dtbook+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.dtd` | application/xml-dtd | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.dwfx` | model/vnd.dwfx+xps | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.dwg` | image/vnd.dwg | DWGParser | header and structural metadata from a .dwg drawing — not the drawn content as text. | | `.e` | text/x-eiffel | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.ear` | application/x-tika-java-enterprise-archive | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.egrm` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.el` | text/x-emacs-lisp | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.emf` | image/emf | EMFParser | header metadata from this vector graphics format — not OCR; EMF is drawing commands, not a photographed page. | | `.eml` | message/rfc822 | RFC822Parser | headers (from/to/subject/date) plus the message body, with attachments recursed and extracted the same as an archive member. | | `.emlx` | message/x-emlx | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.emma` | application/emma+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.emz` | image/x-emf-compressed | CompressorParser | whatever the compressed stream contains, decompressed and then run back through detection — a .gz wrapping a PDF comes back as that PDF's text, not a description of gzip. | | `.ent` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.epub` | application/epub+zip | EpubParser | the book's text, walked in spine order across every chapter file inside the container. | | `.erl` | text/x-erlang | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.es3` | application/vnd.eszigno3+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.et3` | application/vnd.eszigno3+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.etx` | text/x-setext | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.exe` | application/x-dosexec | ExecutableParser | structural metadata about the binary — architecture, linked libraries, sections — never readable prose, because compiled binaries don't contain any. | | `.exp` | text/x-expect | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.f` | text/x-fortran | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.f77` | text/x-fortran | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.f90` | text/x-fortran | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.fb2` | application/x-fictionbook+xml | FictionBookParser | the book's text from this Russian-originated XML ebook format's body. | | `.flac` | audio/x-flac | FlacParser | the file's Vorbis-comment tags (title, artist, album) — never a transcript of the audio itself. | | `.flv` | video/x-flv | FLVParser | container-level metadata (duration, codec) — never a transcript. | | `.flx` | text/vnd.fmi.flexstor | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.fly` | text/vnd.fly | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.fn` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.fo` | application/xslfo+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.fodp` | application/vnd.oasis.opendocument.flat.presentation | FlatOpenDocumentParser | the same document body text as OpenDocumentParser, for ODF's single-XML-file variant (.fodt/.fods/.fodp) instead of the zipped one. | | `.fods` | application/vnd.oasis.opendocument.flat.spreadsheet | FlatOpenDocumentParser | the same document body text as OpenDocumentParser, for ODF's single-XML-file variant (.fodt/.fods/.fodp) instead of the zipped one. | | `.fodt` | application/vnd.oasis.opendocument.flat.text | FlatOpenDocumentParser | the same document body text as OpenDocumentParser, for ODF's single-XML-file variant (.fodt/.fods/.fodp) instead of the zipped one. | | `.for` | text/x-fortran | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.frm` | text/x-vbasic | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.ft` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.fv` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.g` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.gif` | image/gif | ImageParser +OCR | dimensions and embedded EXIF/IPTC tags — no OCR from this parser alone. (Several of the mime types this covers get real OCR in production anyway — see the Tesseract note below.) | | `.go` | text/x-go | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.grm` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.groovy` | text/x-groovy | SourceCodeParser | the raw source as text, same as TextAndCSVParser, plus language-aware metadata for a handful of C-family languages. | | `.grxml` | application/srgs+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.gtar` | application/x-gtar | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.gv` | text/vnd.graphviz | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.gz` | application/gzip | CompressorParser | whatever the compressed stream contains, decompressed and then run back through detection — a .gz wrapping a PDF comes back as that PDF's text, not a description of gzip. | | `.h` | text/x-c++hdr | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.haml` | text/x-haml | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.handlers` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.heic` | image/heic | HeifParser | container and EXIF metadata from Apple's modern photo format — no OCR from this parser alone. | | `.heif` | image/heif | HeifParser | container and EXIF metadata from Apple's modern photo format — no OCR from this parser alone. | | `.hh` | text/x-c++hdr | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.hp` | text/x-c++hdr | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.hpp` | text/x-c++hdr | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.hs` | text/x-haskell | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.htc` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.htm` | text/html | JSoupParser | the visible page text with boilerplate (nav, scripts, styles) stripped, the same reader behind /tools/html-to-text. | | `.html` | text/html | JSoupParser | the visible page text with boilerplate (nav, scripts, styles) stripped, the same reader behind /tools/html-to-text. | | `.hwpx` | application/hwp+zip | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.hx` | text/x-haxe | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.hxx` | text/x-c++hdr | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.i3` | text/x-modula | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.ibooks` | application/x-ibooks+zip | EpubParser | the book's text, walked in spine order across every chapter file inside the container. | | `.icns` | image/icns | ICNSParser | the icon container's embedded image list metadata — not OCR. | | `.ico` | image/vnd.microsoft.icon | ImageParser | dimensions and embedded EXIF/IPTC tags — no OCR from this parser alone. (Several of the mime types this covers get real OCR in production anyway — see the Tesseract note below.) | | `.ics` | text/calendar | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.idl` | text/x-idl | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.idml` | application/vnd.adobe.indesign-idml-package | IDMLParser | story and text-frame content pulled from an InDesign package's XML story files. | | `.ifb` | text/calendar | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.ig` | text/x-modula | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.ihtml` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.in` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.ini` | text/x-ini | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.inx` | application/x-adobe-indesign-interchange | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.ipa` | application/x-itunes-ipa | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.irp` | application/vnd.irepository.package+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.iso19139` | text/iso19139+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.itk` | text/x-tcl | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.jad` | text/vnd.sun.j2me.app-descriptor | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.jar` | application/java-archive | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.java` | text/x-java-source | SourceCodeParser | the raw source as text, same as TextAndCSVParser, plus language-aware metadata for a handful of C-family languages. | | `.jb2` | image/x-jbig2 | ImageParser | dimensions and embedded EXIF/IPTC tags — no OCR from this parser alone. (Several of the mime types this covers get real OCR in production anyway — see the Tesseract note below.) | | `.jbig2` | image/x-jbig2 | ImageParser | dimensions and embedded EXIF/IPTC tags — no OCR from this parser alone. (Several of the mime types this covers get real OCR in production anyway — see the Tesseract note below.) | | `.jfi` | image/jpeg | JpegParser +OCR | EXIF tags (camera, GPS, capture date) — no OCR from this parser alone (JPEG gets real OCR in production; see the Tesseract note below). | | `.jfif` | image/jpeg | JpegParser +OCR | EXIF tags (camera, GPS, capture date) — no OCR from this parser alone (JPEG gets real OCR in production; see the Tesseract note below). | | `.jif` | image/jpeg | JpegParser +OCR | EXIF tags (camera, GPS, capture date) — no OCR from this parser alone (JPEG gets real OCR in production; see the Tesseract note below). | | `.jl` | text/x-common-lisp | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.jmx` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.jnilib` | application/x-java-jnilib | UniversalExecutableParser | structural metadata about the binary (architecture, linked libraries) — never readable prose. | | `.jpe` | image/jpeg | JpegParser +OCR | EXIF tags (camera, GPS, capture date) — no OCR from this parser alone (JPEG gets real OCR in production; see the Tesseract note below). | | `.jpeg` | image/jpeg | JpegParser +OCR | EXIF tags (camera, GPS, capture date) — no OCR from this parser alone (JPEG gets real OCR in production; see the Tesseract note below). | | `.jpg` | image/jpeg | JpegParser +OCR | EXIF tags (camera, GPS, capture date) — no OCR from this parser alone (JPEG gets real OCR in production; see the Tesseract note below). | | `.js` | text/javascript | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.json` | application/json | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.jsp` | text/x-jsp | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.junit` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.jx` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.jxl` | image/jxl | JXLParser | embedded metadata from this newer JPEG successor format — not OCR. | | `.kar` | audio/midi | MidiParser | track and instrument metadata from a MIDI file — MIDI encodes notes, not sound or speech, so there's nothing to transcribe. | | `.key` | application/vnd.apple.keynote | IWorkPackageParser | the document body text from Pages/Numbers/Keynote's newer package format. | | `.kml` | application/vnd.google-earth.kml+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.kmz` | application/vnd.google-earth.kmz | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.l` | text/x-lex | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.latex` | application/x-latex | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.lbe` | application/vnd.llamagraphics.life-balance.exchange+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.less` | text/x-less | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.lhs` | text/x-haskell | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.link66` | application/vnd.route66.link66+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.lisp` | text/x-common-lisp | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.list` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.log` | text/x-log | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.lostxml` | application/lost+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.lsp` | text/x-common-lisp | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.lua` | text/x-lua | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.lyr` | application/x-esri-layer | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.lz4` | application/x-lz4 | CompressorParser | whatever the compressed stream contains, decompressed and then run back through detection — a .gz wrapping a PDF comes back as that PDF's text, not a description of gzip. | | `.lzma` | application/x-lzma | CompressorParser | whatever the compressed stream contains, decompressed and then run back through detection — a .gz wrapping a PDF comes back as that PDF's text, not a description of gzip. | | `.m` | text/x-objcsrc | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.m2a` | audio/mpeg | Mp3Parser | ID3 tag metadata (title, artist, album) — never a transcript of the audio. | | `.m3` | text/x-modula | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.m3a` | audio/mpeg | Mp3Parser | ID3 tag metadata (title, artist, album) — never a transcript of the audio. | | `.m4` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.m4a` | audio/mp4 | MP4Parser | the container's tag metadata (title, duration, codec) for MP4-family video and audio — never a transcript of spoken words. | | `.m4b` | audio/mp4 | MP4Parser | the container's tag metadata (title, duration, codec) for MP4-family video and audio — never a transcript of spoken words. | | `.m4s` | video/iso.segment | MP4Parser | the container's tag metadata (title, duration, codec) for MP4-family video and audio — never a transcript of spoken words. | | `.m4v` | video/x-m4v | MP4Parser | the container's tag metadata (title, duration, codec) for MP4-family video and audio — never a transcript of spoken words. | | `.ma` | application/mathematica | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.man` | text/troff | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.manifest` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.markdown` | text/markdown | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.mat` | application/x-matlab-data | MatParser | variable names and array metadata from a .mat file — numeric data, not prose. | | `.mathml` | application/mathml+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.mb` | application/mathematica | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.mbox` | application/mbox | MboxParser | every message in the mailbox file split out and parsed individually, headers and body both. | | `.mcurl` | text/vnd.curl.mcurl | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.md` | text/markdown | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.mdb` | application/x-msaccess | JackcessParser | table and row data from a Microsoft Access database file, as text. | | `.mdo` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.mdtext` | text/markdown | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.me` | text/troff | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.memgraph` | application/x-memgraph | PListParser | the key/value structure of an Apple binary property list, as text — used for a handful of Apple-adjacent formats, not general documents. | | `.meta` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.mf` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.mg` | text/x-modula | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.mht` | multipart/related | RFC822Parser | headers (from/to/subject/date) plus the message body, with attachments recursed and extracted the same as an archive member. | | `.mhtml` | multipart/related | RFC822Parser | headers (from/to/subject/date) plus the message body, with attachments recursed and extracted the same as an archive member. | | `.mid` | audio/midi | MidiParser | track and instrument metadata from a MIDI file — MIDI encodes notes, not sound or speech, so there's nothing to transcribe. | | `.midi` | audio/midi | MidiParser | track and instrument metadata from a MIDI file — MIDI encodes notes, not sound or speech, so there's nothing to transcribe. | | `.mif` | application/vnd.mif | MIFParser | the document body text from Adobe FrameMaker's plain-text interchange format. | | `.mime` | message/rfc822 | RFC822Parser | headers (from/to/subject/date) plus the message body, with attachments recursed and extracted the same as an archive member. | | `.mjs` | text/javascript | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.mkd` | text/markdown | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.ml` | text/x-ml | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.mli` | text/x-ocaml | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.mmap` | application/vnd.mindjet.mindmanager | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.mmas` | application/vnd.mindjet.mindmanager | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.mmat` | application/vnd.mindjet.mindmanager | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.mmmp` | application/vnd.mindjet.mindmanager | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.mmp` | application/vnd.mindjet.mindmanager | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.mmpt` | application/vnd.mindjet.mindmanager | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.mov` | video/quicktime | MP4Parser | the container's tag metadata (title, duration, codec) for MP4-family video and audio — never a transcript of spoken words. | | `.mp2` | audio/mpeg | Mp3Parser | ID3 tag metadata (title, artist, album) — never a transcript of the audio. | | `.mp2a` | audio/mpeg | Mp3Parser | ID3 tag metadata (title, artist, album) — never a transcript of the audio. | | `.mp3` | audio/mpeg | Mp3Parser | ID3 tag metadata (title, artist, album) — never a transcript of the audio. | | `.mp4` | video/mp4 | MP4Parser | the container's tag metadata (title, duration, codec) for MP4-family video and audio — never a transcript of spoken words. | | `.mp4a` | audio/mp4 | MP4Parser | the container's tag metadata (title, duration, codec) for MP4-family video and audio — never a transcript of spoken words. | | `.mp4s` | application/mp4 | MP4Parser | the container's tag metadata (title, duration, codec) for MP4-family video and audio — never a transcript of spoken words. | | `.mp4v` | video/mp4 | MP4Parser | the container's tag metadata (title, duration, codec) for MP4-family video and audio — never a transcript of spoken words. | | `.mpd` | application/dash+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.mpg4` | video/mp4 | MP4Parser | the container's tag metadata (title, duration, codec) for MP4-family video and audio — never a transcript of spoken words. | | `.mpga` | audio/mpeg | Mp3Parser | ID3 tag metadata (title, artist, album) — never a transcript of the audio. | | `.mpkg` | application/vnd.apple.installer+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.mpp` | application/vnd.ms-project | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.mpt` | application/vnd.ms-project | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.mpx` | application/x-project | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.ms` | text/troff | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.mscml` | application/mediaservercontrol+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.msg` | application/vnd.ms-outlook | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.msi` | application/x-ms-installer | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.msp` | application/x-ms-installer | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.mst` | application/x-ms-installer | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.musicxml` | application/vnd.recordare.musicxml+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.mxml` | application/xv+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.n3` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.nar` | application/vnd.iptc.g2.newsmessage+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.nb` | application/mathematica | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.ncx` | application/x-dtbncx+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.nroff` | text/troff | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.numbers` | application/vnd.apple.numbers | IWorkPackageParser | the document body text from Pages/Numbers/Keynote's newer package format. | | `.ocaml` | text/x-ocaml | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.odc` | application/vnd.oasis.opendocument.chart | OpenDocumentParser | the document body text from ODF files (LibreOffice/OpenOffice's native zipped format) — text, spreadsheet, presentation, and drawing variants alike. | | `.odf` | application/vnd.oasis.opendocument.formula | OpenDocumentParser | the document body text from ODF files (LibreOffice/OpenOffice's native zipped format) — text, spreadsheet, presentation, and drawing variants alike. | | `.odft` | application/vnd.oasis.opendocument.formula-template | OpenDocumentParser | the document body text from ODF files (LibreOffice/OpenOffice's native zipped format) — text, spreadsheet, presentation, and drawing variants alike. | | `.odg` | application/vnd.oasis.opendocument.graphics | OpenDocumentParser | the document body text from ODF files (LibreOffice/OpenOffice's native zipped format) — text, spreadsheet, presentation, and drawing variants alike. | | `.odi` | application/vnd.oasis.opendocument.image | OpenDocumentParser | the document body text from ODF files (LibreOffice/OpenOffice's native zipped format) — text, spreadsheet, presentation, and drawing variants alike. | | `.odp` | application/vnd.oasis.opendocument.presentation | OpenDocumentParser | the document body text from ODF files (LibreOffice/OpenOffice's native zipped format) — text, spreadsheet, presentation, and drawing variants alike. | | `.ods` | application/vnd.oasis.opendocument.spreadsheet | OpenDocumentParser | the document body text from ODF files (LibreOffice/OpenOffice's native zipped format) — text, spreadsheet, presentation, and drawing variants alike. | | `.odt` | application/vnd.oasis.opendocument.text | OpenDocumentParser | the document body text from ODF files (LibreOffice/OpenOffice's native zipped format) — text, spreadsheet, presentation, and drawing variants alike. | | `.oga` | audio/ogg | OggParser | the container's tag metadata (title, artist, encoder) — never a transcript of what's actually said or played. | | `.ogg` | audio/vorbis | VorbisParser | the codec's Vorbis-comment tags — never a transcript. | | `.ogm` | video/x-ogm | OggParser | the container's tag metadata (title, artist, encoder) — never a transcript of what's actually said or played. | | `.ogv` | video/ogg | OggParser | the container's tag metadata (title, artist, encoder) — never a transcript of what's actually said or played. | | `.ogx` | application/ogg | OggParser | the container's tag metadata (title, artist, encoder) — never a transcript of what's actually said or played. | | `.one` | application/onenote; format=one | OneNoteParser | text content from a OneNote section file — the Table of Contents and Package variants of OneNote's format are detect-only (no parser targets them yet). | | `.opf` | application/oebps-package+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.opus` | audio/opus | OpusParser | the codec's Vorbis-comment-style tags — never a transcript. | | `.osfpvg` | application/vnd.yamaha.openscoreformat.osfpvg+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.ost` | application/vnd.ms-outlook-pst | OutlookPSTParser | every message in the .pst/.ost mail store parsed individually — this is a whole mailbox, not a single email (see RFC822Parser/the email guide for a single .eml/.msg). | | `.otc` | application/vnd.oasis.opendocument.chart-template | OpenDocumentParser | the document body text from ODF files (LibreOffice/OpenOffice's native zipped format) — text, spreadsheet, presentation, and drawing variants alike. | | `.otg` | application/vnd.oasis.opendocument.graphics-template | OpenDocumentParser | the document body text from ODF files (LibreOffice/OpenOffice's native zipped format) — text, spreadsheet, presentation, and drawing variants alike. | | `.oth` | application/vnd.oasis.opendocument.text-web | OpenDocumentParser | the document body text from ODF files (LibreOffice/OpenOffice's native zipped format) — text, spreadsheet, presentation, and drawing variants alike. | | `.oti` | application/vnd.oasis.opendocument.image-template | OpenDocumentParser | the document body text from ODF files (LibreOffice/OpenOffice's native zipped format) — text, spreadsheet, presentation, and drawing variants alike. | | `.otm` | application/vnd.oasis.opendocument.text-master | OpenDocumentParser | the document body text from ODF files (LibreOffice/OpenOffice's native zipped format) — text, spreadsheet, presentation, and drawing variants alike. | | `.otp` | application/vnd.oasis.opendocument.presentation-template | OpenDocumentParser | the document body text from ODF files (LibreOffice/OpenOffice's native zipped format) — text, spreadsheet, presentation, and drawing variants alike. | | `.ots` | application/vnd.oasis.opendocument.spreadsheet-template | OpenDocumentParser | the document body text from ODF files (LibreOffice/OpenOffice's native zipped format) — text, spreadsheet, presentation, and drawing variants alike. | | `.ott` | application/vnd.oasis.opendocument.text-template | OpenDocumentParser | the document body text from ODF files (LibreOffice/OpenOffice's native zipped format) — text, spreadsheet, presentation, and drawing variants alike. | | `.owl` | application/rdf+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.oxps` | application/vnd.ms-xpsdocument | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.oxt` | application/vnd.openofficeorg.extension | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.p` | text/x-pascal | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.p7c` | application/pkcs7-mime | Pkcs7Parser | the signed payload unwrapped and re-run through detection — the signature itself isn't extractable text. | | `.p7m` | application/pkcs7-mime | Pkcs7Parser | the signed payload unwrapped and re-run through detection — the signature itself isn't extractable text. | | `.p7s` | application/pkcs7-signature | Pkcs7Parser | the signed payload unwrapped and re-run through detection — the signature itself isn't extractable text. | | `.pack` | application/x-java-pack200 | CompressorParser | whatever the compressed stream contains, decompressed and then run back through detection — a .gz wrapping a PDF comes back as that PDF's text, not a description of gzip. | | `.pages` | application/vnd.apple.pages | IWorkPackageParser | the document body text from Pages/Numbers/Keynote's newer package format. | | `.pas` | text/x-pascal | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.patch` | text/x-diff | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.pdf` | application/pdf | PDFParser | the text layer, reading order reconstructed across columns and pages — if the PDF is a scan with no text layer, this parser returns nothing and Tesseract OCR fills in behind it. | | `.pen` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.perl` | text/x-perl | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.php` | text/x-php | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.php3` | text/x-php | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.php4` | text/x-php | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.pl` | text/x-perl | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.pls` | application/pls+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.pm` | text/x-perl | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.png` | image/png | ImageParser +OCR | dimensions and embedded EXIF/IPTC tags — no OCR from this parser alone. (Several of the mime types this covers get real OCR in production anyway — see the Tesseract note below.) | | `.pod` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.pom` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.pot` | application/vnd.ms-powerpoint | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.potm` | application/vnd.ms-powerpoint.template.macroenabled.12 | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.potx` | application/vnd.openxmlformats-officedocument.presentationml.template | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.pp` | text/x-pascal | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.ppa` | application/vnd.ms-powerpoint | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.ppam` | application/vnd.ms-powerpoint.addin.macroenabled.12 | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.ppj` | image/vnd.adobe.premiere | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.pps` | application/vnd.ms-powerpoint | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.ppsm` | application/vnd.ms-powerpoint.slideshow.macroenabled.12 | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.ppsx` | application/vnd.openxmlformats-officedocument.presentationml.slideshow | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.ppt` | application/vnd.ms-powerpoint | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.pptm` | application/vnd.ms-powerpoint.presentation.macroenabled.12 | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.pptx` | application/vnd.openxmlformats-officedocument.presentationml.presentation | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.ppz` | application/vnd.ms-powerpoint | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.pro` | text/x-prolog | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.project` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.properties` | text/x-java-properties | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.prt` | application/x-prt | PRTParser | structural metadata from this CAD/manufacturing part format — not readable prose. | | `.psd` | image/vnd.adobe.photoshop | PSDParser | layer names and image metadata from a .psd file — not OCR, and not layer contents rendered as an image. | | `.pst` | application/vnd.ms-outlook-pst | OutlookPSTParser | every message in the .pst/.ost mail store parsed individually — this is a whole mailbox, not a single email (see RFC822Parser/the email guide for a single .eml/.msg). | | `.pub` | application/x-mspublisher | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.py` | text/x-python | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.qpw` | application/x-quattro-pro | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.qt` | video/quicktime | MP4Parser | the container's tag metadata (title, duration, codec) for MP4-family video and audio — never a transcript of spoken words. | | `.r` | text/x-rsrc | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.rar` | application/x-rar-compressed | RarParser | every file inside the archive extracted and recursed, the same as the ZIP/7z path. | | `.rb` | text/x-ruby | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.rdf` | application/rdf+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.res` | application/x-dtbresource+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.rest` | text/x-rst | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.restx` | text/x-rst | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.rexx` | text/x-rexx | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.rif` | application/reginfo+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.rl` | application/resource-lists+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.rld` | application/resource-lists-diff+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.rmi` | audio/midi | MidiParser | track and instrument metadata from a MIDI file — MIDI encodes notes, not sound or speech, so there's nothing to transcribe. | | `.rnc` | application/relax-ng-compact-syntax | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.rng` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.rnx` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.roff` | text/troff | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.roles` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.rs` | application/rls-services+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.rsd` | application/rsd+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.rss` | application/rss+xml | FeedParser | every entry's title, summary, and body text pulled out of the feed, not just the feed's own metadata. | | `.rst` | text/x-rst | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.rtf` | application/rtf | RTFParser | the document body text, control words and named special characters resolved to their actual characters. | | `.rtx` | text/richtext | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.s` | text/x-assembly | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.sas` | application/x-sas | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.sas7bdat` | application/x-sas-data | SAS7BDATParser | column names and row values from a SAS statistical dataset, as text. | | `.sbml` | application/sbml+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.scad` | application/x-openscad | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.scala` | text/x-scala | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.schemas` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.scm` | text/x-scheme | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.scurl` | text/vnd.curl.scurl | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.sd7` | application/x-sas-data | SAS7BDATParser | column names and row values from a SAS statistical dataset, as text. | | `.sda` | application/vnd.stardivision.draw | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.sdc` | application/vnd.stardivision.calc | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.sdd` | application/vnd.stardivision.impress | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.sdkd` | application/vnd.solent.sdkm+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.sdkm` | application/vnd.solent.sdkm+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.sdw` | application/vnd.stardivision.writer | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.sed` | text/x-sed | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.sfdu` | application/x-sfdu | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.sgm` | text/sgml | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.sgml` | text/sgml | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.sh` | application/x-sh | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.shf` | application/shf+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.shw` | application/x-corelpresentations | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.sldasm` | application/sldworks | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.slddrw` | application/sldworks | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.sldm` | application/vnd.ms-powerpoint.slide.macroenabled.12 | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.sldprt` | application/sldworks | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.sldx` | application/vnd.openxmlformats-officedocument.presentationml.slide | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.smi` | application/smil+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.smil` | application/smil+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.sml` | application/smil+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.snd` | audio/basic | AudioParser | container-level metadata (sample rate, channels, duration) for uncompressed/basic audio formats — never a transcript. | | `.spot` | text/vnd.in3d.spot | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.spx` | audio/speex | SpeexParser | the codec's comment-header tags — never a transcript. | | `.sql` | text/x-sql | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.srt` | application/x-subrip | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.srx` | application/sparql-results+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.ssml` | application/ssml+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.st` | text/x-stsrc | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.stw` | application/vnd.sun.xml.writer.template | OpenDocumentParser | the document body text from ODF files (LibreOffice/OpenOffice's native zipped format) — text, spreadsheet, presentation, and drawing variants alike. | | `.svg` | image/svg+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.svgz` | image/svg+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.sxc` | application/vnd.sun.xml.calc | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.sxd` | application/vnd.sun.xml.draw | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.sxi` | application/vnd.sun.xml.impress | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.sxw` | application/vnd.sun.xml.writer | OpenDocumentParser | the document body text from ODF files (LibreOffice/OpenOffice's native zipped format) — text, spreadsheet, presentation, and drawing variants alike. | | `.t` | text/troff | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.tar` | application/x-tar | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.tbz` | application/x-bzip | CompressorParser | whatever the compressed stream contains, decompressed and then run back through detection — a .gz wrapping a PDF comes back as that PDF's text, not a description of gzip. | | `.tbz2` | application/x-bzip2 | CompressorParser | whatever the compressed stream contains, decompressed and then run back through detection — a .gz wrapping a PDF comes back as that PDF's text, not a description of gzip. | | `.tcl` | text/x-tcl | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.tex` | application/x-tex | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.text` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.tgz` | application/gzip | CompressorParser | whatever the compressed stream contains, decompressed and then run back through detection — a .gz wrapping a PDF comes back as that PDF's text, not a description of gzip. | | `.thmx` | application/vnd.openxmlformats-officedocument.presentationml.presentation | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.tif` | image/tiff | TiffParser +OCR | dimensions and embedded EXIF tags — no OCR from this parser alone (TIFF gets real OCR in production; see the Tesseract note below). | | `.tiff` | image/tiff | TiffParser +OCR | dimensions and embedded EXIF tags — no OCR from this parser alone (TIFF gets real OCR in production; see the Tesseract note below). | | `.tk` | text/x-tcl | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.tld` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.tmx` | application/x-tmx | TMXParser | every translation unit's source and target text pulled from the TMX file. | | `.tr` | text/troff | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.tsd` | application/timestamped-data | TSDParser | the timestamp and signer metadata wrapping a signed payload — not the payload's own content. | | `.tsv` | text/tab-separated-values | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.ttc` | application/x-font-ttf | TrueTypeParser | the font's name table and other embedded metadata — again, no prose in a font file. | | `.ttf` | application/x-font-ttf | TrueTypeParser | the font's name table and other embedded metadata — again, no prose in a font file. | | `.ttml` | application/ttml+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.txt` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.types` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.udeb` | application/x-debian-package | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.uoml` | application/vnd.uoml+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.uri` | text/uri-list | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.uris` | text/uri-list | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.urls` | text/uri-list | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.uu` | text/x-uuencode | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.v` | text/x-verilog | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.vb` | text/x-vbdotnet | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.vbs` | text/x-vbscript | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.vcf` | text/x-vcard | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.vcs` | text/x-vcalendar | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.vhd` | text/x-vhdl | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.vhdl` | text/x-vhdl | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.vm` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.vor` | application/x-staroffice-template | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.vsd` | application/vnd.visio | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.vsdm` | application/vnd.ms-visio.drawing.macroenabled.12 | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.vsdx` | application/vnd.ms-visio.drawing | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.vsl` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.vss` | application/vnd.visio | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.vssm` | application/vnd.ms-visio.stencil.macroenabled.12 | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.vssx` | application/vnd.ms-visio.stencil | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.vst` | application/vnd.visio | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.vstm` | application/vnd.ms-visio.template.macroenabled.12 | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.vstx` | application/vnd.ms-visio.template | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.vsw` | application/vnd.visio | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.vtt` | text/vtt | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.vxml` | application/voicexml+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.war` | application/x-tika-java-web-archive | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.warc` | application/warc | WARCParser | each captured HTTP response unpacked and re-run through detection — a page crawled into a WARC comes back as that page's extracted text. | | `.wav` | audio/vnd.wave | AudioParser | container-level metadata (sample rate, channels, duration) for uncompressed/basic audio formats — never a transcript. | | `.wb1` | application/x-quattro-pro | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.wb2` | application/x-quattro-pro | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.wb3` | application/x-quattro-pro | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.wbmp` | image/vnd.wap.wbmp | ImageParser | dimensions and embedded EXIF/IPTC tags — no OCR from this parser alone. (Several of the mime types this covers get real OCR in production anyway — see the Tesseract note below.) | | `.wbs` | application/vnd.criticaltools.wbs+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.wcm` | application/vnd.ms-works | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.wdb` | application/vnd.ms-works | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.webarchive` | application/x-webarchive | PListParser | the key/value structure of an Apple binary property list, as text — used for a handful of Apple-adjacent formats, not general documents. | | `.webmanifest` | application/manifest+json | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.webp` | image/webp | WebPParser +OCR | container metadata — no OCR from this parser alone (WebP gets real OCR in production; see the Tesseract note below). | | `.wkq` | application/x-quattro-pro | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.wks` | application/vnd.ms-works | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.wl` | application/vnd.wolfram.wl | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.wmf` | image/wmf | WMFParser | header metadata from this older vector graphics format — not OCR; WMF is drawing commands, not a photographed page. | | `.wml` | text/vnd.wap.wml | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.wmls` | text/vnd.wap.wmlscript | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.wmz` | application/x-ms-wmz | CompressorParser | whatever the compressed stream contains, decompressed and then run back through detection — a .gz wrapping a PDF comes back as that PDF's text, not a description of gzip. | | `.wps` | application/vnd.ms-works | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.wq1` | application/x-quattro-pro | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.wq2` | application/x-quattro-pro | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.wsdd` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.wsdl` | application/wsdl+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.wspolicy` | application/wspolicy+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xargs` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.xbm` | image/x-xbitmap | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.xcat` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.xcf` | image/x-xcf | ImageParser | dimensions and embedded EXIF/IPTC tags — no OCR from this parser alone. (Several of the mime types this covers get real OCR in production anyway — see the Tesseract note below.) | | `.xconf` | text/x-config | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.xdm` | application/vnd.syncml.dm+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xdp` | application/vnd.adobe.xdp+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xegrm` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.xenc` | application/xenc+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xer` | application/patch-ops-error+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xfdf` | application/vnd.adobe.xfdf | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xgrm` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.xht` | application/xhtml+xml | JSoupParser | the visible page text with boilerplate (nav, scripts, styles) stripped, the same reader behind /tools/html-to-text. | | `.xhtml` | application/xhtml+xml | JSoupParser | the visible page text with boilerplate (nav, scripts, styles) stripped, the same reader behind /tools/html-to-text. | | `.xhtml2` | application/xhtml+xml | JSoupParser | the visible page text with boilerplate (nav, scripts, styles) stripped, the same reader behind /tools/html-to-text. | | `.xhvml` | application/xv+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xla` | application/vnd.ms-excel | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.xlam` | application/vnd.ms-excel.addin.macroenabled.12 | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.xlc` | application/vnd.ms-excel | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.xld` | application/vnd.ms-excel | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.xlex` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.xlf` | application/x-xliff+xml | XLIFF12Parser | every translation unit's source and target text from an XLIFF 1.2 file. | | `.xliff` | application/x-xliff+xml | XLIFF12Parser | every translation unit's source and target text from an XLIFF 1.2 file. | | `.xll` | application/vnd.ms-excel | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.xlm` | application/vnd.ms-excel | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.xlog` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.xlr` | application/x-tika-msworks-spreadsheet | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.xls` | application/vnd.ms-excel | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.xlsb` | application/vnd.ms-excel.sheet.binary.macroenabled.12 | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.xlsm` | application/vnd.ms-excel.sheet.macroenabled.12 | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.xlsx` | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.xlt` | application/vnd.ms-excel | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.xltm` | application/vnd.ms-excel.template.macroenabled.12 | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.xltx` | application/vnd.openxmlformats-officedocument.spreadsheetml.template | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.xlw` | application/vnd.ms-excel | OfficeParser | the document body text from old-format Word/Excel/PowerPoint/Outlook/Project/Visio files — the pre-2007 binary formats, not the OOXML zips. | | `.xlz` | application/x-xliff+zip | XLZParser | the same translation-unit text as XLIFF12Parser, unpacked from XLIFF's zipped variant first. | | `.xmap` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.xmind` | application/x-xmind | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.xml` | application/xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xmp` | application/rdf+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xop` | application/xop+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xps` | application/vnd.ms-xpsdocument | OOXMLParser | the document body text from .docx/.xlsx/.pptx-family files, including macro-enabled and template variants, plus embedded content Word/Excel/PowerPoint themselves would show you. | | `.xq` | application/xquery | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.xquery` | application/xquery | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.xroles` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.xsamples` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.xsd` | application/xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xsl` | application/xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xslfo` | application/xslfo+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xslt` | application/xslt+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xsm` | application/vnd.syncml+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xsp` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.xspf` | application/xspf+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xtest` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.xul` | application/vnd.mozilla.xul+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xvm` | application/xv+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xvml` | application/xv+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.xweb` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.xwelcome` | text/plain | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.xz` | application/x-xz | CompressorParser | whatever the compressed stream contains, decompressed and then run back through detection — a .gz wrapping a PDF comes back as that PDF's text, not a description of gzip. | | `.y` | text/x-yacc | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.yaml` | text/x-yaml | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.yml` | text/x-yaml | TextAndCSVParser | the raw text, decoded from whichever character set it auto-detects, with delimited data (CSV/TSV) also parsed into rows and columns. This is why source code, config, and markup-adjacent plain-text files (.py, .json, .yaml, .md, and hundreds of others) all just work: they're subtypes of text/plain. | | `.z` | application/x-compress | CompressorParser | whatever the compressed stream contains, decompressed and then run back through detection — a .gz wrapping a PDF comes back as that PDF's text, not a description of gzip. | | `.zaz` | application/vnd.zzazz.deck+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | | `.zip` | application/zip | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.zipx` | application/zip | PackageParser | every file inside the archive extracted and recursed — each member gets its own pass through the same detection-and-parsing pipeline, so a .zip full of PDFs comes back with all of their text, not just filenames. | | `.zmm` | application/vnd.handheld-entertainment+xml | DcXMLParser | the Dublin Core / OOXML-style metadata fields — title, author, dates — plus any text content the schema carries. The biggest single bucket here: most of it is XML-based application formats (feeds, office-adjacent XML dialects, 3D/CAD interchange formats) that share this generic reader. | ## detect-only 683 extensions Tika names correctly but has no parser for in this build. Sending one of these to the API returns the identified content type and an extraction error, not a fabricated result. Counted by media type rather than by extension, 1,068 of the 1,683 types this build detects have no parser. This table can only show the 534 of them that register a file extension. The other 534 are known to Tika by media type alone. The lookup above covers all of them either way. | Extension | Media type | Description | | --- | --- | --- | | `.123` | application/vnd.lotus-1-2-3 | Lotus 1-2-3 | | `.3ds` | image/x-3ds | 3D Studio (V1) | | `.3fr` | image/x-raw-hasselblad | Hasselblad raw image | | `.aab` | application/x-authorware-bin | — | | `.aac` | audio/x-aac | — | | `.aam` | application/x-authorware-map | — | | `.aas` | application/x-authorware-seg | — | | `.abw` | application/x-abiword | — | | `.ac3` | audio/ac3 | Dolby Digital Audio Compression File | | `.acc` | application/vnd.americandynamics.acc | — | | `.ace` | application/x-ace-compressed | — | | `.acu` | application/vnd.acucobol | — | | `.acutc` | application/vnd.acucorp | — | | `.adf` | application/x-amiga-disk-format | Amiga Disk File | | `.adp` | audio/adpcm | — | | `.aep` | application/vnd.adobe.aftereffects.project | — | | `.aet` | application/vnd.adobe.aftereffects.template | — | | `.afp` | application/vnd.ibm.modcap | — | | `.ami` | application/vnd.amiga.ami | — | | `.amr` | audio/amr | — | | `.application` | application/x-ms-application | — | | `.apr` | application/vnd.lotus-approach | — | | `.arw` | image/x-raw-sony | Sony raw image | | `.asc` | application/pgp-signature | — | | `.asf` | video/x-ms-asf | — | | `.asnd` | audio/vnd.adobe.soundbooth | — | | `.aso` | application/vnd.accpac.simply.aso | — | | `.atc` | application/vnd.acucorp | — | | `.atx` | application/vnd.antix.game-component | — | | `.avi` | video/x-msvideo | Audio Video Interleave File | | `.avif` | image/avif | AV1 Image File | | `.aw` | application/applixware | — | | `.axx` | application/x-axcrypt | AxCrypt | | `.azf` | application/vnd.airzip.filesecure.azf | — | | `.azs` | application/vnd.airzip.filesecure.azs | — | | `.azw` | application/vnd.amazon.ebook | — | | `.bay` | image/x-raw-casio | Casio raw image | | `.bcpio` | application/x-bcpio | — | | `.bdf` | application/x-font-bdf | — | | `.bdm` | application/vnd.syncml.dm+wbxml | — | | `.bh2` | application/vnd.fujitsu.oasysprs | — | | `.bin` | application/octet-stream | — | | `.bmi` | application/vnd.bmi | — | | `.book` | application/vnd.framemaker | — | | `.box` | application/vnd.previewsystems.box | — | | `.bpk` | application/octet-stream | — | | `.btif` | image/prs.btif | — | | `.bup` | application/x-dvd-ifo | DVD information file | | `.c4d` | application/vnd.clonk.c4group | — | | `.c4f` | application/vnd.clonk.c4group | — | | `.c4g` | application/vnd.clonk.c4group | — | | `.c4p` | application/vnd.clonk.c4group | — | | `.c4u` | application/vnd.clonk.c4group | — | | `.cab` | application/vnd.ms-cab-compressed | — | | `.caf` | audio/x-caf | com.apple.coreaudio-format | | `.cap` | application/vnd.tcpdump.pcap | TCPDump pcap packet capture | | `.car` | application/vnd.curl.car | — | | `.cat` | application/vnd.ms-pki.seccat | — | | `.cbor` | application/cbor | Concise Binary Object Representation container | | `.cct` | application/x-director | Shockwave Movie | | `.cda` | application/x-cdf | CD Audio | | `.cdbcmsg` | application/vnd.contact.cmsg | — | | `.cdf` | application/x-netcdf | — | | `.cdkey` | application/vnd.mediastation.cdkey | — | | `.cdr` | application/coreldraw | des: CorelDraw X4 and newer | | `.cdx` | chemical/x-cdx | — | | `.cdy` | application/vnd.cinderella | — | | `.cel` | image/vnd.dgn | — | | `.cer` | application/pkix-cert | — | | `.cgm` | image/cgm | Computer Graphics Metafile | | `.chat` | application/x-chat | — | | `.chrt` | application/vnd.kde.kchart | KChart File | | `.cif` | chemical/x-cif | — | | `.cii` | application/vnd.anser-web-certificate-issue-initiation | — | | `.cil` | application/vnd.ms-artgalry | — | | `.cla` | application/vnd.claymore | — | | `.clkk` | application/vnd.crick.clicker.keyboard | — | | `.clkp` | application/vnd.crick.clicker.palette | — | | `.clkt` | application/vnd.crick.clicker.template | — | | `.clkw` | application/vnd.crick.clicker.wordbank | — | | `.clkx` | application/vnd.crick.clicker | — | | `.clp` | application/x-msclip | — | | `.cmc` | application/vnd.cosmocaller | — | | `.cmdf` | chemical/x-cmdf | — | | `.cml` | chemical/x-cml | — | | `.cmp` | application/vnd.yellowriver-custom-menu | — | | `.cmx` | image/x-cmx | — | | `.cod` | application/vnd.rim.cod | — | | `.cpt` | application/mac-compactpro | — | | `.crd` | application/x-mscardfile | — | | `.crl` | application/pkix-crl | — | | `.crt` | application/x-x509-cert | — | | `.crw` | image/x-raw-canon | Canon raw image | | `.crx` | application/x-chrome-package | Chrome Extension Package | | `.csh` | application/x-csh | — | | `.csml` | chemical/x-csml | — | | `.csp` | application/vnd.commonspace | — | | `.cst` | application/x-director | Shockwave Movie | | `.cu` | application/cu-seeme | — | | `.cwk` | application/x-appleworks | — | | `.cww` | application/prs.cww | — | | `.cxt` | application/x-director | Shockwave Movie | | `.daf` | application/vnd.mobius.daf | — | | `.dataless` | application/vnd.fdsn.seed | — | | `.dcr` | application/x-director | Shockwave Movie | | `.dcs` | image/x-raw-kodak | Kodak raw image | | `.dcx` | image/vnd.zbrush.dcx | ZSoft Multi-Page Paintbrush | | `.ddd` | application/vnd.fujixerox.ddd | — | | `.deploy` | application/octet-stream | — | | `.der` | application/x-x509-cert; format=der | — | | `.dex` | application/x-dex | Dalvik Executable Format | | `.dfac` | application/vnd.dreamfactory | — | | `.dgn` | image/vnd.dgn | — | | `.dgnlib` | image/vnd.dgn | — | | `.dir` | application/x-director | Shockwave Movie | | `.dis` | application/vnd.mobius.dis | — | | `.dist` | application/octet-stream | — | | `.distz` | application/octet-stream | — | | `.djv` | image/vnd.djvu | — | | `.djvu` | image/vnd.djvu | — | | `.dmg` | application/x-apple-diskimage | — | | `.dmp` | application/vnd.tcpdump.pcap | TCPDump pcap packet capture | | `.dms` | application/octet-stream | — | | `.dna` | application/vnd.dna | — | | `.dng` | image/x-raw-adobe | Adobe Digital Negative | | `.do` | application/x-stata-do | Stata DTA Script | | `.dp` | application/vnd.osgi.dp | — | | `.dpg` | application/vnd.dpgraph | — | | `.dpx` | image/x-dpx | Digital Picture Exchange from SMPTE | | `.drf` | image/x-raw-kodak | Kodak raw image | | `.dta` | application/x-stata-dta | Stata DTA Dataset | | `.dts` | audio/vnd.dts | — | | `.dtshd` | audio/vnd.dts.hd | — | | `.dump` | application/octet-stream | — | | `.dvi` | application/x-dvi | TeX Device Independent Document | | `.dwf` | model/vnd.dwf | AutoCAD Design Web Format | | `.dxb` | image/vnd.dxb | AutoCAD DXF simplified Binary | | `.dxf` | image/vnd.dxf | AutoCAD DXF | | `.dxp` | application/vnd.spotfire.dxp | — | | `.dxr` | application/x-director | Shockwave Movie | | `.e57` | model/e57 | 3d imaging data exchange | | `.ecelp4800` | audio/vnd.nuera.ecelp4800 | — | | `.ecelp7470` | audio/vnd.nuera.ecelp7470 | — | | `.ecelp9600` | audio/vnd.nuera.ecelp9600 | — | | `.ecma` | application/ecmascript | — | | `.edm` | application/vnd.novadigm.edm | — | | `.edx` | application/vnd.novadigm.edx | — | | `.efif` | application/vnd.picsel | — | | `.ei6` | application/vnd.pg.osasli | — | | `.elc` | application/octet-stream | — | | `.enr` | application/x-endnote-refer | — | | `.ens` | application/x-endnote-style | — | | `.enw` | application/x-endnote-refer | — | | `.eol` | audio/vnd.digital-winds | — | | `.eot` | application/vnd.ms-fontobject | — | | `.eps` | application/postscript | PostScript | | `.epsf` | application/postscript | PostScript | | `.epsi` | application/postscript | PostScript | | `.erf` | image/x-raw-epson | Epson raw image | | `.esf` | application/vnd.epson.esf | — | | `.exr` | image/aces | ACES Image Container File | | `.ext` | application/vnd.novadigm.ext | — | | `.ez` | application/andrew-inset | — | | `.ez2` | application/vnd.ezpix-album | — | | `.ez3` | application/vnd.ezpix-package | — | | `.f4v` | video/x-f4v | — | | `.fbs` | image/vnd.fastbidsheet | — | | `.fcs` | application/vnd.isac.fcs | Flow Cytometry Standard File | | `.fdf` | application/vnd.fdf | Forms Data Format | | `.fff` | image/x-raw-imacon | Imacon raw image | | `.fg5` | application/vnd.fujitsu.oasysgp | — | | `.fgd` | application/x-director | Shockwave Movie | | `.fh` | image/x-freehand | FreeHand image | | `.fh10` | image/x-freehand | FreeHand image | | `.fh11` | image/x-freehand | FreeHand image | | `.fh12` | image/x-freehand | FreeHand image | | `.fh4` | image/x-freehand | FreeHand image | | `.fh40` | image/x-freehand | FreeHand image | | `.fh5` | image/x-freehand | FreeHand image | | `.fh50` | image/x-freehand | FreeHand image | | `.fh7` | image/x-freehand | FreeHand image | | `.fh8` | image/x-freehand | FreeHand image | | `.fh9` | image/x-freehand | FreeHand image | | `.fhc` | image/x-freehand | FreeHand image | | `.fig` | application/x-xfig | — | | `.fit` | application/fits | Flexible Image Transport System | | `.fits` | application/fits | Flexible Image Transport System | | `.flc` | video/x-flc | — | | `.fli` | video/x-fli | — | | `.flo` | application/vnd.micrografx.flo | — | | `.flw` | application/vnd.kde.kivio | — | | `.fm` | application/vnd.framemaker | — | | `.fnc` | application/vnd.frogans.fnc | — | | `.fp7` | application/x-filemaker | FileMaker Pro 7 | | `.fpx` | image/vnd.fpx | — | | `.frame` | application/vnd.framemaker | — | | `.fsc` | application/vnd.fsc.weblaunch | — | | `.fst` | image/vnd.fst | — | | `.ft10` | image/x-freehand | FreeHand image | | `.ft11` | image/x-freehand | FreeHand image | | `.ft12` | image/x-freehand | FreeHand image | | `.ft7` | image/x-freehand | FreeHand image | | `.ft8` | image/x-freehand | FreeHand image | | `.ft9` | image/x-freehand | FreeHand image | | `.ftc` | application/vnd.fluxtime.clip | — | | `.fti` | application/vnd.anser-web-funds-transfer-initiation | — | | `.fts` | application/fits | Flexible Image Transport System | | `.fvt` | video/vnd.fvt | — | | `.fzs` | application/vnd.fuzzysheet | — | | `.g3` | image/g3fax | — | | `.gac` | application/vnd.groove-account | — | | `.gdl` | model/vnd.gdl | — | | `.geo` | application/vnd.dynageo | — | | `.gex` | application/vnd.geometry-explorer | — | | `.ggb` | application/vnd.geogebra.file | — | | `.ggt` | application/vnd.geogebra.tool | — | | `.ghf` | application/vnd.groove-help | — | | `.gim` | application/vnd.groove-identity-message | — | | `.gmx` | application/vnd.gmx | — | | `.gnucash` | application/x-gnucash | — | | `.gnumeric` | application/x-gnumeric | — | | `.gp4` | application/x-guitar-pro | Guitar Pro | | `.gpg` | application/pgp-encrypted | — | | `.gph` | application/vnd.flographit | — | | `.gpkg` | application/x-geopackage | — | | `.gqf` | application/vnd.grafeq | — | | `.gqs` | application/vnd.grafeq | — | | `.gram` | application/srgs | — | | `.grb` | application/x-grib | General Regularly-distributed Information in Binary form | | `.grb1` | application/x-grib | General Regularly-distributed Information in Binary form | | `.grb2` | application/x-grib | General Regularly-distributed Information in Binary form | | `.gre` | application/vnd.geometry-explorer | — | | `.grv` | application/vnd.groove-injector | — | | `.gsf` | application/x-font-ghostscript | — | | `.gslib` | audio/x-psf | Portable Sound Format | | `.gtm` | application/vnd.groove-tool-message | — | | `.gtw` | model/vnd.gtw | — | | `.h261` | video/h261 | — | | `.h263` | video/h263 | — | | `.h264` | video/h264 | — | | `.h5` | application/x-hdf | Hierarchical Data Format File | | `.hbci` | application/vnd.hbci | — | | `.hdf` | application/x-hdf | Hierarchical Data Format File | | `.hdr` | application/envi.hdr | — | | `.he5` | application/x-hdf | Hierarchical Data Format File | | `.hfa` | application/x-erdas-hfa | — | | `.hlp` | application/winhlp | — | | `.hpgl` | application/vnd.hp-hpgl | — | | `.hpid` | application/vnd.hp-hpid | — | | `.hprof` | application/vnd.java.hprof | Java hprof text file | | `.hps` | application/vnd.hp-hps | — | | `.hqx` | application/mac-binhex40 | — | | `.htke` | application/vnd.kenameaapp | — | | `.hvd` | application/vnd.yamaha.hv-dic | — | | `.hvp` | application/vnd.yamaha.hv-voice | — | | `.hvs` | application/vnd.yamaha.hv-script | — | | `.icb` | image/x-tga | Targa image data | | `.icc` | application/vnd.iccprofile | — | | `.ice` | x-conference/x-cooltalk | Cooltalk Audio | | `.icm` | application/vnd.iccprofile | — | | `.ief` | image/ief | — | | `.ifm` | application/vnd.shana.informed.formdata | — | | `.ifo` | application/x-dvd-ifo | DVD information file | | `.iges` | model/iges | Initial Graphics Exchange Specification Format | | `.igl` | application/vnd.igloader | — | | `.igs` | model/iges | Initial Graphics Exchange Specification Format | | `.igx` | application/vnd.micrografx.igx | — | | `.iif` | application/vnd.shana.informed.interchange | — | | `.iiq` | image/x-raw-phaseone | Phase One raw image | | `.imp` | application/vnd.accpac.simply.imp | — | | `.ims` | application/vnd.ms-ims | — | | `.indd` | application/x-adobe-indesign | Adobe InDesign document | | `.ipk` | application/vnd.shana.informed.package | — | | `.irm` | application/vnd.ibm.rights-management | — | | `.iso` | application/x-iso9660-image | ISO 9660 CD-ROM filesystem data | | `.itp` | application/vnd.shana.informed.formtemplate | — | | `.ivp` | application/vnd.immervision-ivp | — | | `.ivu` | application/vnd.immervision-ivu | — | | `.j2c` | image/x-jp2-codestream | JPEG 2000 Codestream | | `.jam` | application/vnd.jam | — | | `.jdf` | application/x-jeol-jdf | JDF NMR Spectroscopy | | `.jisp` | application/vnd.jisp | — | | `.jks` | application/x-java-keystore | Java Keystore | | `.jlt` | application/vnd.hp-jlyt | — | | `.jng` | video/x-jng | — | | `.jnlp` | application/x-java-jnlp-file | — | | `.joda` | application/vnd.joost.joda-archive | — | | `.jp2` | image/jp2 | JPEG 2000 Part 1 (JP2) | | `.jpf` | image/jpx | JPEG 2000 Part 2 (JPX) | | `.jpgm` | image/jpm | JPEG 2000 Part 6 (JPM) | | `.jpgv` | video/jpeg | — | | `.jpm` | image/jpm | JPEG 2000 Part 6 (JPM) | | `.k25` | image/x-raw-kodak | Kodak raw image | | `.karbon` | application/vnd.kde.karbon | — | | `.kdc` | image/x-raw-kodak | Kodak raw image | | `.kfo` | application/vnd.kde.kformula | — | | `.kia` | application/vnd.kidspiration | — | | `.kil` | application/x-killustrator | KIllustrator File | | `.kne` | application/vnd.kinar | — | | `.knp` | application/vnd.kinar | — | | `.kon` | application/vnd.kde.kontour | — | | `.kpr` | application/vnd.kde.kpresenter | KPresenter File | | `.kpt` | application/vnd.kde.kpresenter | KPresenter File | | `.ksp` | application/vnd.kde.kspread | KSpread File | | `.ktr` | application/vnd.kahootz | — | | `.ktz` | application/vnd.kahootz | — | | `.kwd` | application/vnd.kde.kword | KWord File | | `.kwt` | application/vnd.kde.kword | KWord File | | `.las` | application/x-asprs | ASPRS Lidar Data Exchange Format | | `.laz` | application/x-asprs | ASPRS Lidar Data Exchange Format | | `.lbd` | application/vnd.llamagraphics.life-balance.desktop | — | | `.les` | application/vnd.hhe.lesson-player | — | | `.lha` | application/octet-stream | — | | `.list3820` | application/vnd.ibm.modcap | — | | `.listafp` | application/vnd.ibm.modcap | — | | `.lrf` | application/octet-stream | — | | `.lrm` | application/vnd.ms-lrm | — | | `.ltf` | application/vnd.frogans.ltf | — | | `.lvp` | audio/vnd.lucent.voice | — | | `.lwp` | application/vnd.lotus-wordpro | — | | `.lz` | application/x-lzip | Lzip (LZMA) compressed archive | | `.lzh` | application/octet-stream | — | | `.m13` | application/x-msmediaview | — | | `.m14` | application/x-msmediaview | — | | `.m1v` | video/mpeg | MPEG Movie Clip | | `.m2v` | video/mpeg | MPEG Movie Clip | | `.m3u` | audio/x-mpegurl | MP3 Playlist File | | `.m3u8` | application/vnd.apple.mpegurl | — | | `.m4u` | video/vnd.mpegurl | — | | `.mag` | application/vnd.ecowin.chart | — | | `.maker` | application/vnd.framemaker | — | | `.mbk` | application/vnd.mobius.mbk | — | | `.mc1` | application/vnd.medcalcdata | — | | `.mcd` | application/vnd.mcd | — | | `.mdi` | image/vnd.ms-modi | Microsoft Document Imaging | | `.mef` | image/x-raw-mamiya | Mamiya raw image | | `.mesh` | model/mesh | — | | `.mfm` | application/vnd.mfmp | — | | `.mgz` | application/vnd.proteus.magazine | — | | `.minigsf` | audio/x-psf | Portable Sound Format | | `.minipsf` | audio/x-psf | Portable Sound Format | | `.minipsf1` | audio/x-psf | Portable Sound Format | | `.mj2` | video/mj2 | JPEG 2000 Part 3 (Motion JPEG, MJ2) | | `.mjp2` | video/mj2 | JPEG 2000 Part 3 (Motion JPEG, MJ2) | | `.mka` | audio/x-matroska | — | | `.mkv` | video/x-matroska | — | | `.mlp` | application/vnd.dolby.mlp | — | | `.mmd` | application/vnd.chipnuts.karaoke-mmd | — | | `.mmf` | application/vnd.smaf | — | | `.mmr` | image/vnd.fujixerox.edmics-mmr | — | | `.mng` | video/x-mng | — | | `.mny` | application/x-msmoney | — | | `.mobi` | application/x-mobipocket-ebook | Mobipocket Ebook | | `.mod` | audio/x-mod | — | | `.mos` | image/x-raw-leaf | Leaf raw image | | `.movie` | video/x-sgi-movie | — | | `.mpc` | application/vnd.mophun.certificate | — | | `.mpe` | video/mpeg | MPEG Movie Clip | | `.mpeg` | video/mpeg | MPEG Movie Clip | | `.mpg` | video/mpeg | MPEG Movie Clip | | `.mpm` | application/vnd.blueice.multipass | — | | `.mpn` | application/vnd.mophun.application | — | | `.mpy` | application/vnd.ibm.minipay | — | | `.mqy` | application/vnd.mobius.mqy | — | | `.mrc` | application/marc | — | | `.mrw` | image/x-raw-minolta | Minolta raw image | | `.msa` | application/vnd.msa-disk-image | Magic Shadow Archiver | | `.mseed` | application/vnd.fdsn.mseed | — | | `.mseq` | application/vnd.mseq | — | | `.msf` | application/vnd.epson.msf | — | | `.msh` | model/mesh | — | | `.msl` | application/vnd.mobius.msl | — | | `.msty` | application/vnd.muvee.style | — | | `.mts` | model/vnd.mts | — | | `.mus` | application/vnd.musician | — | | `.mvb` | application/x-msmediaview | — | | `.mwf` | application/vnd.mfer | — | | `.mxf` | application/mxf | — | | `.mxl` | application/vnd.recordare.musicxml | — | | `.mxs` | application/vnd.triscape.mxs | — | | `.mxu` | video/vnd.mpegurl | — | | `.myd` | application/x-mysql-misam-data | MySQL MISAM Data | | `.myi` | application/x-mysql-misam-compressed-index | MySQL MISAM Compressed Index | | `.nc` | application/x-netcdf | — | | `.nef` | image/x-raw-nikon | Nikon raw image | | `.nes` | application/x-nesrom | Nintendo Entertainment System ROM | | `.ngdat` | application/vnd.nokia.n-gage.data | — | | `.nitf` | image/nitf | — | | `.nlu` | application/vnd.neurolanguage.nlu | — | | `.nml` | application/vnd.enliven | — | | `.nnd` | application/vnd.noblenet-directory | — | | `.nns` | application/vnd.noblenet-sealer | — | | `.nnw` | application/vnd.noblenet-web | — | | `.npx` | image/vnd.net-fpx | — | | `.nrw` | image/x-raw-nikon | Nikon raw image | | `.nsf` | application/vnd.lotus-notes | — | | `.ntf` | image/nitf | — | | `.oa2` | application/vnd.fujitsu.oasys2 | — | | `.oa3` | application/vnd.fujitsu.oasys3 | — | | `.oas` | application/vnd.fujitsu.oasys | — | | `.obd` | application/x-msbinder | — | | `.oda` | application/oda | — | | `.odb` | application/vnd.oasis.opendocument.base | — | | `.onepkg` | application/onenote; format=package | OneNote Package | | `.onetmp` | application/onenote | — | | `.onetoc` | application/onenote; format=onetoc2 | OneNote Table of Contents | | `.onetoc2` | application/onenote; format=onetoc2 | OneNote Table of Contents | | `.oprc` | application/vnd.palm | — | | `.orf` | image/x-raw-olympus | Olympus raw image | | `.org` | application/vnd.lotus-organizer | — | | `.osf` | application/vnd.yamaha.openscoreformat | — | | `.otf` | application/x-font-otf | OpenType Font | | `.p10` | application/pkcs10 | — | | `.p12` | application/x-pkcs12 | — | | `.p7b` | application/x-pkcs7-certificates | — | | `.p7r` | application/x-pkcs7-certreqresp | — | | `.pam` | image/x-portable-arbitrarymap | UNIX Portable Bitmap Graphic Arbitrary Map | | `.parquet` | application/x-parquet | — | | `.pbd` | application/vnd.powerbuilder6 | — | | `.pbm` | image/x-portable-bitmap | Portable Bit Map | | `.pcap` | application/vnd.tcpdump.pcap | TCPDump pcap packet capture | | `.pcapng` | application/vnd.tcpdump.pcapng | TCPDump next gen pcap packet capture | | `.pcf` | application/x-font-pcf | — | | `.pcl` | application/vnd.hp-pcl | — | | `.pclxl` | application/vnd.hp-pclxl | — | | `.pct` | image/x-pict | Apple Macintosh QuickDraw/PICT Format | | `.pcurl` | application/vnd.curl.pcurl | — | | `.pcx` | image/vnd.zbrush.pcx | ZSoft Paintbrush PiCture eXchange | | `.pdb` | chemical/x-pdb | Brookhaven Protein Databank File | | `.pef` | image/x-raw-pentax | Pentax raw image | | `.pem` | application/x-x509-cert; format=pem | — | | `.pfa` | application/x-font-type1 | — | | `.pfb` | application/x-font-type1 | — | | `.pfm` | application/x-font-printer-metric | Printer Font Metric | | `.pfr` | application/font-tdpfr | — | | `.pfx` | application/x-pkcs12 | — | | `.pgm` | image/x-portable-graymap | Portable Graymap Graphic | | `.pgn` | application/x-chess-pgn | — | | `.pgp` | application/pgp-encrypted | — | | `.pic` | image/x-pict | Apple Macintosh QuickDraw/PICT Format | | `.pict` | image/x-pict | Apple Macintosh QuickDraw/PICT Format | | `.pkg` | application/octet-stream | — | | `.pki` | application/pkixcmp | — | | `.pkipath` | application/pkix-pkipath | — | | `.plb` | application/vnd.3gpp.pic-bw-large | — | | `.plc` | application/vnd.mobius.plc | — | | `.plf` | application/vnd.pocketlearn | — | | `.pml` | application/vnd.ctc-posml | — | | `.pnm` | image/x-portable-anymap | Portable Any Map | | `.portpkg` | application/vnd.macports.portpkg | — | | `.ppd` | application/vnd.cups-ppd | — | | `.ppm` | image/x-portable-pixmap | UNIX Portable Bitmap Graphic | | `.pqa` | application/vnd.palm | — | | `.prc` | application/x-mobipocket-ebook | Mobipocket Ebook | | `.pre` | application/vnd.lotus-freelance | — | | `.prf` | application/pics-rules | — | | `.ps` | application/postscript | PostScript | | `.psb` | application/vnd.3gpp.pic-bw-small | — | | `.psf` | application/x-font-linux-psf | — | | `.psf1` | audio/x-psf | Portable Sound Format | | `.psflib` | audio/x-psf | Portable Sound Format | | `.ptid` | application/vnd.pvi.ptid1 | — | | `.ptx` | image/x-raw-pentax | Pentax raw image | | `.pvb` | application/vnd.3gpp.pic-bw-var | — | | `.pwn` | application/vnd.3m.post-it-notes | — | | `.pxn` | image/x-raw-logitech | Logitech raw image | | `.pya` | audio/vnd.ms-playready.media.pya | — | | `.pyv` | video/vnd.ms-playready.media.pyv | — | | `.qam` | application/vnd.epson.quickanime | — | | `.qbo` | application/vnd.intu.qbo | — | | `.qfx` | application/vnd.intu.qfx | — | | `.qps` | application/vnd.publishare-delta-tree | — | | `.qwd` | application/vnd.quark.quarkxpress | — | | `.qwt` | application/vnd.quark.quarkxpress | — | | `.qxb` | application/vnd.quark.quarkxpress | — | | `.qxd` | application/vnd.quark.quarkxpress | — | | `.qxl` | application/vnd.quark.quarkxpress | — | | `.qxt` | application/vnd.quark.quarkxpress | — | | `.r3d` | image/x-raw-red | Red raw image | | `.ra` | audio/x-pn-realaudio | Real Audio | | `.raf` | image/x-raw-fuji | Fuji raw image | | `.ram` | audio/x-pn-realaudio | Real Audio | | `.ras` | image/x-cmu-raster | — | | `.raw` | image/x-raw-panasonic | Panasonic raw image | | `.rcprofile` | application/vnd.ipunplugged.rcprofile | — | | `.rdz` | application/vnd.data-vision.rdz | — | | `.rep` | application/vnd.businessobjects | — | | `.rgb` | image/x-rgb | Silicon Graphics RGB Bitmap | | `.rlc` | image/vnd.fujixerox.edmics-rlc | — | | `.rm` | application/vnd.rn-realmedia | — | | `.rmp` | audio/x-pn-realaudio-plugin | RealMedia Player Plug-in | | `.rms` | application/vnd.jcp.javame.midlet-rms | — | | `.rpm` | application/x-rpm | RedHat Package Manager | | `.rpss` | application/vnd.nokia.radio-presets | — | | `.rpst` | application/vnd.nokia.radio-preset | — | | `.rq` | application/sparql-query | — | | `.rw2` | image/x-raw-panasonic | Panasonic raw image | | `.rwz` | image/x-raw-rawzor | Rawzor raw image | | `.s7m` | application/x-sas-dmdb | SAS DMDB Data Mining Database File | | `.sa7` | application/x-sas-access | SAS Access Descriptor | | `.saf` | application/vnd.yamaha.smaf-audio | — | | `.sap` | audio/x-sap | Slight Atari Player | | `.sas7bacs` | application/x-sas-access | SAS Access Descriptor | | `.sas7baud` | application/x-sas-audit | SAS Audit | | `.sas7bbak` | application/x-sas-backup | SAS Backup | | `.sas7bcat` | application/x-sas-catalog | SAS Catalog | | `.sas7bdmd` | application/x-sas-dmdb | SAS DMDB Data Mining Database File | | `.sas7bfdb` | application/x-sas-fdb | SAS FDB Consolidation Database File | | `.sas7bitm` | application/x-sas-itemstor | SAS Item Store (ItemStor) File | | `.sas7bmdb` | application/x-sas-mddb | SAS MDDB Multi-Dimensional Database File | | `.sas7bndx` | application/x-sas-data-index | SAS Data Set Index | | `.sas7bpgm` | application/x-sas-program-data | SAS Stored Program (DATA Step) | | `.sas7bput` | application/x-sas-putility | SAS Permanent Utility | | `.sas7butl` | application/x-sas-utility | SAS Utility | | `.sas7bvew` | application/x-sas-view | SAS Data Set View | | `.sav` | application/x-spss-sav | SPSS Data File | | `.sc` | application/vnd.ibm.secure-container | — | | `.sc7` | application/x-sas-catalog | SAS Catalog | | `.scd` | application/x-msschedule | — | | `.scq` | application/scvp-cv-request | — | | `.scs` | application/scvp-cv-response | — | | `.sd2` | application/x-sas-data-v6 | SAS v6 Data Set | | `.sdp` | application/sdp | — | | `.see` | application/vnd.seemail | — | | `.seed` | application/vnd.fdsn.seed | — | | `.sema` | application/vnd.sema | — | | `.semd` | application/vnd.semd | — | | `.semf` | application/vnd.semf | — | | `.ser` | application/java-serialized-object | — | | `.setpay` | application/set-payment-initiation | — | | `.setreg` | application/set-registration-initiation | — | | `.sf7` | application/x-sas-fdb | SAS FDB Consolidation Database File | | `.sfs` | application/vnd.spotfire.sfs | — | | `.sgl` | application/vnd.stardivision.writer-global | — | | `.shar` | application/x-shar | — | | `.shp` | application/x-shapefile | ESRI Shapefiles | | `.si7` | application/x-sas-data-index | SAS Data Set Index | | `.sib` | application/x-sibelius | Sibelius | | `.sig` | application/pgp-signature | — | | `.silo` | model/mesh | — | | `.sis` | application/vnd.symbian.install | — | | `.sisx` | application/vnd.symbian.install | — | | `.sit` | application/x-stuffit | — | | `.sitx` | application/x-stuffitx | — | | `.skd` | application/vnd.koan | SSEYO Koan File | | `.skm` | application/vnd.koan | SSEYO Koan File | | `.skp` | application/vnd.koan | SSEYO Koan File | | `.skt` | application/vnd.koan | SSEYO Koan File | | `.slt` | application/vnd.epson.salt | — | | `.sm7` | application/x-sas-mddb | SAS MDDB Multi-Dimensional Database File | | `.smf` | application/vnd.stardivision.math | — | | `.snf` | application/x-font-snf | — | | `.so` | application/octet-stream | — | | `.sp7` | application/x-sas-putility | SAS Permanent Utility | | `.spc` | application/x-pkcs7-certificates | — | | `.spf` | application/vnd.yamaha.smaf-phrase | — | | `.spl` | application/x-futuresplash | Macromedia FutureSplash File | | `.spp` | application/scvp-vp-response | — | | `.spq` | application/scvp-vp-request | — | | `.sr2` | image/x-raw-sony | Sony raw image | | `.sr7` | application/x-sas-itemstor | SAS Item Store (ItemStor) File | | `.src` | application/x-wais-source | — | | `.srf` | image/x-raw-sony | Sony raw image | | `.srl` | application/sereal | Sereal binary serialization format | | `.ss7` | application/x-sas-program-data | SAS Stored Program (DATA Step) | | `.sse` | application/vnd.kodak-descriptor | — | | `.ssf` | application/vnd.epson.ssf | — | | `.st7` | application/x-sas-audit | SAS Audit | | `.stc` | application/vnd.sun.xml.calc.template | — | | `.std` | application/vnd.sun.xml.draw.template | — | | `.stf` | application/vnd.wt.stf | — | | `.sti` | application/vnd.sun.xml.impress.template | — | | `.stk` | application/hyperstudio | — | | `.stl` | model/x.stl-binary | no magic available | | `.str` | application/vnd.pg.format | — | | `.stx` | application/x-sas-transport | SAS Transport File | | `.su7` | application/x-sas-utility | SAS Utility | | `.sus` | application/vnd.sus-calendar | — | | `.susp` | application/vnd.sus-calendar | — | | `.sv4cpio` | application/x-sv4cpio | — | | `.sv4crc` | application/x-sv4crc | — | | `.sv7` | application/x-sas-view | SAS Data Set View | | `.svd` | application/vnd.svd | — | | `.swa` | application/x-director | Shockwave Movie | | `.swf` | application/x-shockwave-flash | Adobe Flash | | `.swi` | application/vnd.arastra.swi | — | | `.sxg` | application/vnd.sun.xml.writer.global | — | | `.sxm` | application/vnd.sun.xml.math | — | | `.sz` | application/x-snappy-framed | Snappy Framed | | `.tao` | application/vnd.tao.intent-module-archive | — | | `.tcap` | application/vnd.3gpp2.tcap | — | | `.tcsh` | application/x-csh | — | | `.teacher` | application/vnd.smart.teacher | — | | `.texi` | application/x-texinfo | — | | `.texinfo` | application/x-texinfo | — | | `.tfm` | application/x-tex-tfm | — | | `.tga` | image/x-tga | Targa image data | | `.tmo` | application/vnd.tmobile-livetv | — | | `.toast` | application/x-roxio-toast | — | | `.torrent` | application/x-bittorrent | — | | `.tpl` | application/vnd.groove-tool-template | — | | `.tpt` | application/vnd.trid.tpt | — | | `.tra` | application/vnd.trueapp | — | | `.trm` | application/x-msterminal | — | | `.twd` | application/vnd.simtech-mindmapper | — | | `.twds` | application/vnd.simtech-mindmapper | — | | `.txd` | application/vnd.genomatix.tuxedo | — | | `.txf` | application/vnd.mobius.txf | — | | `.tzx` | application/x-spectrum-tzx | TAP (ZX Spectrum) | | `.u32` | application/x-authorware-bin | — | | `.uc2` | application/x-uc2-compressed | — | | `.ufd` | application/vnd.ufdl | — | | `.ufdl` | application/vnd.ufdl | — | | `.umj` | application/vnd.umajin | — | | `.unityweb` | application/vnd.unity | — | | `.ustar` | application/x-ustar | — | | `.utz` | application/vnd.uiq.theme | — | | `.vcd` | application/x-cdlink | Virtual CD-ROM CD Image File | | `.vcg` | application/vnd.groove-vcard | — | | `.vcx` | application/vnd.vcx | — | | `.vda` | image/x-tga | Targa image data | | `.vf` | application/x-tex-virtual-font | TeX Virtual Font format | | `.vis` | application/vnd.visionary | — | | `.viv` | video/vnd.vivo | — | | `.vmdk` | application/x-vmdk | Virtual Disk Format | | `.vox` | application/x-authorware-bin | — | | `.vrml` | model/vrml | — | | `.vsf` | application/vnd.vsf | — | | `.vtu` | model/vnd.vtu | — | | `.w3d` | application/x-director | Shockwave Movie | | `.w60` | application/vnd.wordperfect | WordPerfect - Corel Word Processing | | `.wad` | application/x-doom | — | | `.wasm` | application/wasm | Web Assembly | | `.wax` | audio/x-ms-wax | — | | `.wbxml` | application/vnd.wap.wbxml | — | | `.webm` | video/webm | — | | `.wk1` | application/vnd.lotus-1-2-3 | Lotus 1-2-3 | | `.wk2` | application/vnd.lotus-1-2-3 | Lotus 1-2-3 | | `.wk3` | application/vnd.lotus-1-2-3 | Lotus 1-2-3 | | `.wk4` | application/vnd.lotus-1-2-3 | Lotus 1-2-3 | | `.wm` | video/x-ms-wm | — | | `.wma` | audio/x-ms-wma | — | | `.wmd` | application/x-ms-wmd | — | | `.wmlc` | application/vnd.wap.wmlc | Compiled WML Document | | `.wmlsc` | application/vnd.wap.wmlscriptc | Compiled WML Script | | `.wmv` | video/x-ms-wmv | — | | `.wmx` | video/x-ms-wmx | — | | `.wp` | application/vnd.wordperfect | WordPerfect - Corel Word Processing | | `.wp5` | application/vnd.wordperfect | WordPerfect - Corel Word Processing | | `.wp6` | application/vnd.wordperfect | WordPerfect - Corel Word Processing | | `.wp61` | application/vnd.wordperfect | WordPerfect - Corel Word Processing | | `.wpd` | application/vnd.wordperfect | WordPerfect - Corel Word Processing | | `.wpl` | application/vnd.ms-wpl | — | | `.wpt` | application/vnd.wordperfect | WordPerfect - Corel Word Processing | | `.wqd` | application/vnd.wqd | — | | `.wri` | application/x-mswrite | — | | `.wrl` | model/vrml | — | | `.wtb` | application/vnd.webturbo | — | | `.wvx` | video/x-ms-wvx | — | | `.x32` | application/x-authorware-bin | — | | `.x3d` | application/vnd.hzn-3d-crossword | — | | `.x3f` | image/x-raw-sigma | Sigma raw image | | `.xap` | application/x-silverlight-app | — | | `.xar` | application/vnd.xara | — | | `.xbap` | application/x-ms-xbap | — | | `.xbd` | application/vnd.fujixerox.docuworks.binder | — | | `.xdw` | application/vnd.fujixerox.docuworks | — | | `.xfdl` | application/vnd.xfdl | — | | `.xif` | image/vnd.xiff | — | | `.xo` | application/vnd.olpc-sugar | — | | `.xpi` | application/x-xpinstall | — | | `.xpm` | image/x-xpixmap | — | | `.xport` | application/x-sas-xport | SAS XPORT Transfer File | | `.xpr` | application/vnd.is-xpr | — | | `.xpt` | application/x-sas-xport | SAS XPORT Transfer File | | `.xpw` | application/vnd.intercon.formnet | — | | `.xpx` | application/vnd.intercon.formnet | — | | `.xwd` | image/x-xwindowdump | X Windows Dump | | `.xyz` | chemical/x-xyz | — | | `.zir` | application/vnd.zul | — | | `.zirz` | application/vnd.zul | — | | `.zoo` | application/x-zoo | — | | `.zst` | application/zstd | https://tools.ietf.org/id/draft-kucherawy-dispatch-zstd-01.html | ## what-to-do-next If your format is on the parsed list, you're already covered. See [the browsable format directory](https://txtfetch.com/formats) for the deep-dive guide and free in-browser tool for common ones. Or just call the API directly. If it's detect-only or missing entirely, [tell us what you're working with](https://txtfetch.com/contact). That's how .wpd, .mobi, .jp2, and .azw3 got caught and fixed on this exact page instead of staying overclaimed. ## faq **Why does txtfetch say "1,683 detected" but only "615 parsed"?**: Apache Tika's detector recognizes a media type from its bytes. It looks at magic numbers, container structure, or XML namespaces. This works independently of whether any parser can read that type's content. Detection just means Tika can NAME the file correctly; parsing means it can hand back text or metadata. tika-server-standard 3.3.1 detects 1,683 media types. It has a real parser, direct or via supertype fallback, behind 615 of them. The other 1,068 come back correctly typed and empty. **What happens if I send a detect-only file to the API?**: You get the correctly identified content type and an extraction error, never a fabricated text dump and never metadata dressed up as extracted text. The lookup above says so explicitly for every detect-only type it knows about, including the exact media type Tika assigns it. **Why do some formats look supported on other pages but show up detect-only here?**: Because this page checks the actual pinned build instead of assuming from the file extension. .wpd (plain WordPerfect, as opposed to its versioned variants), .mobi, and .jp2 all detect cleanly in tika-server-standard 3.3.1, but none has a parser behind it. .azw3 isn't a media type this Tika version recognizes at all. All four are called out honestly below instead of quietly staying on a "we handle this" list. **Does OCR change any of these verdicts?**: For 6 raster image types (PNG, JPEG, GIF, BMP, TIFF, WebP), yes. The generator that built this data runs without Tesseract on PATH. It can only see Tika's fallback metadata-only image parsers. The Lambda has Tesseract installed. Tika prefers TesseractOCRParser for those exact types. In production, they get real OCR text, not just dimensions and EXIF tags. The lookup and the table both flag this wherever it applies. **How was this generated, and can I check it myself?**: Dump.java (marketing/scripts/tika-coverage/) loads Tika's default config and walks every registered media type. It resolves each one to its concrete parser class. This follows Tika's own supertype fallback, the same lookup CompositeParser itself does. Because of that, a type like text/x-python correctly shows up parsed via TextAndCSVParser instead of falsely reading as unsupported. build-coverage.mjs downloads tika-server-standard 3.3.1 and the Corretto JDK pinned in terraform/lambda-layers/build/versions.env. It verifies both against the checksums pinned there. Then it runs the dump and writes the JSON this page reads. See docs/tika-coverage.md for the exact commands. **Why is this data committed instead of generated on every build?**: Regenerating it needs a ~75MB jar and a JDK on the build machine. That's reasonable to run by hand when the Tika version bumps, not something to add to every CI run. Instead, a test asserts the committed data's tikaVersion matches versions.env, so a layer bump that isn't accompanied by regenerating this file fails the build. **1,683 media types is a lot of table rows. Why not one page per format?**: That's a doorway-page pattern: hundreds of thin, templated pages targeting long-tail keywords. It would drag the whole domain's quality signal down. A single well-organized reference page can capture the same traffic just as well. This page is deliberately one deep resource with a real lookup tool, not 1,683 near-duplicates. ## 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/glossary # The vocabulary of text extraction, defined plainly. These are the terms developers search for before they know they need an extraction API. They include what a PDF text layer actually is, why mojibake happens, and what OCR does and doesn't do. Each one links to the page with the full story. Every entry below is one honest sentence. The full explanation, examples, and what to do about it live on the term's own page. Looking for a symptom instead of a definition? See [fixes for broken extracted text](https://txtfetch.com/fixes). ## PDF & scans - **[PDF text layer](https://txtfetch.com/glossary/text-layer)** The grid of character codes and positions drawn into a PDF page, separate from what the page image shows. No text layer means nothing to extract. - **[Reading order](https://txtfetch.com/glossary/reading-order)** The sequence a document's text should be read in, left to right and top to bottom within each column. Extraction can recover it correctly or scramble it. - **[OCR (optical character recognition)](https://txtfetch.com/glossary/ocr)** Reading the shapes of characters in a picture of text and outputting real characters. It is the only route into a page that has no text layer at all. - **[Searchable PDF](https://txtfetch.com/glossary/searchable-pdf)** A scanned page with an invisible OCR text layer added behind the image, so the page looks unchanged but its text becomes selectable and extractable. - **[Font subsetting](https://txtfetch.com/glossary/font-subsetting)** Embedding only the glyphs a PDF actually uses instead of a whole typeface, which shrinks files but can leave out the character-to-Unicode map extraction needs. - **[DPI (dots per inch)](https://txtfetch.com/glossary/dpi)** A resolution unit for a scanned image, though a file's declared DPI can lie, so OCR depends on the text's pixel height, not the tag. ## Characters & encoding - **[Character encoding](https://txtfetch.com/glossary/character-encoding)** The rulebook mapping bytes to characters, so reading a file with the wrong one can turn every character wrong, even though every byte survives intact. - **[Mojibake](https://txtfetch.com/glossary/mojibake)** Text decoded with the wrong character encoding: the right bytes, read by the wrong rulebook. Fully reversible by re-decoding correctly. - **[Byte order mark (BOM)](https://txtfetch.com/glossary/byte-order-mark)** A few invisible bytes at the start of a text file declaring its encoding and byte order. Helpful for a reader, a stray character if a parser doesn't strip it. - **[Ligature](https://txtfetch.com/glossary/ligature)** A single glyph that visually fuses two or more letters (fi, fl), left unexpanded, extracts as an unrecognized symbol instead of separate characters. - **[Replacement character](https://txtfetch.com/glossary/replacement-character)** Marks a byte sequence a decoder couldn't turn into any real character at all. Unlike mojibake, there's nothing left to recover. ## File identity - **[Media type (MIME type)](https://txtfetch.com/glossary/media-type)** The standard label for a file's format, like application/pdf or image/png, trustworthy only when derived from the file's bytes, not its extension or a header. - **[Magic bytes](https://txtfetch.com/glossary/magic-bytes)** The fixed byte sequence at the start of a file that identifies its real format: %PDF for a PDF, PK for a zip. It works regardless of what the extension claims. - **[OOXML (Office Open XML)](https://txtfetch.com/glossary/ooxml)** The zip-of-XML format behind modern .docx/.xlsx/.pptx, where each piece of content lives in its own XML part, so a parser must know where to look. - **[Document metadata](https://txtfetch.com/glossary/document-metadata)** Data about a file, like its type, size, page count, and whether OCR ran, as opposed to its body content. What an extraction API reports alongside the text. - **[Caption track](https://txtfetch.com/glossary/caption-track)** Text already stored inside or alongside a video/audio file, in a sidecar file or muxed into the container. Not the same thing as transcribing speech. ## Into an LLM pipeline - **[Chunking](https://txtfetch.com/glossary/chunking)** Splitting a long document's extracted text into smaller pieces sized for an embedding model. No chunking strategy recovers a reading order extraction already destroyed. - **[Token](https://txtfetch.com/glossary/token)** The unit an LLM actually counts and charges for, usually a word piece rather than a whole word. A character-based estimate of it is a heuristic, not the real count. ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/glossary/byte-order-mark # Byte order mark (BOM) A few invisible bytes at the start of a text file declaring its encoding and byte order. Helpful for a reader, a stray character if a parser doesn't strip it. definition A byte order mark (BOM) is an optional sequence of bytes placed at the start of a text file. It declares the file's encoding and, for multi-byte encodings, the byte order used. Also called: BOM, UTF-8 BOM in-plain-terms UTF-16 can store each character's bytes in two different orders. A reader has no way to tell which one a file uses without being told. The BOM's job is to say so before any real content begins. UTF-8 doesn't need a BOM for this reason, since it has no byte-order ambiguity. But some tools write one anyway, mostly for legacy compatibility. A reader that doesn't recognize and strip the BOM treats it as ordinary content. It shows up as an invisible or oddly-rendered character glued to the very first word of the file. This is a common, easy-to-miss extraction artifact. why-it-matters - A stray BOM at the start of extracted text can silently break an exact-match comparison or a first-line parser. It can also break a chunk boundary that assumes the file starts cleanly. - Detecting and stripping the BOM correctly is also how a reader knows which encoding and byte order to apply to the rest of the file. Get it wrong and every character after it can decode incorrectly too. how-to-check - Check the first few bytes of a text file for a UTF-8 or UTF-16 BOM signature before assuming the file starts with real content. related-terms - [Character encoding →](https://txtfetch.com/glossary/character-encoding) - [Mojibake →](https://txtfetch.com/glossary/mojibake) faq **Does every UTF-8 file have a BOM?**: No. A BOM is optional in UTF-8 and isn't needed for it, since UTF-8 has no byte-order ambiguity to resolve. Some tools write one anyway for legacy compatibility, which is why a reader still needs to detect and strip it. **What happens if a BOM isn't stripped?**: It gets treated as ordinary content. It typically shows up as an invisible or oddly-rendered character glued to the very first word of the extracted text. That can break an exact-match comparison or a first-line parser downstream. related-reading - [Extract text from CSV →](https://txtfetch.com/extract/csv) - [Fix: mojibake and wrong-encoding text →](https://txtfetch.com/fixes/mojibake-wrong-encoding) - [All glossary terms →](https://txtfetch.com/glossary) Characters & encoding - [Character encoding →](https://txtfetch.com/glossary/character-encoding) - [Mojibake →](https://txtfetch.com/glossary/mojibake) - [Ligature →](https://txtfetch.com/glossary/ligature) - [Replacement character →](https://txtfetch.com/glossary/replacement-character) - [All terms →](https://txtfetch.com/glossary) ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/glossary/caption-track # Caption track Text already stored inside or alongside a video/audio file, in a sidecar file or muxed into the container. Not the same thing as transcribing speech. definition A caption track is a stream of timed text: captions or subtitles. It's stored as a separate sidecar file (.srt, .vtt, .ttml), or muxed directly into a video container, distinct from the audio or video content itself. Also called: subtitle track, closed captions, embedded subtitles in-plain-terms Text in a video or audio file lives in one of a few places, and which one decides what an extraction call actually returns. A caption sidecar carries a real transcript someone wrote, wrapped in cue numbers and timestamps. A tagged .mp3 or .mp4 carries whatever tag metadata its encoder wrote, like a title or an artist. 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 at all: they detect fine from their bytes and hand back nothing. This matters because "extract text from video" bundles two genuinely different jobs that get conflated constantly. One is reading text that already exists in the file. The other is listening to the audio and writing down what's said. txtfetch does the first: it reads a caption track or a container's tag data if one is present. It does not transcribe speech. If a file's words only ever existed as sound with no caption track, that's a speech-to-text job for a different tool. Run that first, then send the resulting transcript through txtfetch like any other text file. why-it-matters - A plain audio file, or a video with no caption track, won't come back with a transcript. Expect an honest error instead, not a silent empty success. There's no text-bearing data in the file to find, so that's the correct outcome. - A caption sidecar's raw text still carries cue numbers and timestamps until it's cleaned up separately. The raw extraction and the clean transcript are two different outputs. how-to-check - Clean captions out of a video or subtitle file into a real transcript, entirely in your browser. [Clean a caption file into a transcript](https://txtfetch.com/tools/subtitles-to-text) - See exactly what each media container returns: caption sidecar, tag data only, or an explicit error. [See what each media format returns](https://txtfetch.com/extract/captions) related-terms - [Media type (MIME type) →](https://txtfetch.com/glossary/media-type) - [Document metadata →](https://txtfetch.com/glossary/document-metadata) faq **Does txtfetch transcribe speech from a video?**: No. It reads text that already exists in the file: a caption sidecar or a container's tag data. If a file's words only ever existed as sound with no caption track, that requires a separate speech-to-text tool run first. **What happens if a video file has no caption track and no tags?**: An explicit error, not a silent empty result. There's genuinely no text-bearing data in the file for extraction to find, and txtfetch says so rather than returning an empty success. related-reading - [Getting text out of video and audio →](https://txtfetch.com/blog/text-from-video-and-audio) - [All glossary terms →](https://txtfetch.com/glossary) File identity - [Media type (MIME type) →](https://txtfetch.com/glossary/media-type) - [Magic bytes →](https://txtfetch.com/glossary/magic-bytes) - [OOXML (Office Open XML) →](https://txtfetch.com/glossary/ooxml) - [Document metadata →](https://txtfetch.com/glossary/document-metadata) - [All terms →](https://txtfetch.com/glossary) ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/glossary/character-encoding # Character encoding The rulebook mapping bytes to characters, so reading a file with the wrong one can turn every character wrong, even though every byte survives intact. definition Character encoding is a mapping between numeric byte values and the characters they represent, letting a computer store and interpret text as bytes. Also called: text encoding, charset in-plain-terms Text isn't stored as letters. It's stored as bytes, and an encoding is the agreed rule for which byte sequences represent which characters. UTF-8 is the modern default and can represent any Unicode character using one to four bytes. Legacy single-byte encodings like Windows-1252 only cover a few hundred characters each, and different encodings assign different characters to the same byte value. Reading a byte stream with the wrong encoding doesn't lose data, since the original bytes are still there. It produces the wrong characters instead. That's the entire mechanism behind mojibake: correct bytes, wrong rulebook applied when decoding them. why-it-matters - A file with no declared encoding forces a reader to guess. Reliable detection handles most real files, but any guesser can occasionally pick wrong on a short or unusual byte sequence. - Re-decoding with the right encoding fully recovers the original text when the bytes are intact. Mojibake is reversible in a way a genuinely undecodable byte, which becomes a replacement character, is not. how-to-check - Scan extracted text for mojibake or replacement-character patterns. This is a quick signal that the wrong encoding was assumed somewhere in the pipeline. [Scan extracted text for encoding damage](https://txtfetch.com/tools/chunk-preview) related-terms - [Mojibake →](https://txtfetch.com/glossary/mojibake) - [Byte order mark (BOM) →](https://txtfetch.com/glossary/byte-order-mark) - [Replacement character →](https://txtfetch.com/glossary/replacement-character) faq **What's the difference between an encoding and a font?**: An encoding maps bytes to characters, deciding which character a given byte sequence represents. A font then maps each character to a visual glyph shape. Getting the encoding wrong produces the wrong characters entirely; a font issue only affects how a correct character looks. **Why does the same file sometimes decode differently in different tools?**: Because a byte stream doesn't announce its own encoding. A reader that assumes the wrong one, or guesses wrong when none is declared, produces different, wrong characters than a reader that assumes correctly. This happens even though both readers see identical bytes. related-reading - [Fix: mojibake and wrong-encoding text →](https://txtfetch.com/fixes/mojibake-wrong-encoding) - [All glossary terms →](https://txtfetch.com/glossary) Characters & encoding - [Mojibake →](https://txtfetch.com/glossary/mojibake) - [Byte order mark (BOM) →](https://txtfetch.com/glossary/byte-order-mark) - [Ligature →](https://txtfetch.com/glossary/ligature) - [Replacement character →](https://txtfetch.com/glossary/replacement-character) - [All terms →](https://txtfetch.com/glossary) ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/glossary/chunking # Chunking Splitting a long document's extracted text into smaller pieces sized for an embedding model. No chunking strategy recovers a reading order extraction already destroyed. definition Chunking is the process of splitting a document's text into smaller, bounded pieces sized to fit within an embedding model's input limit. It's a step used in retrieval-augmented generation and search indexing. Also called: text chunking, document chunking in-plain-terms An embedding model and most LLM context windows have a token budget, so a whole document rarely fits, or embeds usefully, as one unit. Chunking divides the extracted text into pieces small enough to embed individually. It usually uses a target token count and some overlap between adjacent chunks, so a sentence straddling a boundary still appears in full somewhere. Different chunking strategies trade off predictability against respecting structure. A fixed-size window cuts at a hard character count regardless of sentence or paragraph boundaries. Recursive chunking splits on paragraph, then line, then sentence, then word, only falling back to a smaller unit when the current one still overflows. Structure-aware chunking sections on detected headings first. All three inherit whatever the extraction stage handed them, since chunking runs after extraction. No strategy can recover a reading order or missing text that extraction already got wrong. why-it-matters - Chunk quality is bounded by extraction quality. A scrambled reading order or a repeated header line gets faithfully chunked right along with the rest. Chunking has no way to tell damage from real content. - Too little overlap risks losing context at a chunk boundary. Too much means embedding and storing the same content repeatedly for no retrieval benefit past a certain point. how-to-check - Paste extracted text and see chunk boundaries, overlap, and extraction-damage signals for three chunking strategies, entirely in your browser. [Preview how your text will chunk](https://txtfetch.com/tools/chunk-preview) related-terms - [Token →](https://txtfetch.com/glossary/token) - [Reading order →](https://txtfetch.com/glossary/reading-order) faq **Should I chunk before or after extraction?**: After, always. Chunking operates on whatever text extraction produced, and no chunking strategy recovers a reading order or missing content that extraction already got wrong. **Which chunking strategy should I use?**: Recursive chunking with a moderate token target and 10-15% overlap is a reasonable default for most prose. Reach for structure-aware chunking when the source has reliable headings, and fixed-size only when predictable chunk counts matter more than clean boundaries. related-reading - [Chunking strategies for RAG →](https://txtfetch.com/blog/chunking-strategies-for-rag) - [The RAG recipe end to end →](https://txtfetch.com/docs/recipe) - [All glossary terms →](https://txtfetch.com/glossary) Into an LLM pipeline - [Token →](https://txtfetch.com/glossary/token) - [All terms →](https://txtfetch.com/glossary) ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/glossary/document-metadata # Document metadata Data about a file, like its type, size, page count, and whether OCR ran, as opposed to its body content. What an extraction API reports alongside the text. definition Document metadata is structured information describing a file: its format, size, authorship, creation date, page count, and similar properties. It is distinct from the body content the file carries. Also called: file metadata, extraction metadata in-plain-terms A PDF's own metadata can include a title, author, and creation and modification dates. It stores this in the file itself, rather than in its visible text, along with the page count. An extraction API layers its own metadata on top of that. txtfetch's response includes content\_type, byte and character counts, and whether OCR ran on that particular request. These are facts about how the extraction happened, not facts embedded in the source file. Metadata answers questions a caller needs before or after trusting the extracted text itself. What format was this really? How big was it? Did OCR run, and which quality tier processed it? It answers all of this without re-parsing the document to find out. why-it-matters - metadata.ocr in a txtfetch response tells you whether that specific request actually ran OCR. This matters when confirming a scan was processed correctly rather than silently skipped. - A file's own embedded metadata, like an author name or creation date, can be wrong, stale, or stripped entirely. It's a property of the file, not a guarantee, and shouldn't be trusted the same way the extracted body text is. how-to-check - Check metadata.content\_type and metadata.ocr on an extraction response to confirm what was actually detected and processed, rather than assuming. [See the full response shape](https://txtfetch.com/docs) related-terms - [Media type (MIME type) →](https://txtfetch.com/glossary/media-type) - [OOXML (Office Open XML) →](https://txtfetch.com/glossary/ooxml) faq **Is a file's embedded metadata always accurate?**: No. An author name, creation date, or title embedded in a file is a property the file's creator (or their software) wrote. It's not something an extraction pipeline verifies, so it can be wrong, stale, or missing. **What metadata does txtfetch return alongside extracted text?**: content_type, byte and character counts, and whether OCR ran, on every response. Additional fields like page count, quality tier, and token usage appear depending on which format and quality mode was requested. related-reading - [Error reference →](https://txtfetch.com/docs/errors) - [All glossary terms →](https://txtfetch.com/glossary) File identity - [Media type (MIME type) →](https://txtfetch.com/glossary/media-type) - [Magic bytes →](https://txtfetch.com/glossary/magic-bytes) - [OOXML (Office Open XML) →](https://txtfetch.com/glossary/ooxml) - [Caption track →](https://txtfetch.com/glossary/caption-track) - [All terms →](https://txtfetch.com/glossary) ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/glossary/dpi # DPI (dots per inch) A resolution unit for a scanned image, though a file's declared DPI can lie, so OCR depends on the text's pixel height, not the tag. definition DPI (dots per inch) is a measure of how many pixels a scanned image packs into each inch of the original physical page. This is the resolution the scan was captured at. Also called: scan resolution, effective resolution in-plain-terms For OCR, what matters isn't the DPI a file declares. It's how tall the actual character shapes are in pixels once the image is decoded. That's what an OCR engine's line-finder and glyph classifier actually see. Tesseract's own documentation recommends at least 300 dpi for reliable results. A file's declared DPI can be wrong in both directions. A phone photo often declares a low DPI, a leftover screen-resolution default, despite having plenty of real detail. An upscaled fax can declare a high DPI with the actual text only a handful of blurry pixels tall. The number that predicts OCR quality is the measured, effective one, not the declared one. why-it-matters - Catching a genuinely too-low-resolution scan before OCR saves an API call that would only produce garbled output. Missing it wastes a request and burns quota on unusable text. - Downsampling a scan for a preview or thumbnail before OCR throws away exactly the pixel detail OCR depends on. Always OCR the full-resolution original. how-to-check - Measure an image's effective resolution directly from its pixels rather than trusting the declared DPI tag. [Check an image's OCR readiness](https://txtfetch.com/tools/image-ocr-check) related-terms - [OCR (optical character recognition) →](https://txtfetch.com/glossary/ocr) - [Searchable PDF →](https://txtfetch.com/glossary/searchable-pdf) faq **Can I trust a scanned file's declared DPI?**: Not on its own, since it can be wrong in both directions. A phone photo often declares a low DPI despite having plenty of real detail. An upscaled fax can declare a high DPI with the actual text only a few blurry pixels tall. The measured, effective resolution is what predicts OCR quality. **What DPI does OCR actually need?**: Tesseract's own documentation recommends at least 300 dpi for reliable results. But that figure is about the text's real pixel size once decoded, not the number stamped in the file's metadata. related-reading - [Extract text from images →](https://txtfetch.com/extract/image) - [All glossary terms →](https://txtfetch.com/glossary) PDF & scans - [PDF text layer →](https://txtfetch.com/glossary/text-layer) - [Reading order →](https://txtfetch.com/glossary/reading-order) - [OCR (optical character recognition) →](https://txtfetch.com/glossary/ocr) - [Searchable PDF →](https://txtfetch.com/glossary/searchable-pdf) - [Font subsetting →](https://txtfetch.com/glossary/font-subsetting) - [All terms →](https://txtfetch.com/glossary) ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/glossary/font-subsetting # Font subsetting Embedding only the glyphs a PDF actually uses instead of a whole typeface, which shrinks files but can leave out the character-to-Unicode map extraction needs. definition Font subsetting is the practice of embedding only the specific glyphs a document actually uses from a typeface. It embeds those glyphs instead of the complete font file, to reduce PDF file size. Also called: subsetted font, embedded subset font in-plain-terms A typeface can contain thousands of glyphs. A given document might use a few hundred. Subsetting strips a font down to just those, cutting file size substantially. It's standard practice for any PDF producer that embeds fonts rather than relying on the reader having them installed. The catch is /ToUnicode, a separate table mapping each glyph back to a Unicode character. Subsetting the glyphs doesn't require regenerating that map, and some producers skip it. The page renders perfectly, since glyph shapes are all there. But extraction reads the map instead of the shapes, so it has nothing reliable to translate character codes into. why-it-matters - A missing /ToUnicode map on a subsetted font is the single most common cause of a PDF that renders fine but extracts as garbage. It comes out as boxes, question marks, or (cid:N)-style tokens. - There's no reading-side fix for a missing map. The original glyph identity generally can't be recovered from the output text alone. The practical fix is reading the rendered page, with OCR or a vision-model pass, instead of the character codes. how-to-check - The PDF text-layer checker's structural scan flags a Type0/Identity-H font with no ToUnicode map directly. It's the one case the checker can be certain about from structure alone. [Check a PDF's font encoding](https://txtfetch.com/tools/pdf-text-check) related-terms - [PDF text layer →](https://txtfetch.com/glossary/text-layer) - [Character encoding →](https://txtfetch.com/glossary/character-encoding) - [Replacement character →](https://txtfetch.com/glossary/replacement-character) faq **Why does a PDF render perfectly but extract as garbage?**: Rendering only needs a font's glyph shapes, all of which are present even in a subsetted font. Reading text back out needs a separate /ToUnicode map, and subsetting doesn't always regenerate it. When it's missing, extraction has nothing reliable to translate character codes into. **Can I fix a missing ToUnicode map after the file is created?**: Not from the output text alone. The original glyph identity generally can't be recovered from it. Re-exporting from the source with a font that writes ToUnicode, or reading the rendered page instead of its codes, are the two practical routes. related-reading - [Fix: garbled PDF text (the missing ToUnicode CMap) →](https://txtfetch.com/fixes/garbled-pdf-text) - [All glossary terms →](https://txtfetch.com/glossary) PDF & scans - [PDF text layer →](https://txtfetch.com/glossary/text-layer) - [Reading order →](https://txtfetch.com/glossary/reading-order) - [OCR (optical character recognition) →](https://txtfetch.com/glossary/ocr) - [Searchable PDF →](https://txtfetch.com/glossary/searchable-pdf) - [DPI (dots per inch) →](https://txtfetch.com/glossary/dpi) - [All terms →](https://txtfetch.com/glossary) ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/glossary/ligature # Ligature A single glyph that visually fuses two or more letters (fi, fl), left unexpanded, extracts as an unrecognized symbol instead of separate characters. definition A ligature is a single glyph in a font that visually combines two or more letters, commonly fi, fl, or ffi, into one connected shape. It exists for typographic quality, not meaning. Also called: typographic ligature, fi/fl ligature in-plain-terms Typesetting has used ligatures for centuries. Certain letter pairs collide awkwardly when set as separate glyphs, so a ligature glyph redraws them as one connected shape that reads more cleanly. The font still only has one glyph for "fi", not two. A good text extractor expands a ligature glyph back into its component letters as part of normalization. One that doesn't leaves the ligature's own Unicode codepoint in the output. Most downstream tools, tokenizers, and search indexes simply don't recognize that codepoint as the letters it represents. example ``` The office workflow relies on efficient filing ``` Unexpanded ligature glyphs (ffi, fl) sitting inside otherwise normal words. A plain-text search for "office" or "workflow" won't match either one. why-it-matters - An unexpanded ligature reads as one unrecognized character to word- and token-boundary logic. This quietly changes word counts, token counts, and search-matching near it. - A search or exact-match query for a word like "office" silently fails against text that still contains the ligature glyph for "ffi". The two are different codepoints even though a human reads them as identical. how-to-check - Scan extracted text for the Unicode ligature block. It surfaces as a distinct, counted signal rather than something you'd have to spot by eye. [Scan for unexpanded ligatures](https://txtfetch.com/tools/chunk-preview) related-terms - [Character encoding →](https://txtfetch.com/glossary/character-encoding) - [Replacement character →](https://txtfetch.com/glossary/replacement-character) - [PDF text layer →](https://txtfetch.com/glossary/text-layer) faq **Does an unexpanded ligature look wrong on the page?**: No, it renders exactly as intended, since the ligature glyph is a legitimate part of the font. The problem only shows up in the extracted text, where the ligature's own Unicode codepoint isn't the letters it visually represents. **Will a search for a word with "fi" or "fl" in it match text containing the ligature glyph?**: Not unless the ligature is expanded back to separate letters first. The ligature codepoint and the two-letter sequence it visually represents are different characters to a plain-text search, even though a human reads them as identical. related-reading - [Fix: ligatures and smart punctuation →](https://txtfetch.com/fixes/ligatures-and-smart-punctuation) - [All glossary terms →](https://txtfetch.com/glossary) Characters & encoding - [Character encoding →](https://txtfetch.com/glossary/character-encoding) - [Mojibake →](https://txtfetch.com/glossary/mojibake) - [Byte order mark (BOM) →](https://txtfetch.com/glossary/byte-order-mark) - [Replacement character →](https://txtfetch.com/glossary/replacement-character) - [All terms →](https://txtfetch.com/glossary) ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/glossary/magic-bytes # Magic bytes The fixed byte sequence at the start of a file that identifies its real format: %PDF for a PDF, PK for a zip. It works regardless of what the extension claims. definition Magic bytes (or a file signature) are a fixed sequence of bytes, usually at the start of a file. They reliably identify a file's format, independent of filename or metadata. Also called: file signature, magic number in-plain-terms A PDF always begins with the four bytes %PDF. A zip-based format begins with the zip signature PK, and this covers DOCX, XLSX, PPTX, and ODF alike, since they're all zip archives underneath. A PNG begins with an eight-byte fixed signature. Reading these first few bytes reliably identifies a file's real container format before any content is parsed. Detection based on magic bytes is why an extraction pipeline can catch a file that's been renamed, mislabeled, or served with a wrong Content-Type. The bytes at the front of the file don't change no matter what the filename says. A .pdf extension on a file that's actually a renamed image gets treated as an image. It doesn't get force-fed to a PDF parser that would fail on it. why-it-matters - Extension-trust detection breaks the moment a file is renamed. It also breaks when a redirect changes the name, or a source doesn't set an accurate Content-Type. Magic-byte detection doesn't depend on any of that. - Two very different formats can share the same magic bytes at the container level. Telling a DOCX from an XLSX needs a further look inside the archive's own manifest, not just the first four bytes. how-to-check - Drop a file into the file type inspector and see exactly which bytes it read to identify the format. [Inspect a file's magic bytes](https://txtfetch.com/formats) related-terms - [Media type (MIME type) →](https://txtfetch.com/glossary/media-type) - [OOXML (Office Open XML) →](https://txtfetch.com/glossary/ooxml) faq **Can two different formats share the same magic bytes?**: Yes. DOCX, XLSX, PPTX, and ODF are all zip archives underneath, so they all start with the same zip signature. Telling them apart needs a further look inside the archive's manifest, not just the first few bytes. **Does renaming a file's extension change its magic bytes?**: No. Magic bytes live in the file's actual content, at the very start of it, and renaming only changes the filename. That's exactly why byte-level detection catches a mislabeled file that extension-based detection would miss. related-reading - [How txtfetch detects a file's real type →](https://txtfetch.com/how-it-works) - [All glossary terms →](https://txtfetch.com/glossary) File identity - [Media type (MIME type) →](https://txtfetch.com/glossary/media-type) - [OOXML (Office Open XML) →](https://txtfetch.com/glossary/ooxml) - [Document metadata →](https://txtfetch.com/glossary/document-metadata) - [Caption track →](https://txtfetch.com/glossary/caption-track) - [All terms →](https://txtfetch.com/glossary) ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/glossary/media-type # Media type (MIME type) The standard label for a file's format, like application/pdf or image/png, trustworthy only when derived from the file's bytes, not its extension or a header. definition A media type (formerly MIME type) is a standardized two-part label: a type and a subtype, like application/pdf or image/png. It identifies the format of a piece of data. Also called: MIME type, content type in-plain-terms An HTTP Content-Type header, an email attachment, or an extraction API's response all use a media type to say what kind of file it is. The label itself is just a string agreed on by a registry. Nothing enforces that a file's extension, its claimed Content-Type, or even a byte sequence someone renamed actually matches its label. The trustworthy version of a media type is one derived from the file's own bytes: its magic-bytes signature and, beyond that, deeper container inspection. This is more reliable than one taken on faith from a filename or a header a client happened to send. why-it-matters - A file renamed with the wrong extension, or served with an incorrect Content-Type header, will silently misroute in a pipeline that trusts the label. That's why a pipeline should check the bytes instead. - txtfetch reports metadata.content\_type in every extraction response, derived from its own byte-level detection, so a caller can confirm what was actually processed. how-to-check - Drop a file into the file type inspector. It fingerprints the file from its bytes rather than trusting its extension or claimed type. [Fingerprint a file's real type](https://txtfetch.com/formats) related-terms - [Magic bytes →](https://txtfetch.com/glossary/magic-bytes) - [Document metadata →](https://txtfetch.com/glossary/document-metadata) - [OOXML (Office Open XML) →](https://txtfetch.com/glossary/ooxml) faq **Is a file's media type the same as its extension?**: Not reliably. An extension is just a filename convention with nothing enforcing that it matches the file's actual content. A trustworthy media type comes from inspecting the file's bytes, not reading its name. **Where does txtfetch report a file's media type?**: In metadata.content_type on every extraction response, derived from its own byte-level detection rather than the filename or a client-supplied header. related-reading - [Every media type txtfetch actually parses →](https://txtfetch.com/formats/coverage) - [All glossary terms →](https://txtfetch.com/glossary) File identity - [Magic bytes →](https://txtfetch.com/glossary/magic-bytes) - [OOXML (Office Open XML) →](https://txtfetch.com/glossary/ooxml) - [Document metadata →](https://txtfetch.com/glossary/document-metadata) - [Caption track →](https://txtfetch.com/glossary/caption-track) - [All terms →](https://txtfetch.com/glossary) ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/glossary/mojibake # Mojibake Text decoded with the wrong character encoding: the right bytes, read by the wrong rulebook. Fully reversible by re-decoding correctly. definition Mojibake is the visible result of decoding a byte sequence with the wrong character encoding. It produces a string of technically valid but semantically wrong characters in place of the original text. Also called: garbled text, encoding garbage in-plain-terms The classic case is UTF-8 bytes read as Windows-1252. A right single quote in a word like "won't" is three UTF-8 bytes. Reinterpreting them one byte at a time under the wrong encoding turns one character into three wrong ones. The bytes never changed. Only the rule used to turn them back into characters did. Because the original bytes are intact, mojibake is reversible. Re-encode the wrongly-decoded string back to bytes, then decode those bytes with the correct encoding, and the original text returns. This is different from a replacement character, which marks a byte sequence that couldn't be decoded at all and has no bytes left to recover. example ``` It won’t behave the way you expect ``` Classic UTF-8-decoded-as-Windows-1252 mojibake. The right single quote in "won't" is three UTF-8 bytes, misread one byte at a time. why-it-matters - Mojibake breaks sentence- and word-boundary detection right where it occurs. A chunker draws boundaries blind through the damage, and embeddings computed over the garbled tokens pollute the vector space around genuinely related content. - Confusing mojibake with a replacement character wastes effort. Mojibake is a re-decode away from full recovery; a replacement character is not. how-to-check - Paste a sample of the suspect text into the chunk previewer. Mojibake surfaces as a named, counted signal. [Scan for mojibake](https://txtfetch.com/tools/chunk-preview) related-terms - [Character encoding →](https://txtfetch.com/glossary/character-encoding) - [Replacement character →](https://txtfetch.com/glossary/replacement-character) - [Byte order mark (BOM) →](https://txtfetch.com/glossary/byte-order-mark) faq **Can mojibake always be fixed after the fact?**: Yes, as long as the original bytes are still available. Re-encoding the wrongly-decoded string back to bytes and re-decoding with the correct encoding recovers the original text, because nothing was actually lost, only misread. **How is mojibake different from a replacement character?**: Mojibake is a successful decode using the wrong encoding, so it's reversible, because the right bytes are still there. A replacement character marks a decode that failed outright; there's no valid character behind it to recover. related-reading - [Fix: mojibake and wrong-encoding text →](https://txtfetch.com/fixes/mojibake-wrong-encoding) - [All glossary terms →](https://txtfetch.com/glossary) Characters & encoding - [Character encoding →](https://txtfetch.com/glossary/character-encoding) - [Byte order mark (BOM) →](https://txtfetch.com/glossary/byte-order-mark) - [Ligature →](https://txtfetch.com/glossary/ligature) - [Replacement character →](https://txtfetch.com/glossary/replacement-character) - [All terms →](https://txtfetch.com/glossary) ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/glossary/ocr # OCR (optical character recognition) Reading the shapes of characters in a picture of text and outputting real characters. It is the only route into a page that has no text layer at all. definition Optical character recognition (OCR) is the process of recognizing individual character shapes in a raster image and converting them into machine-readable text. Also called: optical character recognition, OCR fallback in-plain-terms OCR looks at pixels, not codes. It segments an image into lines, then words, then individual glyph shapes. Each shape is matched against a trained model of what a character looks like. This is the only way to get text out of a scan, a fax, or a photographed page. Those files never had character codes to begin with, only a picture of one. txtfetch runs Tesseract for this. Every image upload always gets OCR'd. A PDF only escalates to OCR when its first, plain-text extraction pass comes back entirely blank. See the caveat about mixed documents on the pdf-extracts-no-text fix page. A digital cover page ahead of a scanned body doesn't trigger it automatically. why-it-matters - OCR accuracy depends heavily on image quality. Resolution, focus, skew, and contrast all affect it. That's why the same recognizable page can OCR cleanly or produce garbage, depending on how it was scanned. - OCR is not speech-to-text. It reads visible characters in an image, and has nothing to do with audio. how-to-check - Check whether a scan is likely to OCR cleanly before spending a request on it. Resolution, blur, and skew are all measurable from the image itself. [Check OCR readiness](https://txtfetch.com/tools/image-ocr-check) - Look for metadata.ocr: true in a txtfetch response to confirm OCR actually ran on that request. related-terms - [DPI (dots per inch) →](https://txtfetch.com/glossary/dpi) - [PDF text layer →](https://txtfetch.com/glossary/text-layer) - [Searchable PDF →](https://txtfetch.com/glossary/searchable-pdf) faq **Does OCR run on every request?**: Every image upload always gets OCR'd. A PDF only gets OCR'd automatically when its first, plain-text extraction pass comes back entirely blank. A mixed document with a digital cover page ahead of scanned pages doesn't trigger it on its own. **Is OCR the same as speech-to-text?**: No. OCR reads visible character shapes in an image. It has nothing to do with audio; transcribing speech is a separate technology entirely. related-reading - [Extract text from images →](https://txtfetch.com/extract/image) - [OCR scanned documents through one API call →](https://txtfetch.com/blog/ocr-scanned-documents-api) - [All glossary terms →](https://txtfetch.com/glossary) PDF & scans - [PDF text layer →](https://txtfetch.com/glossary/text-layer) - [Reading order →](https://txtfetch.com/glossary/reading-order) - [Searchable PDF →](https://txtfetch.com/glossary/searchable-pdf) - [Font subsetting →](https://txtfetch.com/glossary/font-subsetting) - [DPI (dots per inch) →](https://txtfetch.com/glossary/dpi) - [All terms →](https://txtfetch.com/glossary) ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/glossary/ooxml # OOXML (Office Open XML) The zip-of-XML format behind modern .docx/.xlsx/.pptx, where each piece of content lives in its own XML part, so a parser must know where to look. definition OOXML (Office Open XML) is the zip-archive-based XML file format underlying modern .docx, .xlsx, and .pptx files, standardized as ECMA-376/ISO-29500. Also called: Office Open XML, docx/xlsx/pptx format in-plain-terms Unzip a .pptx and you get a folder tree, not one flat document. Slide bodies live in ppt/slides/slideN.xml, speaker notes live entirely separately in ppt/notesSlides/, and a relationships file ties them together. A .docx keeps tracked changes and comments in their own parts rather than inline in the main body flow. A .xlsx stores its string cell values as indices into a shared xl/sharedStrings.xml table rather than inline text. None of this shows up as garbled or damaged text if a parser misses a part. The extraction just doesn't include it. A response can look completely clean while quietly leaving out an entire category of content, like a whole deck's worth of speaker notes. why-it-matters - A parser that only reads a document's main body part silently drops speaker notes, comments, and tracked changes. The response looks successful, and nothing about it signals what's missing. - Because OOXML is just a zip archive, you can always verify what's actually in a file yourself. Unzip it and grep the XML parts directly, rather than assuming a parser bug. how-to-check - Unzip the file and list its parts. Every .docx/.xlsx/.pptx is a zip you can inspect directly. - See exactly which OOXML parts txtfetch's Office parsing covers, part by part. [See what's covered in .docx/.pptx/.xlsx](https://txtfetch.com/extract/docx) related-terms - [Media type (MIME type) →](https://txtfetch.com/glossary/media-type) - [Magic bytes →](https://txtfetch.com/glossary/magic-bytes) - [Document metadata →](https://txtfetch.com/glossary/document-metadata) faq **Why would text be missing from an Office file with no error at all?**: Because OOXML stores different kinds of content, like body text, speaker notes, comments, and tracked changes, in separate XML parts inside the zip archive. A parser that doesn't resolve a specific part returns a perfectly successful response that simply never included that part's text. **Can I check what's actually inside an Office file myself?**: Yes, every .docx/.pptx/.xlsx is a zip archive. Unzipping it and grepping the relevant XML parts (ppt/notesSlides/, word/comments.xml, xl/sharedStrings.xml) confirms exactly what text the file contains before assuming a parser bug. related-reading - [Fix: Office text silently missing →](https://txtfetch.com/fixes/office-text-missing) - [Parsing Office docs into clean text →](https://txtfetch.com/blog/parse-office-docs-docx-pptx-xlsx-for-llms) - [All glossary terms →](https://txtfetch.com/glossary) File identity - [Media type (MIME type) →](https://txtfetch.com/glossary/media-type) - [Magic bytes →](https://txtfetch.com/glossary/magic-bytes) - [Document metadata →](https://txtfetch.com/glossary/document-metadata) - [Caption track →](https://txtfetch.com/glossary/caption-track) - [All terms →](https://txtfetch.com/glossary) ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/glossary/reading-order # Reading order The sequence a document's text should be read in, left to right and top to bottom within each column. Extraction can recover it correctly or scramble it. definition Reading order is the sequence in which a document's text is meant to be read. This is not always the order its underlying data happens to store it in. Also called: column order, text flow order in-plain-terms A PDF page doesn't store 'column one, then column two' as a concept. It stores individual text-drawing operators, each with its own position, in whatever order the file's producer happened to write them. A layout-aware extractor recovers reading order by clustering those operators back into columns and lines using their positions. A naive one just reads the operators in file order. On a multi-column page, this interleaves both columns mid-line. Reading-order failures don't look like missing data. Every word from the page is usually present somewhere in the output. They look like a scrambled sentence instead, which is worse, because it reads as plausible instead of obviously broken. example ``` Revenue grew Q3 was a strong quarter 34% year over year for the company, driven by ``` Two columns interleaved mid-sentence. Every word from the page is present, just not in the order a reader would encounter them. why-it-matters - A chunker draws sentence and paragraph boundaries over whatever order the text arrives in. It can't detect an interleaved column and un-scramble it after the fact. - Reading-order failures pass a check that only asks 'did we get text back', since there's no missing-data or error signal to trip. Only reading the sentences back reveals they don't parse. how-to-check - Read the extracted text back. A scrambled multi-column page produces sentences that don't parse, even though every individual word is correct. [See the columns-out-of-order symptom](https://txtfetch.com/fixes/columns-out-of-order) related-terms - [PDF text layer →](https://txtfetch.com/glossary/text-layer) - [Chunking →](https://txtfetch.com/glossary/chunking) - [OOXML (Office Open XML) →](https://txtfetch.com/glossary/ooxml) faq **Is a reading-order failure the same as missing text?**: No. Every word from the page is usually present somewhere in the output. A reading-order failure means they're in the wrong sequence, which reads as a plausible but scrambled sentence rather than an obvious gap. **Can I fix reading order after extraction?**: Not reliably. Chunking and downstream processing inherit whatever order extraction produced. A layout-aware extraction pass, or a higher-quality tier for a stubborn document, is what actually recovers the correct column order. related-reading - [Fix: columns out of order →](https://txtfetch.com/fixes/columns-out-of-order) - [Measured reading-order accuracy →](https://txtfetch.com/benchmarks) - [All glossary terms →](https://txtfetch.com/glossary) PDF & scans - [PDF text layer →](https://txtfetch.com/glossary/text-layer) - [OCR (optical character recognition) →](https://txtfetch.com/glossary/ocr) - [Searchable PDF →](https://txtfetch.com/glossary/searchable-pdf) - [Font subsetting →](https://txtfetch.com/glossary/font-subsetting) - [DPI (dots per inch) →](https://txtfetch.com/glossary/dpi) - [All terms →](https://txtfetch.com/glossary) ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/glossary/replacement-character # Replacement character Marks a byte sequence a decoder couldn't turn into any real character at all. Unlike mojibake, there's nothing left to recover. definition The replacement character (Unicode U+FFFD) is the placeholder a decoder substitutes for a byte sequence it cannot read. This happens when the bytes are not a valid character under the encoding in use. Also called: U+FFFD, mojibake vs. replacement character in-plain-terms Sometimes a decoder hits bytes that don't form a valid character under the encoding it's applying. Truncated multi-byte UTF-8 sequences and genuinely corrupted data are two examples. When this happens, the decoder has no correct character to produce. Rather than crash or silently drop the bytes, most decoders substitute the replacement character and continue. The rest of the document still comes through. This is a stronger failure than mojibake. Mojibake is the wrong character produced from a valid decode with the wrong encoding. It's reversible by re-decoding correctly, because the original bytes are recoverable from what came out. A replacement character means the decode failed outright. The bytes it stood in for are gone from the output, and there's no encoding fix that brings them back. example ``` The scanner hit a byte sequence it could not decode: � and continued ``` A single replacement character marks exactly where decoding failed. Everything before and after it decoded fine. why-it-matters - A spike in replacement characters is a strong signal of real, unrecoverable data loss at that point in the text. It's not a cosmetic glitch to clean up after the fact. - Confusing a replacement character with mojibake leads to the wrong fix. Re-decoding with a different encoding does nothing for a replacement character, because there's no valid decode to find. how-to-check - Count replacement characters and their surrounding context in extracted text. A spike is a signal worth investigating, not something to filter out and ignore. [Scan for replacement characters](https://txtfetch.com/tools/chunk-preview) related-terms - [Mojibake →](https://txtfetch.com/glossary/mojibake) - [Character encoding →](https://txtfetch.com/glossary/character-encoding) - [Font subsetting →](https://txtfetch.com/glossary/font-subsetting) faq **Can I recover the text a replacement character replaced?**: Generally no. It marks bytes that couldn't be decoded as any valid character under the encoding applied. The decode failed outright, rather than producing the wrong character, so there's no encoding fix that reconstructs what was there. **Should I just strip replacement characters from extracted text?**: Only after understanding what caused them. A spike is a signal of real data loss at that point in the document, worth investigating before treating it as a cosmetic cleanup step. related-reading - [Fix: mojibake and wrong-encoding text →](https://txtfetch.com/fixes/mojibake-wrong-encoding) - [Fix: garbled PDF text →](https://txtfetch.com/fixes/garbled-pdf-text) - [All glossary terms →](https://txtfetch.com/glossary) Characters & encoding - [Character encoding →](https://txtfetch.com/glossary/character-encoding) - [Mojibake →](https://txtfetch.com/glossary/mojibake) - [Byte order mark (BOM) →](https://txtfetch.com/glossary/byte-order-mark) - [Ligature →](https://txtfetch.com/glossary/ligature) - [All terms →](https://txtfetch.com/glossary) ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/glossary/searchable-pdf # Searchable PDF A scanned page with an invisible OCR text layer added behind the image, so the page looks unchanged but its text becomes selectable and extractable. definition A searchable PDF is a scanned or image-based PDF that has had an invisible, OCR-generated text layer positioned behind the visible page image. The page looks identical, but its text becomes selectable, searchable, and extractable. Also called: OCR'd PDF, text-behind-image PDF in-plain-terms Tools like ocrmypdf don't replace the scanned image. They OCR it, then paste the recognized text back onto the page as an invisible layer, positioned to line up with the original glyphs. Open the file and it looks exactly like the scan. Select text on it, and the invisible layer is what your cursor actually highlights. This is the practical difference between 'my PDF has no text' and 'my PDF now has text'. The pixels never change. Only whether a second, hidden layer of real characters sits underneath them changes. example ``` ocrmypdf input.pdf output.pdf ``` Adds an invisible OCR text layer behind the existing scanned page images. It's the fastest way to get selectable text out of a scan without discarding the originals. why-it-matters - OCR accuracy limits what a searchable PDF's hidden layer actually contains. A good-looking scan can still OCR into a text layer full of recognition errors if the source image was low-resolution or skewed. - A searchable PDF's hidden text and its visible image can, in principle, disagree if the OCR pass misread something. The picture looking right is not proof the extracted text is correct. how-to-check - Try selecting text on the page. A searchable PDF highlights real characters even though the visible page looks like a plain scan. - Run the file through the PDF text-layer checker. It reports whether a text layer exists at all, separate from whether it's accurate. [Check a PDF's text layer](https://txtfetch.com/tools/pdf-text-check) related-terms - [OCR (optical character recognition) →](https://txtfetch.com/glossary/ocr) - [PDF text layer →](https://txtfetch.com/glossary/text-layer) - [DPI (dots per inch) →](https://txtfetch.com/glossary/dpi) faq **Does making a PDF searchable change how it looks?**: No. The visible page image is untouched. Only an invisible text layer, positioned behind the image, is added, and that's what your cursor selects when you highlight text on the page. **Is the hidden text in a searchable PDF guaranteed to be accurate?**: No, it's only as accurate as the OCR pass that produced it. A good-looking scan can still yield a text layer with recognition errors if the source image was low-resolution, skewed, or low-contrast. related-reading - [Fix: empty text from a PDF →](https://txtfetch.com/fixes/pdf-extracts-no-text) - [Extract text from scanned PDFs →](https://txtfetch.com/extract/scanned-pdf) - [All glossary terms →](https://txtfetch.com/glossary) PDF & scans - [PDF text layer →](https://txtfetch.com/glossary/text-layer) - [Reading order →](https://txtfetch.com/glossary/reading-order) - [OCR (optical character recognition) →](https://txtfetch.com/glossary/ocr) - [Font subsetting →](https://txtfetch.com/glossary/font-subsetting) - [DPI (dots per inch) →](https://txtfetch.com/glossary/dpi) - [All terms →](https://txtfetch.com/glossary) ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/glossary/text-layer # PDF text layer The grid of character codes and positions drawn into a PDF page, separate from what the page image shows. No text layer means nothing to extract. definition A PDF text layer is the set of font-encoded character-position operators drawn into a PDF page. This data lets you select, search, and extract text on the page, separate from how the page looks. Also called: text layer, invisible text layer, PDF text layer vs. image in-plain-terms Every PDF page renders as pixels on screen. But whether those pixels came with actual characters behind them is a separate question. A born-digital PDF is one made by a word processor, a report generator, or a print-to-PDF step. It writes both the glyph shapes to draw and the character codes and positions that let a reader select and copy text. A scanned page saved as a PDF has only the picture. It has no text layer to read, because none was ever written. This is why an empty extraction from a PDF usually isn't a bug. If you can't select a word in your PDF viewer, no extraction library can read one either, because there's genuinely nothing there. OCR is the fix when a page has none. See the ocr and searchable-pdf entries below. example ``` >>> extracted_text '' ``` A scanned page has no text layer. Any attempt to extract text from it returns nothing. That is the correct answer, not a parsing failure. why-it-matters - Some documents have a text layer on some pages but not others, like a digital cover sheet ahead of a scanned body. This case fools a whole-document blank check, because the document isn't empty overall. - OCR only helps pages that don't already have a text layer. Running it over a page that already has real, selectable text just adds recognition errors where there were none. how-to-check - Try selecting text on the page in any PDF viewer. No highlight means no text layer on that page. - Run a file through the PDF text-layer checker. It reports per page instead of per document. [Check a PDF's text layer](https://txtfetch.com/tools/pdf-text-check) related-terms - [OCR (optical character recognition) →](https://txtfetch.com/glossary/ocr) - [Searchable PDF →](https://txtfetch.com/glossary/searchable-pdf) - [Reading order →](https://txtfetch.com/glossary/reading-order) faq **How do I know if a PDF has a text layer?**: Try selecting text on the page in any PDF viewer. If nothing highlights, there's no text layer to select, and no extraction library will find one either. The PDF text-layer checker automates this per page rather than per document. **Why did my mixed PDF come back with some text but not all of it?**: A digital cover page ahead of a scanned body means the document isn't blank overall. So a whole-document check doesn't flag the scanned pages behind it as missing their own text layer. Check per page, not per document. related-reading - [Fix: empty text from a PDF →](https://txtfetch.com/fixes/pdf-extracts-no-text) - [Extract text from scanned PDFs →](https://txtfetch.com/extract/scanned-pdf) - [All glossary terms →](https://txtfetch.com/glossary) PDF & scans - [Reading order →](https://txtfetch.com/glossary/reading-order) - [OCR (optical character recognition) →](https://txtfetch.com/glossary/ocr) - [Searchable PDF →](https://txtfetch.com/glossary/searchable-pdf) - [Font subsetting →](https://txtfetch.com/glossary/font-subsetting) - [DPI (dots per inch) →](https://txtfetch.com/glossary/dpi) - [All terms →](https://txtfetch.com/glossary) ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/glossary/token # Token The unit an LLM actually counts and charges for, usually a word piece rather than a whole word. A character-based estimate of it is a heuristic, not the real count. definition A token is the unit of text an LLM's tokenizer produces by splitting a string into sub-word pieces. It is what the model's context limit and usage-based pricing are actually measured in, rather than characters or words. Also called: LLM token, token count in-plain-terms A tokenizer, typically a byte-pair-encoding scheme, breaks text into pieces that are often smaller than a whole word. Common words may be one token, while rarer or compound words split into several. This means a rough "characters divided by four" estimate is a useful sizing heuristic, not a real token count. Actual counts are model-specific, and the same text can tokenize to meaningfully different totals across different embedding or chat models. Token counts matter twice in a RAG pipeline. Once for chunk sizing, staying under an embedding model's input limit. And once for cost, since most LLM APIs charge per token processed and generated. why-it-matters - Sizing chunks off a character-count heuristic instead of a real tokenizer can under- or over-shoot an embedding model's actual input limit. This is especially true on non-English text or heavy punctuation, where the rule of thumb is least accurate. - Token counts, not word or character counts, are what usage-based LLM pricing is metered against. A rough estimate is fine for chunk sizing but isn't a substitute for a real tokenizer when cost accuracy matters. how-to-check - See both an estimated token count and the exact character count for pasted text, side by side. This lets you tell the estimate from the one number that isn't an estimate. [Estimate tokens for your text](https://txtfetch.com/tools/chunk-preview) related-terms - [Chunking →](https://txtfetch.com/glossary/chunking) faq **Is a token the same as a word?**: No. A tokenizer often splits a single word into multiple sub-word tokens. Common words may be a single token, while rarer or compound words split into several. Token counts and word counts diverge, sometimes substantially. **Can I get an exact token count without a real tokenizer?**: Not precisely. A character-based estimate, roughly four characters per token, is a useful sizing guide. Actual counts are model-specific, though, and can differ meaningfully on non-English text or heavy punctuation. related-reading - [Chunking strategies for RAG →](https://txtfetch.com/blog/chunking-strategies-for-rag) - [All glossary terms →](https://txtfetch.com/glossary) Into an LLM pipeline - [Chunking →](https://txtfetch.com/glossary/chunking) - [All terms →](https://txtfetch.com/glossary) ## See the term in real output. Drop a file into the free reader and watch it happen. [Open the file reader →](https://txtfetch.com/tools/file-to-text) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/how-it-works # One endpoint, five stages, zero parsers on your side. Everything between “here's a file” and “here's its text” is txtfetch's job. This is what happens in the milliseconds in between. 1. fetch ## Ingest POST the bytes directly (multipart or base64), or pass a URL. txtfetch fetches the document server-side. This works well for links your users paste and files already sitting on a CDN. 2. detect ## Detect Content-type sniffing on the actual bytes, not the file extension. A .pdf that's really a scanned TIFF in a trench coat still comes out as text. 3. extract ## Extract Apache Tika parses the document structurally. It reads body text, tables, slide notes, email bodies, and attachments, then normalizes the result to clean UTF-8 plain text. 4. ocr ## OCR fallback Some pages have no text layer: scans, faxes, screenshots, photographed receipts. Tesseract OCR takes over automatically. You don't opt in; it's just what happens when pixels arrive. 5. respond ## Respond & forget You get JSON with the extracted text and basic metadata. The document itself is processed in memory and discarded. We meter requests but never store content. under-the-hood ## Boring technology, deliberately. txtfetch doesn't reinvent extraction. It operationalizes the best open-source tools ever built for the job. **Apache Tika** has spent two decades learning every format's dialects and corruptions. See [exactly what it parses](https://txtfetch.com/formats/coverage), checked against the build we run. **Tesseract** is the OCR engine everything else is measured against. Both tools are free to run yourself. See [what that build and its runtime limits actually involve](https://txtfetch.com/compare/self-hosted-tika). We run them serverless, keep them patched, scale them to your traffic, and wrap them in an API you can integrate in five minutes. Your documents stay yours: processed in memory, never written to storage, never used for anything but your response. Tika's structural recovery holds up well on Office docs, HTML, and email. Hard PDFs are different: multi-column layouts, scans, wide tables. That's where a premium VLM tier closes a real accuracy gap. See the measured numbers on [the benchmarks page](https://txtfetch.com/benchmarks). New to the vocabulary? The [PDF text layer](https://txtfetch.com/glossary/text-layer) and [OCR](https://txtfetch.com/glossary/ocr) entries in the [glossary of extraction terms](https://txtfetch.com/glossary) explain both in plain language. ## 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/ingest # txtfetch gets you the text. Here's the rest of the pipeline. Extract, chunk, embed, upsert. Every store below needs something different from the chunking step. Here's what each one needs, and a runnable pipeline for it. ## The four steps, one pipeline A document goes in, and a row in a vector store comes out. txtfetch owns the first step. You own the rest, and each store below changes what "the rest" looks like. 1. **Extract.** One HTTP call turns the file or URL into plain text. 2. **Chunk.** Split the text into pieces sized for retrieval, not for a limit. 3. **Embed.** Turn each chunk into a vector with the embedding model you already run. 4. **Upsert.** Write the vector, its metadata, and often the chunk text, to the store. the five-way matrix | Constraint | pgvector | Pinecone | Qdrant | Chroma | Weaviate | | --- | --- | --- | --- | --- | --- | | Per-vector metadata limit | None from pgvector itself. You're bound by ordinary Postgres limits: up to 1 GB per field, with TOAST moving large text out of the row automatically. | 40 KB per vector, filterable fields only. Values must be a string, number, boolean, or list of strings. No nested objects. | No fixed cap on a point's payload. Bound by the request-size ceiling instead: 32 MB per REST call by default, or a much smaller 4 MB default on the gRPC client unless you raise it. | No published byte ceiling on a document or metadata value. | No published per-property byte ceiling. Bound by the overall request size, mostly relevant over gRPC. | | Stores the chunk text itself | Yes. The chunk text is just another column in the same table as the vector. | Not as a dedicated field. Put the chunk text in metadata, inside that 40 KB, or keep it in your own store, keyed by the vector id. | Yes. The payload is arbitrary JSON stored with the point, so the chunk text is a normal payload field. | Yes, as a dedicated documents field, alongside embeddings and metadatas. | Yes. The chunk text is an ordinary schema property, for example content, on the same object as the vector. | | ID format | None. Use whatever primary key type the table declares: bigserial, uuid, or text. | An ASCII string, 1 to 512 characters. | An unsigned 64-bit integer or a UUID string. No other string is accepted. | Any unique string within the collection. | A UUID. Weaviate can generate one deterministically from your own fields with generate\_uuid5(...) if you don't supply one. | | Max vector dimensions | 16,000 for a plain vector column. Building an HNSW or IVFFlat index on it caps out at 2,000 dimensions, or 4,000 for a halfvec column. | 20,000 dimensions per vector. | 65,535 dimensions for a dense vector. | No published ceiling. The first vector you insert fixes the collection's dimension, and every later insert must match it. | 65,535 dimensions, stored as a uint16 index. Memory runs out long before that ceiling matters. | | Batch upsert size | None from pgvector. You're bound by ordinary Postgres statement size. Use COPY for a bulk load. | 2 MB per upsert request. Pinecone's own guidance keeps a single call near 1,000 vectors, fewer at higher dimensions. | No hard point-count cap. Qdrant's own guidance suggests 64 to 256 points per batch, with a few parallel upload threads for a big job. | Governed by client.get\_max\_batch\_size(), a SQLite parameter ceiling that varies by build (roughly 5,000 to 44,000 records). Chroma won't split an oversized call for you. | No fixed cap. collection.data.insert\_many(...) batches server-side, so you don't tune a batch size by hand. | | Index type | Exact scan with no index, IVFFlat, or HNSW. HNSW is the common default today. | Serverless indexes only today. Metric is cosine, dotproduct, or euclidean, set once at index creation. | HNSW, with optional scalar or binary quantization to cut memory use. | HNSW. Distance metric (l2, cosine, or ip) is set once at collection creation and can't change after. | HNSW by default, with flat, dynamic, and hfresh alternatives, plus PQ, BQ, SQ, and RQ quantization. | Full citations, dated, live on each spoke below. No row states an accuracy, speed, or recall figure. See [/benchmarks](https://txtfetch.com/benchmarks) for txtfetch's own numbers. ## No connector ships for any of them txtfetch ships no plugin or client for pgvector, Pinecone, Qdrant, Chroma, or Weaviate. Each spoke below shows the whole integration. Extract with txtfetch, then write to the store with its own client, the same way you would for any other source of text. pick a destination **[pgvector](https://txtfetch.com/ingest/pgvector)** A Postgres extension that adds a vector column type to a database you already run. Exact scan with no index, IVFFlat, or HNSW. HNSW is the common default today. **[Pinecone](https://txtfetch.com/ingest/pinecone)** A managed, serverless vector database with a hard ceiling on per-vector metadata. Serverless indexes only today. Metric is cosine, dotproduct, or euclidean, set once at index creation. **[Qdrant](https://txtfetch.com/ingest/qdrant)** An open-source and managed vector database with a strict, easy-to-miss ID format. HNSW, with optional scalar or binary quantization to cut memory use. **[Chroma](https://txtfetch.com/ingest/chroma)** An open-source embedding database with a first-class field for the chunk text itself. HNSW. Distance metric (l2, cosine, or ip) is set once at collection creation and can't change after. **[Weaviate](https://txtfetch.com/ingest/weaviate)** An open-source and managed vector database whose client API renamed its own core method this year. HNSW by default, with flat, dynamic, and hfresh alternatives, plus PQ, BQ, SQ, and RQ quantization. ## More on this pipeline - [RAG & LLM ingestion](https://txtfetch.com/solutions/rag-ingestion) — the wider use case this pipeline feeds. - [RAG recipe: chunk, embed, index](https://txtfetch.com/docs/recipe) — the same pipeline through LangChain or LlamaIndex instead of plain Python. - [Chunk previewer](https://txtfetch.com/tools/chunk-preview) — test your own extracted text before you write the first row. frequently asked questions **Does txtfetch connect directly to pgvector, Pinecone, Qdrant, Chroma, or Weaviate?**: No. txtfetch ships no connector, plugin, or client for any of the five. It returns extracted text over one HTTP call. Chunking, embedding, and the write step stay yours, using each store's own client. **Which store should I pick?**: It depends on what you already run. Already on Postgres? pgvector adds a column type, not a new service. Want fully managed with no ops? Pinecone or Weaviate Cloud. Want open source you can also self-host? Qdrant or Chroma. Each spoke names its own real constraints. **Why does this page focus on constraints instead of features?**: Chunking happens before the write, not after. A metadata ceiling, an ID format rule, or a batch limit changes how you chunk and what you can store. That matters more to this pipeline than a feature list would. **Does txtfetch benchmark accuracy, speed, or recall for any of these stores?**: No. None of these five is a text-extraction service, so there's no such comparison to make here. See /benchmarks for txtfetch's own measured extraction numbers. ## Wire the extraction step in. The RAG recipe shows the whole path, from a file to the vector store upsert. [Read the RAG recipe →](https://txtfetch.com/docs/recipe) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/ingest/chroma # Extract text into Chroma An open-source embedding database with a first-class field for the chunk text itself. Here's what changes between the text txtfetch returns and the first row you write to Chroma. ## The problem Chroma's add() call takes the chunk text directly, so there's no metadata workaround to design around. The trap is batch size instead. It's capped by a SQLite parameter ceiling that varies by build, roughly 5,000 to 44,000 records. Chroma won't split an oversized call for you. what chroma needs | Per-vector metadata limit | No published byte ceiling on a document or metadata value. | | --- | --- | | Stores the chunk text itself | Yes, as a dedicated documents field, alongside embeddings and metadatas. | | ID format | Any unique string within the collection. | | Max vector dimensions | No published ceiling. The first vector you insert fixes the collection's dimension, and every later insert must match it. | | Batch upsert size | Governed by client.get\_max\_batch\_size(), a SQLite parameter ceiling that varies by build (roughly 5,000 to 44,000 records). Chroma won't split an oversized call for you. | | Index type | HNSW. Distance metric (l2, cosine, or ip) is set once at collection creation and can't change after. | No accuracy, speed, or recall figure is measured against Chroma on this page. See [/benchmarks](https://txtfetch.com/benchmarks) for txtfetch's own measured extraction numbers. the pipeline Extract, chunk, embed, and upsert. No framework, just the [txtfetch Python SDK](https://txtfetch.com/docs/quickstarts) and Chroma's own client. Python ```python import os import chromadb from openai import OpenAI from txtfetch import Txtfetch txtfetch = Txtfetch(api_key=os.environ["TXTFETCH_KEY"]) oai = OpenAI() client = chromadb.PersistentClient(path="./chroma") collection = client.get_or_create_collection(name="docs") def chunk_text(text: str, size: int = 1000, overlap: int = 100) -> list[str]: chunks = [] start = 0 while start < len(text): end = start + size chunks.append(text[start:end]) start = end - overlap return chunks # 1. Extract result = txtfetch.extract(file="whitepaper.pdf") # 2. Chunk chunks = chunk_text(result.extracted_text) # 3. Embed response = oai.embeddings.create(model="text-embedding-3-small", input=chunks) embeddings = [item.embedding for item in response.data] # 4. Upsert — batch size is capped by client.get_max_batch_size(), which # varies by build. Split a large chunk list yourself if you're close to it. ids = [f"whitepaper-{i}" for i in range(len(chunks))] collection.add( ids=ids, embeddings=embeddings, documents=chunks, metadatas=[{"source": "whitepaper.pdf"} for _ in chunks], ) ``` Every call above was resolved against chromadb 1.5.9, in September 2026. Client APIs move, so check the vendor's own docs for the version you pin. ## Chunking for Chroma Chroma's documents field carries the full chunk text with no published size ceiling. Pick a chunk size for retrieval quality, not to fit a limit. Watch the batch size instead. client.get\_max\_batch\_size() varies by build, so a script that works on your laptop can still hit a wall in a different deployment. Preview your chunker's real boundaries and token counts in the chunk previewer first. large batches A large file, or a slow OCR pass, routes to an async job automatically. That returns a `202` plus a `job_id` to poll, and `?async=true` forces that path for any request. Pass `webhook_url` instead, and txtfetch calls you back when the text is ready, rather than you polling for it. See [async jobs & webhooks](https://txtfetch.com/docs/async) for the full lifecycle. Submit (async) ```submit curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/whitepaper.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. ``` curl ```curl curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/whitepaper.pdf" \ -H "Authorization: Bearer $TXTFETCH_KEY" \ --data-urlencode "webhook_url=https://example.com/webhooks/txtfetch" ``` A retried request, from a client timeout or a dropped response, shouldn't extract and upsert the same document twice. Send the same `Idempotency-Key` on a retry and txtfetch returns the first result instead of running the job again. See [idempotency](https://txtfetch.com/docs/idempotency) for the full guarantee. 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-...") ``` txtfetch ships no connector, plugin, or client for Chroma. The pipeline above is the whole integration. Extract with txtfetch, then write to Chroma with Chroma's own client, the same as you would for any other source of text. frequently asked questions **Does txtfetch write directly to Chroma?**: No. txtfetch returns extracted text over one HTTP call. Chunking, embedding, and the add() call itself stay a caller-side step, exactly like the pipeline above shows. **Is there a real limit on how many chunks I can add at once?**: Yes, though it isn't a fixed published number. Chroma exposes it as client.get_max_batch_size(), a SQLite parameter ceiling that varies by build. Call it at runtime, and split a large batch yourself if you're close to it. **Does txtfetch benchmark accuracy or speed against Chroma?**: No. Chroma is an embedding database, not a text-extraction service, so there's no accuracy or speed comparison to make. See /benchmarks for txtfetch's own measured extraction numbers. sources - [Chroma: Client setup (Persistent, Http, Cloud)](https://docs.trychroma.com/docs/run-chroma/clients) Accessed 2026-09 - [Chroma: Getting started (add, documents, metadatas)](https://docs.trychroma.com/docs/overview/getting-started) Accessed 2026-09 - [Chroma: Collection API reference](https://docs.trychroma.com/reference/python/collection) Accessed 2026-09 - [Chroma: Client API reference (get\_max\_batch\_size)](https://docs.trychroma.com/reference/python/client) Accessed 2026-09 - [Chroma: Configure collections (distance metric)](https://docs.trychroma.com/docs/collections/configure) Accessed 2026-09 - [Chroma Cookbook: batching strategies (batch ceiling varies by build)](https://cookbook.chromadb.dev/strategies/batching/) Accessed 2026-09 directional ## Related - [RAG & LLM ingestion](https://txtfetch.com/solutions/rag-ingestion), the wider use case this pipeline feeds. - [RAG recipe: chunk, embed, index](https://txtfetch.com/docs/recipe), the same pipeline through LangChain or LlamaIndex instead. - [Chunk previewer](https://txtfetch.com/tools/chunk-preview), to test your own extracted text before you write the first row. - [Async jobs & webhooks](https://txtfetch.com/docs/async), the full lifecycle for a large batch. other stores - [Extract text into pgvector →](https://txtfetch.com/ingest/pgvector) - [Extract text into Pinecone →](https://txtfetch.com/ingest/pinecone) - [Extract text into Qdrant →](https://txtfetch.com/ingest/qdrant) - [Extract text into Weaviate →](https://txtfetch.com/ingest/weaviate) - [All destinations →](https://txtfetch.com/ingest) ## Wire the extraction step in. The RAG recipe shows the whole path, from a file to the Chroma upsert. [Read the RAG recipe →](https://txtfetch.com/docs/recipe) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/ingest/pgvector # Extract text into pgvector A Postgres extension that adds a vector column type to a database you already run. Here's what changes between the text txtfetch returns and the first row you write to pgvector. ## The problem pgvector is a Postgres column type, not a service. Postgres has no idea what a chunk is. You design the table, pick a vector width that matches your embedding model, and write the insert yourself. A bad chunk just becomes a bad row, with no store-side warning. what pgvector needs | Per-vector metadata limit | None from pgvector itself. You're bound by ordinary Postgres limits: up to 1 GB per field, with TOAST moving large text out of the row automatically. | | --- | --- | | Stores the chunk text itself | Yes. The chunk text is just another column in the same table as the vector. | | ID format | None. Use whatever primary key type the table declares: bigserial, uuid, or text. | | Max vector dimensions | 16,000 for a plain vector column. Building an HNSW or IVFFlat index on it caps out at 2,000 dimensions, or 4,000 for a halfvec column. | | Batch upsert size | None from pgvector. You're bound by ordinary Postgres statement size. Use COPY for a bulk load. | | Index type | Exact scan with no index, IVFFlat, or HNSW. HNSW is the common default today. | No accuracy, speed, or recall figure is measured against pgvector on this page. See [/benchmarks](https://txtfetch.com/benchmarks) for txtfetch's own measured extraction numbers. the pipeline Extract, chunk, embed, and upsert. No framework, just the [txtfetch Python SDK](https://txtfetch.com/docs/quickstarts) and pgvector's own client. Python ```python import os import psycopg from openai import OpenAI from pgvector import Vector from pgvector.psycopg import register_vector from txtfetch import Txtfetch txtfetch = Txtfetch(api_key=os.environ["TXTFETCH_KEY"]) oai = OpenAI() conn = psycopg.connect(os.environ["DATABASE_URL"], autocommit=True) conn.execute("CREATE EXTENSION IF NOT EXISTS vector") register_vector(conn) conn.execute( """ CREATE TABLE IF NOT EXISTS chunks ( id bigserial PRIMARY KEY, source text, content text, embedding vector(1536) ) """ ) def chunk_text(text: str, size: int = 1000, overlap: int = 100) -> list[str]: chunks = [] start = 0 while start < len(text): end = start + size chunks.append(text[start:end]) start = end - overlap return chunks # 1. Extract result = txtfetch.extract(file="whitepaper.pdf") # 2. Chunk chunks = chunk_text(result.extracted_text) # 3. Embed response = oai.embeddings.create(model="text-embedding-3-small", input=chunks) vectors = [Vector(item.embedding) for item in response.data] # 4. Upsert rows = [("whitepaper.pdf", text, vector) for text, vector in zip(chunks, vectors)] with conn.cursor() as cur: cur.executemany( "INSERT INTO chunks (source, content, embedding) VALUES (%s, %s, %s)", rows, ) # Postgres requires an index name when you write IF NOT EXISTS. conn.execute( "CREATE INDEX IF NOT EXISTS chunks_embedding_idx " "ON chunks USING hnsw (embedding vector_cosine_ops)" ) ``` Every call above was resolved against pgvector 0.4.2 with psycopg 3.2.13, in September 2026. Client APIs move, so check the vendor's own docs for the version you pin. ## Chunking for pgvector pgvector puts no ceiling on chunk length. The real constraint is your vector column width. It has to match your embedding model's output size, and an HNSW index on that column caps out at 2,000 dimensions, or 4,000 for halfvec. Pick a chunk size for retrieval quality, then check the actual boundaries in the chunk previewer before you write a single row. large batches A large file, or a slow OCR pass, routes to an async job automatically. That returns a `202` plus a `job_id` to poll, and `?async=true` forces that path for any request. Pass `webhook_url` instead, and txtfetch calls you back when the text is ready, rather than you polling for it. See [async jobs & webhooks](https://txtfetch.com/docs/async) for the full lifecycle. Submit (async) ```submit curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/whitepaper.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. ``` curl ```curl curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/whitepaper.pdf" \ -H "Authorization: Bearer $TXTFETCH_KEY" \ --data-urlencode "webhook_url=https://example.com/webhooks/txtfetch" ``` A retried request, from a client timeout or a dropped response, shouldn't extract and upsert the same document twice. Send the same `Idempotency-Key` on a retry and txtfetch returns the first result instead of running the job again. See [idempotency](https://txtfetch.com/docs/idempotency) for the full guarantee. 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-...") ``` txtfetch ships no connector, plugin, or client for pgvector. The pipeline above is the whole integration. Extract with txtfetch, then write to pgvector with pgvector's own client, the same as you would for any other source of text. frequently asked questions **Does txtfetch write directly to pgvector?**: No. txtfetch returns extracted text over one HTTP call. Chunking, embedding, and the INSERT itself stay a caller-side step, exactly like the pipeline above shows. **Why does the index dimension limit matter more than the column limit?**: A plain vector column accepts up to 16,000 dimensions, but building an HNSW or IVFFlat index on it caps out at 2,000. Most embedding models stay well under that, but check yours before you commit to a schema. **Does txtfetch benchmark accuracy or speed against pgvector?**: No. pgvector is a database column type, not a text-extraction service, so there's no accuracy or speed comparison to make. See /benchmarks for txtfetch's own measured extraction numbers. sources - [pgvector: README (types, indexes, dimension limits)](https://github.com/pgvector/pgvector) Accessed 2026-09 - [pgvector-python: README (install, register\_vector, insert pattern)](https://github.com/pgvector/pgvector-python) Accessed 2026-09 - [PostgreSQL: Appendix K, limits (row size, TOAST)](https://www.postgresql.org/docs/current/limits.html) Accessed 2026-09 ## Related - [RAG & LLM ingestion](https://txtfetch.com/solutions/rag-ingestion), the wider use case this pipeline feeds. - [RAG recipe: chunk, embed, index](https://txtfetch.com/docs/recipe), the same pipeline through LangChain or LlamaIndex instead. - [Chunk previewer](https://txtfetch.com/tools/chunk-preview), to test your own extracted text before you write the first row. - [Async jobs & webhooks](https://txtfetch.com/docs/async), the full lifecycle for a large batch. other stores - [Extract text into Pinecone →](https://txtfetch.com/ingest/pinecone) - [Extract text into Qdrant →](https://txtfetch.com/ingest/qdrant) - [Extract text into Chroma →](https://txtfetch.com/ingest/chroma) - [Extract text into Weaviate →](https://txtfetch.com/ingest/weaviate) - [All destinations →](https://txtfetch.com/ingest) ## Wire the extraction step in. The RAG recipe shows the whole path, from a file to the pgvector upsert. [Read the RAG recipe →](https://txtfetch.com/docs/recipe) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/ingest/pinecone # Extract text into Pinecone A managed, serverless vector database with a hard ceiling on per-vector metadata. Here's what changes between the text txtfetch returns and the first row you write to Pinecone. ## The problem Pinecone is fully managed, so there's no schema to design. But its metadata is capped at 40 KB per vector, and only holds strings, numbers, booleans, and lists of strings. A chunk record that carries its own JSON blob, the way you might store it elsewhere, doesn't fit as-is. what pinecone needs | Per-vector metadata limit | 40 KB per vector, filterable fields only. Values must be a string, number, boolean, or list of strings. No nested objects. | | --- | --- | | Stores the chunk text itself | Not as a dedicated field. Put the chunk text in metadata, inside that 40 KB, or keep it in your own store, keyed by the vector id. | | ID format | An ASCII string, 1 to 512 characters. | | Max vector dimensions | 20,000 dimensions per vector. | | Batch upsert size | 2 MB per upsert request. Pinecone's own guidance keeps a single call near 1,000 vectors, fewer at higher dimensions. | | Index type | Serverless indexes only today. Metric is cosine, dotproduct, or euclidean, set once at index creation. | No accuracy, speed, or recall figure is measured against Pinecone on this page. See [/benchmarks](https://txtfetch.com/benchmarks) for txtfetch's own measured extraction numbers. the pipeline Extract, chunk, embed, and upsert. No framework, just the [txtfetch Python SDK](https://txtfetch.com/docs/quickstarts) and Pinecone's own client. Python ```python import os from openai import OpenAI from pinecone import Pinecone from txtfetch import Txtfetch txtfetch = Txtfetch(api_key=os.environ["TXTFETCH_KEY"]) oai = OpenAI() pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"]) index = pc.Index("docs") def chunk_text(text: str, size: int = 1000, overlap: int = 100) -> list[str]: chunks = [] start = 0 while start < len(text): end = start + size chunks.append(text[start:end]) start = end - overlap return chunks # 1. Extract result = txtfetch.extract(file="whitepaper.pdf") # 2. Chunk chunks = chunk_text(result.extracted_text) # 3. Embed response = oai.embeddings.create(model="text-embedding-3-small", input=chunks) # 4. Upsert — metadata is capped at 40 KB per vector, so only an excerpt of # the chunk goes here. Keep the full text in your own store, keyed by this # same id, if you need it back verbatim. vectors = [ { "id": f"whitepaper-{i}", "values": item.embedding, "metadata": {"source": "whitepaper.pdf", "text": chunks[i][:2000]}, } for i, item in enumerate(response.data) ] index.upsert(vectors=vectors, namespace="docs") ``` Every call above was resolved against pinecone 7.3.0, in September 2026. Client APIs move, so check the vendor's own docs for the version you pin. ## Chunking for Pinecone Pinecone's 40 KB metadata ceiling is the real constraint on chunk size, not any limit on the vector itself. Store a short excerpt in metadata, not the full chunk, once your chunks run long. Check the actual token counts and boundaries your chunker produces in the chunk previewer before you commit to a size. large batches A large file, or a slow OCR pass, routes to an async job automatically. That returns a `202` plus a `job_id` to poll, and `?async=true` forces that path for any request. Pass `webhook_url` instead, and txtfetch calls you back when the text is ready, rather than you polling for it. See [async jobs & webhooks](https://txtfetch.com/docs/async) for the full lifecycle. Submit (async) ```submit curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/whitepaper.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. ``` curl ```curl curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/whitepaper.pdf" \ -H "Authorization: Bearer $TXTFETCH_KEY" \ --data-urlencode "webhook_url=https://example.com/webhooks/txtfetch" ``` A retried request, from a client timeout or a dropped response, shouldn't extract and upsert the same document twice. Send the same `Idempotency-Key` on a retry and txtfetch returns the first result instead of running the job again. See [idempotency](https://txtfetch.com/docs/idempotency) for the full guarantee. 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-...") ``` txtfetch ships no connector, plugin, or client for Pinecone. The pipeline above is the whole integration. Extract with txtfetch, then write to Pinecone with Pinecone's own client, the same as you would for any other source of text. frequently asked questions **Does txtfetch write directly to Pinecone?**: No. txtfetch returns extracted text over one HTTP call. Chunking, embedding, and the upsert itself stay a caller-side step, exactly like the pipeline above shows. **Can I store the full chunk text in Pinecone?**: Only up to the 40 KB per-vector metadata ceiling, and metadata only holds strings, numbers, booleans, and lists of strings. A longer chunk, or a chunk record with nested fields, needs a separate store keyed by the same vector id. **Does txtfetch benchmark accuracy or speed against Pinecone?**: No. Pinecone is a vector database, not a text-extraction service, so there's no accuracy or speed comparison to make. See /benchmarks for txtfetch's own measured extraction numbers. sources - [Pinecone: Python SDK overview](https://docs.pinecone.io/reference/sdks/python/overview) Accessed 2026-09 - [Pinecone: Upsert reference](https://docs.pinecone.io/reference/upsert) Accessed 2026-09 - [Pinecone: Indexing overview (metadata limits and types)](https://docs.pinecone.io/guides/index-data/indexing-overview) Accessed 2026-09 - [Pinecone: Upsert data guide (batch and request-size limits)](https://docs.pinecone.io/guides/index-data/upsert-data) Accessed 2026-09 ## Related - [RAG & LLM ingestion](https://txtfetch.com/solutions/rag-ingestion), the wider use case this pipeline feeds. - [RAG recipe: chunk, embed, index](https://txtfetch.com/docs/recipe), the same pipeline through LangChain or LlamaIndex instead. - [Chunk previewer](https://txtfetch.com/tools/chunk-preview), to test your own extracted text before you write the first row. - [Async jobs & webhooks](https://txtfetch.com/docs/async), the full lifecycle for a large batch. other stores - [Extract text into pgvector →](https://txtfetch.com/ingest/pgvector) - [Extract text into Qdrant →](https://txtfetch.com/ingest/qdrant) - [Extract text into Chroma →](https://txtfetch.com/ingest/chroma) - [Extract text into Weaviate →](https://txtfetch.com/ingest/weaviate) - [All destinations →](https://txtfetch.com/ingest) ## Wire the extraction step in. The RAG recipe shows the whole path, from a file to the Pinecone upsert. [Read the RAG recipe →](https://txtfetch.com/docs/recipe) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/ingest/qdrant # Extract text into Qdrant An open-source and managed vector database with a strict, easy-to-miss ID format. Here's what changes between the text txtfetch returns and the first row you write to Qdrant. ## The problem Qdrant's payload holds arbitrary JSON, so most chunk records fit without changes. The trap is the point ID: Qdrant only accepts an unsigned 64-bit integer or a UUID string. A chunk ID scheme like "doc-3-chunk-9" is rejected outright and needs converting first. what qdrant needs | Per-vector metadata limit | No fixed cap on a point's payload. Bound by the request-size ceiling instead: 32 MB per REST call by default, or a much smaller 4 MB default on the gRPC client unless you raise it. | | --- | --- | | Stores the chunk text itself | Yes. The payload is arbitrary JSON stored with the point, so the chunk text is a normal payload field. | | ID format | An unsigned 64-bit integer or a UUID string. No other string is accepted. | | Max vector dimensions | 65,535 dimensions for a dense vector. | | Batch upsert size | No hard point-count cap. Qdrant's own guidance suggests 64 to 256 points per batch, with a few parallel upload threads for a big job. | | Index type | HNSW, with optional scalar or binary quantization to cut memory use. | No accuracy, speed, or recall figure is measured against Qdrant on this page. See [/benchmarks](https://txtfetch.com/benchmarks) for txtfetch's own measured extraction numbers. the pipeline Extract, chunk, embed, and upsert. No framework, just the [txtfetch Python SDK](https://txtfetch.com/docs/quickstarts) and Qdrant's own client. Python ```python import os import uuid from openai import OpenAI from qdrant_client import QdrantClient from qdrant_client.models import Distance, PointStruct, VectorParams from txtfetch import Txtfetch txtfetch = Txtfetch(api_key=os.environ["TXTFETCH_KEY"]) oai = OpenAI() client = QdrantClient(url=os.environ["QDRANT_URL"], api_key=os.environ["QDRANT_API_KEY"]) # create_collection raises if the collection is already there, so a second # run of this script needs the guard. if not client.collection_exists("docs"): client.create_collection( collection_name="docs", vectors_config=VectorParams(size=1536, distance=Distance.COSINE), ) def chunk_text(text: str, size: int = 1000, overlap: int = 100) -> list[str]: chunks = [] start = 0 while start < len(text): end = start + size chunks.append(text[start:end]) start = end - overlap return chunks # 1. Extract result = txtfetch.extract(file="whitepaper.pdf") # 2. Chunk chunks = chunk_text(result.extracted_text) # 3. Embed response = oai.embeddings.create(model="text-embedding-3-small", input=chunks) # 4. Upsert — point ids must be an unsigned 64-bit integer or a UUID, so a # generated UUID replaces any string-based chunk id scheme. points = [ PointStruct( id=str(uuid.uuid4()), vector=item.embedding, payload={"source": "whitepaper.pdf", "text": chunk}, ) for chunk, item in zip(chunks, response.data) ] BATCH_SIZE = 128 for i in range(0, len(points), BATCH_SIZE): client.upsert(collection_name="docs", wait=True, points=points[i : i + BATCH_SIZE]) ``` Every call above was resolved against qdrant-client 1.16.1, in September 2026. Client APIs move, so check the vendor's own docs for the version you pin. ## Chunking for Qdrant Qdrant's payload carries no size ceiling of its own, only the surrounding request-size limit. Chunk length is your call, not Qdrant's. The real trap is the point ID. A chunk ID scheme like "doc-3-chunk-9" needs converting to a UUID first, since Qdrant only accepts an integer or a UUID. Check your chunk boundaries in the chunk previewer before you generate ids for the whole batch. large batches A large file, or a slow OCR pass, routes to an async job automatically. That returns a `202` plus a `job_id` to poll, and `?async=true` forces that path for any request. Pass `webhook_url` instead, and txtfetch calls you back when the text is ready, rather than you polling for it. See [async jobs & webhooks](https://txtfetch.com/docs/async) for the full lifecycle. Submit (async) ```submit curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/whitepaper.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. ``` curl ```curl curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/whitepaper.pdf" \ -H "Authorization: Bearer $TXTFETCH_KEY" \ --data-urlencode "webhook_url=https://example.com/webhooks/txtfetch" ``` A retried request, from a client timeout or a dropped response, shouldn't extract and upsert the same document twice. Send the same `Idempotency-Key` on a retry and txtfetch returns the first result instead of running the job again. See [idempotency](https://txtfetch.com/docs/idempotency) for the full guarantee. 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-...") ``` txtfetch ships no connector, plugin, or client for Qdrant. The pipeline above is the whole integration. Extract with txtfetch, then write to Qdrant with Qdrant's own client, the same as you would for any other source of text. frequently asked questions **Does txtfetch write directly to Qdrant?**: No. txtfetch returns extracted text over one HTTP call. Chunking, embedding, and the upsert itself stay a caller-side step, exactly like the pipeline above shows. **Why does the point ID format trip people up?**: Qdrant only accepts an unsigned 64-bit integer or a UUID string as a point id. A chunk ID scheme built from a document name and chunk index, like "doc-3-chunk-9", is rejected outright until you convert it to a UUID. **Does txtfetch benchmark accuracy or speed against Qdrant?**: No. Qdrant is a vector database, not a text-extraction service, so there's no accuracy or speed comparison to make. See /benchmarks for txtfetch's own measured extraction numbers. sources - [Qdrant: Quickstart (client, collection, upsert)](https://qdrant.tech/documentation/quickstart/) Accessed 2026-09 - [Qdrant: Points concept (payload, point ID rules)](https://qdrant.tech/documentation/concepts/points/) Accessed 2026-09 - [Qdrant: Indexing concept (HNSW, quantization)](https://qdrant.tech/documentation/concepts/indexing/) Accessed 2026-09 - [Qdrant: Bulk upload guide (batching)](https://qdrant.tech/documentation/manage-data/bulk-upload/) Accessed 2026-09 - [Qdrant: FAQ (dimension limits)](https://qdrant.tech/documentation/faq/qdrant-fundamentals/) Accessed 2026-09 - [Qdrant: Configuration reference (request-size limits)](https://qdrant.tech/documentation/guides/configuration/) Accessed 2026-09 ## Related - [RAG & LLM ingestion](https://txtfetch.com/solutions/rag-ingestion), the wider use case this pipeline feeds. - [RAG recipe: chunk, embed, index](https://txtfetch.com/docs/recipe), the same pipeline through LangChain or LlamaIndex instead. - [Chunk previewer](https://txtfetch.com/tools/chunk-preview), to test your own extracted text before you write the first row. - [Async jobs & webhooks](https://txtfetch.com/docs/async), the full lifecycle for a large batch. other stores - [Extract text into pgvector →](https://txtfetch.com/ingest/pgvector) - [Extract text into Pinecone →](https://txtfetch.com/ingest/pinecone) - [Extract text into Chroma →](https://txtfetch.com/ingest/chroma) - [Extract text into Weaviate →](https://txtfetch.com/ingest/weaviate) - [All destinations →](https://txtfetch.com/ingest) ## Wire the extraction step in. The RAG recipe shows the whole path, from a file to the Qdrant upsert. [Read the RAG recipe →](https://txtfetch.com/docs/recipe) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/ingest/weaviate # Extract text into Weaviate An open-source and managed vector database whose client API renamed its own core method this year. Here's what changes between the text txtfetch returns and the first row you write to Weaviate. ## The problem Weaviate's object properties hold the chunk text with no fuss. The trap is elsewhere. Object ids must be a valid UUID, and the Python client's own v4 API keeps moving, down to renaming the method that fetches a collection. A tutorial from even a year ago can silently call the wrong thing. what weaviate needs | Per-vector metadata limit | No published per-property byte ceiling. Bound by the overall request size, mostly relevant over gRPC. | | --- | --- | | Stores the chunk text itself | Yes. The chunk text is an ordinary schema property, for example content, on the same object as the vector. | | ID format | A UUID. Weaviate can generate one deterministically from your own fields with generate\_uuid5(...) if you don't supply one. | | Max vector dimensions | 65,535 dimensions, stored as a uint16 index. Memory runs out long before that ceiling matters. | | Batch upsert size | No fixed cap. collection.data.insert\_many(...) batches server-side, so you don't tune a batch size by hand. | | Index type | HNSW by default, with flat, dynamic, and hfresh alternatives, plus PQ, BQ, SQ, and RQ quantization. | No accuracy, speed, or recall figure is measured against Weaviate on this page. See [/benchmarks](https://txtfetch.com/benchmarks) for txtfetch's own measured extraction numbers. the pipeline Extract, chunk, embed, and upsert. No framework, just the [txtfetch Python SDK](https://txtfetch.com/docs/quickstarts) and Weaviate's own client. Python ```python import os import weaviate from openai import OpenAI from weaviate.classes.data import DataObject from weaviate.classes.init import Auth from weaviate.util import generate_uuid5 from txtfetch import Txtfetch txtfetch = Txtfetch(api_key=os.environ["TXTFETCH_KEY"]) oai = OpenAI() client = weaviate.connect_to_weaviate_cloud( cluster_url=os.environ["WEAVIATE_URL"], auth_credentials=Auth.api_key(os.environ["WEAVIATE_API_KEY"]), ) collection = client.collections.use("Docs") def chunk_text(text: str, size: int = 1000, overlap: int = 100) -> list[str]: chunks = [] start = 0 while start < len(text): end = start + size chunks.append(text[start:end]) start = end - overlap return chunks # 1. Extract result = txtfetch.extract(file="whitepaper.pdf") # 2. Chunk chunks = chunk_text(result.extracted_text) # 3. Embed response = oai.embeddings.create(model="text-embedding-3-small", input=chunks) # 4. Upsert — object ids must be a UUID, generated here from the chunk's own # content so a re-run overwrites the same object instead of duplicating it. objects = [ DataObject( properties={"source": "whitepaper.pdf", "content": chunk}, vector=item.embedding, uuid=generate_uuid5(chunk), ) for chunk, item in zip(chunks, response.data) ] collection.data.insert_many(objects) client.close() ``` Every call above was resolved against weaviate-client 4.18.3, in September 2026. Client APIs move, so check the vendor's own docs for the version you pin. ## Chunking for Weaviate Weaviate's property values carry no published size ceiling, so chunk size is a retrieval-quality decision, not a Weaviate constraint. The real trap is the object id. Weaviate only accepts a UUID, so generate one deterministically from the chunk's own content with generate\_uuid5, rather than inventing your own scheme. Preview your chunker's actual boundaries in the chunk previewer before your first batch. large batches A large file, or a slow OCR pass, routes to an async job automatically. That returns a `202` plus a `job_id` to poll, and `?async=true` forces that path for any request. Pass `webhook_url` instead, and txtfetch calls you back when the text is ready, rather than you polling for it. See [async jobs & webhooks](https://txtfetch.com/docs/async) for the full lifecycle. Submit (async) ```submit curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/whitepaper.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. ``` curl ```curl curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/whitepaper.pdf" \ -H "Authorization: Bearer $TXTFETCH_KEY" \ --data-urlencode "webhook_url=https://example.com/webhooks/txtfetch" ``` A retried request, from a client timeout or a dropped response, shouldn't extract and upsert the same document twice. Send the same `Idempotency-Key` on a retry and txtfetch returns the first result instead of running the job again. See [idempotency](https://txtfetch.com/docs/idempotency) for the full guarantee. 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-...") ``` txtfetch ships no connector, plugin, or client for Weaviate. The pipeline above is the whole integration. Extract with txtfetch, then write to Weaviate with Weaviate's own client, the same as you would for any other source of text. frequently asked questions **Does txtfetch write directly to Weaviate?**: No. txtfetch returns extracted text over one HTTP call. Chunking, embedding, and the upsert itself stay a caller-side step, exactly like the pipeline above shows. **Why does the client API matter this much for Weaviate specifically?**: Weaviate's Python client moved to a new v4 shape, and even within v4 the method that fetches a collection was renamed. A snippet copied from an older guide can call a method that no longer exists. Check it against Weaviate's own current docs before you ship it. **Does txtfetch benchmark accuracy or speed against Weaviate?**: No. Weaviate is a vector database, not a text-extraction service, so there's no accuracy or speed comparison to make. See /benchmarks for txtfetch's own measured extraction numbers. sources - [Weaviate: Python client library (v4, connect, collections.use)](https://docs.weaviate.io/weaviate/client-libraries/python) Accessed 2026-09 - [Weaviate Cloud: connect from Python](https://docs.weaviate.io/cloud/manage-clusters/connect) Accessed 2026-09 - [Weaviate: Import data (batch ingest, custom vectors, UUIDs)](https://docs.weaviate.io/weaviate/manage-data/import) Accessed 2026-09 - [Weaviate: FAQ (vector dimension limit)](https://weaviate.io/developers/weaviate/more-resources/faq) Accessed 2026-09 - [Weaviate: Collection configuration reference (index types)](https://docs.weaviate.io/weaviate/config-refs/collections) Accessed 2026-09 ## Related - [RAG & LLM ingestion](https://txtfetch.com/solutions/rag-ingestion), the wider use case this pipeline feeds. - [RAG recipe: chunk, embed, index](https://txtfetch.com/docs/recipe), the same pipeline through LangChain or LlamaIndex instead. - [Chunk previewer](https://txtfetch.com/tools/chunk-preview), to test your own extracted text before you write the first row. - [Async jobs & webhooks](https://txtfetch.com/docs/async), the full lifecycle for a large batch. other stores - [Extract text into pgvector →](https://txtfetch.com/ingest/pgvector) - [Extract text into Pinecone →](https://txtfetch.com/ingest/pinecone) - [Extract text into Qdrant →](https://txtfetch.com/ingest/qdrant) - [Extract text into Chroma →](https://txtfetch.com/ingest/chroma) - [All destinations →](https://txtfetch.com/ingest) ## Wire the extraction step in. The RAG recipe shows the whole path, from a file to the Weaviate upsert. [Read the RAG recipe →](https://txtfetch.com/docs/recipe) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/integrations # Wire txtfetch into the platform you already run. No plugin ships for n8n, Zapier, Make, Airflow, or S3 and Lambda. Each page below shows the platform's own generic HTTP step, wired to the real request and the real fields. Most of this site assumes you write a request by hand. These five pages don't. They are for a workflow builder, a data engineer, or anyone who wires systems together. These people use a platform's own tools, not a hand-written client. Every page names the exact generic step the platform already has: an HTTP node, a webhook action, a DAG task, or an event trigger. It also lists the exact fields to set on it. None of these platforms has a dedicated txtfetch app, module, or provider. That is deliberate: the honest answer is a link and a header, not a plugin to install. Two facts do most of the work on every page. Pass a document's link with the url query parameter. Then the platform never has to forward raw file bytes through an HTTP step built for small JSON payloads. Pass async=true or webhook\_url on the same request. Then a slow OCR job never trips the platform's own request timeout. **[n8n](https://txtfetch.com/integrations/n8n)** One HTTP Request node, a link instead of a file, and an optional webhook for big documents. explore → **[Zapier](https://txtfetch.com/integrations/zapier)** Webhooks by Zapier's Custom Request action, a link instead of a file, and a Catch Hook for big jobs. explore → **[Make](https://txtfetch.com/integrations/make)** The HTTP app's Make a request module, a link instead of a file, and a Webhooks module for big jobs. explore → **[Apache Airflow](https://txtfetch.com/integrations/airflow)** A DAG task calling the API directly, a link instead of a file, and a poll-or-webhook path for big batches. explore → **[S3 and Lambda](https://txtfetch.com/integrations/aws-s3-lambda)** An S3 object-created event, a presigned URL instead of the raw bytes, and async mode for big uploads. explore → ## Add the step to your automation flow. One HTTP node with your key does the whole job. [Get an API key →](https://app.txtfetch.com/signup) [See every integration →](https://txtfetch.com/integrations) --- # https://txtfetch.com/integrations/airflow # Extract text from an Airflow DAG task. txtfetch has no Airflow provider package. A DAG task calling the API directly, with SimpleHttpOperator or plain Python, does the job with nothing extra to install. the-problem A document-ingestion DAG typically reads a batch of files staged in object storage before the pipeline can run any real logic on them. Loading every file into a task's memory and forwarding it as a request body works for small batches. It scales badly, though, and adds a real risk of a task running out of memory on a big file. A worker-heavy DAG also can't hold one task open indefinitely for a slow OCR job. Doing so starves other tasks of a worker slot. how-to-wire-it-up There is no txtfetch provider package for Airflow — no operator to install from PyPI, no connection type to register beyond a plain HTTP connection. A task built with SimpleHttpOperator, or a PythonOperator running requests.post directly, reaches the same REST API a curl command would. Point the request at each file's existing object-storage URL with the url query parameter. No task ever loads a full file into memory just to forward it. For a batch with slow or large documents, pass async=true. Then either poll GET /v1/extract/{job\_id} from a downstream task, or supply webhook\_url and let a separate consumer pick up each result. - Pass ?url= with each file's existing storage URL, so no DAG task loads a full document into memory first. - Automatic OCR covers scanned batches the same way as digital-native files, with no separate OCR operator. - Async mode returns a job\_id per document, so one slow OCR job never occupies a worker slot for the whole run. - Polling GET /v1/extract/{job\_id} from a downstream task, or supplying webhook\_url, both fit Airflow's own retry and sensor patterns. - Idempotency-Key support keeps a re-triggered or backfilled DAG run from re-billing extractions it already completed. pass-a-link-not-a-file curl ```curl curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/report.pdf" \ -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.pdf"}, ) 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.pdf"); const res = await fetch(endpoint, { method: "POST", headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` }, }); const { extracted_text } = await res.json(); console.log(extracted_text); ``` Go ```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.pdf") endpoint.RawQuery = q.Encode() req, err := http.NewRequest("POST", endpoint.String(), nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result extractResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Println(result.ExtractedText) } ``` ``` { "status": "success", "extracted_text": "..." } ``` large-or-slow-documents `POST /v1/extract` returns `202` with a `job_id` whenever the document is large or slow to process, or whenever `async=true` is set. Poll `GET /v1/extract/{job_id}` for the result, or set `webhook_url` and have txtfetch push it instead. No single request on this page is safe to assume will always finish synchronously. 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. ``` 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" ``` setup-steps 1. Add a task that calls the API, using SimpleHttpOperator or a PythonOperator running requests.post. 2. Set the endpoint to https://api.txtfetch.com/v1/extract with url as a query parameter set to each document's link. 3. Set the Authorization header to Bearer, followed by your API key, read from an Airflow connection or a secrets backend. 4. For a large batch, pass async set to true and either poll GET /v1/extract/{job\_id} in a following task, or supply webhook\_url instead. 5. Read extracted\_text from the response and hand it to the next task with XCom. faq **Is there an Airflow provider package for txtfetch?**: No. txtfetch publishes no Airflow provider or operator. A DAG task built with SimpleHttpOperator or a plain requests.post call reaches the same REST API directly. **Should a task download each file before calling txtfetch?**: No, not if the file already sits in reachable object storage. Pass its URL as the url query parameter and let txtfetch fetch it server-side, so the task never loads the full document into memory. **How does a DAG handle a batch with a mix of small and large documents?**: Set async=true for the whole batch. Small documents still return a result quickly, and large ones return a job_id that a downstream task polls or a webhook_url delivers to. **Should a failed extraction fail the whole DAG run?**: That depends on the pipeline. The task can check status and raise on a real error.code, while a retryable failure can lean on Airflow's own task retry settings instead. related-reading - [Batch and large-document ingestion →](https://txtfetch.com/blog/batch-and-large-document-ingestion) - [Extract text from a PDF for RAG →](https://txtfetch.com/blog/extract-text-from-pdf-for-rag) - [Async jobs & webhooks →](https://txtfetch.com/docs/async) - [Idempotency →](https://txtfetch.com/docs/idempotency) - [Get an API key →](https://app.txtfetch.com/signup) other-integrations - [n8n →](https://txtfetch.com/integrations/n8n) - [Zapier →](https://txtfetch.com/integrations/zapier) - [Make →](https://txtfetch.com/integrations/make) - [S3 and Lambda →](https://txtfetch.com/integrations/aws-s3-lambda) ## Add the step to your Apache Airflow flow. One HTTP node with your key does the whole job. [Get an API key →](https://app.txtfetch.com/signup) [See every integration →](https://txtfetch.com/integrations) --- # https://txtfetch.com/integrations/aws-s3-lambda # Extract text the moment a file lands in S3. txtfetch ships no S3 or Lambda integration. An object-created event triggering a small Lambda function, calling the API directly, is the whole pipeline. the-problem A Lambda triggered by an S3 upload usually has to read the object into memory before it can do anything with it. Lambda's own memory and execution-time budget is the same limited pool the rest of the function's logic has to share. Streaming a large file through the function just to re-upload it as a multipart request wastes that budget. It spends resources on a document the function itself never needs to inspect. Lambda's execution limit also makes it a poor place to sit and wait on a slow OCR job. how-to-wire-it-up There is no txtfetch Lambda layer or S3 integration to attach. An S3 object-created event triggering a plain Lambda function, which calls the API directly, is the entire integration. Rather than streaming the uploaded object through the function, generate a short-lived presigned GET URL for it. Pass that as the url query parameter — txtfetch fetches the object directly from S3. The function's own memory footprint stays small, regardless of the file's size. For a large or slow document, pass async=true and webhook\_url so the function returns immediately. A second Lambda, subscribed to the callback, finishes the job. - Pass ?url= with a presigned S3 URL, so the Lambda function never streams the full object through its own memory. - Automatic OCR covers scanned uploads and photographed documents, with no extra service to wire into the pipeline. - Async mode returns a job\_id immediately, so a slow OCR job never risks the function's execution-time limit. - A second Lambda, subscribed to the webhook\_url callback, keeps the upload-triggered function itself short and cheap to run. - Idempotency-Key support keeps a re-delivered S3 event notification from re-billing the same extraction. pass-a-link-not-a-file curl ```curl curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/report.pdf" \ -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.pdf"}, ) 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.pdf"); const res = await fetch(endpoint, { method: "POST", headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` }, }); const { extracted_text } = await res.json(); console.log(extracted_text); ``` Go ```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.pdf") endpoint.RawQuery = q.Encode() req, err := http.NewRequest("POST", endpoint.String(), nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result extractResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Println(result.ExtractedText) } ``` ``` { "status": "success", "extracted_text": "..." } ``` large-or-slow-documents `POST /v1/extract` returns `202` with a `job_id` whenever the document is large or slow to process, or whenever `async=true` is set. Poll `GET /v1/extract/{job_id}` for the result, or set `webhook_url` and have txtfetch push it instead. No single request on this page is safe to assume will always finish synchronously. 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. ``` 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" ``` setup-steps 1. Create an S3 event notification for ObjectCreated on the bucket that receives uploads. 2. Point the event at a Lambda function. 3. In the function, generate a short-lived presigned GET URL for the uploaded object. 4. Call POST https://api.txtfetch.com/v1/extract with url set to that presigned URL and an Authorization header of Bearer, followed by your API key. 5. For a large object, pass async set to true and webhook\_url set to a second Lambda's function URL, so the upload-triggered function returns right away. 6. Read extracted\_text from the response, or from the webhook payload, and write it wherever the pipeline needs it next. faq **Does txtfetch offer a Lambda layer or an S3 event integration?**: No. There is no txtfetch Lambda layer or S3 integration to install. A plain Lambda function, triggered by an S3 object-created event, calls the REST API directly. **Why generate a presigned URL instead of sending the object's bytes?**: A presigned GET URL lets txtfetch fetch the object directly from S3. The function never has to stream the file through its own limited memory just to forward it. **How should a Lambda function handle a document too large to process before it times out?**: Pass async=true and webhook_url on the request. The upload-triggered function returns immediately, and a second, webhook-triggered Lambda picks up the finished result. **What should the upload-triggered function do if txtfetch returns an error?**: Log the error.code and move the failed event to a dead-letter queue. Don't retry blindly, since a bad file or an unsupported format fails the same way on every attempt. related-reading - [Batch and large-document ingestion →](https://txtfetch.com/blog/batch-and-large-document-ingestion) - [OCR scanned documents through one API →](https://txtfetch.com/blog/ocr-scanned-documents-api) - [Async jobs & webhooks →](https://txtfetch.com/docs/async) - [Idempotency →](https://txtfetch.com/docs/idempotency) - [Get an API key →](https://app.txtfetch.com/signup) other-integrations - [n8n →](https://txtfetch.com/integrations/n8n) - [Zapier →](https://txtfetch.com/integrations/zapier) - [Make →](https://txtfetch.com/integrations/make) - [Apache Airflow →](https://txtfetch.com/integrations/airflow) ## Add the step to your S3 and Lambda flow. One HTTP node with your key does the whole job. [Get an API key →](https://app.txtfetch.com/signup) [See every integration →](https://txtfetch.com/integrations) --- # https://txtfetch.com/integrations/make # Extract text in Make, no dedicated module needed. Make's module directory has no txtfetch entry. The generic HTTP app's Make a request module calls the same API, and a document link is a cleaner input than a binary bundle. the-problem A Make scenario that ingests documents usually pulls a file from a watched folder, an email module, or a form trigger. Forwarding that file as a binary bundle into a generic HTTP module works, but it adds a data-mapping step first. It also carries a real risk of getting the content type wrong. Make also caps how long a single module can run, so a scenario that waits on slow OCR can fail before the module returns anything. how-to-wire-it-up txtfetch has no dedicated Make module — no app to add, no OAuth connection to authorize. The HTTP app's Make a request module reaches the same REST endpoint a curl command would. Most Make triggers already expose a file's public or signed URL alongside its bundle. Pass that as the url query string parameter, instead of mapping the binary field. For a document that's large or slow, add async and webhook\_url to the same request. Then let a second scenario start from a Webhooks module when the result lands. - Pass ?url= with the file's URL from the trigger bundle, so the HTTP module never maps a binary field. - Automatic OCR runs on scanned and photographed documents, in the same module call as any other format. - Async mode returns a job\_id right away, so a slow extraction never exceeds the module's run limit. - A Webhooks module on a second scenario catches the webhook\_url callback and continues from there. - Idempotency-Key support stops a re-run scenario from re-billing an extraction it already completed. pass-a-link-not-a-file curl ```curl curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/report.pdf" \ -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.pdf"}, ) 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.pdf"); const res = await fetch(endpoint, { method: "POST", headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` }, }); const { extracted_text } = await res.json(); console.log(extracted_text); ``` Go ```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.pdf") endpoint.RawQuery = q.Encode() req, err := http.NewRequest("POST", endpoint.String(), nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result extractResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Println(result.ExtractedText) } ``` ``` { "status": "success", "extracted_text": "..." } ``` large-or-slow-documents `POST /v1/extract` returns `202` with a `job_id` whenever the document is large or slow to process, or whenever `async=true` is set. Poll `GET /v1/extract/{job_id}` for the result, or set `webhook_url` and have txtfetch push it instead. No single request on this page is safe to assume will always finish synchronously. 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. ``` 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" ``` setup-steps 1. Add an HTTP module and choose Make a request. 2. Set Method to POST. 3. Set URL to https://api.txtfetch.com/v1/extract, and add url as a query string parameter mapped to the document's link. 4. Under Headers, add Authorization with the value Bearer, followed by your API key. 5. For a large document, add async set to true and webhook\_url set to a second scenario's Webhooks module address. 6. Run the scenario once. The module's output bundle carries extracted\_text for the next module to use. faq **Does Make have a built-in txtfetch module?**: No. There is no txtfetch app in the Make module library. The generic HTTP app's Make a request module calls the same REST API with no extra app to add. **How do I avoid mapping a binary field into the HTTP module?**: Pass the file's URL from the trigger bundle as the url query string parameter instead. txtfetch fetches the document server-side, so the module never handles raw bytes. **What happens if a scenario's run limit is shorter than the OCR job?**: Add async=true and webhook_url to the request. The module returns a job_id immediately, and a second scenario's Webhooks module picks up the finished result. **Can a Make scenario branch on a failed extraction?**: Yes. Add a filter after the HTTP module that checks the status field. A failed request, marked by an error.code, routes down a different path than a success. related-reading - [Batch and large-document ingestion →](https://txtfetch.com/blog/batch-and-large-document-ingestion) - [OCR scanned documents through one API →](https://txtfetch.com/blog/ocr-scanned-documents-api) - [Async jobs & webhooks →](https://txtfetch.com/docs/async) - [Idempotency →](https://txtfetch.com/docs/idempotency) - [Get an API key →](https://app.txtfetch.com/signup) other-integrations - [n8n →](https://txtfetch.com/integrations/n8n) - [Zapier →](https://txtfetch.com/integrations/zapier) - [Apache Airflow →](https://txtfetch.com/integrations/airflow) - [S3 and Lambda →](https://txtfetch.com/integrations/aws-s3-lambda) ## Add the step to your Make flow. One HTTP node with your key does the whole job. [Get an API key →](https://app.txtfetch.com/signup) [See every integration →](https://txtfetch.com/integrations) --- # https://txtfetch.com/integrations/n8n # Extract text in n8n, no plugin required. txtfetch ships no n8n node of its own. The HTTP Request node already does the job. Point it at a link, add one header, and the rest of your workflow gets clean text. the-problem An n8n workflow often needs to read a PDF, a scanned form, or an email attachment before it can act on it. The HTTP Request node can forward a file, but binary data adds a Move Binary Data step first. It also risks a truncated or corrupted body on a large upload. Long-running OCR jobs are worse: n8n's own HTTP node has a request timeout, and a slow extraction can trip it before the response ever arrives. how-to-wire-it-up txtfetch ships no n8n plugin or connector. You wire it up with n8n's own HTTP Request node, calling the same REST API a curl command would. Pass the document as a link with the url query parameter, instead of a binary body. n8n fetches nothing itself; txtfetch fetches the document server-side, and the node never touches raw file bytes. For a document that's large or slow to OCR, set async to true, or leave it out and let txtfetch decide. Either way, add webhook\_url and a Webhook node, so n8n picks up the result on its own trigger instead of holding a request open. - Pass ?url= instead of a binary body, so the HTTP Request node never has to forward raw file bytes through n8n. - Automatic OCR runs on scanned pages and photographed documents, in the same request as a digital-native file. - Async mode returns a job\_id immediately, so a slow OCR job never trips the HTTP Request node's timeout. - A Webhook node catches webhook\_url callbacks directly, so the workflow can branch off a finished extraction instead of polling. - Idempotency-Key support means a retried n8n execution never re-runs and re-bills the same extraction. pass-a-link-not-a-file curl ```curl curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/report.pdf" \ -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.pdf"}, ) 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.pdf"); const res = await fetch(endpoint, { method: "POST", headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` }, }); const { extracted_text } = await res.json(); console.log(extracted_text); ``` Go ```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.pdf") endpoint.RawQuery = q.Encode() req, err := http.NewRequest("POST", endpoint.String(), nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result extractResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Println(result.ExtractedText) } ``` ``` { "status": "success", "extracted_text": "..." } ``` large-or-slow-documents `POST /v1/extract` returns `202` with a `job_id` whenever the document is large or slow to process, or whenever `async=true` is set. Poll `GET /v1/extract/{job_id}` for the result, or set `webhook_url` and have txtfetch push it instead. No single request on this page is safe to assume will always finish synchronously. 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. ``` 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" ``` setup-steps 1. Add an HTTP Request node to the workflow. 2. Set Method to POST. 3. Set URL to https://api.txtfetch.com/v1/extract, then add a query parameter named url set to the document's link. 4. Under Authentication, add a header named Authorization with the value Bearer, followed by your API key. 5. For a large or slow document, add a query parameter async set to true. Add another parameter named webhook\_url, pointing at a Webhook node's test or production URL. 6. Execute the node once. The response body carries extracted\_text, ready for the next step in the workflow. faq **Does txtfetch have an official n8n node?**: No. There is no txtfetch node in the n8n node library. The built-in HTTP Request node calls the same REST API directly, with no plugin to install. **Can the HTTP Request node send a file straight from a previous n8n step?**: Yes, as a binary body, but a link is simpler and safer for large files. Pass the document's URL with the url query parameter and let txtfetch fetch it server-side instead. **What happens if OCR takes longer than the HTTP Request node's timeout?**: Set async to true, or let txtfetch route it automatically for a large input. The workflow gets a job_id right away, and a Webhook node picks up the result through webhook_url with no timeout risk. **Does the HTTP Request node need an IF node to check for errors?**: Yes, that's good practice. An error response still carries a machine-readable error.code. An IF node can branch on it before the workflow tries to use a missing extracted_text field. related-reading - [Batch and large-document ingestion →](https://txtfetch.com/blog/batch-and-large-document-ingestion) - [OCR scanned documents through one API →](https://txtfetch.com/blog/ocr-scanned-documents-api) - [Async jobs & webhooks →](https://txtfetch.com/docs/async) - [Idempotency →](https://txtfetch.com/docs/idempotency) - [Get an API key →](https://app.txtfetch.com/signup) other-integrations - [Zapier →](https://txtfetch.com/integrations/zapier) - [Make →](https://txtfetch.com/integrations/make) - [Apache Airflow →](https://txtfetch.com/integrations/airflow) - [S3 and Lambda →](https://txtfetch.com/integrations/aws-s3-lambda) ## Add the step to your n8n flow. One HTTP node with your key does the whole job. [Get an API key →](https://app.txtfetch.com/signup) [See every integration →](https://txtfetch.com/integrations) --- # https://txtfetch.com/integrations/zapier # Extract text in a Zap, no txtfetch app required. There is no txtfetch app in the Zapier directory. Webhooks by Zapier's Custom Request action calls the same API directly, and a link works better than a file step ever would. the-problem A Zap that needs text out of an incoming document usually starts from a Google Drive file, a Gmail attachment, or a form upload. Zapier's steps pass those around as file objects. Forwarding one to a Custom Request action as binary data is fiddly and easy to get wrong. Zaps also run on a fixed step timeout. A document that takes a while to OCR can fail the step before it ever gets a result back. how-to-wire-it-up txtfetch has no Zapier integration listed in the app directory. Webhooks by Zapier's Custom Request action reaches the same REST endpoint a curl command would, with no app to install first. Most upstream steps already expose a shareable link for the file — a Google Drive share link, a Gmail attachment URL. Pass that as the url query parameter, instead of trying to forward the file's bytes. For a document that's large or slow, add async and webhook\_url to the same request. Then let a second Zap start from a Catch Hook trigger when the result is ready. - Pass ?url= with a file's existing share link, so the Custom Request action never handles binary file data. - Automatic OCR covers scanned PDFs and photographed forms, with no separate OCR app needed in the Zap. - Async mode returns a job\_id immediately, so a slow extraction never times out the Custom Request step. - A Catch Hook trigger on a second Zap picks up the webhook\_url callback and continues the automation from there. - Idempotency-Key support keeps a Zapier auto-replay from re-running and re-billing the same extraction. pass-a-link-not-a-file curl ```curl curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/report.pdf" \ -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.pdf"}, ) 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.pdf"); const res = await fetch(endpoint, { method: "POST", headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` }, }); const { extracted_text } = await res.json(); console.log(extracted_text); ``` Go ```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.pdf") endpoint.RawQuery = q.Encode() req, err := http.NewRequest("POST", endpoint.String(), nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result extractResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Println(result.ExtractedText) } ``` ``` { "status": "success", "extracted_text": "..." } ``` large-or-slow-documents `POST /v1/extract` returns `202` with a `job_id` whenever the document is large or slow to process, or whenever `async=true` is set. Poll `GET /v1/extract/{job_id}` for the result, or set `webhook_url` and have txtfetch push it instead. No single request on this page is safe to assume will always finish synchronously. 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. ``` 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" ``` setup-steps 1. Add a Webhooks by Zapier action and choose Custom Request. 2. Set Method to POST. 3. Set URL to https://api.txtfetch.com/v1/extract, then add url as a query string parameter with the document's link. 4. Under Headers, add Authorization with the value Bearer, followed by your API key. 5. For a large document, add async set to true and webhook\_url set to a second Zap's Catch Hook address. 6. Test the action. Zapier shows extracted\_text in the returned data, ready to map into the next step. faq **Is there a txtfetch app in the Zapier directory?**: No. txtfetch has no listed Zapier app or connector. Webhooks by Zapier's Custom Request action reaches the same REST API with no app install needed. **How do I send a file from a Zap without a txtfetch app?**: Use the file's share link from the upstream step — Google Drive, Dropbox, and Gmail attachments all expose one. Pass it as the url query parameter, rather than forwarding the file itself. **Can a Zap wait for a slow OCR job without timing out?**: Yes. Add async=true and webhook_url to the request, and end the first Zap there. Start a second Zap from a Catch Hook trigger when the callback arrives. **How does a Zap tell a failed extraction from a successful one?**: Check the status field in the response. A failed request still returns a typed error.code instead of extracted_text, so a Filter step can catch it before the Zap continues. related-reading - [Batch and large-document ingestion →](https://txtfetch.com/blog/batch-and-large-document-ingestion) - [Parsing Office docs into clean text →](https://txtfetch.com/blog/parse-office-docs-docx-pptx-xlsx-for-llms) - [Async jobs & webhooks →](https://txtfetch.com/docs/async) - [Error reference →](https://txtfetch.com/docs/errors) - [Get an API key →](https://app.txtfetch.com/signup) other-integrations - [n8n →](https://txtfetch.com/integrations/n8n) - [Make →](https://txtfetch.com/integrations/make) - [Apache Airflow →](https://txtfetch.com/integrations/airflow) - [S3 and Lambda →](https://txtfetch.com/integrations/aws-s3-lambda) ## Add the step to your Zapier flow. One HTTP node with your key does the whole job. [Get an API key →](https://app.txtfetch.com/signup) [See every integration →](https://txtfetch.com/integrations) --- # https://txtfetch.com/languages # Extract text from documents in your language. Nine languages, each with its own legacy encodings, chunking behavior, and honest OCR status. Not the programming-language guides. See those at /for. A born-digital file needs no OCR, in any of these languages. Tika reads a document's text layer as Unicode, so the script doesn't matter. A Word file, a PDF with a real text layer, or an HTML page all work the same way. Each one hands back clean text, whether it's written in Spanish or Japanese. The honest limit sits with scans. The standard OCR tier ships English trained data plus script detection only (`TESSDATA_LANGS=eng osd`). It can tell a scanned page isn't Latin script. It can't turn Japanese, Chinese, Korean, or Arabic glyphs into real text. For a hard scan in any language, request `quality=premium`. This routes a vision-language model instead of Tika. txtfetch never detects or labels a document's language. It never invents an accuracy figure it hasn't measured. Each page below states the real legacy encodings, the real chunking risk, and the real OCR status for that language. If your script needs more than the standard tier gives it, [contact us](https://txtfetch.com/contact). **[Spanish Español](https://txtfetch.com/languages/spanish)** Legacy Windows-1252 files, NFC/NFD accent mismatches, and the standard tier's English-only OCR limit. explore → **[French Français](https://txtfetch.com/languages/french)** The œ ligature, typographic apostrophes, legacy Windows-1252 files, and the honest OCR limit. explore → **[German Deutsch](https://txtfetch.com/languages/german)** Compound words that break word-count chunking, ß folding, and legacy Windows-1252 files. explore → **[Portuguese Português](https://txtfetch.com/languages/portuguese)** Brazilian and European spelling, NFC/NFD accent mismatches, and the standard tier's English-only OCR. explore → **[Japanese 日本語](https://txtfetch.com/languages/japanese)** Legacy Shift\_JIS and EUC-JP files, no-space word segmentation, and the standard tier's real OCR gap. explore → **[Chinese 中文](https://txtfetch.com/languages/chinese)** Legacy GB and Big5 encodings, Simplified against Traditional, and the standard tier's real OCR gap. explore → **[Korean 한국어](https://txtfetch.com/languages/korean)** Legacy EUC-KR and CP949 files, Hangul jamo composition, and the standard tier's English-only OCR. explore → **[Arabic العربية](https://txtfetch.com/languages/arabic)** Reading order, Arabic presentation forms, and the standard tier's English-only OCR. explore → **[Russian Русский](https://txtfetch.com/languages/russian)** Legacy KOI8-R and Windows-1251 files, Cyrillic homoglyphs, and the standard tier's English-only OCR. explore → ## Check your document scan first. The free scan check tells you if a page needs OCR. It runs in your browser. [Check a scan →](https://txtfetch.com/tools/image-ocr-check) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/languages/arabic # Extract text from Arabic documents, in the right order. A born-digital Arabic file needs no OCR. The real risks are reading order, presentation-form glyphs, and the standard tier's OCR limit, not the script itself. العربية · Arabic, right-to-left what-already-works A born-digital Arabic file, a Word document or a modern PDF with a real text layer, needs no OCR. Tika reads its Unicode text layer directly, in logical reading order, regardless of the right-to-left direction the text displays in. character-encodings | encoding | seen in | note | | --- | --- | --- | | `Windows-1256 (CP1256)` | Older Arabic Windows software, and legacy email or text exports. | A single-byte Arabic encoding. Read as UTF-8 or Latin-1 by mistake, every Arabic letter corrupts. | common-pitfalls **Arabic text extracts in the wrong order, reversed from how it reads.**: Some PDF producers store right-to-left text in visual order, the order the glyphs appear on screen, rather than logical order, the order a reader reads them in. This is a reading-order problem, not a missing-character one. Check the reading-order fix for how to detect and correct it. Fix columns and reading order → **Arabic letters extract correctly but look like isolated, disconnected forms instead of their normal joined shape.**: Some old PDFs embed Arabic presentation forms, contextual glyph shapes from the Unicode U+FE70 block, instead of the base Arabic letters. Fold presentation-form characters back to their base Arabic letters before using the text for search or comparison. **Vowel marks (tashkeel) above and below the letters are missing from the extracted text.**: Most everyday Arabic writing, print and digital, omits these marks entirely. Their absence is normal, not a defect. Treat unmarked Arabic as the expected form. Extraction can't add marks that were never in the source. chunking-and-tokens Arabic words are separated by spaces, so a word-count chunker splits Arabic text in roughly the right places. Presentation-form ligatures and the right-to-left layout don't change that word count, only the glyph shapes. The chunk previewer's ~4-characters-per-token estimate is a reasonable starting point for Arabic. ocr-status The standard OCR tier ships English trained data plus script detection only (TESSDATA\_LANGS=eng osd). Latin-trained OCR data cannot read Arabic letterforms, since the shapes share almost nothing with the Latin alphabet. For any scanned or photographed Arabic document, request quality=premium, which routes a vision-language model instead of Tika. No accuracy figure is published for Arabic OCR under either tier. If Arabic scans are a large part of your workload, contact us before you commit to a plan. faq **Does txtfetch detect or label the document's language?**: No. txtfetch extracts the text as written and hands it back as-is. It does not detect, tag, or translate a document's language. There is no language parameter to set. **Can txtfetch OCR a scanned Arabic document?**: Not reliably with the standard tier, which ships English trained data plus script detection only (TESSDATA_LANGS=eng osd). Request quality=premium for the documented route to reading an Arabic scan. **Why does my Arabic text come out in the wrong order?**: Some PDF producers store right-to-left text in visual, on-screen order rather than logical reading order. This is a reading-order issue, addressed in the fix for columns and reading order. **Why are the vowel marks missing from my extracted Arabic text?**: Most Arabic writing omits tashkeel vowel marks in the source document itself. Their absence in the extracted text reflects the source, not a loss during extraction. related-reading - [Fix garbled text from the wrong encoding →](https://txtfetch.com/fixes/mojibake-wrong-encoding) - [What comes back: text, Markdown, or JSON →](https://txtfetch.com/output) - [Glossary: reading order →](https://txtfetch.com/glossary/reading-order) - [Contact →](https://txtfetch.com/contact) - [Get an API key →](https://app.txtfetch.com/signup) other-languages - [Spanish →](https://txtfetch.com/languages/spanish) - [French →](https://txtfetch.com/languages/french) - [German →](https://txtfetch.com/languages/german) - [Portuguese →](https://txtfetch.com/languages/portuguese) - [Japanese →](https://txtfetch.com/languages/japanese) - [Chinese →](https://txtfetch.com/languages/chinese) - [Korean →](https://txtfetch.com/languages/korean) - [Russian →](https://txtfetch.com/languages/russian) - [All languages →](https://txtfetch.com/languages) ## Check your Arabic scan first. The free scan check tells you if a page needs OCR. It runs in your browser. [Check a scan →](https://txtfetch.com/tools/image-ocr-check) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/languages/chinese # Extract text from Chinese documents, either character set. A born-digital Chinese file needs no OCR, Simplified or Traditional. A scanned one hits the same real limit as Japanese: the standard OCR tier can't read Han glyphs. 中文 · Han, Simplified and Traditional what-already-works A born-digital Chinese file, a Word document or a modern PDF with a real text layer, needs no OCR. Tika reads its Unicode text layer directly, whether the document uses Simplified or Traditional characters. Each Han character is its own fixed Unicode code point. character-encodings | encoding | seen in | note | | --- | --- | --- | | `GB2312` | Early Simplified Chinese systems and some legacy exports. | Covers a limited set of common Simplified characters. A rarer character outside that set can't be represented at all. | | `GBK` | A wider successor to GB2312, still found in older Windows software. | Extends GB2312's character coverage but keeps the same two-byte structure, so it isn't interchangeable with UTF-8 or Big5. | | `GB18030` | The mandated national standard for Simplified Chinese text in China today. | Covers the full Unicode range, unlike GB2312 or GBK, but still uses byte sequences different from UTF-8 for the same characters. | | `Big5` | Traditional Chinese systems, mainly in Taiwan and Hong Kong. | A separate two-byte scheme from the GB family. A Big5 file read as GBK garbles just as badly as one read as UTF-8. | common-pitfalls **Chinese characters turn into unrelated symbols or boxes.**: GB2312, GBK, GB18030, or Big5 bytes read under the wrong encoding, including UTF-8. This is mojibake from a legacy Chinese encoding. Re-read the file with its real encoding. Fix garbled text from the wrong encoding → **A word-count chunk size is meaningless for Chinese text.**: Chinese writing has no spaces between words, the same underlying issue as Japanese. Chunk Chinese text by character or token count instead of by word count. What chunking actually does → **A search for a Simplified Chinese term finds nothing in a Traditional Chinese document, or the reverse.**: Simplified and Traditional Chinese use different character forms for many common words. They are different character sets, not encoding variants of each other. Search both forms, or convert one to the other with a known Simplified-Traditional mapping before matching. chunking-and-tokens Chinese has no spaces between words, so a word-count chunker cannot split it at all, Simplified or Traditional. Each character carries more meaning than a Latin letter does. The chunk previewer's ~4-characters-per-token estimate badly understates the real token count for Chinese text. ocr-status A scanned or photographed Chinese document is a different problem than a born-digital one. The standard OCR tier ships English trained data plus script detection only (TESSDATA\_LANGS=eng osd). It can tell a page isn't Latin script. It can't turn Han characters into text, since that needs Chinese-trained OCR data the standard tier doesn't ship. For a scanned Chinese document, request quality=premium, which routes a vision-language model instead of Tika. No accuracy figure is published for Chinese OCR under either tier. If scanned Chinese is a large part of your workload, contact us before you commit to a plan. faq **Does txtfetch detect or label the document's language?**: No. txtfetch extracts the text as written and hands it back as-is. It does not detect, tag, or translate a document's language. There is no language parameter to set. **Can txtfetch OCR a scanned Chinese document?**: Not reliably with the standard tier, which ships English trained data plus script detection only (TESSDATA_LANGS=eng osd). Request quality=premium for the documented route to reading a Chinese scan. **Does txtfetch convert between Simplified and Traditional Chinese?**: No. txtfetch extracts the text exactly as written in the source document, Simplified or Traditional, and does not convert between them. **Why is my word-count chunk size wrong for Chinese text?**: Chinese has no spaces between words, so a word-count chunker can't split it correctly. Chunk by character or token count instead. related-reading - [What comes back: text, Markdown, or JSON →](https://txtfetch.com/output) - [Chunk previewer →](https://txtfetch.com/tools/chunk-preview) - [Glossary: token →](https://txtfetch.com/glossary/token) - [Contact →](https://txtfetch.com/contact) - [Get an API key →](https://app.txtfetch.com/signup) other-languages - [Spanish →](https://txtfetch.com/languages/spanish) - [French →](https://txtfetch.com/languages/french) - [German →](https://txtfetch.com/languages/german) - [Portuguese →](https://txtfetch.com/languages/portuguese) - [Japanese →](https://txtfetch.com/languages/japanese) - [Korean →](https://txtfetch.com/languages/korean) - [Arabic →](https://txtfetch.com/languages/arabic) - [Russian →](https://txtfetch.com/languages/russian) - [All languages →](https://txtfetch.com/languages) ## Check your Chinese scan first. The free scan check tells you if a page needs OCR. It runs in your browser. [Check a scan →](https://txtfetch.com/tools/image-ocr-check) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/languages/french # Extract text from French documents, œ and all. French text is plain Unicode once it's born digital. The failure modes are a dropped ligature, a legacy encoding, or a typographic apostrophe the extractor misreads. Français · Latin, with ligatures and diacritics what-already-works A born-digital French file, a Word document, a PDF with a real text layer, or an HTML page, needs no OCR at all. Tika reads its Unicode text layer directly. é, à, ç, and the œ ligature are each their own fixed code point, so the script itself isn't the hard part. character-encodings | encoding | seen in | note | | --- | --- | --- | | `Windows-1252 (CP1252)` | Older Word and Excel files, and email exported from legacy Windows systems. | Covers é, à, ç, and œ in a single byte each. Read as UTF-8 by mistake, every one of them garbles. | | `ISO-8859-1 (Latin-1)` | Older web pages and plain-text files that predate UTF-8. | Does not include œ at all. A file in this encoding often substitutes a placeholder character for it, well before extraction ever sees it. | common-pitfalls **œ in a word like cœur or œuvre splits into two separate letters, oe.**: A font or a PDF's internal character map never gave the ligature its own code point, or an editor auto-corrected it before the file was saved. This is a lost ligature, not a bug in extraction. Check the fix for ligatures and smart punctuation. Fix ligatures and smart punctuation → **The space before a French question mark, colon, or exclamation point has vanished.**: French typography places a narrow non-breaking space before ; : ! ?, and some fonts or PDF producers drop it or substitute an ordinary space during layout. The words themselves are intact and in order. Treat the missing space as a style detail, not a data loss. **An apostrophe in l'année or c'est renders as a box or a stray, unrelated character.**: The document used a typographic apostrophe (’), and the extractor's encoding guess didn't include it. Confirm the file's real encoding, then re-run extraction with it identified correctly. Fix garbled text from the wrong encoding → chunking-and-tokens French uses spaces between words, so a word-count chunker splits French text about as well as it splits English. The œ ligature counts as one visual character. An unfolded copy can carry two code points instead of one, which slightly skews a character-based token estimate. The chunk previewer's ~4-characters-per-token guide still holds up reasonably well overall. ocr-status A scanned French document needs OCR, and Tesseract's Latin-alphabet training covers most of French's letterforms. But the standard OCR tier ships English trained data plus script detection only (TESSDATA\_LANGS=eng osd). Accented letters and the œ ligature are where accuracy drops fastest on a low-resolution scan. For a scan where accuracy matters, request quality=premium, which routes a vision-language model instead of Tika. No accuracy figure is published for French OCR under either tier. faq **Does txtfetch detect or label the document's language?**: No. txtfetch extracts the text as written and hands it back as-is. It does not detect, tag, or translate a document's language. There is no language parameter to set. **Can txtfetch OCR a scanned French document?**: Yes, but the standard OCR tier ships English trained data only (TESSDATA_LANGS=eng osd). Accented letters and the œ ligature are the first things to lose accuracy on a low-resolution scan. Request quality=premium for a hard scan. **Why does cœur come back as coeur?**: The œ ligature lost its own code point somewhere upstream, usually in the font or the PDF's character map, not during extraction itself. See the ligature fix for the full explanation. **Why is the space before a French question mark missing?**: French typography uses a narrow non-breaking space there, and some PDF producers drop or substitute it during layout. The words and their order are unaffected. related-reading - [What comes back: text, Markdown, or JSON →](https://txtfetch.com/output) - [Glossary: ligature →](https://txtfetch.com/glossary/ligature) - [Chunk previewer →](https://txtfetch.com/tools/chunk-preview) - [Error reference →](https://txtfetch.com/docs/errors) - [Get an API key →](https://app.txtfetch.com/signup) other-languages - [Spanish →](https://txtfetch.com/languages/spanish) - [German →](https://txtfetch.com/languages/german) - [Portuguese →](https://txtfetch.com/languages/portuguese) - [Japanese →](https://txtfetch.com/languages/japanese) - [Chinese →](https://txtfetch.com/languages/chinese) - [Korean →](https://txtfetch.com/languages/korean) - [Arabic →](https://txtfetch.com/languages/arabic) - [Russian →](https://txtfetch.com/languages/russian) - [All languages →](https://txtfetch.com/languages) ## Check your French scan first. The free scan check tells you if a page needs OCR. It runs in your browser. [Check a scan →](https://txtfetch.com/tools/image-ocr-check) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/languages/german # Extract text from German documents, compounds and ß included. German text is plain Unicode once it's born digital. The real risks are ß folding to ss, a legacy encoding, and compound words that break a word-count chunker. Deutsch · Latin, with umlauts and ß what-already-works A born-digital German file, a Word document, a PDF with a real text layer, or an HTML page, needs no OCR. Tika reads its Unicode text layer directly. ä, ö, ü, and ß are each their own fixed code point, so extraction keeps them exactly as written. character-encodings | encoding | seen in | note | | --- | --- | --- | | `Windows-1252 (CP1252)` | Older Word and Excel files, and email exported from legacy Windows systems. | Fits ä, ö, ü, and ß into a single byte each. Read as UTF-8 by mistake, every one garbles. | | `ISO-8859-1 (Latin-1)` | Older web pages and plain-text files that predate UTF-8. | Covers the same German letters as Windows-1252, but not identically laid out, so a wrong guess still corrupts text. | | `ISO-8859-15 (Latin-9)` | German business documents produced after the late 1990s. | Adds the euro sign € over Latin-1. A file in this encoding read as Latin-1 misreads every € as a different character entirely. | common-pitfalls **ß in a word like Straße or groß turns into ss, or into a garbled character.**: Some fonts and encodings fold ß to ss for legacy compatibility, and others misencode the single ß character outright. Both spellings are valid German today, but they are not the same string. Normalize ß consistently across search and comparison. **A long compound word like Rechtsschutzversicherungsgesellschaften blows past a downstream length limit.**: German freely joins nouns into one unbroken compound word with no internal spaces. A word-count chunker sees a single 'word', but that word alone can run to dozens of characters. Size chunks by character or token count, not by word count, whenever German text is likely. What chunking actually does → **ö, ü, or ä turns into a boxy replacement character or a two-character garble.**: The source bytes were Windows-1252, Latin-1, or Latin-9, and something read them with the wrong encoding. Confirm the file's real encoding before extraction, the same fix as any other mojibake case. Fix garbled text from the wrong encoding → chunking-and-tokens German compounds words without spaces inside them, so a single 'word' can run past 30 characters. A word-count chunker treats that whole compound as one unit and badly misjudges its size. The chunk previewer's ~4-characters-per-token estimate is a better guide for German than a word count. A real tokenizer usually still splits a long compound into several tokens on its own. ocr-status A scanned German document needs OCR, and Tesseract's Latin-alphabet training covers most German letterforms. But the standard OCR tier ships English trained data plus script detection only (TESSDATA\_LANGS=eng osd). Umlauts and ß are where accuracy drops fastest on a low-resolution scan. For a scan where accuracy matters, request quality=premium, which routes a vision-language model instead of Tika. No accuracy figure is published for German OCR under either tier. faq **Does txtfetch detect or label the document's language?**: No. txtfetch extracts the text as written and hands it back as-is. It does not detect, tag, or translate a document's language. There is no language parameter to set. **Can txtfetch OCR a scanned German document?**: Yes, but the standard OCR tier ships English trained data only (TESSDATA_LANGS=eng osd). Umlauts and ß lose accuracy fastest on a low-resolution scan. Request quality=premium for a hard scan. **Why do German compound words break my chunk sizes?**: German joins nouns into one long word with no internal spaces. A word-count chunker can't see inside it. Chunk by character or token count for German text instead. **Does txtfetch normalize ß to ss, or the reverse?**: No. txtfetch returns the text exactly as extracted. If your pipeline needs one consistent spelling, normalize ß yourself after extraction. related-reading - [What comes back: text, Markdown, or JSON →](https://txtfetch.com/output) - [Chunk previewer →](https://txtfetch.com/tools/chunk-preview) - [Glossary: token →](https://txtfetch.com/glossary/token) - [Idempotency →](https://txtfetch.com/docs/idempotency) - [Get an API key →](https://app.txtfetch.com/signup) other-languages - [Spanish →](https://txtfetch.com/languages/spanish) - [French →](https://txtfetch.com/languages/french) - [Portuguese →](https://txtfetch.com/languages/portuguese) - [Japanese →](https://txtfetch.com/languages/japanese) - [Chinese →](https://txtfetch.com/languages/chinese) - [Korean →](https://txtfetch.com/languages/korean) - [Arabic →](https://txtfetch.com/languages/arabic) - [Russian →](https://txtfetch.com/languages/russian) - [All languages →](https://txtfetch.com/languages) ## Check your German scan first. The free scan check tells you if a page needs OCR. It runs in your browser. [Check a scan →](https://txtfetch.com/tools/image-ocr-check) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/languages/japanese # Extract text from Japanese documents, kanji and kana intact. A born-digital Japanese file needs no OCR. A scanned one hits a real limit today: the standard OCR tier can't read Japanese glyphs at all. 日本語 · Han, Hiragana, Katakana what-already-works A born-digital Japanese file, a Word document or a modern PDF with a real text layer, needs no OCR at all. Tika reads its Unicode text layer directly, the same way it reads English. Kanji, hiragana, and katakana are each an ordinary Unicode code point, so the script isn't the hard part. character-encodings | encoding | seen in | note | | --- | --- | --- | | `Shift_JIS` | Older Japanese Windows software, and many legacy .txt and .csv exports. | A single-byte ASCII range plus a two-byte Japanese range. Reading it as UTF-8 corrupts every kanji and kana character. | | `EUC-JP` | Older Unix and Linux systems, and some legacy web pages. | A different two-byte scheme from Shift\_JIS. The two aren't interchangeable, and a wrong guess garbles text the same way. | | `ISO-2022-JP` | Old Japanese email, since it was the standard encoding for years. | Switches between ASCII and Japanese character sets with escape sequences inside the byte stream, not fixed byte ranges. | common-pitfalls **Every kanji and kana character turns into a run of unrelated symbols or question marks.**: The file was Shift_JIS, EUC-JP, or ISO-2022-JP, and got read as UTF-8 or Latin-1 instead. This is mojibake from a legacy Japanese encoding, not lost data. Re-read the file with its real encoding. Fix garbled text from the wrong encoding → **A downstream word-count chunk size is wildly wrong for Japanese text.**: Japanese writing has no spaces between words. A word-count chunker, built to split on spaces, sees the whole passage as one long 'word'. Chunk Japanese text by character or token count instead of by word count. What chunking actually does → **The same word appears in a document twice, once full-width and once half-width, and a search only finds one.**: Japanese text mixes full-width (zenkaku) and half-width (hankaku) forms for numbers and Latin letters. These are different Unicode code points. Normalize full-width and half-width variants to one form before comparing or indexing the extracted text. chunking-and-tokens Japanese has no spaces between words, so a word-count chunker cannot split it at all. Each character also carries more meaning than a Latin letter does. The chunk previewer's ~4-characters-per-token estimate badly understates the real token count for Japanese. A real tokenizer usually spends closer to one token per character, not one per four. ocr-status A scanned or photographed Japanese document is a different problem than a born-digital one. The standard OCR tier ships English trained data plus script detection only (TESSDATA\_LANGS=eng osd). Script detection can tell a page isn't Latin script. It cannot turn Japanese glyphs into Japanese text, since that needs Japanese-trained OCR data the standard tier doesn't ship. For a scanned Japanese document, request quality=premium, which routes a vision-language model instead of Tika. No accuracy figure is published for Japanese OCR under either tier. If scanned Japanese is a large part of your workload, contact us before you commit to a plan. faq **Does txtfetch detect or label the document's language?**: No. txtfetch extracts the text as written and hands it back as-is. It does not detect, tag, or translate a document's language. There is no language parameter to set. **Can txtfetch OCR a scanned Japanese document?**: Not reliably with the standard tier, which ships English trained data plus script detection only (TESSDATA_LANGS=eng osd). Request quality=premium, which routes a vision-language model instead of Tika, for the documented route to reading a Japanese scan. **Why is my word-count chunk size wrong for Japanese text?**: Japanese has no spaces between words, so a word-count chunker can't split it correctly. Chunk by character or token count instead, and treat the chunk previewer's character-based estimate as a rough floor, not an exact count. **Why does my Japanese text show unrelated symbols instead of kanji?**: The source file likely used Shift_JIS, EUC-JP, or ISO-2022-JP, and something read it as UTF-8. Re-reading it with the correct encoding fixes the mojibake. related-reading - [What comes back: text, Markdown, or JSON →](https://txtfetch.com/output) - [Chunk previewer →](https://txtfetch.com/tools/chunk-preview) - [Glossary: token →](https://txtfetch.com/glossary/token) - [Contact →](https://txtfetch.com/contact) - [Get an API key →](https://app.txtfetch.com/signup) other-languages - [Spanish →](https://txtfetch.com/languages/spanish) - [French →](https://txtfetch.com/languages/french) - [German →](https://txtfetch.com/languages/german) - [Portuguese →](https://txtfetch.com/languages/portuguese) - [Chinese →](https://txtfetch.com/languages/chinese) - [Korean →](https://txtfetch.com/languages/korean) - [Arabic →](https://txtfetch.com/languages/arabic) - [Russian →](https://txtfetch.com/languages/russian) - [All languages →](https://txtfetch.com/languages) ## Check your Japanese scan first. The free scan check tells you if a page needs OCR. It runs in your browser. [Check a scan →](https://txtfetch.com/tools/image-ocr-check) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/languages/korean # Extract text from Korean documents, Hangul intact. A born-digital Korean file needs no OCR. The real risks are a legacy encoding and a jamo composition mismatch, not the script itself. 한국어 · Hangul, with occasional Hanja what-already-works A born-digital Korean file, a Word document or a modern PDF with a real text layer, needs no OCR. Tika reads its Unicode text layer directly. Occasional Hanja, Chinese-derived characters used in formal or legal Korean writing, are read the same way as any other Unicode text. character-encodings | encoding | seen in | note | | --- | --- | --- | | `EUC-KR` | Older Korean Windows and Unix systems, and many legacy exports. | Covers the most common few thousand Hangul syllable blocks, not the full modern set. | | `CP949 (Unified Hangul Code)` | Windows software, as a Microsoft-specific superset of EUC-KR. | Extends EUC-KR's coverage but keeps a similar two-byte layout, so the two still aren't interchangeable with UTF-8. | common-pitfalls **Hangul turns into an unrelated string of symbols.**: EUC-KR or CP949 bytes read as UTF-8, or the reverse. This is mojibake from a legacy Korean encoding. Re-read the file with its real encoding. Fix garbled text from the wrong encoding → **The same Korean word, spelled the same way, fails to match itself in a search index.**: One copy stores each syllable as a single precomposed Hangul block. The other stores it as separate jamo, the individual consonant and vowel letters, which render the same way but differ byte for byte. Normalize Hangul jamo sequences to precomposed NFC form before comparing or indexing. What character encoding controls → **Chunk sizes look reasonable for Korean, but real token counts run higher than the character-based estimate predicts.**: Korean spaces separate phrases, not always single dictionary words, so a word-count chunker undercounts real segment density, and each Hangul block carries more information than one Latin letter. Treat the chunk previewer's character-based estimate as a floor for Korean text, not an exact count. What chunking actually does → chunking-and-tokens Korean text does use spaces, unlike Japanese or Chinese, so a word-count chunker isn't completely blind to it. But those spaces mark phrases, not always single words, and each Hangul block packs more meaning than a Latin letter. The chunk previewer's ~4-characters-per-token estimate still runs low for Korean, just less severely than it does for Japanese or Chinese. ocr-status A scanned or photographed Korean document is a different problem than a born-digital one. The standard OCR tier ships English trained data plus script detection only (TESSDATA\_LANGS=eng osd). It can tell a page isn't Latin script, but it can't read Hangul, since that needs Korean-trained OCR data the standard tier doesn't ship. For a scanned Korean document, request quality=premium, which routes a vision-language model instead of Tika. No accuracy figure is published for Korean OCR under either tier. If scanned Korean is a large part of your workload, contact us before you commit to a plan. faq **Does txtfetch detect or label the document's language?**: No. txtfetch extracts the text as written and hands it back as-is. It does not detect, tag, or translate a document's language. There is no language parameter to set. **Can txtfetch OCR a scanned Korean document?**: Not reliably with the standard tier, which ships English trained data plus script detection only (TESSDATA_LANGS=eng osd). Request quality=premium for the documented route to reading a Korean scan. **Does Korean text need special chunking, since it has spaces?**: Korean spaces mark phrases, not always single words, and each Hangul block carries more meaning than one Latin letter. The character-based token estimate still runs a bit low for Korean. **Why does a Korean word fail to match itself in a search index?**: One copy likely stores Hangul as precomposed syllable blocks, the other as separate jamo letters. Normalize both to NFC before comparing. related-reading - [What comes back: text, Markdown, or JSON →](https://txtfetch.com/output) - [Chunk previewer →](https://txtfetch.com/tools/chunk-preview) - [Glossary: character encoding →](https://txtfetch.com/glossary/character-encoding) - [Contact →](https://txtfetch.com/contact) - [Get an API key →](https://app.txtfetch.com/signup) other-languages - [Spanish →](https://txtfetch.com/languages/spanish) - [French →](https://txtfetch.com/languages/french) - [German →](https://txtfetch.com/languages/german) - [Portuguese →](https://txtfetch.com/languages/portuguese) - [Japanese →](https://txtfetch.com/languages/japanese) - [Chinese →](https://txtfetch.com/languages/chinese) - [Arabic →](https://txtfetch.com/languages/arabic) - [Russian →](https://txtfetch.com/languages/russian) - [All languages →](https://txtfetch.com/languages) ## Check your Korean scan first. The free scan check tells you if a page needs OCR. It runs in your browser. [Check a scan →](https://txtfetch.com/tools/image-ocr-check) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/languages/portuguese # Extract text from Portuguese documents, either variety. Brazilian and European Portuguese are the same language stored as the same Unicode text. The real risks are a legacy encoding and a normalization mismatch on ã, õ, and ç. Português · Latin, with diacritics what-already-works Most Portuguese documents are born digital, whether written in Brazil or Portugal. Tika reads the Unicode text layer of a Word file, PDF, or web page directly. ã, õ, ç, á, and â are each a single fixed code point, kept exactly as written. character-encodings | encoding | seen in | note | | --- | --- | --- | | `Windows-1252 (CP1252)` | Older Word and Excel files, and email exported from legacy Windows systems. | Fits Portuguese's accented letters and ç into a single byte each. Read as UTF-8 by mistake, each one garbles into two characters. | | `ISO-8859-1 (Latin-1)` | Older web pages and plain-text files that predate UTF-8. | Close to Windows-1252 but not identical in byte layout, so a wrong guess still corrupts the text. | common-pitfalls **ã, õ, ç, or â turns into a garbled two-character sequence.**: The file's real bytes were Windows-1252 or Latin-1, and something read them as UTF-8. This is mojibake. Read the file with its real encoding instead of a guessed one. Fix garbled text from the wrong encoding → **A search for 'não' or 'informação' misses text that visibly contains it.**: One copy stores each accented letter as a single precomposed character (NFC). The other stores it as a base letter plus a separate combining mark (NFD). The strings differ byte for byte. Normalize both the extracted text and the search query to NFC before comparing them. What character encoding controls → **Brazilian spelling ('fato') and European spelling ('facto') for the same word don't match in a keyword search.**: The two are not spelling errors. They are the same language written under two different national orthographies. Search both spellings, or normalize known variant pairs yourself. txtfetch extracts text as written and does not localize spelling. chunking-and-tokens Portuguese sentences use spaces between words, so a word-count chunker splits them in the right places. Diacritics don't change that count, whether written in Brazil or Portugal. The chunk previewer's ~4-characters-per-token estimate holds up well for Portuguese. ocr-status A scanned Portuguese document still needs OCR, and Tesseract still reads the Latin alphabet underneath. But the standard OCR tier ships English trained data plus script detection only (TESSDATA\_LANGS=eng osd), tuned for English letter shapes. Accented letters usually read fine on a clean scan, but misreads climb on low resolution or stylized fonts. For a scan where accuracy matters, request quality=premium, which routes a vision-language model instead of Tika. No accuracy figure is published for Portuguese OCR under either tier. faq **Does txtfetch detect or label the document's language?**: No. txtfetch extracts the text as written and hands it back as-is. It does not detect, tag, or translate a document's language. There is no language parameter to set. **Does txtfetch tell Brazilian and European Portuguese apart?**: No. txtfetch extracts the text exactly as written, in either spelling variant, and doesn't tag which one it is. **Can txtfetch OCR a scanned Portuguese document?**: Yes, but the standard OCR tier ships English trained data only (TESSDATA_LANGS=eng osd). For a scan where accuracy matters, request quality=premium. **Why doesn't a search for 'informação' match text that clearly shows the word?**: NFC and NFD store the same accented letter as different byte sequences. Normalize the extracted text and your search query to NFC first. related-reading - [What comes back: text, Markdown, or JSON →](https://txtfetch.com/output) - [Chunk previewer →](https://txtfetch.com/tools/chunk-preview) - [Glossary: character encoding →](https://txtfetch.com/glossary/character-encoding) - [Async jobs & webhooks →](https://txtfetch.com/docs/async) - [Get an API key →](https://app.txtfetch.com/signup) other-languages - [Spanish →](https://txtfetch.com/languages/spanish) - [French →](https://txtfetch.com/languages/french) - [German →](https://txtfetch.com/languages/german) - [Japanese →](https://txtfetch.com/languages/japanese) - [Chinese →](https://txtfetch.com/languages/chinese) - [Korean →](https://txtfetch.com/languages/korean) - [Arabic →](https://txtfetch.com/languages/arabic) - [Russian →](https://txtfetch.com/languages/russian) - [All languages →](https://txtfetch.com/languages) ## Check your Portuguese scan first. The free scan check tells you if a page needs OCR. It runs in your browser. [Check a scan →](https://txtfetch.com/tools/image-ocr-check) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/languages/russian # Extract text from Russian documents, Cyrillic intact. A born-digital Russian file needs no OCR. The real risks are a legacy encoding and a look-alike Latin character, not the script itself. Русский · Cyrillic what-already-works A born-digital Russian file, a Word document or a modern PDF with a real text layer, needs no OCR. Tika reads its Unicode text layer directly. Every Cyrillic letter is its own fixed code point, kept exactly as written. character-encodings | encoding | seen in | note | | --- | --- | --- | | `KOI8-R` | Older Unix and Linux systems, and Russian email from the 1990s and 2000s. | An eight-bit Cyrillic encoding designed to degrade to readable transliteration over ASCII-only links. Read as UTF-8, it garbles completely. | | `Windows-1251 (CP1251)` | Older Word and Excel files, and Windows-authored email and text exports. | The default Cyrillic encoding on Windows for years. A different byte layout from KOI8-R, so a wrong guess garbles either way. | common-pitfalls **Cyrillic text turns into an unrelated run of symbols.**: KOI8-R or Windows-1251 bytes read as UTF-8, or the two Cyrillic encodings confused for each other. This is mojibake from a legacy Cyrillic encoding. Re-read the file with its real encoding. Fix garbled text from the wrong encoding → **A search for a Russian word returns a false match on a visually identical Latin word, or misses a real Cyrillic match.**: Several Cyrillic letters (Р, С, Х, А) look identical to Latin letters (P, C, X, A) but are different Unicode code points. These are called homoglyphs. Don't assume a visual match means a character match. Compare the actual Unicode code points, not just the rendered shape. **A capitalized Russian word doesn't match its lowercase form in a case-insensitive search.**: Some search and comparison tools default to ASCII-only case folding, and never learned Cyrillic's upper- and lower-case pairs. Use a Unicode-aware case-folding function, not an ASCII-only one, before comparing Russian text. chunking-and-tokens Russian words are separated by spaces, so word-count chunking works about as well for Russian as it does for English. Cyrillic letters are each one Unicode code point. They don't inflate the character count the way an Arabic presentation form or a Korean jamo sequence can. The chunk previewer's ~4-characters-per-token estimate is a fair guide for Russian text. ocr-status A scanned Russian document still needs OCR. Tesseract can read Cyrillic shapes structurally, but the standard OCR tier ships English trained data plus script detection only (TESSDATA\_LANGS=eng osd). Script detection can tell a page isn't Latin, without accurately reading Cyrillic letters. For a scan where accuracy matters, request quality=premium, which routes a vision-language model instead of Tika. No accuracy figure is published for Russian OCR under either tier. faq **Does txtfetch detect or label the document's language?**: No. txtfetch extracts the text as written and hands it back as-is. It does not detect, tag, or translate a document's language. There is no language parameter to set. **Can txtfetch OCR a scanned Russian document?**: The standard tier ships English trained data plus script detection only (TESSDATA_LANGS=eng osd). It can detect the page isn't Latin script, without reading Cyrillic accurately. Request quality=premium for a hard scan. **Why does my Russian text show unrelated symbols instead of Cyrillic?**: The source file likely used KOI8-R or Windows-1251, and something read it as UTF-8. Re-reading it with the correct encoding fixes the mojibake. **Why does a search match the wrong word when a Cyrillic and a Latin letter look the same?**: Р, С, Х, and А look identical to Latin P, C, X, and A but are different Unicode code points. Compare the actual code points, not just the rendered shape. related-reading - [What comes back: text, Markdown, or JSON →](https://txtfetch.com/output) - [Glossary: mojibake →](https://txtfetch.com/glossary/mojibake) - [Chunk previewer →](https://txtfetch.com/tools/chunk-preview) - [Contact →](https://txtfetch.com/contact) - [Get an API key →](https://app.txtfetch.com/signup) other-languages - [Spanish →](https://txtfetch.com/languages/spanish) - [French →](https://txtfetch.com/languages/french) - [German →](https://txtfetch.com/languages/german) - [Portuguese →](https://txtfetch.com/languages/portuguese) - [Japanese →](https://txtfetch.com/languages/japanese) - [Chinese →](https://txtfetch.com/languages/chinese) - [Korean →](https://txtfetch.com/languages/korean) - [Arabic →](https://txtfetch.com/languages/arabic) - [All languages →](https://txtfetch.com/languages) ## Check your Russian scan first. The free scan check tells you if a page needs OCR. It runs in your browser. [Check a scan →](https://txtfetch.com/tools/image-ocr-check) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/languages/spanish # Extract text from Spanish documents, accents intact. A Spanish PDF, contract, or web page is usually plain Unicode text already. The real risk is a legacy encoding or a normalization mismatch, not the accents themselves. Español · Latin, with diacritics what-already-works Most Spanish documents are born digital. A Word contract, a government PDF, or an HTML page store their words as Unicode text, not as pictures. Tika reads that text layer directly. It keeps á, é, í, ó, ú, ñ, ¿, and ¡ exactly as written, because each one has a single fixed Unicode code point. character-encodings | encoding | seen in | note | | --- | --- | --- | | `Windows-1252 (CP1252)` | Older Word and Excel files, and email exported from legacy Windows systems. | Fits Spanish's accented letters and ñ into a single byte each. Read as UTF-8 by mistake, each one turns into a two-character garble. | | `ISO-8859-1 (Latin-1)` | Older web pages and plain-text files that predate UTF-8. | Close to Windows-1252, but not identical. A handful of punctuation marks sit in different byte positions, so a wrong guess still corrupts the text. | common-pitfalls **Accented letters turn into pairs of odd characters, like é in place of é.**: The file's real bytes were Windows-1252 or Latin-1, and something read them as UTF-8 instead. This is mojibake. The fix is to read the file with its real encoding, not a guessed one. Fix garbled text from the wrong encoding → **A search for 'año' finds nothing, even though the page clearly shows the word.**: One copy of the text stores ñ as a single precomposed character (NFC). Another stores it as n plus a separate combining tilde (NFD). The two strings differ byte for byte. Normalize both the extracted text and the search query to NFC before comparing them. What character encoding controls → **¿ or ¡ goes missing from the start of a question or an exclamation.**: Some fonts subset their character set to save space and drop the inverted marks entirely. This is a font or PDF production issue, not something extraction can add back. Check the source document. Fix ligatures and smart punctuation → chunking-and-tokens Spanish sentences use spaces between words, the same as English. A word-count chunker splits them in the right places. Diacritics don't change that count. The chunk previewer's ~4-characters-per-token estimate holds up well for Spanish, better than it does for a script with no spaces at all. ocr-status A scanned Spanish document still needs OCR, and Tesseract still reads the Latin alphabet underneath. But the standard OCR tier ships English trained data plus script detection only (TESSDATA\_LANGS=eng osd), tuned for English letter shapes. Plain accented letters usually read fine, since each is one Unicode code point, but misreads climb on stylized fonts or low-resolution scans. For a scan where accuracy matters, request quality=premium, which routes a vision-language model instead of Tika. txtfetch publishes no accuracy figure for Spanish OCR under either tier. faq **Does txtfetch detect or label the document's language?**: No. txtfetch extracts the text as written and hands it back as-is. It does not detect, tag, or translate a document's language. There is no language parameter to set. **Can txtfetch OCR a scanned Spanish document?**: Yes, but the standard OCR tier ships English trained data only (TESSDATA_LANGS=eng osd). It handles plain accented Latin letters reasonably. For a scan where accuracy matters, request quality=premium, the documented route to a vision-language model instead of Tika. **Why does my extracted Spanish text show é instead of é?**: The source file used Windows-1252 or Latin-1 bytes, and something downstream assumed UTF-8. Re-read the file with its correct encoding to fix it. **Why doesn't a search for an accented word match text that visibly contains it?**: NFC and NFD store the same accented letter as different byte sequences. Normalize both the extracted text and your search query to NFC first. related-reading - [What comes back: text, Markdown, or JSON →](https://txtfetch.com/output) - [Chunk previewer →](https://txtfetch.com/tools/chunk-preview) - [Glossary: token →](https://txtfetch.com/glossary/token) - [Async jobs & webhooks →](https://txtfetch.com/docs/async) - [Get an API key →](https://app.txtfetch.com/signup) other-languages - [French →](https://txtfetch.com/languages/french) - [German →](https://txtfetch.com/languages/german) - [Portuguese →](https://txtfetch.com/languages/portuguese) - [Japanese →](https://txtfetch.com/languages/japanese) - [Chinese →](https://txtfetch.com/languages/chinese) - [Korean →](https://txtfetch.com/languages/korean) - [Arabic →](https://txtfetch.com/languages/arabic) - [Russian →](https://txtfetch.com/languages/russian) - [All languages →](https://txtfetch.com/languages) ## Check your Spanish scan first. The free scan check tells you if a page needs OCR. It runs in your browser. [Check a scan →](https://txtfetch.com/tools/image-ocr-check) [Get an API key →](https://app.txtfetch.com/signup) --- # https://txtfetch.com/launch # txtfetch is live. Delete your parser zoo. One HTTP endpoint turns any document into clean plain text: PDF, DOCX, PPTX, XLSX, HTML, email, scans. Built for the pipelines that feed LLMs, search indexes, and anything else that reads text for a living. the wedge ## Four reasons to rip out your parser stack. breadth ### 1,000+ formats, one call. PDF, DOCX, PPTX, XLSX, HTML, EML, scanned images. Apache Tika parses all of them. Stop maintaining a parser per format. one endpoint ### URL in or file in, text out. One HTTP endpoint. Pass ?url= and we fetch it server-side, or POST the bytes. No SDK to install, no client library to pin. honest price ### Priced per document. Not per page, not per megabyte. A 300-page PDF and a one-page receipt both count as one document, at a price you can see on /pricing. free tier ### Durable, not a trial. 500 documents a month, free, for as long as you're building. This tier is meant to be lived in, not raced through in 14 days. see it work ## Two calls. That's the whole API. \# fetch a URL server-side ``` $ curl "https://api.txtfetch.com/v1/extract?url=https://example.com/report.pdf" { "status": "success", "extracted_text": "Revenue for the fiscal year grew 34%…" } ``` \# upload bytes directly ``` $ curl -F file=@scanned-invoice.png \ https://api.txtfetch.com/v1/extract { "status": "success", "extracted_text": "INVOICE #2041 — Acme Industrial Supply…" } ``` Same endpoint either way. There's no client library and no format switch statement on your side. Tika detects the real content type. Tesseract OCR kicks in automatically when a page has no text layer. what txtfetch doesn't do yet ## The honest limits. We'd rather you learn these from us than from a bad extraction in production. Nothing below is a secret. It's the current edge of what's shipped. - Tables reconstruct into structured rows and cells: ?format=markdown returns GFM pipe tables, and ?format=json returns table elements with cells. Neither is flattened text anymore. - OCR accuracy tracks scan quality. Clean 300 DPI scans do well; noisy faxes, skewed photos, and low-DPI images will lose characters. Always spot-check OCR output for anything you didn't scan yourself. - No SLA today. txtfetch is early access, and we're honest about uptime. We're not yet claiming a number we'd have to defend. - Structured markdown and element-JSON document output ship today (?format=markdown / ?format=json). Schema-defined field extraction is not yet offered. That means pulling typed fields per your own schema, not just structured document output. faq ## Questions we'd ask too. **Does txtfetch reconstruct tables into structured rows and columns?**: Yes. With ?format=markdown or ?format=json, tables come back as structured rows and cells (GFM pipe tables, or element JSON with a cells array), not flattened text. What we don't yet offer is schema-defined field extraction: pulling typed fields per your own schema. **How accurate is OCR on scanned documents?**: It depends on scan quality. Clean, high-DPI scans extract well with Tesseract OCR. Noisy faxes, skewed photos, and low-DPI images will lose characters. Treat OCR output on rough scans as a draft, not ground truth. **Why not just run Apache Tika myself?**: You can. Tika is open source. txtfetch is for when you'd rather not run and patch a JVM and Tesseract stack yourself. It's hosted, it scales to zero, and it ships OCR out of the box. The free tier covers most side projects outright. **Why price per document instead of per page?**: Per-page pricing punishes exactly the documents people most want extracted, like long reports, contracts, and books. Per-document pricing is one predictable unit: one URL or one upload, one price, regardless of length. **Does txtfetch store the documents I send it?**: No. Documents are processed in memory for the duration of the request and discarded once the response is returned. We keep request metadata for metering, never the content. ## 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/migrate # Same document in. Here's what changes in your code. /compare told you why. This page shows the switch itself: the call you run today, the call that replaces it, and an adapter that keeps your pipeline untouched. ## What each spoke shows - Where you are today: the vendor call you run now, cited against their current docs. - The call that replaces it: one txtfetch request, in curl, Python, and JavaScript. - A drop-in adapter: a short function shaped like the vendor field your downstream code already reads. - A field-mapping table: their response field, the txtfetch equivalent, and a note where there isn't one. - What you lose, and what you gain, both stated plainly. - A cutover step: dual-run both calls and diff the output before you cut traffic over. pick a vendor **[Migrate from Unstructured.io](https://txtfetch.com/migrate/unstructured)** Its own Partition endpoint is now legacy. That's a migration moment either way. **[Migrate from LlamaParse](https://txtfetch.com/migrate/llamaparse)** Credit-metered tiers and a job to poll, for parsing you may not need. **[Migrate from AWS Textract](https://txtfetch.com/migrate/aws-textract)** A flat Blocks array, joined by hand. That join is the whole adapter. **[Migrate from Azure AI Document Intelligence](https://txtfetch.com/migrate/azure-document-intelligence)** The prebuilt-read model, minus the SDK, the poller, and the Azure resource. **[Migrate from Mindee](https://txtfetch.com/migrate/mindee)** Typed fields, not text. Read this one before you switch anything. partial migration ## Not every migration is a full swap Mindee returns typed fields, like an invoice total or a supplier name. txtfetch returns plain text. The Mindee page below says so directly, and does not pretend the two are equal. frequently asked questions **How is /migrate different from /compare?**: /compare answers why to switch: pricing and capability tables. /migrate answers what your code looks like afterwards: the call you run today, the call that replaces it, and a drop-in adapter. **Does every vendor here have a full migration path?**: No. Mindee returns typed fields txtfetch doesn't return. That page says so plainly and does not claim parity. **Are the vendor calls kept current?**: Each spoke cites the vendor's own docs with an accessed date, and a client version the call was checked against. Re-check before you copy a snippet past that date. **Do I have to give something up to switch?**: Usually, yes. Every spoke lists what you lose alongside what you gain, and links the full capability table on the matching /compare page. ## Swap the call. Keep the pipeline. Point your existing code at txtfetch, then diff the output before you cut over. [Get an API key →](https://app.txtfetch.com/signup) [See the migration guide →](https://txtfetch.com/migrate) --- # https://txtfetch.com/migrate/aws-textract # Migrate from AWS Textract to txtfetch A flat Blocks array, joined by hand. That join is the whole adapter. Here's what your code looks like before and after. ## Where you are today The call below is DetectDocumentText, checked against AWS Textract's current docs. Python ```python import boto3 client = boto3.client("textract") with open("report.pdf", "rb") as f: response = client.detect_document_text(Document={"Bytes": f.read()}) lines = [b["Text"] for b in response["Blocks"] if b["BlockType"] == "LINE"] text = "\n".join(lines) ``` Checked against [AWS Textract's docs](https://docs.aws.amazon.com/textract/latest/dg/API_DetectDocumentText.html) on 2026-09-10, using boto3 1.43.x. the call that replaces it One HTTP call. No SDK to install, and no job to poll. curl ```curl curl -X POST https://api.txtfetch.com/v1/extract \ -H "Authorization: Bearer $TXTFETCH_KEY" \ -F file=@report.pdf ``` Python ```python import os import requests with open("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("report.pdf")]); const form = new FormData(); form.append("file", file, "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); ``` drop-in adapter A short function standing in for `[b["Text"] for b in response["Blocks"] if b["BlockType"] == "LINE"]`. Change one function, not your whole pipeline. Python ```python import os import requests def detect_text_like_blocks(file_path: str) -> list[dict]: """Drop-in swap for the LINE blocks in response["Blocks"]. Downstream code that reads b["Text"] per LINE block keeps working unmodified. txtfetch returns one block for the whole document, not one per line. """ with open(file_path, "rb") as f: r = requests.post( "https://api.txtfetch.com/v1/extract", headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"}, files={"file": f}, ) r.raise_for_status() text = r.json()["extracted_text"] return [{"BlockType": "LINE", "Text": line} for line in text.splitlines()] ``` JavaScript ```javascript import { readFile } from "node:fs/promises"; // Drop-in swap for the LINE blocks in response.Blocks. Downstream code // that reads b.Text per LINE block keeps working unmodified. async function detectTextLikeBlocks(filePath) { const file = new Blob([await readFile(filePath)]); const form = new FormData(); form.append("file", file, filePath); 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(); return extracted_text.split("\n").map((line) => ({ BlockType: "LINE", Text: line })); } ``` field mapping | AWS Textract field | txtfetch equivalent | Note | | --- | --- | --- | | Blocks\[\] where BlockType == "LINE" | extracted\_text, split on newline | Textract returns lines in reading order per page. Join them, or split txtfetch's text the same way. | | Block.Confidence (per word/line) | not returned | The plain-text response carries no per-line or per-word confidence score. | | Block.Geometry (BoundingBox, Polygon) | not returned | No coordinates come back. See what you lose below. | | AnalyzeDocument FORMS (key-value pairs) | not returned | txtfetch has no forms-analysis mode. Keep AnalyzeDocument for key-value extraction. | what you lose - Per-word and per-line Confidence scores. - Geometry: BoundingBox and Polygon coordinates for every block. - AnalyzeDocument's FORMS mode (key-value pairs) and TABLES mode (structured cells). - SIGNATURES detection. - Deep AWS IAM/VPC integration if you're already running there. See the full capability table on [txtfetch vs AWS Textract](https://txtfetch.com/compare/aws-textract). what you gain - Hundreds of formats through one endpoint (see /formats/coverage), not PDF, PNG, JPEG, and TIFF only. - One HTTP call instead of a boto3 client and a Blocks array to walk. - Billing per document, not per 1,000 pages. See the pricing math. The pricing math behind per-document billing lives on [why per-page pricing punishes long documents](https://txtfetch.com/compare/per-page-pricing). ## Cutover Run both calls on the same file while you switch traffic over. Diff the two outputs, or clean up formatting differences first on pages where a table mattered. - [Compare outputs side by side](https://txtfetch.com/diff), to dual-run both calls on the same file. - [Clean up extracted text](https://txtfetch.com/tools/clean-extracted-text), if the two outputs disagree on formatting. sources - [AWS: DetectDocumentText API reference](https://docs.aws.amazon.com/textract/latest/dg/API_DetectDocumentText.html) Accessed 2026-09-10 - [boto3: Textract client reference (detect\_document\_text)](https://docs.aws.amazon.com/boto3/latest/reference/services/textract/client/detect_document_text.html) Accessed 2026-09-10 - [AWS: How Amazon Textract Works: Lines and Words](https://docs.aws.amazon.com/textract/latest/dg/how-it-works-lines-words.html) Accessed 2026-09-10 - [AWS Textract: Pricing](https://aws.amazon.com/textract/pricing/) Accessed 2026-09-10 frequently asked questions **Does txtfetch return Textract's Blocks array?**: No. txtfetch returns one plain-text string. If your code reads response["Blocks"], use the adapter above or rewrite it to read one string. **Can I keep Textract for forms and tables, and use txtfetch for plain text?**: Yes. Some teams run AnalyzeDocument only on the documents that need key-value pairs or structured tables, and send everything else to txtfetch. **Do Textract's LINE blocks already come back in reading order?**: Yes, per AWS's own docs, for a single-column page. A multi-column layout can still need reordering on either side. other migrations - [Migrate from Unstructured.io →](https://txtfetch.com/migrate/unstructured) - [Migrate from LlamaParse →](https://txtfetch.com/migrate/llamaparse) - [Migrate from Azure AI Document Intelligence →](https://txtfetch.com/migrate/azure-document-intelligence) - [Migrate from Mindee →](https://txtfetch.com/migrate/mindee) - [All migrations →](https://txtfetch.com/migrate) ## Swap the call. Keep the pipeline. Point your AWS Textract code at txtfetch, then diff the output before you cut over. [Get an API key →](https://app.txtfetch.com/signup) [See the migration guide →](https://txtfetch.com/migrate) --- # https://txtfetch.com/migrate/azure-document-intelligence # Migrate from Azure AI Document Intelligence to txtfetch The prebuilt-read model, minus the SDK, the poller, and the Azure resource. Here's what your code looks like before and after. ## Where you are today The call below is prebuilt-read, checked against Azure AI Document Intelligence's current docs. Python ```python import os from azure.ai.documentintelligence import DocumentIntelligenceClient from azure.core.credentials import AzureKeyCredential client = DocumentIntelligenceClient( endpoint=os.environ["DOCUMENTINTELLIGENCE_ENDPOINT"], credential=AzureKeyCredential(os.environ["DOCUMENTINTELLIGENCE_API_KEY"]), ) with open("report.pdf", "rb") as f: poller = client.begin_analyze_document("prebuilt-read", body=f) result = poller.result() text = result.content ``` Checked against [Azure AI Document Intelligence's docs](https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/quickstarts/get-started-sdks-rest-api) on 2026-09-10, using azure-ai-documentintelligence 1.0.2. the call that replaces it One HTTP call. No SDK to install, and no job to poll. curl ```curl curl -X POST https://api.txtfetch.com/v1/extract \ -H "Authorization: Bearer $TXTFETCH_KEY" \ -F file=@report.pdf ``` Python ```python import os import requests with open("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("report.pdf")]); const form = new FormData(); form.append("file", file, "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); ``` drop-in adapter A short function standing in for `result.content`. Change one function, not your whole pipeline. Python ```python import os import requests def analyze_like_result(file_path: str) -> str: """Drop-in swap for poller.result().content. Downstream code that reads result.content as one string keeps working unmodified. Code that reads result.pages or result.paragraphs needs a rewrite. txtfetch returns no per-page or per-paragraph split. """ with open(file_path, "rb") as f: r = requests.post( "https://api.txtfetch.com/v1/extract", headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"}, files={"file": f}, ) r.raise_for_status() return r.json()["extracted_text"] ``` JavaScript ```javascript import { readFile } from "node:fs/promises"; // Drop-in swap for poller.result().content. Downstream code that reads // result.content as one string keeps working unmodified. async function analyzeLikeResult(filePath) { const file = new Blob([await readFile(filePath)]); const form = new FormData(); form.append("file", file, filePath); 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(); return extracted_text; } ``` field mapping | Azure AI Document Intelligence field | txtfetch equivalent | Note | | --- | --- | --- | | result.content | extracted\_text | Both are one plain-text string for the whole document. This is the closest field pair in this whole cluster. | | result.pages\[\].lines\[\].content | not split out | txtfetch's response carries no per-page or per-line breakdown. | | result.words\[\].confidence | not returned | No per-word confidence score comes back. | | prebuilt-invoice / prebuilt-receipt typed fields | not returned | txtfetch has no prebuilt document-type models. Keep Document Intelligence for typed field extraction. | what you lose - Prebuilt models for invoices, receipts, IDs, and W-2s that return named fields. - Bounding regions (polygons) per line, word, and paragraph. - Per-word confidence scores. - prebuilt-layout's table extraction with row and column indices. See the full capability table on [txtfetch vs Azure AI Document Intelligence](https://txtfetch.com/compare/azure-document-intelligence). what you gain - Hundreds of formats through one endpoint (see /formats/coverage), not PDF, images, and a limited set of Office formats. - One HTTP call. No Azure resource, no SDK, no poller to await. - Billing per document, not per 1,000 pages. See the pricing math. The pricing math behind per-document billing lives on [why per-page pricing punishes long documents](https://txtfetch.com/compare/per-page-pricing). ## Cutover Run both calls on the same file while you switch traffic over. Diff the two outputs, since result.content and extracted\_text usually match closely for plain text extraction. - [Compare outputs side by side](https://txtfetch.com/diff), to dual-run both calls on the same file. - [Clean up extracted text](https://txtfetch.com/tools/clean-extracted-text), if the two outputs disagree on formatting. sources - [Microsoft Learn: Document Intelligence SDK quickstart (Python)](https://learn.microsoft.com/en-us/azure/ai-services/document-intelligence/quickstarts/get-started-sdks-rest-api) Accessed 2026-09-10 - [PyPI: azure-ai-documentintelligence](https://pypi.org/project/azure-ai-documentintelligence/) Accessed 2026-09-10 - [Azure AI Document Intelligence: Pricing](https://azure.microsoft.com/en-us/pricing/details/document-intelligence/) Accessed 2026-09-10 directional frequently asked questions **Is result.content close enough to txtfetch's extracted_text to swap directly?**: For plain text, usually yes. Both return one string for the whole document. Run the diff tool on a real file before you cut over. **Does txtfetch support Document Intelligence's prebuilt invoice or receipt models?**: No. Those return typed fields. txtfetch returns plain text only. Keep Document Intelligence for that part of your pipeline. **Do I need result.pages or result.paragraphs, or just the full text?**: If your code only reads result.content, this is a direct swap. If it reads per-page or per-paragraph structure, that structure has no txtfetch equivalent. other migrations - [Migrate from Unstructured.io →](https://txtfetch.com/migrate/unstructured) - [Migrate from LlamaParse →](https://txtfetch.com/migrate/llamaparse) - [Migrate from AWS Textract →](https://txtfetch.com/migrate/aws-textract) - [Migrate from Mindee →](https://txtfetch.com/migrate/mindee) - [All migrations →](https://txtfetch.com/migrate) ## Swap the call. Keep the pipeline. Point your Azure AI Document Intelligence code at txtfetch, then diff the output before you cut over. [Get an API key →](https://app.txtfetch.com/signup) [See the migration guide →](https://txtfetch.com/migrate) --- # https://txtfetch.com/migrate/llamaparse # Migrate from LlamaParse to txtfetch Credit-metered tiers and a job to poll, for parsing you may not need. Here's what your code looks like before and after. ## Where you are today The call below is the current llama-cloud Parsing API, checked against LlamaParse's current docs. Python ```python import os from llama_cloud import LlamaCloud client = LlamaCloud(api_key=os.environ["LLAMA_CLOUD_API_KEY"]) file = client.files.create(file="report.pdf", purpose="parse") result = client.parsing.parse( file_id=file.id, tier="agentic", version="latest", expand=["markdown"], ) text = result.markdown.pages[0].markdown ``` Checked against [LlamaParse's docs](https://developers.llamaindex.ai/python/cloud/llamaparse/getting_started) on 2026-09-10, using llama-cloud 2.16.0. the call that replaces it One HTTP call. No SDK to install, and no job to poll. curl ```curl curl -X POST https://api.txtfetch.com/v1/extract \ -H "Authorization: Bearer $TXTFETCH_KEY" \ -F file=@report.pdf ``` Python ```python import os import requests with open("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("report.pdf")]); const form = new FormData(); form.append("file", file, "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); ``` drop-in adapter A short function standing in for `result.markdown.pages[n].markdown`. Change one function, not your whole pipeline. Python ```python import os import requests def parse_like_pages(file_path: str) -> list[str]: """Drop-in swap for result.markdown.pages[n].markdown. txtfetch has no per-page split, so this returns a one-item list. Downstream code that joins all pages into one string keeps working unmodified; code that indexes a specific page needs a rewrite. """ with open(file_path, "rb") as f: r = requests.post( "https://api.txtfetch.com/v1/extract", headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"}, files={"file": f}, ) r.raise_for_status() return [r.json()["extracted_text"]] ``` JavaScript ```javascript import { readFile } from "node:fs/promises"; // Drop-in swap for result.markdown.pages[n].markdown. txtfetch has no // per-page split, so this returns a one-item array. async function parseLikePages(filePath) { const file = new Blob([await readFile(filePath)]); const form = new FormData(); form.append("file", file, filePath); 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(); return [extracted_text]; } ``` field mapping | LlamaParse field | txtfetch equivalent | Note | | --- | --- | --- | | result.markdown.pages\[n\].markdown | extracted\_text (?format=markdown) | Ask for markdown output to keep heading and list structure. LlamaParse splits by page; txtfetch does not. | | result.items (per-element JSON) | ?format=json element list | txtfetch's element JSON is coarser than LlamaParse's agentic-tier item breakdown. | | tier (fast / cost\_effective / agentic / agentic\_plus) | not applicable | txtfetch runs one extraction path. There is no fidelity dial to set per request. | | take\_screenshot=True (page images) | not returned | txtfetch returns text only, never a page image. | what you lose - The agentic and agentic-plus tiers' fidelity on multi-column layouts, embedded tables, and math notation. - Per-page screenshots and structured item JSON. - A pinnable parser version for reproducible output across runs. - output\_tables\_as\_HTML and other tier-specific output options. See the full capability table on [txtfetch vs LlamaParse](https://txtfetch.com/compare/llamaparse). what you gain - Hundreds of formats through one endpoint (see /formats/coverage), not a PDF-first parser. - One flat call, no credits to track and no job tier to pick per request. - Billing per document, not per credit. See the pricing math. The pricing math behind per-document billing lives on [why per-page pricing punishes long documents](https://txtfetch.com/compare/per-page-pricing). ## Cutover Run both calls on the same PDF while you switch over. Diff the two outputs, or clean up formatting differences first on the pages where LlamaParse's fidelity mattered most. - [Compare outputs side by side](https://txtfetch.com/diff), to dual-run both calls on the same file. - [Clean up extracted text](https://txtfetch.com/tools/clean-extracted-text), if the two outputs disagree on formatting. sources - [LlamaIndex: LlamaParse getting started (llama-cloud)](https://developers.llamaindex.ai/python/cloud/llamaparse/getting_started) Accessed 2026-09-10 - [PyPI: llama-cloud](https://pypi.org/project/llama-cloud/) Accessed 2026-09-10 - [PyPI: llama-cloud-services (deprecated May 1, 2026)](https://pypi.org/project/llama-cloud-services/) Accessed 2026-09-10 directional frequently asked questions **Do I need the agentic tier's fidelity, or is plain text enough?**: Depends on the document. If your pipeline only ever reads plain text out of LlamaParse's markdown, txtfetch is a straight swap. If it reads structured items or table HTML, keep LlamaParse for those files. **Does txtfetch support LlamaParse's parsing tiers?**: No. txtfetch runs one extraction path per document. There is no fast/agentic/agentic_plus tier to choose. **What happened to the llama-cloud-services package?**: LlamaIndex deprecated it in favor of llama-cloud. The snippet above uses the current package, checked against LlamaIndex's own docs. other migrations - [Migrate from Unstructured.io →](https://txtfetch.com/migrate/unstructured) - [Migrate from AWS Textract →](https://txtfetch.com/migrate/aws-textract) - [Migrate from Azure AI Document Intelligence →](https://txtfetch.com/migrate/azure-document-intelligence) - [Migrate from Mindee →](https://txtfetch.com/migrate/mindee) - [All migrations →](https://txtfetch.com/migrate) ## Swap the call. Keep the pipeline. Point your LlamaParse code at txtfetch, then diff the output before you cut over. [Get an API key →](https://app.txtfetch.com/signup) [See the migration guide →](https://txtfetch.com/migrate) --- # https://txtfetch.com/migrate/mindee # Migrate from Mindee to txtfetch Typed fields, not text. Read this one before you switch anything. Here's what your code looks like before and after. txtfetch does not return typed fields. Its structured-output capability is not yet shipped. Keep Mindee for field extraction, or move to plain text plus your own parser. This page does not claim parity. ## Where you are today The call below is the v1 InvoiceV4 product API, checked against Mindee's current docs. Python ```python import os from mindee import PathInput from mindee.v1 import Client, product client = Client(api_key=os.environ["MINDEE_API_KEY"]) input_doc = PathInput("invoice.pdf") result = client.parse(product.InvoiceV4, input_doc) invoice = result.document ``` Mindee runs two API generations. Mindee does not call V1 deprecated. Its own V1 overview says V1 stays maintained, gets no new features, and has no shut-off date. You cannot open a new V1 account today. New signups use Mindee's V2 model-id extraction API instead. Checked against [Mindee's docs](https://mindee.github.io/mindee-api-python/) on 2026-09-10, using mindee 5.3.0. the call that replaces it One HTTP call. No SDK to install, and no job to poll. 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); ``` drop-in adapter A short function standing in for `result.document..value (no equivalent, see below)`. Change one function, not your whole pipeline. Python ```python import os import requests def extracted_text_only(file_path: str) -> str: """Not a drop-in for result.document. There is no typed-field equivalent here, only the raw text those fields were pulled from. Use this to feed your own parser, not to replace field reads. """ with open(file_path, "rb") as f: r = requests.post( "https://api.txtfetch.com/v1/extract", headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"}, files={"file": f}, ) r.raise_for_status() return r.json()["extracted_text"] ``` JavaScript ```javascript import { readFile } from "node:fs/promises"; // Not a drop-in for result.document. There is no typed-field equivalent // here, only the raw text those fields were pulled from. async function extractedTextOnly(filePath) { const file = new Blob([await readFile(filePath)]); const form = new FormData(); form.append("file", file, filePath); 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(); return extracted_text; } ``` field mapping | Mindee field | txtfetch equivalent | Note | | --- | --- | --- | | invoice.total\_amount.value | not returned | No typed fields come back. You would parse the total out of extracted\_text yourself. | | invoice.supplier\_name.value | not returned | Same gap. This is the field extraction that makes Mindee Mindee. | | invoice.line\_items (list of typed rows) | not returned | No structured line items. The row text is in extracted\_text, unparsed. | | field.confidence (per field) | not returned | No confidence score exists, because no field exists to score. | what you lose - Every typed field: totals, dates, supplier name, line items, and the rest of the invoice schema. - Per-field confidence scores for automating approval thresholds. - Prebuilt models for receipts, passports, IDs, and driver's licenses. - Custom Document APIs for training a field-extraction model on your own document types. See the full capability table on [txtfetch vs Mindee](https://txtfetch.com/compare/mindee). what you gain - Hundreds of formats through one endpoint (see /formats/coverage), not a narrow set of invoice-shaped documents. - One flat call with no per-document-type model to pick. - 500 documents a month on the free Hobby plan, with no card and no trial clock. The pricing math behind per-document billing lives on [why per-page pricing punishes long documents](https://txtfetch.com/compare/per-page-pricing). ## Cutover This is not a cutover. Keep Mindee running for the fields your pipeline reads today. Add txtfetch only where you need the raw text Mindee's fields were extracted from, or where your own parser can read plain text well enough. - [Compare outputs side by side](https://txtfetch.com/diff), to dual-run both calls on the same file. - [Clean up extracted text](https://txtfetch.com/tools/clean-extracted-text), if the two outputs disagree on formatting. sources - [Mindee: Python client reference](https://mindee.github.io/mindee-api-python/) Accessed 2026-09-10 - [Mindee: V1 overview (maintained, no new features, no shut-off date)](https://docs.mindee.com/v1/get-started/readme) Accessed 2026-09-10 - [Mindee: V2 extraction quick start (where new signups go)](https://docs.mindee.com/extraction-models/sdk-integration/quick-start) Accessed 2026-09-10 - [PyPI: mindee](https://pypi.org/project/mindee/) Accessed 2026-09-10 - [Mindee: Pricing](https://www.mindee.com/pricing) Accessed 2026-09-10 frequently asked questions **Can txtfetch replace Mindee outright?**: No, not today. Mindee returns typed fields like total_amount and supplier_name. txtfetch returns plain text only. Keep Mindee if your pipeline reads those fields. **When would I add txtfetch alongside Mindee, not instead of it?**: Add it when you need raw text a Mindee model doesn't cover. That includes a document type with no prebuilt model, or your own parser that only needs clean input text. **Does txtfetch have a roadmap for typed field extraction?**: Structured markdown and element-JSON output already ship. Schema-defined field extraction is on the roadmap, not shipped. This page won't claim it early. other migrations - [Migrate from Unstructured.io →](https://txtfetch.com/migrate/unstructured) - [Migrate from LlamaParse →](https://txtfetch.com/migrate/llamaparse) - [Migrate from AWS Textract →](https://txtfetch.com/migrate/aws-textract) - [Migrate from Azure AI Document Intelligence →](https://txtfetch.com/migrate/azure-document-intelligence) - [All migrations →](https://txtfetch.com/migrate) ## Swap the call. Keep the pipeline. Point your Mindee code at txtfetch, then diff the output before you cut over. [Get an API key →](https://app.txtfetch.com/signup) [See the migration guide →](https://txtfetch.com/migrate) --- # https://txtfetch.com/migrate/unstructured # Migrate from Unstructured.io to txtfetch Its own Partition endpoint is now legacy. That's a migration moment either way. Here's what your code looks like before and after. ## Where you are today The call below is the legacy Partition endpoint, checked against Unstructured.io's current docs. Python ```python import os import unstructured_client from unstructured_client.models import operations, shared client = unstructured_client.UnstructuredClient( api_key_auth=os.environ["UNSTRUCTURED_API_KEY"], ) req = operations.PartitionRequest( partition_parameters=shared.PartitionParameters( files=shared.Files(content=open("report.pdf", "rb"), file_name="report.pdf"), strategy=shared.Strategy.AUTO, ), ) res = client.general.partition(request=req) elements = res.elements ``` Checked against [Unstructured.io's docs](https://docs.unstructured.io/api-reference/legacy-api/partition/sdk-python) on 2026-09-10, using unstructured-client 0.46.2. the call that replaces it One HTTP call. No SDK to install, and no job to poll. curl ```curl curl -X POST https://api.txtfetch.com/v1/extract \ -H "Authorization: Bearer $TXTFETCH_KEY" \ -F file=@report.pdf ``` Python ```python import os import requests with open("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("report.pdf")]); const form = new FormData(); form.append("file", file, "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); ``` drop-in adapter A short function standing in for `client.general.partition(request).elements`. Change one function, not your whole pipeline. Python ```python import os import requests def partition_like_elements(file_path: str) -> list[dict]: """Drop-in swap for client.general.partition(request).elements. Downstream code that reads el["text"] per element keeps working unmodified. txtfetch returns one element for the whole document, not one per paragraph or table. """ with open(file_path, "rb") as f: r = requests.post( "https://api.txtfetch.com/v1/extract", headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"}, files={"file": f}, ) r.raise_for_status() text = r.json()["extracted_text"] return [{"type": "NarrativeText", "text": text, "metadata": {}}] ``` JavaScript ```javascript import { readFile } from "node:fs/promises"; // Drop-in swap for client.general.partition(request).elements. Downstream code // that reads el.text per element keeps working unmodified. async function partitionLikeElements(filePath) { const file = new Blob([await readFile(filePath)]); const form = new FormData(); form.append("file", file, filePath); 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(); return [{ type: "NarrativeText", text: extracted_text, metadata: {} }]; } ``` field mapping | Unstructured.io field | txtfetch equivalent | Note | | --- | --- | --- | | element.text (many per document) | extracted\_text (one string) | Unstructured splits a document into many elements. txtfetch returns one string for the whole document. | | element.type (Title, NarrativeText, Table, Image) | not returned | The plain-text response carries no per-element structural tag. | | element.metadata.page\_number | not returned | No per-element page numbers come back in the plain-text response. | | element.metadata.text\_as\_html (Table elements) | ?format=markdown table rows | Ask for markdown output and read the GFM pipe table instead of the HTML fragment. | what you lose - Typed element types: Title, NarrativeText, Table, Image, ListItem. txtfetch returns one string, not a tagged list. - Per-element page numbers and other layout metadata. - Unstructured's own chunking strategies (by\_title, by\_page, by\_similarity) built into the parse step. - The hi\_res and vlm partitioning strategies tuned for hard scans. - A self-hostable open-source core. See the full capability table on [txtfetch vs Unstructured.io](https://txtfetch.com/compare/unstructured). what you gain - Hundreds of formats through one endpoint (see /formats/coverage), not a curated subset. - One HTTP call. No SDK, no async job, no strategy parameter to tune. - Billing per document, not per page. A 300-page report costs the same as a one-pager. The pricing math behind per-document billing lives on [why per-page pricing punishes long documents](https://txtfetch.com/compare/per-page-pricing). ## Cutover Run both calls on the same file while you switch traffic over. Compare the two outputs side by side, or clean up formatting differences first if the two extractions disagree on whitespace. - [Compare outputs side by side](https://txtfetch.com/diff), to dual-run both calls on the same file. - [Clean up extracted text](https://txtfetch.com/tools/clean-extracted-text), if the two outputs disagree on formatting. sources - [Unstructured: legacy Partition Endpoint, Python SDK reference](https://docs.unstructured.io/api-reference/legacy-api/partition/sdk-python) Accessed 2026-09-10 - [Unstructured: Partitioning via the Platform API (Pipelines)](https://docs.unstructured.io/platform-api/partition-api/partitioning) Accessed 2026-09-10 - [PyPI: unstructured-client](https://pypi.org/project/unstructured-client/) Accessed 2026-09-10 frequently asked questions **Is Unstructured's Partition endpoint really deprecated?**: Unstructured's own docs call it the legacy endpoint and point new work at the Pipeline API instead. The Python call above still works today, but plan the switch either way. **Does txtfetch return typed elements like Unstructured does?**: No. txtfetch returns one plain-text string per document. If your code reads el.type or el.metadata.page_number, that code needs the adapter above or a rewrite. **Can I keep Unstructured for chunking and use txtfetch for extraction?**: Yes. Some teams run both: txtfetch for format breadth and flat pricing, Unstructured (or your own chunker) for the by_title or by_similarity chunking strategy. other migrations - [Migrate from LlamaParse →](https://txtfetch.com/migrate/llamaparse) - [Migrate from AWS Textract →](https://txtfetch.com/migrate/aws-textract) - [Migrate from Azure AI Document Intelligence →](https://txtfetch.com/migrate/azure-document-intelligence) - [Migrate from Mindee →](https://txtfetch.com/migrate/mindee) - [All migrations →](https://txtfetch.com/migrate) ## Swap the call. Keep the pipeline. Point your Unstructured.io code at txtfetch, then diff the output before you cut over. [Get an API key →](https://app.txtfetch.com/signup) [See the migration guide →](https://txtfetch.com/migrate) --- # https://txtfetch.com/output # /formats is what goes in. This is what comes back. One endpoint gives you three response shapes and two quality tiers. The same document renders as plain text, structured markdown, or typed JSON. Pick the shape your pipeline needs. ## one-document-three-shapes The same report, extracted three ways. `format` defaults to `text` when omitted. The default shape doesn't change. format=text (default) ```text { "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=markdown ```markdown { "status": "success", "markdown": "# Quarterly Report\n\nQ3 revenue grew 34% year over year, driven by expansion in EMEA.\n\n| Region | Revenue |\n| --- | --- |\n| North America | $4.2M |\n| EMEA | $3.1M |\n", "metadata": { "content_type": "application/pdf", "bytes": 482913, "chars": 812, "ocr": false, "format": "markdown", "tier": "standard", "pages": 4, "vlm": false } } ``` format=json ```json { "status": "success", "elements": [ { "type": "heading", "text": "Quarterly Report", "level": 1, "page": 1, "offset": 0, "bbox": null }, { "type": "paragraph", "text": "Q3 revenue grew 34% year over year, driven by expansion in EMEA.", "page": 1, "offset": 18, "bbox": null }, { "type": "table", "text": "Region\tRevenue\nNorth America\t$4.2M\nEMEA\t$3.1M", "markdown": "| Region | Revenue |\n| --- | --- |\n| North America | $4.2M |\n| EMEA | $3.1M |", "html": "
RegionRevenue
North America$4.2M
EMEA$3.1M
", "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