---
title: "Unstructured Alternative for PDF Ingestion | EnConvert"
description: "EnConvert is an Unstructured alternative for PDF ingestion that returns heading-aware JSONL chunks with section metadata from one upload and no library to run"
canonical: "https://www.enconvert.com/solutions/unstructured-alternative-for-pdf-ingestion-enconvert"
locale: "en"
---

# Best Unstructured Alternative for PDF Ingestion

A developer building a RAG pipeline that ingests PDFs needs clean, heading-aware chunks from a simple API call, not a platform with several API surfaces, an open-source runtime to manage, or extraction options to tune. Unstructured is a document parsing platform with an open-source library and a managed API, and its extraction is element-level, returning typed document elements by default, with chunking as a strategy you opt into and configure. For a developer who needs PDF content as heading-aware chunks with metadata intact, that means choosing a chunking strategy, setting size limits, and mapping element metadata to the fields a vector store needs. EnConvert is a REST API that accepts a PDF upload and returns heading-aware JSONL chunks with section metadata, with no library to install and no element types to map, so you get RAG-ready chunks from any PDF in one API call.

## Example

```python
import os
import json
import time

import requests

# Step 1: The Unstructured Partition Endpoint partitions the PDF and returns typed elements by default
# Chunking is opt-in: add chunking_strategy "by_title" and tune size limits
with open("/path/to/your/report.pdf", "rb") as f:
    partition = requests.post(
        "https://api.unstructuredapp.io/general/v0/general",
        headers={"unstructured-api-key": os.environ["UNSTRUCTURED_API_KEY"]},
        files={"files": ("report.pdf", f, "application/pdf")},
        data={
            "strategy": "hi_res",
            # Add "chunking_strategy": "by_title" to receive chunks instead of elements
            # Add "max_characters" and "overlap" to tune chunk size
        },
        timeout=300,
    )
partition.raise_for_status()
elements = partition.json()

for element in elements:
    print(element["type"], element["text"])

# Output: typed elements such as Title, NarrativeText, Table, ListItem
# Map element types and metadata to your vector store schema before loading

# Step 2: EnConvert uploads the PDF once and queues an ingest job
with open("/path/to/your/report.pdf", "rb") as f:
    upload = requests.post(
        "https://api.enconvert.com/v2/ingest/files",
        headers={"X-API-Key": os.environ["ENCONVERT_API_KEY"]},
        files={"files": ("report.pdf", f, "application/pdf")},
        timeout=120,
    )
upload.raise_for_status()
job_id = upload.json()["job_id"]

# Step 3: Poll until the job completes (queued, processing, then completed)
while True:
    status_response = requests.get(
        f"https://api.enconvert.com/v2/ingest/{job_id}",
        headers={"X-API-Key": os.environ["ENCONVERT_API_KEY"]},
        timeout=30,
    )
    status_response.raise_for_status()
    job = status_response.json()
    if job["status"] == "completed":
        break
    if job["status"] in ("failed", "canceled"):
        raise RuntimeError(job.get("error_message") or f"Ingest job {job['status']}")
    time.sleep(2)

# Step 4: Download the JSONL, one chunk per line
# The output_url is signed (valid 15 minutes). Plain GET, no API key.
jsonl_response = requests.get(job["output_url"], timeout=60)
jsonl_response.raise_for_status()

# Split on "\n" only: str.splitlines() would also split on unicode line separators inside chunk text
chunks = [
    json.loads(line)
    for line in jsonl_response.content.decode("utf-8").split("\n")
    if line.strip()
]
print(f"Loaded {len(chunks)} chunks ready for embedding")

# Each chunk has "content" (the text to embed) plus "metadata" with source_url,
# title, headings_path (a list), section, chunk_index and word_count.
# Embed chunk["content"] and store chunk["metadata"] alongside it. If your vector
# store only accepts scalar metadata, join the list: " > ".join(headings_path)
for chunk in chunks[:3]:
    print(" > ".join(chunk["metadata"]["headings_path"]), "|", chunk["content"][:80])
```

## Output

| Capability | EnConvert | Unstructured |
| --- | --- | --- |
| Output format | JSONL chunks split at H1 to H3 boundaries, 512 word soft cap, tables and code blocks kept whole; each chunk carries headings_path, section, chunk_index, and word_count | Typed document elements as JSON by default; chunked output available through opt-in strategies |
| RAG-ready chunked output | Confirmed: chunks split at H1 to H3 heading boundaries, 512 word default soft cap, tables and code blocks kept whole even when they exceed the cap; each chunk carries headings_path, section, chunk_index, and word_count | Confirmed: chunking strategies including by_title available; requires opt-in configuration |
| Heading structure preserved | Confirmed: chunks split at H1 to H3 boundaries, 512 word soft cap, tables and code blocks kept whole; each chunk carries headings_path, section, chunk_index, and word_count | Section boundaries detected via Title elements; no equivalent breadcrumb path documented |
| Web page ingestion | Perceive: any URL to clean Markdown, including JavaScript-rendered pages; Ingest also accepts URL lists, sitemaps, and website crawls returning the same JSONL chunk schema as file ingestion | Not documented for JavaScript-rendered pages |
| Document conversion | Convert: 45+ conversion routes across documents, images, and data formats | Not documented as a general-purpose file format conversion endpoint |
| MCP server | Native, exposes full ingestion toolkit as MCP tools | Available through its SDK’s MCP server |
| Open-source option | No; managed API only | Confirmed: open-source library alongside the managed API, Apache 2.0 licensed |
| Free tier | 500 ops per month, no card required | Open-source library free to run; managed API trial documented as 1,000 pages per day for 14 days |

**WHY ENCONVERT FOR PDF INGESTION**

Heading-aware JSONL chunks split at document heading boundaries so chunking respects document structure, and each chunk carries a full `headings_path` breadcrumb so retrieval knows exactly where in the PDF a result came from. You do not write element type mapping, configure a chunking strategy, or transform the output schema before loading into a vector store. When the pipeline grows beyond PDFs, the same API handles DOCX and XLSX with the same output schema, and web pages through Perceive with no integration changes.

## Get started

[Get free API key — 500 ops per month, no card required.](https://www.enconvert.com/auth?mode=signup)

## Related solutions

- [How to Ingest Markdown Files into LlamaIndex with EnConvert](https://www.enconvert.com/solutions/ingest-markdown-files-into-llamaindex-with-enconvert.md)
- [How to Ingest Web Pages into Vertex AI with EnConvert](https://www.enconvert.com/solutions/ingest-web-pages-into-vertex-ai-with-enconvert.md)
- [How to Ingest Markdown into OpenAI File Search with EnConvert](https://www.enconvert.com/solutions/ingest-markdown-into-openai-file-search-enconvert.md)
- [How to Ingest HTML Files into LlamaIndex with EnConvert](https://www.enconvert.com/solutions/ingest-html-files-into-llamaindex-with-enconvert.md)
- [Best Browserless Alternative for Document Parsing](https://www.enconvert.com/solutions/browserless-alternative-for-document-parsing-enconvert.md)
- [How to Ingest DOCX into CrewAI with EnConvert](https://www.enconvert.com/solutions/ingest-docx-into-crewai-with-enconvert.md)
