Build a RAG Pipeline in Python with Api2Pdf OpenDataLoader and MarkItDown

Summary. This tutorial builds a working retrieval-augmented generation (RAG) pipeline in Python: Office and PDF documents are converted to Markdown with Api2Pdf's MarkItDown and OpenDataLoader APIs, then chunked, embedded, stored in a vector index, and queried with an LLM. You will leave with copy-paste code for conversion, chunking, embeddings, and a sample Q&A loop.

You need an Api2Pdf API key from portal.api2pdf.com. Base URL is https://v2.api2pdf.com; send your key in the Authorization header. Generated files are hosted for 24 hours. Api2Pdf converts documents to Markdown; it does not embed or run LLMs. Embeddings and generation stay in your stack (OpenAI, local sentence-transformers, or similar).

The two linked posts above are release notes. This post is the hands-on Python path that wires both APIs into a RAG ingestion and query loop.

Pipeline overview

A typical document RAG flow looks like this:

Office / PDF URLs
        │
        ▼
MarkItDown  and/or  OpenDataLoader
        │
        ▼
    Markdown text
        │
        ▼
Chunk by headings / ~500–1000 token windows (with overlap)
        │
        ▼
Embeddings (your model — not Api2Pdf)
        │
        ▼
Vector store (in-memory, Chroma, pgvector, …)
        │
        ▼
Query → retrieve top-k chunks → LLM answer

Api2Pdf sits at the first step: turn heterogeneous source files into clean Markdown so the rest of the pipeline can treat every document the same way.

When to use MarkItDown vs OpenDataLoader

NeedAPIEndpoint
PDF, Word, Excel, PowerPoint, and other files → MarkdownMarkItDownPOST https://v2.api2pdf.com/markitdown
PDF only → Markdown (best for RAG text)OpenDataLoaderPOST /opendataloader/markdown
PDF only → structured JSONOpenDataLoaderPOST /opendataloader/json
PDF only → HTMLOpenDataLoaderPOST /opendataloader/html

Rule of thumb: use MarkItDown when the corpus mixes Office formats (docx, xlsx, pptx) with PDFs. Use OpenDataLoader when the source is PDF-only and you want structured extraction; prefer Markdown for RAG text, and JSON when you need layout or field-level structure.

MCP note: the Api2Pdf MCP server exposes pdf_to_markdown (OpenDataLoader) for PDFs. It does not cover MarkItDown Office conversion. For Word/Excel/PowerPoint → Markdown, call the REST POST /markitdown endpoint (or the Python client below).

Convert with the Python client

Install the official package and create a client:

pip install api2pdf requests
from api2pdf import Api2Pdf

client = Api2Pdf("YOUR-API-KEY")

# MarkItDown: PDF, Office (docx/xlsx/pptx), and other supported files → Markdown
md = client.Markitdown.convert_to_markdown("https://example.com/sample.docx")

# OpenDataLoader: PDFs only
md2 = client.OpenDataLoader.pdf_to_markdown("https://example.com/sample.pdf")
js = client.OpenDataLoader.pdf_to_json("https://example.com/sample.pdf")

Successful responses include Success, FileUrl, Cost, Seconds, MbOut, ResponseId, and Error. Download the Markdown from FileUrl (hosted for 24 hours):

import requests

result = client.Markitdown.convert_to_markdown("https://example.com/handbook.docx")
assert result.Success, result.Error
markdown_text = requests.get(result.FileUrl).text
print(markdown_text[:500])

Equivalent curl

MarkItDown:

curl -X POST "https://v2.api2pdf.com/markitdown" \
  -H "Authorization: YOUR-API-KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/sample.docx",
    "fileName": "doc.md"
  }'

OpenDataLoader Markdown:

curl -X POST "https://v2.api2pdf.com/opendataloader/markdown" \
  -H "Authorization: YOUR-API-KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/sample.pdf",
    "fileName": "doc.md"
  }'

Body fields: url (required), optional fileName, and optional extraHTTPHeaders when the source URL needs auth. The JSON response's FileUrl points at the generated Markdown.

Minimal RAG sketch in Python

