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 →
Authenticate
Every request carries your API key in the Authorization header. Create a key in your dashboard.
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 for the full auth-failure semantics.
Extract a file
POST the bytes as multipart form data. Any of the supported formats works. 615 of the 1,683 Tika detects have a real parser, checked against the exact build we run. txtfetch reads the real type from the bytes, not the extension.
curl -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@quarterly-report.pdfimport os
import requests
with open("quarterly-report.pdf", "rb") as f:
r = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
files={"file": f},
)
print(r.json()["extracted_text"])import { readFile } from "node:fs/promises";
const file = new Blob([await readFile("quarterly-report.pdf")]);
const form = new FormData();
form.append("file", file, "quarterly-report.pdf");
const res = await fetch("https://api.txtfetch.com/v1/extract", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` },
body: form,
});
const { extracted_text } = await res.json();
console.log(extracted_text);package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
type extractResponse struct {
Status string `json:"status"`
ExtractedText string `json:"extracted_text"`
}
func main() {
f, err := os.Open("quarterly-report.pdf")
if err != nil {
panic(err)
}
defer f.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", "quarterly-report.pdf")
if err != nil {
panic(err)
}
if _, err := io.Copy(part, f); err != nil {
panic(err)
}
writer.Close()
req, err := http.NewRequest("POST", "https://api.txtfetch.com/v1/extract", &body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY"))
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var result extractResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
panic(err)
}
fmt.Println(result.ExtractedText)
}Extract from a URL
Or skip the download: pass a url parameter and txtfetch fetches the document server-side.
curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/whitepaper.docx" \
-H "Authorization: Bearer $TXTFETCH_KEY"import os
import requests
r = requests.post(
"https://api.txtfetch.com/v1/extract",
headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
params={"url": "https://example.com/whitepaper.docx"},
)
print(r.json()["extracted_text"])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);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": "<table>...</table>",
"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 →
Deciding which of the three to request? See the same document rendered all three ways →
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 →
Conceptual guide to both tiers, plus the async routing and downgrade caveat: what comes back →
Wondering why you'd extract at all instead of sending the file straight to a model? see the documented token cost →
Errors
Failures are explicit and machine-readable. They're almost never a silent empty string. One case we found: a ZIP with an encrypted member returns success with the member names only. See Password-protected files. Failed extractions don't count against your quota. See the complete error reference 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 →
Every one of these limits, with its exact current value: See every number →
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_textAsync 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 →
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 →
Machine-readable spec
The full contract covers every field, error code, and status. It's published as OpenAPI 3.1. Generate a typed client for whatever language you're in.
docs-map
Everything in the docs.
Get started
Quickstarts
Per-framework install + extract: SDK JS, SDK Python, LangChain (Python + JS), LlamaIndex.
install → extract
RAG recipe
Chunk → embed → index a document end-to-end with a loader, then query it.
chunk → embed → index
Build on the API
Async & webhooks
Job lifecycle, polling, webhook_url delivery, and verifying the signature.
202 → poll or push
Idempotency
Idempotency-Key semantics: replay, mismatch handling, and SDK auto-retry.
retry-safe by default
Re-indexing
There is no content cache, so a nightly re-run pays for every file. Four steps to re-index only what changed.
hash first, call second
Error reference
Every error code, its HTTP status, cause, and remediation, all in one table.
code → status → fix
Limits and quotas
Every number txtfetch enforces. The page and the extraction Lambda read the same config file. The build fails if they differ.
every number, one page
What comes back
Quality checks
The automatic checks that flag likely-bad extracted text, how to sample the rest, and how to build a golden set.
200 ≠ correct
Handwriting
Not on the standard tier. What comes back for each kind of input, and where to send handwritten pages instead.
printed text, not handwriting
Equations and math
Word equations vanish. PDF math loses its layout. What comes back for each input, and what to do instead.
text out, not LaTeX
Fillable forms
Do the values you typed into a form come back? Yes, after the last page. What PDF, XFA and Word forms return, and how to find the values.
values yes, labels no
Barcodes and QR codes
Does txtfetch decode them? No. What comes back for each kind of code, and how to decode them yourself.
printed text, not decoded
Text in pictures and scans
Does the text in a picture come back? Most of it does. What PDF, Word, PowerPoint, Excel, HTML and email return, and where it lands.
comes back, not in place
Links and URLs
Do hyperlink targets come back? Not for HTML or Word. What each format returns, and how to read the URLs yourself.
link text yes, targets mostly no
Attachments and archives
Do files inside a ZIP or an email come back? Yes. File names come back for a ZIP and an Outlook .msg. An .eml drops its headers.
files yes, .eml headers no
Headers, footers, footnotes and comments
Where does the text outside the body go? It comes back inline. A Word comment carries the reviewer's name.
all come back, none stripped
Tracked changes
Do deleted words come back? Yes, with no marker. Here is how to get the accepted text.
deletions come back
Password-protected files
Does a locked file come back? No for a PDF, Word or Excel file. A locked ZIP says success and drops the content. How to unlock a file first.
locked files do not come back
Tables
Do tables come back as tables? Yes for HTML, Word, Excel, PowerPoint and ODT in Markdown and JSON. No for PDF and CSV. A merged cell moves in HTML and Word.
rows yes, merged cells no
Page numbers and citations
Can your RAG app cite page 12? Yes, for PDFs. Which formats return a page, and how to keep it through chunking.
pages for PDFs, no coordinates
Your data
Where your file goes
Every hop a document takes inside txtfetch, before the response comes back: who holds the bytes, in which region, for how long.
inside the door
Personal data
What a response actually carries, where a document body hides personal data, and where the extracted text lands next.
the reader's half of privacy
Untrusted documents
txtfetch returns every character and does not strip prompt injections. Where hidden text hides, and what your pipeline should do.
the injection objection
Leaving txtfetch
The full API surface, what there is to export (nothing), and what each replacement costs you.
no lock-in
Also useful
Watch it run, then take a key.
The playground replays a real recorded response for every sample document.
Open the playground →