Convert PDF to Markdown with the TranslatePDFOnline API: A Complete Guide
Overview
TranslatePDFOnline provides a REST API v1 that enables programmatic end-to-end PDF-to-Markdown conversion via API Key. This guide walks through the full pipeline — uploading a PDF, triggering OCR recognition, and retrieving the Markdown output — using curl.
Use cases: CI/CD document processing pipelines, AI Agent tool calls, batch document digitization.
Prerequisites
- Create an account and generate an API Key at Settings → API Keys (format:
sk-prefix, shown only once) - Ensure sufficient credits (default: 10 credits per page)
- Authentication:
Authorization: Bearer sk-xxxorX-API-Key: sk-xxx - Rate Limit: 60 requests/min shared across all v1 endpoints
Flow Overview (6 Steps)
Client API Server R2 Storage OCR Pipeline
│──① presigned──>│ │
│<── upload_url──│ │
│──② PUT PDF ─────────────────────────>│ │
│<── 200 ─────────────────────────────│ │
│──③ complete───>│ │
│<── doc_id ─────│ │
│──④ ocr ────────>│ │
│<── task_id ────│─────────────────────────────────────>│
│──⑤ poll ──────>│ BaiduOCR+DeepSeek │
│<── status ─────│ │
│──⑥ markdown───>│ │
│<── {markdown} ─│ │
Key design: two-step presigned upload — files go directly from client to Cloudflare R2, bypassing the API server and avoiding Worker body size limits.
Step 1–3: Upload the PDF
# ① Request a presigned upload URL (valid 10 min)
curl -X POST $BASE/api/v1/upload/presigned \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"size_bytes": 779264, "content_type": "application/pdf"}'
# → {"upload_url": "https://...", "object_key": "uploads/xxx.pdf", "expires_at": "..."}
# ② PUT directly to R2 (URL is self-signed, no auth header needed)
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/pdf" \
--data-binary @"/path/to/file.pdf"
# ③ Confirm upload, create document record
curl -X POST $BASE/api/v1/upload/presigned/complete \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"object_key":"uploads/xxx.pdf","size_bytes":779264,"filename":"doc.pdf"}'
# → {"document_id": "moR-xxx", "page_count": 10, "page_count_ready": true}
Request fields for presigned:
| Field | Type | Notes |
|---|---|---|
size_bytes |
number | File size in bytes, 1 B – 100 MB |
content_type |
string | Must be application/pdf |
Request fields for complete:
| Field | Type | Notes |
|---|---|---|
object_key |
string | The object_key from step ① |
size_bytes |
number | Same as step ① |
filename |
string | Original filename (display only) |
Step 4: Create the OCR Task (/api/v1/ocr)
curl -X POST $BASE/api/v1/ocr \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"document_id": "moR-xxx",
"source_lang": "zh",
"target_lang": "zh",
"page_range": "1",
"preprocess_with_ocr": true
}'
# → {"task_id": "abc123", "status": "queued", "credits_estimated": 10}
Request fields:
| Field | Type | Required | Notes |
|---|---|---|---|
document_id |
string | ✅ | From step ③ |
source_lang |
string | — | Default en |
target_lang |
string | — | Default zh |
page_range |
string | — | "1" for a single page, "1-10" for a range |
preprocess_with_ocr |
boolean | — | Must be true for scanned PDFs |
Supported languages: en, zh, es, fr, it, el, ja, ko, de, ru
Same-language trick: Set source_lang = target_lang (e.g., both "zh") to run OCR recognition without translation — extracting text directly into Markdown.
Step 5: Poll Task Status
curl $BASE/api/v1/tasks/$TASK_ID -H "Authorization: Bearer $KEY"
Response:
{
"id": "abc123def456ghi789jkl",
"document_id": "moR-xxx",
"source_lang": "zh",
"target_lang": "zh",
"page_range": "1",
"status": "processing",
"progress_percent": 45,
"progress_stage": "ocr_submit_poll",
"progress_current": 2,
"progress_total": 4,
"preprocess_with_ocr": true,
"created_at": "2026-08-06T07:30:00.000Z",
"updated_at": "2026-08-06T07:31:00.000Z",
"error_code": null,
"error_message": null,
"post_complete_hint": null
}
Status lifecycle: queued → processing → completed | failed
Recommended polling: every 3 seconds. A single page typically completes in 15–60 seconds.
for i in $(seq 1 60); do
ST=$(curl -s $BASE/api/v1/tasks/$TASK_ID -H "Authorization: Bearer $KEY" | jq -r .status)
echo "poll $i: $ST"
[ "$ST" = "completed" ] && break
[ "$ST" = "failed" ] && break
sleep 3
done
Step 6: Retrieve Markdown
curl $BASE/api/v1/tasks/$TASK_ID/markdown \
-H "Authorization: Bearer $KEY" | jq -r .markdown > output.md
Response:
{
"markdown": "# Document Title\n\n## Page 1\n\nExtracted text content...",
"object_key": "translations/abc123/ocr-source.md",
"source": "parse_result_rebuild",
"updated_at": "2026-08-06T07:32:00.000Z"
}
source: "parse_result_rebuild"— Markdown is rebuilt in real time from the OCR layout JSON- Images appear as relative-path references:

