https://txtfetch.com/migrate/aws-textract/
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.
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 -X POST https://api.txtfetch.com/v1/extract \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@report.pdfimport 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"])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.
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()]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 field | txtfetch equivalent | Note |
|---|---|---|
| Blocks[] where BlockType == "LINE" | extracted_text, split on newline | Textract returns lines in reading order per page. Join them, or split txtfetch's text the same way. |
| Block.Confidence (per word/line) | not returned | The plain-text response carries no per-line or per-word confidence score. |
| Block.Geometry (BoundingBox, Polygon) | not returned | No coordinates come back. See what you lose below. |
| AnalyzeDocument FORMS (key-value pairs) | not returned | txtfetch 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.
- Compare outputs side by side, to dual-run both calls on the same file.
- Clean up extracted text, if the two outputs disagree on formatting.
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 →