Migrate from the MinerU API
Already calling the official MinerU cloud API (mineru.net/api/v4/...) and hitting a daily page quota, a privacy/region wall, or just want control over cost and model version? You can run the same MinerU engine on your own RunPod serverless endpoint and keep almost all of your existing code.
MineruApiClient reproduces the official API’s surface — create_task / get_task, returning the same response dicts — but talks to your RunPod endpoint underneath. Migration is import + constructor + auth.
Install
Section titled “Install”pip install "mineru-client @ git+https://github.com/sergeyshmakov/mineru-runpod"uv pip install "mineru-client @ git+https://github.com/sergeyshmakov/mineru-runpod"Compare before you commit (recommended first step)
Section titled “Compare before you commit (recommended first step)”The lowest-risk way to evaluate self-hosting is to run your real documents through both the MinerU SaaS and a RunPod endpoint with the same loop, then diff the markdown and compare cost. The compat client makes the two paths almost identical:
import requestsfrom mineru_client import MineruApiClient
pdf_url = "https://example.com/report.pdf"
# A — official MinerU cloud APIsaas = requests.post( "https://mineru.net/api/v4/extract/task", headers={"Authorization": f"Bearer {MINERU_TOKEN}"}, json={"url": pdf_url, "model_version": "vlm"},).json()saas_task_id = saas["data"]["task_id"]# ... poll GET /api/v4/extract/task/{saas_task_id} until state == "done" ...
# B — self-hosted on RunPod (same params, same lifecycle)rp = MineruApiClient(endpoint_id="<endpoint-id>", api_key="<runpod-key>")rp_task_id = rp.create_task(pdf_url, model_version="vlm")["data"]["task_id"]rp_done = rp.wait_for_task(rp_task_id) # convenience: poll to completionrp.download_results(rp_done, "./out") # unpack the result archiveCompare the two full.md outputs side by side; pair this with the cost math in the launch post to estimate $/page for your volume.
Side-by-side migration
Section titled “Side-by-side migration”The lifecycle is the same on both sides — create a task, poll until done, fetch the result archive — so most code changes are just the client object.
import requests, time
H = {"Authorization": f"Bearer {MINERU_TOKEN}", "Content-Type": "application/json"}
res = requests.post( "https://mineru.net/api/v4/extract/task", headers=H, json={"url": pdf_url, "model_version": "vlm", "enable_table": True},).json()task_id = res["data"]["task_id"]
while True: data = requests.get( f"https://mineru.net/api/v4/extract/task/{task_id}", headers=H ).json()["data"] if data["state"] in ("done", "failed"): break time.sleep(2)
zip_url = data["full_zip_url"] # download + unzip yourselffrom mineru_client import MineruApiClient
client = MineruApiClient(endpoint_id="<endpoint-id>", api_key="<runpod-key>")
res = client.create_task(pdf_url, model_version="vlm", enable_table=True)task_id = res["data"]["task_id"]
done = client.wait_for_task(task_id) # or poll client.get_task(task_id)data = done["data"]
client.download_results(done, "./out") # unpacks the archive for youThe response dicts have the same shape ({"code": 0, "msg": "ok", "data": {...}}), so any code that reads data["state"], data["full_zip_url"], or data["err_msg"] keeps working.
Parameter mapping
Section titled “Parameter mapping”create_task(url, ...) accepts the official parameter names and translates them to the worker’s:
| MinerU API param | Maps to | Notes |
|---|---|---|
url |
worker file_url |
URL-based submission (as on the SaaS single-task endpoint) |
model_version |
backend |
pipeline → pipeline, vlm → vlm-auto-engine |
enable_formula / enable_table |
formula_enable / table_enable |
direct |
language |
lang |
default ch, like MinerU; affects the pipeline backend only |
page_ranges |
start_page / end_page |
single contiguous 1-based range ("5" or "2-6") |
data_id |
echoed back in get_task |
tracking id; see the gaps note below |
callback |
not supported → raises | RunPod’s webhook payload (raw /status) differs from MinerU’s signed {checksum, content} callback; poll with get_task / wait_for_task |
Task states
Section titled “Task states”get_task returns the same state machine, mapped from RunPod’s job status:
| RunPod status | data.state |
|---|---|
IN_QUEUE |
pending |
IN_PROGRESS |
running |
COMPLETED |
done (carries full_zip_url) |
FAILED / TIMED_OUT / CANCELLED |
failed (carries err_msg) |
Getting the result: full_zip_url
Section titled “Getting the result: full_zip_url”full_zip_url is a presigned .zip — the compat client requests archive_format: "zip", so the container matches the official cloud API. One requirement:
- Your endpoint must have object storage configured. Set the
BUCKET_*env vars (BUCKET_ENDPOINT_URL,BUCKET_NAME,BUCKET_ACCESS_KEY_ID,BUCKET_SECRET_ACCESS_KEY) on the endpoint — any S3-compatible store (Cloudflare R2, Backblaze B2, MinIO) works. See Output modes → s3. If they’re missing, the task comes backfailedwith an actionable message.
client.download_results(done, "./out") fetches and unpacks the archive for you (autodetecting .zip vs .tar.gz), so you usually don’t need to touch the URL directly.
Known gaps
Section titled “Known gaps”The compat client raises a clear error rather than silently mis-parsing when you ask for something the self-hosted worker can’t do:
| Feature | Status |
|---|---|
create_task / get_task, state machine, full_zip_url |
✅ supported (S3 configured) |
model_version: "MinerU-HTML" |
❌ no HTML-specialised model → raises |
extra_formats (docx / html / latex) |
❌ worker emits markdown + content_list + middle + images only → raises |
page_ranges multi-range ("2,4-6") |
❌ one contiguous slice per job → raises (submit separate tasks) |
is_ocr, no_cache, cache_tolerance |
accepted, no-op (no worker equivalent) |
callback / seed |
❌ callback raises — RunPod webhooks deliver a different, unsigned payload than MinerU’s signed {checksum, content} callback; seed accepted but unused |
data_id |
echoed from an in-process map; a get_task call in a different process won’t echo it |
data.extract_progress (page counts) |
not populated — RunPod’s status API exposes state, not per-page progress |
Batch (POST /extract/task/batch, file-urls/batch) |
not offered — submit tasks individually and raise workers_max; RunPod’s queue runs them in parallel across workers (Scaling) |
When to switch to the native client
Section titled “When to switch to the native client”Once you’ve validated the output and decided to stay, move to MineruClient. Self-hosting’s whole payoff is the features the SaaS API doesn’t expose:
- Inline markdown without unpacking an archive (
transport: "inline",formats: ["markdown"]) - Local / volume inputs (
file_b64,volume_path) — no need to host files at a URL first - Every backend, including
hybrid-*and*-http-client - No S3 requirement for getting results back
The compat client’s job is to get you comparing and migrating; the native client is where you live afterward.