Four npm packages, one fetch() call.

pdf-parse, mammoth, xlsx, tesseract.js — each one solid at its single job, none of them aware the other three exist.

the-parser-zoo

Node's document-parsing story is a shelf of single-purpose packages rather than one library, and each has a real limitation worth knowing before you build around it. pdf-parse is a thin, long-unmaintained wrapper around Mozilla's pdf.js — it gets plain text out of a normal PDF, but has no OCR path and no graceful handling for encrypted or malformed files; a corrupt PDF throws instead of degrading. mammoth converts .docx to plain text or HTML — but only .docx: no .doc, no .pptx, no .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 — same trap as Python's openpyxl — and its API surface has shifted across versions in ways that break upgrades. tesseract.js runs real OCR via 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) and running CPU-bound recognition on Node's single main thread unless you wire up worker threads yourself.

librarycoversstops at
pdf-parseDigital-native PDF text via pdf.jsNo OCR; throws rather than degrading on encrypted or malformed PDFs
mammoth.docx → plain text or HTML, style-awaredocx only — no .doc, .pptx, or .xlsx; images are dropped, not described
xlsxSpreadsheet cells across .xlsx/.xls/.csv and moreFormula values depend on a pre-existing cached result, same as Excel's own file format quirk
tesseract.jsIn-process OCR via a WASM Tesseract buildDownloads 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 { "status": "success", "extracted_text": "..." } back, 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, so there's no per-format if/else to route a file to the right package before extraction can even start.

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, ships its own .d.ts.

Install
npm install @txtfetch/sdk
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
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 for every code and HTTP status txtfetch can return.

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 — 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 for the full lifecycle, including webhook delivery instead of polling.

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 with a fresh container on every cold start re-downloads them every time unless you bundle 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's 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 majors; pin the version deliberately rather than accepting ^ ranges in a document-ingestion pipeline you don't want to babysit.

formats

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": "..." } — and 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, so there's 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, so you can catch and branch on error.code without a bare try/catch around a library internals.
Is there an official JavaScript or TypeScript SDK?
Yes — npm install @txtfetch/sdk. It has zero runtime dependencies, ships both ESM and CJS builds with .d.ts types, and 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

Stop parsing. Start shipping.

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

Get started →