Migrate from Unstructured.io to txtfetch

Its own Partition endpoint is now legacy. That's a migration moment either way. Here's what your code looks like before and after.

Where you are today

The call below is the legacy Partition endpoint, checked against Unstructured.io's current docs.

Python
import os

import unstructured_client
from unstructured_client.models import operations, shared

client = unstructured_client.UnstructuredClient(
    api_key_auth=os.environ["UNSTRUCTURED_API_KEY"],
)

req = operations.PartitionRequest(
    partition_parameters=shared.PartitionParameters(
        files=shared.Files(content=open("report.pdf", "rb"), file_name="report.pdf"),
        strategy=shared.Strategy.AUTO,
    ),
)

res = client.general.partition(request=req)
elements = res.elements

Checked against Unstructured.io's docs on 2026-09-10, using unstructured-client 0.46.2.

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 client.general.partition(request).elements. Change one function, not your whole pipeline.

Python
import os

import requests


def partition_like_elements(file_path: str) -> list[dict]:
    """Drop-in swap for client.general.partition(request).elements.

    Downstream code that reads el["text"] per element keeps working
    unmodified. txtfetch returns one element for the whole document, not
    one per paragraph or table.
    """
    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 [{"type": "NarrativeText", "text": text, "metadata": {}}]
JavaScript
import { readFile } from "node:fs/promises";

// Drop-in swap for client.general.partition(request).elements. Downstream code
// that reads el.text per element keeps working unmodified.
async function partitionLikeElements(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 [{ type: "NarrativeText", text: extracted_text, metadata: {} }];
}

field mapping

Unstructured.io's response fields, mapped to txtfetch's
Unstructured.io fieldtxtfetch equivalentNote
element.text (many per document)extracted_text (one string)Unstructured splits a document into many elements. txtfetch returns one string for the whole document.
element.type (Title, NarrativeText, Table, Image)not returnedThe plain-text response carries no per-element structural tag.
element.metadata.page_numbernot returnedNo per-element page numbers come back in the plain-text response.
element.metadata.text_as_html (Table elements)?format=markdown table rowsAsk for markdown output and read the GFM pipe table instead of the HTML fragment.

what you lose

  • Typed element types: Title, NarrativeText, Table, Image, ListItem. txtfetch returns one string, not a tagged list.
  • Per-element page numbers and other layout metadata.
  • Unstructured's own chunking strategies (by_title, by_page, by_similarity) built into the parse step.
  • The hi_res and vlm partitioning strategies tuned for hard scans.
  • A self-hostable open-source core.

See the full capability table on txtfetch vs Unstructured.io.

what you gain

  • Hundreds of formats through one endpoint (see /formats/coverage), not a curated subset.
  • One HTTP call. No SDK, no async job, no strategy parameter to tune.
  • Billing per document, not per page. A 300-page report costs the same as a one-pager.

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. Compare the two outputs side by side, or clean up formatting differences first if the two extractions disagree on whitespace.

frequently asked questions

Is Unstructured's Partition endpoint really deprecated?
Unstructured's own docs call it the legacy endpoint and point new work at the Pipeline API instead. The Python call above still works today, but plan the switch either way.
Does txtfetch return typed elements like Unstructured does?
No. txtfetch returns one plain-text string per document. If your code reads el.type or el.metadata.page_number, that code needs the adapter above or a rewrite.
Can I keep Unstructured for chunking and use txtfetch for extraction?
Yes. Some teams run both: txtfetch for format breadth and flat pricing, Unstructured (or your own chunker) for the by_title or by_similarity chunking strategy.

Swap the call. Keep the pipeline.

Point your Unstructured.io code at txtfetch, then diff the output before you cut over.

Get an API key →

See the migration guide →