The sketch below converts a mixed list of PDF and docx URLs, downloads Markdown, chunks by headings with a ~750-token fallback window and overlap, embeds with OpenAI (swap in sentence-transformers if you prefer local models), stores vectors in memory, and answers one sample question. Example deps (openai, optional chromadb) are clearly marked; the conversion path itself only needs api2pdf and requests.

"""Minimal RAG over Api2Pdf Markdown. Example deps: openai (embeddings + chat)."""
import os, re, math, requests
from api2pdf import Api2Pdf

# Optional example deps — not required for conversion:
#   pip install openai
from openai import OpenAI

A2P_KEY = os.environ["API2PDF_API_KEY"]
OAI_KEY = os.environ["OPENAI_API_KEY"]
a2p = Api2Pdf(A2P_KEY)
oai = OpenAI(api_key=OAI_KEY)

DOCS = [
    # (url, kind) — kind picks the converter
    ("https://example.com/policy.pdf", "pdf"),
    ("https://example.com/handbook.docx", "office"),
]

def to_markdown(url: str, kind: str) -> str:
    if kind == "pdf":
        r = a2p.OpenDataLoader.pdf_to_markdown(url)
    else:
        r = a2p.Markitdown.convert_to_markdown(url)
    assert r.Success, r.Error
    return requests.get(r.FileUrl).text

def chunk_markdown(text: str, max_chars: int = 3000, overlap: int = 400) -> list[str]:
    """Split on ##/### headings; fall back to sliding windows with overlap."""
    parts = re.split(r"(?m)(?=^#{1,3} )", text)
    parts = [p.strip() for p in parts if p.strip()]
    chunks = []
    for part in parts:
        if len(part) <= max_chars:
            chunks.append(part)
            continue
        step = max_chars - overlap
        for i in range(0, len(part), step):
            chunks.append(part[i : i + max_chars])
    return chunks or [text]

def embed(texts: list[str]) -> list[list[float]]:
    # Api2Pdf does NOT provide embeddings — this is your model.
    resp = oai.embeddings.create(model="text-embedding-3-small", input=texts)
    return [d.embedding for d in resp.data]

def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    na = math.sqrt(sum(x * x for x in a)) or 1.0
    nb = math.sqrt(sum(y * y for y in b)) or 1.0
    return dot / (na * nb)

# --- ingest ---
corpus = []  # list of {"text", "vec", "source"}
for url, kind in DOCS:
    md = to_markdown(url, kind)
    for ch in chunk_markdown(md):
        vec = embed([ch])[0]
        corpus.append({"text": ch, "vec": vec, "source": url})

# --- query ---
question = "What is the refund policy window?"
qvec = embed([question])[0]
top = sorted(corpus, key=lambda c: cosine(qvec, c["vec"]), reverse=True)[:4]
context = "\n\n---\n\n".join(c["text"] for c in top)

answer = oai.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {"role": "system", "content": "Answer using only the provided context. Cite nothing outside it."},
        {"role": "user", "content": f"Context:\n{context}\n\nQuestion: {question}"},
    ],
)
print(answer.choices[0].message.content)

Local embeddings alternative: replace embed() with sentence-transformers (SentenceTransformer("all-MiniLM-L6-v2").encode(texts)) and keep the same cosine retrieval. For persistence, write vectors to Chroma, FAISS, or pgvector instead of the in-memory list.

Practical notes

  • Authenticated source URLs. If the document sits behind auth, pass headers Api2Pdf should use when fetching it. REST: extraHTTPHeaders. Python client: check the method kwargs for extra_http_headers / equivalent options on your installed api2pdf version.
  • Delete on command. Use the Utilities delete endpoint when you want a generated file removed before the 24-hour expiry.
  • Pricing. Pay-as-you-go: bandwidth is billed at $0.001/MB plus compute. See pricing for current details; response Cost and MbOut reflect each call.
  • Huge documents. For very large files, use the V2 XL base URL https://v2-xl.api2pdf.com with the same paths and auth header.
  • Keys. Create and rotate keys at portal.api2pdf.com.

Next steps

Full request schemas live in Swagger: https://v2.api2pdf.com/swagger/index.html.

Release notes for the two conversion APIs:

Wire those converters into your ingest job, keep embeddings and the LLM in your own stack, and you have a production-shaped RAG path without running document parsers yourself.