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
| Need | API | Endpoint |
|---|---|---|
| PDF, Word, Excel, PowerPoint, and other files → Markdown | MarkItDown | POST https://v2.api2pdf.com/markitdown |
| PDF only → Markdown (best for RAG text) | OpenDataLoader | POST /opendataloader/markdown |
| PDF only → structured JSON | OpenDataLoader | POST /opendataloader/json |
| PDF only → HTML | OpenDataLoader | POST /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 forextra_http_headers/ equivalent options on your installedapi2pdfversion. - 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
CostandMbOutreflect each call. - Huge documents. For very large files, use the V2 XL base URL
https://v2-xl.api2pdf.comwith 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:
- Extract Data from PDFs with the API2PDF OpenDataLoader API
- Convert Files to Markdown with the API2PDF MarkItDown API
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.



