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 -X POST https://api.txtfetch.com/v1/extract \
  -H "Authorization: Bearer $TXTFETCH_KEY" \
  -F file=@1998-contract-template.doc
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
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
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 -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/archive/legacy-invoice.xls" \
  -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/archive/legacy-invoice.xls"},
)

print(r.json()["extracted_text"])
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
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 has no recorded .doc document yet, so we have nothing honest to show you here. Run one of your own instead. The free converter 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

Send a real .doc through it.

One HTTP call returns the text. Read one in your browser first, for free.

Get an API key →

Open the free .doc reader →