All-in-One Shell Script
#!/bin/bash
set -e; BASE="https://www.translatepdfonline.com"; KEY="${TRANS_API_KEY:?}"; PDF="$1"
SIZE=$(stat -c %s "$PDF")
# ① Presigned upload
R=$(curl -s -X POST "$BASE/api/v1/upload/presigned" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d "{\"size_bytes\":$SIZE,\"content_type\":\"application/pdf\"}")
UPLOAD_URL=$(echo "$R"|jq -r .upload_url); OBJ_KEY=$(echo "$R"|jq -r .object_key)
# ② PUT to R2
curl -s -X PUT "$UPLOAD_URL" -H "Content-Type: application/pdf" --data-binary "@$PDF"
# ③ Complete upload
R=$(curl -s -X POST "$BASE/api/v1/upload/presigned/complete" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d "{\"object_key\":\"$OBJ_KEY\",\"size_bytes\":$SIZE,\"filename\":\"$(basename "$PDF")\"}")
DOC_ID=$(echo "$R"|jq -r .document_id)
# ④ Create OCR task
R=$(curl -s -X POST "$BASE/api/v1/ocr" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d "{\"document_id\":\"$DOC_ID\",\"source_lang\":\"zh\",\"target_lang\":\"zh\",\"page_range\":\"1\",\"preprocess_with_ocr\":true}")
TASK_ID=$(echo "$R"|jq -r .task_id)
# ⑤ Poll
for i in $(seq 1 60); do
ST=$(curl -s "$BASE/api/v1/tasks/$TASK_ID" -H "Authorization: Bearer $KEY"|jq -r .status)
echo "[$i] $ST"; [ "$ST" = "completed" ] && break; [ "$ST" = "failed" ] && exit 1; sleep 3
done
# ⑥ Download Markdown
curl -s "$BASE/api/v1/tasks/$TASK_ID/markdown" -H "Authorization: Bearer $KEY" \
| jq -r .markdown > "${PDF%.pdf}.md"
echo "Done → ${PDF%.pdf}.md"
Usage:
export TRANS_API_KEY="sk-your-api-key"
bash pdf2md.sh /path/to/document.pdf
Python Example
import requests, time, os, sys
BASE = "https://www.translatepdfonline.com"
KEY = os.environ["TRANS_API_KEY"]
H = {"Authorization": f"Bearer {KEY}"}
pdf = sys.argv[1]
size = os.path.getsize(pdf)
# ① Presigned upload
r = requests.post(f"{BASE}/api/v1/upload/presigned", headers=H,
json={"size_bytes": size, "content_type": "application/pdf"})
data = r.json()
# ② PUT directly to R2
with open(pdf, "rb") as f:
requests.put(data["upload_url"], data=f, headers={"Content-Type": "application/pdf"})
# ③ Complete
r = requests.post(f"{BASE}/api/v1/upload/presigned/complete", headers=H,
json={"object_key": data["object_key"], "size_bytes": size, "filename": os.path.basename(pdf)})
doc_id = r.json()["document_id"]
# ④ Create task
r = requests.post(f"{BASE}/api/v1/ocr", headers=H,
json={"document_id": doc_id, "source_lang": "zh", "target_lang": "zh",
"page_range": "1", "preprocess_with_ocr": True})
task_id = r.json()["task_id"]
# ⑤ Poll
for i in range(60):
r = requests.get(f"{BASE}/api/v1/tasks/{task_id}", headers=H)
s = r.json()["status"]
print(f"[{i+1}] {s}")
if s in ("completed", "failed"): break
time.sleep(3)
# ⑥ Download Markdown
r = requests.get(f"{BASE}/api/v1/tasks/{task_id}/markdown", headers=H)
with open(f"{os.path.splitext(pdf)[0]}.md", "w") as f:
f.write(r.json()["markdown"])
print("Done")
Node.js Example
const fs = require("fs"), path = require("path");
const BASE = "https://www.translatepdfonline.com", KEY = process.env.TRANS_API_KEY;
const H = (extra) => ({ Authorization: `Bearer ${KEY}`, ...extra });
async function pdf2md(pdfPath) {
const size = fs.statSync(pdfPath).size;
// ① Presigned
let r = await fetch(`${BASE}/api/v1/upload/presigned`, { method: "POST",
headers: H({"Content-Type": "application/json"}),
body: JSON.stringify({ size_bytes: size, content_type: "application/pdf" }) });
const { upload_url, object_key } = await r.json();
// ② PUT
await fetch(upload_url, { method: "PUT",
headers: { "Content-Type": "application/pdf" }, body: fs.readFileSync(pdfPath) });
// ③ Complete
r = await fetch(`${BASE}/api/v1/upload/presigned/complete`, { method: "POST",
headers: H({"Content-Type": "application/json"}),
body: JSON.stringify({ object_key, size_bytes: size, filename: path.basename(pdfPath) }) });
const { document_id } = await r.json();
// ④ Create task
r = await fetch(`${BASE}/api/v1/ocr`, { method: "POST",
headers: H({"Content-Type": "application/json"}),
body: JSON.stringify({ document_id, source_lang:"zh", target_lang:"zh",
page_range:"1", preprocess_with_ocr:true }) });
const { task_id } = await r.json();
// ⑤ Poll
for (let i = 0; i < 60; i++) {
r = await fetch(`${BASE}/api/v1/tasks/${task_id}`, { headers: H() });
const { status } = await r.json();
console.log(`[${i+1}] ${status}`);
if (status === "completed" || status === "failed") break;
await new Promise(r => setTimeout(r, 3000));
}
// ⑥ Download
r = await fetch(`${BASE}/api/v1/tasks/${task_id}/markdown`, { headers: H() });
const out = pdfPath.replace(/\.pdf$/i, ".md");
fs.writeFileSync(out, (await r.json()).markdown);
console.log(`Done → ${out}`);
}
pdf2md(process.argv[2]);
Error Codes
| HTTP | code | Meaning |
|---|---|---|
| 401 | unauthorized |
API Key invalid or missing |
| 402 | insufficient_credits |
Not enough credits (response includes need/have) |
| 400 | invalid_language |
Unsupported language code |
| 400 | page_range_no_overlap |
Page range exceeds document page count |
| 400 | invalid_page_range |
Malformed page range (e.g., "abc") |
| 404 | — | Document or task not found (or not owned by this API Key) |
| 429 | rate_limited |
Exceeded 60 RPM, retry shortly |
| 503 | — | Storage not configured |
FAQ
When are credits deducted? POST /api/v1/ocr returns a credits_estimated estimate. The actual deduction happens when the OCR pipeline completes successfully.
Scanned PDFs? Set "preprocess_with_ocr": true to route through Baidu OCR for text recognition.
What about images in the Markdown? The current v1 API returns text only, with images as relative-path references (). Full export including image ZIP files is available via the Web OCR Workbench.
How long are uploaded files kept? PDFs are retained for 7 days in R2 storage.
Large files? 100 MB max. Split larger PDFs before uploading.
Same-language conversion (zh→zh)? When source and target are the same, the pipeline performs OCR recognition only — no translation — producing a clean Markdown text extraction.
Based on TranslatePDFOnline REST API v1. Production endpoint: https://www.translatepdfonline.com. See the official docs for updates.