Best Browserless Alternative for Document Parsing

EnConvert is a Browserless alternative for document parsing that returns heading-aware JSONL chunks from files and Markdown from URLs behind a single API

Get API key

A developer using Browserless for document parsing is working around the tool: Browserless is a headless browser API for automation, scraping, screenshots, and PDF capture, not a document ingestion API. It can extract page content as rendered HTML or as Markdown through its smart scraper, and it can render a URL to PDF, but it does not parse uploaded files and its output has no heading-aware chunking and no metadata a retrieval layer can filter on. For a developer building a RAG pipeline or an agent that reads documents, the extracted content still needs chunking, metadata, and structure before it can be loaded into a vector store. EnConvert’s Ingest endpoint accepts file uploads and returns JSONL chunks split at heading boundaries with full metadata, and its Perceive endpoint converts any URL to clean Markdown, so document content is ready for retrieval from one API without browser infrastructure.

Example

python
import json
import os
import time

import requests

BROWSERLESS_API_TOKEN = os.environ["BROWSERLESS_API_TOKEN"]
ENCONVERT_API_KEY = os.environ["ENCONVERT_API_KEY"]
PAGE_URL = os.environ["PAGE_URL"]  # the web page URL to extract
FILE_PATH = "/path/to/your/report.pdf"  # a local PDF, DOCX, PPTX, XLSX, CSV, or EPUB

# Step 1: Browserless /content returns the rendered HTML of the page
# Raw markup, navigation, and scripts are still in the output.
# Content is not chunked or labelled with sections.
browserless_response = requests.post(
    "https://production-sfo.browserless.io/content",
    params={"token": BROWSERLESS_API_TOKEN},
    json={"url": PAGE_URL},
    timeout=120,
)
browserless_response.raise_for_status()
raw_html = browserless_response.text
# raw_html requires extraction, chunking, and metadata enrichment
# before it can be loaded into a vector store.
print(f"Browserless returned {len(raw_html)} characters of raw HTML")

# Step 2: EnConvert Perceive, same URL, returns clean Markdown
perceive_response = requests.post(
    "https://api.enconvert.com/v2/perceive",
    headers={
        "X-API-Key": ENCONVERT_API_KEY,
        "Content-Type": "application/json",
    },
    json={
        "url": PAGE_URL,
        "outputs": ["markdown"],
        "only_main_content": True,
    },
    timeout=60,
)
perceive_response.raise_for_status()
perceived = perceive_response.json()

# A blocked page, login wall, or HTTP error page comes back as a 200 with no outputs
if perceived.get("error") or "markdown" not in perceived.get("outputs", {}):
    raise RuntimeError(
        f"Perceive failed for {PAGE_URL}: "
        f"{perceived.get('error') or perceived.get('deductions')}"
    )

# The markdown is a signed artifact URL (valid 15 minutes). Plain GET, no API key.
markdown_response = requests.get(perceived["outputs"]["markdown"]["url"], timeout=60)
markdown_response.raise_for_status()
markdown = markdown_response.text
# Clean Markdown with headings, tables, and code blocks preserved and navigation
# chrome stripped. It is one file for the whole page, not yet split into chunks.
print(markdown[:500])

# Step 3: EnConvert Ingest, upload a document, get heading-aware chunks back
# For web pages, POST /v2/ingest with "mode": "urls" returns the same JSONL.
with open(FILE_PATH, "rb") as document:
    ingest_response = requests.post(
        "https://api.enconvert.com/v2/ingest/files",
        headers={"X-API-Key": ENCONVERT_API_KEY},
        files=[("files", (os.path.basename(FILE_PATH), document))],
        timeout=120,
    )
ingest_response.raise_for_status()
job_id = ingest_response.json()["job_id"]

# Step 4: Poll the job until it finishes (queued, processing, then a final status)
while True:
    status_response = requests.get(
        f"https://api.enconvert.com/v2/ingest/{job_id}",
        headers={"X-API-Key": ENCONVERT_API_KEY},
        timeout=60,
    )
    status_response.raise_for_status()
    job = status_response.json()
    if job["status"] in ("completed", "failed", "canceled"):
        break
    time.sleep(5)

if job["status"] != "completed":
    raise RuntimeError(f"Ingest {job['status']}: {job.get('error_message')}")

# Step 5: 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()
chunks = [json.loads(line) for line in jsonl_response.iter_lines() if line]
print(f"{job['total_chunks']} chunks from {FILE_PATH}")

# Each chunk is {"id", "content", "metadata"}. The metadata is what a retrieval
# layer can filter on. headings_path is a list, so join it for display or for a
# vector store that only accepts scalar metadata values.
for chunk in chunks[:3]:
    metadata = chunk["metadata"]
    print(f"{metadata['source_url']} | {metadata['title']}")
    print(
        f"  headings_path: {' > '.join(metadata['headings_path'])} | "
        f"section: {metadata['section']} | "
        f"chunk_index: {metadata['chunk_index']} | "
        f"word_count: {metadata['word_count']}"
    )
    print(f"  {chunk['content'][:200]}")

What you get back

Feature

EnConvert

Browserless

Primary use case

File and URL ingestion into RAG-ready chunks and clean Markdown for AI agents

Headless Chrome API for browser automation, scraping, screenshots, and PDF generation

File ingestion (PDF, DOCX, XLSX, CSV)

Ingest: PDF, DOCX, PPTX, XLSX, CSV, Markdown, plain text, TXT, HTML files, and audio transcripts

Not supported; Browserless does not accept file uploads for parsing

Web page to clean Markdown

Perceive: any URL to clean Markdown, navigation chrome stripped

Smart scraper returns Markdown or HTML; /content returns rendered HTML

RAG-ready chunked output

JSONL chunks per job, split at H1 to H3 heading boundaries with 512 word soft cap

Not documented

Heading-aware metadata

headings_path, section, chunk_index, word_count, source_url, and title on every chunk

Not documented

JavaScript-rendered pages

Yes, Perceive handles JS-rendered pages without browser setup

Yes, built-in headless Chrome rendering

Setup required

API key only

API token for the cloud service, or a Docker image to self-host

Pricing model

Monthly ops allowance by tier; 500 ops free with no card required

Usage-based units; free tier then paid plans

WHEN TO USE BROWSERLESS AND WHEN TO USE ENCONVERT

Use Browserless when the task requires full browser automation, such as form interaction, screenshots, or PDF generation from a URL. It is designed for controlling a browser and fits workflows that go beyond extracting document content for retrieval.

Use EnConvert when document content needs to be structured for retrieval. Ingest accepts supported file uploads and returns heading-aware JSONL chunks with section metadata. Perceive converts any URL to clean Markdown without requiring browser infrastructure, and both endpoints use the same flat op counter.

Get free API key — 500 ops per month, no card required.