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
curl -X POST https://api.txtfetch.com/v1/extract \
  -H "Authorization: Bearer $TXTFETCH_KEY" \
  -F file=@quarterly-report.pdf
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
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
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 -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/whitepaper.docx" \
  -H "Authorization: Bearer $TXTFETCH_KEY"
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
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
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 429rate_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_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 →

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.

Stop parsing. Start shipping.

Create an account and get an API key in minutes — the free Hobby plan needs no card.

Get started →