Best Captain Alternative for RAG Pipeline Ingestion

EnConvert is a Captain alternative for RAG pipeline ingestion that turns web pages and files into clean Markdown you load into the vector store of your choice

Get API key

A developer building a RAG pipeline has to decide how much of the stack to hand to a vendor. Captain is a managed RAG platform for files that handles OCR, chunking, embedding, vector storage, and search behind one query API. That suits teams that want retrieval managed for them, but it ties the index to Captain’s storage layer and does not cover web pages as a general input source. EnConvert sits earlier in the pipeline, with endpoints that turn web pages and files into clean, consistent Markdown or chunked output you load into whichever vector store you choose. The choice is between managed retrieval and an ingestion layer you control.

Example

python
import json
import os
import time

import requests

PDF_PATH = "/path/to/your/contract.pdf"

# Captain: index a PDF into a managed collection, then query it
CAPTAIN_URL = "https://api.filesearch.dev"
CAPTAIN_COLLECTION = "contracts"
# The organisation is derived from the API key, so X-Organization-ID is optional
captain_headers = {"Authorization": f"Bearer {os.environ['CAPTAIN_API_KEY']}"}

# Step 1: Create the collection (idempotent: 201 if created, 200 if it exists)
requests.put(
    f"{CAPTAIN_URL}/v2/collections/{CAPTAIN_COLLECTION}",
    headers=captain_headers,
    json={},
    timeout=30,
).raise_for_status()

# Step 2: Upload the PDF for indexing
with open(PDF_PATH, "rb") as f:
    index_response = requests.post(
        f"{CAPTAIN_URL}/v2/collections/{CAPTAIN_COLLECTION}/index/file",
        headers=captain_headers,
        files={"files": ("contract.pdf", f, "application/pdf")},
        data={"processing_type": "advanced"},
        timeout=120,
    )
index_response.raise_for_status()
captain_job_id = index_response.json()["job_id"]

# Step 3: Poll until indexing completes
while True:
    captain_status = requests.get(
        f"{CAPTAIN_URL}/v2/jobs/{captain_job_id}",
        headers=captain_headers,
        timeout=30,
    )
    captain_status.raise_for_status()
    captain_job = captain_status.json()
    if captain_job["status"] == "completed":
        break
    if captain_job["status"] in ("completed_with_errors", "failed", "cancelled", "timed_out"):
        raise RuntimeError(
            f"Captain indexing {captain_job['status']}: "
            f"{captain_job.get('error_message') or captain_job.get('result')}"
        )
    time.sleep(5)

# Step 4: Query the managed collection
query_response = requests.post(
    f"{CAPTAIN_URL}/v3/collections/{CAPTAIN_COLLECTION}/query",
    headers=captain_headers,
    json={"query": "What are the termination terms?", "limit": 10},
    timeout=60,
)
query_response.raise_for_status()
for hit in query_response.json()["results"]:
    print(f"{hit['score']:.3f}  {hit['text'][:120]}")

# Chunking, embedding, storage, and ranking are managed by Captain

# EnConvert: ingest the PDF, then load chunks into the store you control
enconvert_headers = {"X-API-Key": os.environ["ENCONVERT_API_KEY"]}

# Step 1: Upload the PDF (multipart field is "files"; always answers 202 with a job_id)
with open(PDF_PATH, "rb") as f:
    submit_response = requests.post(
        "https://api.enconvert.com/v2/ingest/files",
        headers=enconvert_headers,
        files={"files": ("contract.pdf", f, "application/pdf")},
        timeout=60,
    )
submit_response.raise_for_status()
job_id = submit_response.json()["job_id"]

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

# Step 3: Download the JSONL, one chunk per line. output_url is a signed URL
# (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 break on U+2028 inside chunk text
chunks = [
    json.loads(line)
    for line in jsonl_response.content.decode("utf-8").split("\n")
    if line.strip()
]

# Step 4: Map chunks to the inputs a vector store expects
ids = [chunk["id"] for chunk in chunks]
texts = [chunk["content"] for chunk in chunks]
# headings_path is a list; many stores accept only scalar metadata, so join it
metadatas = [
    {
        **chunk["metadata"],
        "headings_path": " > ".join(chunk["metadata"]["headings_path"]),
    }
    for chunk in chunks
]

print(f"Loaded {len(chunks)} chunks ready for embedding")

# Embed texts with your embedding model, then add ids, texts and metadatas
# to the vector store you choose

What you get back

Capability

EnConvert

Captain

Primary function

Ingestion layer: web pages and files to Markdown or JSONL chunks split at H1 to H3 boundaries, tables and code blocks kept whole, for your own vector store

Managed RAG: OCR, chunking, embedding, vector storage, and search behind one query API

Web page extraction

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 as a general web page ingestion source

File ingestion

Ingest: PDF, DOCX, XLSX, CSV, Markdown, and plain text; chunks split at H1 to H3 boundaries, 512 word soft cap, tables and code blocks kept whole

Confirmed: PDF, DOCX, XLSX, CSV, images, and connected cloud storage sources with OCR

Document conversion

Convert: 45+ conversion routes across documents, images, and data formats

Not documented as a standalone conversion endpoint

Vector storage and search

Not provided; developer brings their own vector store and embedding model

Managed by Captain; query API returns ranked chunks with scores and citations

Output format

One consistent schema across all endpoints: Markdown from Perceive and Distill, JSONL chunks from Ingest split at H1 to H3 boundaries with tables and code blocks kept whole

Search results with relevance scores and source metadata

MCP server

Native, exposes full ingestion toolkit as MCP tools

Confirmed: hosted MCP server for indexing, collection management, and search

Free tier

500 ops per month, no card required

Developer plan: 500 indexing credits and 1,000 queries per month

WHICH TO CHOOSE

Choose Captain if you want retrieval fully managed, your inputs are files and connected storage, and you would rather not operate a vector store. Choose EnConvert if your inputs include web pages, if you want to keep your own vector store and embedding model, or if you need one consistent output schema across every source type. The two sit at different points in the pipeline, and some teams could use EnConvert to prepare content and Captain to search it.

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