Migrate from the MinerU API
Evaluate a move from the MinerU cloud API to your own RunPod endpoint with a compatibility client for the create, poll, and download lifecycle.
Already calling the official MinerU cloud API (mineru.net/api/v4/...) and hitting a daily page quota, a region requirement, or a need to control cost and versions? You can deploy the open-source MinerU runtime in your RunPod account and preserve the create, poll, and download shape for the supported subset below. Cloud and self-hosted output can still differ when model versions, backends, or configuration differ, so compare real documents before migrating.
MineruApiClient provides compatible create_task and get_task response
shapes over a RunPod endpoint. It is a migration facade, not a complete clone
of the cloud API.
What this is — and isn't
This is a trial and migration on-ramp, not a permanent production interface. It lets you evaluate self-hosting against the SaaS with near-identical code. Once you've switched, the native MineruClient gives you the worker's richer features (inline markdown, format filtering, volume_path, the hybrid / http-client backends). Use the compat client to get in the door; use the native client long-term.
Prerequisites
- A deployed mineru-runpod endpoint and RunPod API key
- An S3-compatible bucket configured through the endpoint's four
BUCKET_*variables; the compatibility client needs it to returnfull_zip_url - A representative test set and expected outputs from the cloud API
Install
pip install "mineru-client @ git+https://github.com/sergeyshmakov/mineru-runpod"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 requests
from mineru_client import MineruApiClient
pdf_url = "https://example.com/report.pdf"
# A — official MinerU cloud API
saas = 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 (compatible 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 completion
rp.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
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 yourselfThe 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
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
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
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
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
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.
Last updated on