Migrate from AWS Textract to txtfetch

A flat Blocks array, joined by hand. That join is the whole adapter. Here's what your code looks like before and after.

Where you are today

The call below is DetectDocumentText, checked against AWS Textract's current docs.

Python
import boto3

client = boto3.client("textract")

with open("report.pdf", "rb") as f:
    response = client.detect_document_text(Document={"Bytes": f.read()})

lines = [b["Text"] for b in response["Blocks"] if b["BlockType"] == "LINE"]
text = "\n".join(lines)

Checked against AWS Textract's docs on 2026-09-10, using boto3 1.43.x.

the call that replaces it

One HTTP call. No SDK to install, and no job to poll.

curl
curl -X POST https://api.txtfetch.com/v1/extract \
  -H "Authorization: Bearer $TXTFETCH_KEY" \
  -F file=@report.pdf
Python
import os

import requests

with open("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("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);

drop-in adapter

A short function standing in for [b["Text"] for b in response["Blocks"] if b["BlockType"] == "LINE"]. Change one function, not your whole pipeline.

Python
import os

import requests


def detect_text_like_blocks(file_path: str) -> list[dict]:
    """Drop-in swap for the LINE blocks in response["Blocks"].

    Downstream code that reads b["Text"] per LINE block keeps working
    unmodified. txtfetch returns one block for the whole document, not
    one per line.
    """
    with open(file_path, "rb") as f:
        r = requests.post(
            "https://api.txtfetch.com/v1/extract",
            headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"},
            files={"file": f},
        )
    r.raise_for_status()
    text = r.json()["extracted_text"]
    return [{"BlockType": "LINE", "Text": line} for line in text.splitlines()]
JavaScript
import { readFile } from "node:fs/promises";

// Drop-in swap for the LINE blocks in response.Blocks. Downstream code
// that reads b.Text per LINE block keeps working unmodified.
async function detectTextLikeBlocks(filePath) {
  const file = new Blob([await readFile(filePath)]);
  const form = new FormData();
  form.append("file", file, filePath);

  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();
  return extracted_text.split("\n").map((line) => ({ BlockType: "LINE", Text: line }));
}

field mapping

AWS Textract's response fields, mapped to txtfetch's
AWS Textract fieldtxtfetch equivalentNote
Blocks[] where BlockType == "LINE"extracted_text, split on newlineTextract returns lines in reading order per page. Join them, or split txtfetch's text the same way.
Block.Confidence (per word/line)not returnedThe plain-text response carries no per-line or per-word confidence score.
Block.Geometry (BoundingBox, Polygon)not returnedNo coordinates come back. See what you lose below.
AnalyzeDocument FORMS (key-value pairs)not returnedtxtfetch has no forms-analysis mode. Keep AnalyzeDocument for key-value extraction.

what you lose

  • Per-word and per-line Confidence scores.
  • Geometry: BoundingBox and Polygon coordinates for every block.
  • AnalyzeDocument's FORMS mode (key-value pairs) and TABLES mode (structured cells).
  • SIGNATURES detection.
  • Deep AWS IAM/VPC integration if you're already running there.

See the full capability table on txtfetch vs AWS Textract.

what you gain

  • Hundreds of formats through one endpoint (see /formats/coverage), not PDF, PNG, JPEG, and TIFF only.
  • One HTTP call instead of a boto3 client and a Blocks array to walk.
  • Billing per document, not per 1,000 pages. See the pricing math.

The pricing math behind per-document billing lives on why per-page pricing punishes long documents.

Cutover

Run both calls on the same file while you switch traffic over. Diff the two outputs, or clean up formatting differences first on pages where a table mattered.

frequently asked questions

Does txtfetch return Textract's Blocks array?
No. txtfetch returns one plain-text string. If your code reads response["Blocks"], use the adapter above or rewrite it to read one string.
Can I keep Textract for forms and tables, and use txtfetch for plain text?
Yes. Some teams run AnalyzeDocument only on the documents that need key-value pairs or structured tables, and send everything else to txtfetch.
Do Textract's LINE blocks already come back in reading order?
Yes, per AWS's own docs, for a single-column page. A multi-column layout can still need reordering on either side.

Swap the call. Keep the pipeline.

Point your AWS Textract code at txtfetch, then diff the output before you cut over.

Get an API key →

See the migration guide →