Read a whole contract, attachments and all.

Legal teams review old files: .doc contracts, .msg and .eml threads with attachments, and scanned signature pages inside a digital PDF. txtfetch reads all of them.

the-problem

Legal operations and e-discovery deal with an old corpus. A contract might be a .doc file from 2009, or a .msg thread with three attachments. A signature page is often a scan glued inside an otherwise digital PDF. Generic extraction breaks the reading order on multi-column exhibits and loses the thread on nested email attachments.

how-txtfetch-solves-it

txtfetch reads .doc, .msg, and .eml threads, attachments included, plus scanned signature pages inside a digital PDF. It extracts in reading order for most PDFs, and /fixes/columns-out-of-order covers the multi-column exhibits that still need a check. It does not classify clauses and it does not redact text. Pricing is per document, not per page, which matters when a contract runs to 300 pages.

  • One endpoint reads .doc, .msg, and .eml threads with attachments.
  • Multi-column exhibits can still interleave, and /fixes/columns-out-of-order covers how to check.
  • OCR runs automatically on scanned pages inside an otherwise digital-native PDF.
  • Per-document pricing means a 300-page contract costs the same as a one-pager.
  • Async job and webhook mode handles large discovery batches.
  • The response text keeps attachment boundaries clear, so one email thread never blends into the next.
  • The same endpoint reads a native .docx exhibit and a scanned signature page with no format check on your side.
curl
curl -X POST https://api.txtfetch.com/v1/extract \
  -H "Authorization: Bearer $TXTFETCH_KEY" \
  -F file=@contract.doc
Python
import os
import requests

with open("contract.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("contract.doc")]);
const form = new FormData();
form.append("file", file, "contract.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("contract.doc")
	if err != nil {
		panic(err)
	}
	defer f.Close()

	var body bytes.Buffer
	writer := multipart.NewWriter(&body)
	part, err := writer.CreateFormFile("file", "contract.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)
}
{
  "status": "success",
  "extracted_text": "..."
}

faq

Does txtfetch classify contract clauses?
No. txtfetch returns the contract as plain text. Clause classification and redaction are up to your own tooling.
Can txtfetch read an .msg or .eml email thread with attachments?
Yes. It extracts the message body, headers, and each attachment's text in one call.
How does per-document pricing help with long contracts?
A 300-page contract costs the same as a one-page NDA, so long exhibits don't multiply your bill.

related-reading

Start on the free plan.

Run your own documents through it before you commit. The Hobby plan needs no card.

Get started →

See the pricing →