Convert a PDF to Markdown in Python
Process PDF pages sequentially, preserve each completed result and assemble Markdown without repeating finished work.
This guide describes production API usage. The website demo has a separate, smaller anonymous allowance. Subscribe to an available listing before running the scripts.
Choose your marketplace
The complete scripts below target RapidAPI. Use the provider tabs for the matching base URL and authentication pattern; ApyHub capabilities may be split across listings and remain unavailable until published. Never use a pending listing as a production endpoint.
Replace the base64 placeholder with your file. See the tutorial for an upload example.
import os
import httpx
base_url = "https://ocr-pdf-to-markdown-api.p.rapidapi.com"
headers = {
"X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"],
"X-RapidAPI-Host": "ocr-pdf-to-markdown-api.p.rapidapi.com",
}
payload = {
"page": 1,
"format": "json",
"document": {
"base64": "BASE64_PDF",
},
}
response = httpx.post(
base_url + "/v1/ocr/document",
headers=headers, json=payload, timeout=120
)
response.raise_for_status()
print(response.json())Process a PDF sequentially and save each completed page. A checkpoint directory prevents a restart from requesting pages already saved.
Prerequisites
Subscribe on RapidAPI, set RAPIDAPI_KEY, and keep it on your server. Python examples require Python 3.10+ and requests (python -m pip install requests); file examples also require the local input named in the code. Follow your plan's per-minute limit.
Try the endpoint with cURL
export RAPIDAPI_KEY='your-key-from-rapidapi'
export RAPIDAPI_HOST='ocr-pdf-to-markdown-api.p.rapidapi.com'
curl --fail-with-body --request POST \
--url "https://${RAPIDAPI_HOST}/v1/ocr/document" \
--header 'content-type: application/json' \
--header "x-rapidapi-host: ${RAPIDAPI_HOST}" \
--header "x-rapidapi-key: ${RAPIDAPI_KEY}" \
--data '{"document":{"url":"https://arxiv.org/pdf/1706.03762v7"},"page":1,"format":"json"}' \
--output response.json
Check the HTTP status before opening response.json; an error body is not a successful output.
Complete application example
import os
from pathlib import Path
import requests
HOST = "ocr-pdf-to-markdown-api.p.rapidapi.com"
HEADERS = {"x-rapidapi-host": HOST, "x-rapidapi-key": os.environ["RAPIDAPI_KEY"]}
import random
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
INTERVAL = float(os.environ.get("MIN_INTERVAL_SECONDS", "30"))
last_call = 0.0
def request(route, body, attempts=5):
global last_call
for attempt in range(attempts):
time.sleep(max(0, INTERVAL - (time.monotonic() - last_call)))
last_call = time.monotonic()
response = requests.post(f"https://{HOST}{route}", headers=HEADERS, json=body, timeout=90)
if response.status_code == 429 and any(word in response.text.lower() for word in ("quota", "monthly", "allowance")):
response.raise_for_status()
if response.status_code not in {429, 502, 503, 504}:
response.raise_for_status()
return response
if attempt == attempts - 1:
response.raise_for_status()
value = response.headers.get("retry-after", "")
try:
delay = float(value)
except ValueError:
try:
delay = (parsedate_to_datetime(value) - datetime.now(timezone.utc)).total_seconds()
except (ValueError, TypeError, OverflowError):
delay = 2 ** attempt
time.sleep(max(0, delay) + random.random())
raise RuntimeError("request did not complete")
PDF = {"url": "https://arxiv.org/pdf/1706.03762v7"}
folder = Path("paper-pages")
folder.mkdir(exist_ok=True)
def page(number):
checkpoint = folder / f"page-{number:04d}.json"
import json
if checkpoint.exists():
return json.loads(checkpoint.read_text(encoding="utf-8"))
result = request("/v1/ocr/document", {"document": PDF, "page": number, "include_blocks": False}).json()
checkpoint.write_text(json.dumps(result, ensure_ascii=False), encoding="utf-8")
return result
first = page(1)
parts = [first["markdown"]]
for number in range(2, first["page"]["count"] + 1):
parts.append(page(number)["markdown"])
Path("paper.md").write_text("\n\n---\n\n".join(parts), encoding="utf-8")
print("saved paper.md")
Expected output and important limits
An actual two-page synthetic report returned this page metadata for page 1:
{"width":1700,"height":2200,"index":1,"count":2,"dpi":200}
Page 2 returned index: 2 and count: 2. Requesting page 3 returned HTTP 422
with detail.code: page_out_of_range and detail.page_count: 2.
Use a new checkpoint directory for each source PDF and option set. page.count is the source's page count; each new page is one API call. The default 30-second interval suits BASIC. Change MIN_INTERVAL_SECONDS only to match your active plan. PDF page ceilings are not a guarantee that every page finishes within the time budget.
Every API call and retry may consume your allowance, including repeated inputs and failed requests. Fix 422 input errors rather than retrying. Honor Retry-After for temporary 429/503 responses and use bounded backoff; a monthly quota error needs a renewed allowance or another plan. Do not upload material you are not authorized to process. See the Developer guide's limits and Privacy Notice before handling sensitive files.
Make it part of your application.
Get credentials and a production allowance from an available marketplace.