← Back to Blog

Convert PDF to Markdown with the TranslatePDFOnline API: A Complete Guide

Published on August 6, 2026
06bee2ef-1b53-4f84-b305-e5c698294cd7

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

  1. Create an account and generate an API Key at Settings → API Keys (format: sk- prefix, shown only once)
  2. Ensure sufficient credits (default: 10 credits per page)
  3. Authentication: Authorization: Bearer sk-xxx or X-API-Key: sk-xxx
  4. 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: queuedprocessingcompleted | 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: ![image:layout_id](./ocr-{taskId}_assets/...)

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 (![image:id](./ocr-{taskId}_assets/...)). 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.