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 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 1,000+ supported formats — txtfetch detects 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 — same request, 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. Same endpoint, same auth, same error shape; 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
}
}Full element schema — every type, required field, and the offset/page monotonicity guarantee — is part of the published contract: OpenAPI spec →
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 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: on a cost/timeout/size guardrail breach it safely falls back to the same Tika baseline as the standard tier, 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 →
Errors
Failures are explicit and machine-readable — never a silent empty string. 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 is a typed 429 — rate_limited vs quota_exceeded, disambiguated by error.code — with a Retry-After telling 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 →
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 — every field, error code, and status — is published as OpenAPI 3.1. Generate a typed client for whatever language you're in.
guides-and-sdks
Past the first curl.
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
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
Error reference
Every error code, its HTTP status, cause, and remediation — one table.
code → status → fix
Stop parsing. Start shipping.
Create an account and get an API key in minutes — the free Hobby plan needs no card.
Get started →