API reference
The JSON job-input contract for the mineru-runpod worker, plus the success and failure response shapes.
This page documents the JSON payload contract the worker accepts. It mirrors the docstring in handler.py, which is the source of truth; this is a friendlier rendering.
Job input
Send a POST to /v2/{endpoint_id}/runsync (or /run for async) with an input object:
{
"input": {
"file_url": "https://example.com/report.pdf",
"start_page": 0,
"end_page": 99,
"lang": "en",
"backend": "vlm-auto-engine",
"formula_enable": true,
"table_enable": true,
"transport": "tarball_b64",
"formats": ["markdown", "content_list", "middle", "images"],
"basename": "my-doc"
}
}Complete cURL request:
curl --request POST "https://api.runpod.ai/v2/<endpoint-id>/runsync" \
--header "Authorization: Bearer $RUNPOD_API_KEY" \
--header "Content-Type: application/json" \
--data '{"input":{"file_url":"https://example.com/report.pdf","transport":"inline","formats":["markdown"]}}'Required (exactly one of)
The worker accepts PDF, image (PNG/JPEG/GIF/BMP/TIFF/WebP), DOCX, PPTX, and XLSX, auto-detected from the bytes. Provide exactly one input source; providing zero or two raises a validation error.
| Field | Type | Notes |
|---|---|---|
file_url | string | HTTP/HTTPS URL the worker can GET, resolving to a publicly routable host. Downloaded server-side with a 200 MB cap and a 120 s timeout. See MINERU_ALLOW_LOCAL_FETCH for serving documents from inside your own network |
file_b64 | string | Base64-encoded file bytes. RunPod caps the whole JSON request at 10 MB on /run and 20 MB on /runsync. Base64 adds about 33%, so keep raw files below roughly 7.5 MB and 15 MB respectively; for larger inputs use file_url or volume_path. |
volume_path | string | Absolute path to a file inside the container, under one of the worker's input roots. Useful for files mounted via a RunPod network volume or baked into the image |
Optional
| Field | Type | Default | Notes |
|---|---|---|---|
start_page | int | 0 | 0-based, inclusive |
end_page | int | -1 | 0-based, inclusive. Any negative value or omission means "to end of document". When set, must be >= start_page |
lang | string | "en" | OCR model hint for the pipeline backend; VLM backends ignore it. MinerU 3.4's public choices are ch, ch_server, korean, ta, te, ka, th, el, arabic, east_slavic, cyrillic, and devanagari. The default en remains a compatibility alias that normalizes to ch. See Input formats. |
backend | string | "vlm-auto-engine" | One of pipeline | vlm-auto-engine | vlm-http-client | hybrid-auto-engine | hybrid-http-client. The pipeline and hybrid-* backends use MinerU's PP-OCRv6 OCR models. See Choosing a GPU → Picking a backend |
effort | string | null | Hybrid backends only (hybrid-auto-engine / hybrid-http-client). "medium" (MinerU's default when omitted) or "high": "high" enables image/chart analysis at a speed cost, "medium" skips it for throughput. Rejected on non-hybrid backends. |
server_url | string | null | Required for *-http-client backends. URL of an external vLLM OpenAI-compatible server (e.g. https://your-host/v1) |
formula_enable | bool | true | Extract LaTeX equations |
table_enable | bool | true | Extract structured HTML tables |
transport | string | "tarball_b64" | How the worker ships output. "tarball_b64" (default, base64-encoded .tar.gz inside the entry), "inline" (per-format keys inside the entry, filterable via formats), or "s3" (presigned URL — requires BUCKET_* env vars on the endpoint). See Output modes. |
formats | array of string | all four | Subset of ["markdown", "content_list", "middle", "images"]. Selects which artifacts the inline payload contains. Omit (or pass all four) to get everything. No-op for tarball_b64 and s3 — those transports always ship a self-contained archive with all four artifacts. Empty list is rejected. |
basename | string | "doc" | Filename stem for output files. Must be alphanumeric with - or _, at most 128 characters, and short enough that the longest generated filename stays within 255 bytes — non-ASCII characters cost more than one byte each, so a CJK stem is limited to 78 characters |
archive_format | string | "tar.gz" | Archive container for the tarball_b64 and s3 transports: "tar.gz" (default) or "zip". No-op for inline. See Output modes → archive format. |
probe | bool | false | Diagnostic mode. {"probe": true} skips parsing and every field above, then returns the worker's filesystem layout plus the running MinerU version instead of a parse result — for checking baked model paths and volume mounts. On by default; set MINERU_DISABLE_PROBE=1 on the endpoint to turn it off, where callers should not see filesystem paths or model-cache details. Any value other than 0/false/no/off disables it, so a typo denies rather than exposes. |
URL inputs
file_url and server_url are checked before the worker acts on them:
-
Both must be
http://orhttps://with a host. Anything else is rejected with the field name in the message. -
file_urlmust additionally resolve to a publicly routable address — what a document URL handed to a serverless worker normally is. SetMINERU_ALLOW_LOCAL_FETCH=1on the endpoint when you serve documents from a host inside your own network, or when running the handler locally againsthttp://localhost. -
server_urlgets the shape check by default. SettingMINERU_ENFORCE_TARGET_POLICY=1additionally requires its host to be named inMINERU_ALLOWED_SERVER_HOSTS(comma-separated, exact host match) — including public hosts. Worth understanding, because the default is not the safe one.server_urlarrives in the job payload rather than the endpoint environment, so without enforcement any caller of your endpoint can point the worker at a loopback or link-local address — a cloud metadata endpoint included — and have it issue requests there from inside your network. The default is compatibility: a private model server is the ordinary way to run one, and enforcing by default would break every such deployment on a patch upgrade. If your endpoint is reachable by callers you do not control, setMINERU_ENFORCE_TARGET_POLICY=1and list your model server's host.Enforcement is an allow-list rather than an address check, and that is deliberate. Checking where a host resolves cannot protect this field: the engine's own HTTP client resolves the name again and opens the connection, so a host answering publicly at validation can answer privately at connect time.
mineru_vl_utilsbuilds itshttpx.Clientinternally with no transport to inject, and pinning the checked address would mean handing it a literal IP with aHostheader — which fails certificate validation for anyhttpstarget. A host the operator named is not subject to rebinding by a caller, which is what makes the list the stronger check.Do not use
MINERU_ALLOW_LOCAL_FETCHfor this. That flag is the operator's exemption for fetching documents from their own network, and it no longer reachesserver_urlat all.
I just want the Markdown — how do I get it?
Set "transport": "inline" and (optionally) filter to just markdown:
{
"input": {
"file_url": "https://example.com/report.pdf",
"transport": "inline",
"formats": ["markdown"]
}
}Response (truncated):
{
"ok": true,
"elapsed_seconds": 4.2,
"mineru_version": "3.4.x",
"results": [
{
"basename": "doc",
"source": "url:https://example.com/report.pdf",
"pages_requested": -1,
"markdown": "# Document title\n\nFirst paragraph...\n\n## Section\n\nA table:\n\n<table>...</table>\n"
}
],
"debug": {"...": "..."}
}For single-file jobs the markdown lives at result.results[0].markdown. Higher-level helpers: the Python MineruClient exposes MineruClient.first(result) to skip the indexing.
tarball_b64 (the default) also includes a .md file inside the gzipped tarball at {basename}.md — extract the tarball and the markdown is there too. Use inline when you want to read the markdown directly without unpacking; use tarball_b64 (or s3) when you also want the structured JSON / image files together.
Success response
{
"ok": true,
"elapsed_seconds": 18.4,
"mineru_version": "3.4.x",
"results": [
{
"basename": "doc",
"source": "url:https://example.com/report.pdf",
"pages_requested": 100,
"tarball_b64": "<base64-encoded gzipped tarball>"
}
],
"debug": {"...": "..."}
}Top-level keys (job-scoped)
| Field | Type | Notes |
|---|---|---|
ok | bool | Always true on success |
elapsed_seconds | float | Wall time inside the handler. Does not include cold-start time or transport |
mineru_version | string | The MinerU version that produced the parse (e.g. 3.4.4) |
results | array | One entry per parsed file. Single-file jobs have a one-element list. See below for the entry shape. |
debug | object | Observability data: backend, GPU info, phase timings, and a best-effort cached VLM path. See below. |
refresh_worker | bool | Present only when a REFRESH_WORKER_AFTER_* threshold has tripped. Tells RunPod to recycle the worker after returning. |
Per-entry keys (file-scoped, inside results[])
| Field | Type | Notes |
|---|---|---|
basename | string | Echo of the input basename, handy for correlating many concurrent jobs |
source | string | Echo of the input source: url:..., b64, or volume:/path/... |
pages_requested | int | The slice the caller asked for. -1 if end_page was open-ended |
tarball_b64 | string | Present when transport: "tarball_b64". Base64-encoded archive of the output directory — .tar.gz by default, .zip when archive_format: "zip" |
markdown, content_list, middle, images | various | Present when transport: "inline". The set of keys reflects the formats filter — see below |
tarball_url, tarball_url_expires_in, bucket_key, bucket_bytes | various | Present when transport: "s3". The archive is .tar.gz by default, .zip when archive_format: "zip". See Output modes → s3 |
degraded | object | Present only when the response is short of something. See Incomplete responses |
Each job parses one file. To process many documents, submit them as individual jobs and raise workers_max — RunPod's queue runs them in parallel across workers (see Scaling).
When transport: "inline"
Each entry contains the requested format keys directly. With the default formats=["markdown", "content_list", "middle", "images"]:
| Field | Type | Notes |
|---|---|---|
markdown | string | The full Markdown rendering of the document |
content_list | array | Flat list of MinerU block objects. Common types include text, equation, table, image, code, and list; backend-specific auxiliary types such as header and page_number can also appear. Suitable for RAG chunking |
middle | object | MinerU's intermediate representation with layout, bounding boxes, reading order |
images | object | {filename: base64-encoded-bytes} for each extracted image; the original image filename and format are preserved |
content_list is a convenient integration surface, but it is MinerU output,
not a schema owned by this wrapper. Fields can vary by backend and MinerU
version. These are the common 3.4.x fields used by the examples in this repo:
Block type | Common fields | Notes |
|---|---|---|
text | text, optional text_level | text_level can indicate a heading, but still needs document-specific validation. |
list | list_items | Ordered markers may be preserved inside the item strings. |
table | table_body, table_caption, table_footnote | table_body is HTML. Caption and footnote fields are arrays when present. |
code | code_body, code_caption | Code may already include a fenced block. |
equation | text | Usually LaTeX, sometimes already wrapped in math delimiters. |
image | content, image_caption, sometimes img_path | content is a generated description; use the images map or archive for image bytes. |
header, page_number, and other auxiliary types | backend-specific | Headers, footers, page numbers, margin notes, and footnotes can appear alongside document content. |
Most blocks also include zero-based page_idx. Treat unknown block types and
extra fields as forward-compatible data rather than rejecting the response.
The document-tree case study
shows the rendering and verification rules used on a 5,039-page parse.
Filtering with formats
The formats field whitelists which artifacts the inline payload contains. Omitted keys are absent from the entry — not present-as-empty. Useful when only the markdown matters and the per-page images would otherwise bloat the response.
{
"input": {
"file_url": "https://example.com/report.pdf",
"transport": "inline",
"formats": ["markdown", "content_list"]
}
}The resulting entry has markdown and content_list only; middle and images are not present in the response. Empty list is rejected (formats: [] is a validation error).
For transport: "tarball_b64" and transport: "s3", formats is a no-op: the tarball always carries the full set of four artifacts (filtering inside an archive would be confusing for downstream callers).
Incomplete responses
A parse can produce a file that cannot then be read — bytes that are not the UTF-8 the artifact is declared as, JSON truncated by a disk that filled mid-write, an image the filesystem will no longer describe. One corrupt file does not fail the job: the artifact comes back as its empty default, or the archive ships without that member, and the entry gains a degraded object saying so.
{
"basename": "doc",
"source": "url:https://example.com/report.pdf",
"markdown": "# The document's real text\n",
"content_list": [],
"degraded": {
"count": 1,
"items": [
{"artifact": "content_list", "file": "doc_content_list.json",
"reason": "unreadable", "error_type": "JSONDecodeError"}
]
}
}| Field | Type | Notes |
|---|---|---|
count | int | Everything lost. items stops at 50, so count above items.length means the list is abridged |
items[].artifact | string | null | Which response key it cost. null for an archive member, which is not one of the four formats |
items[].file | string | Filename only — the output directory is a temp path inside the worker |
items[].reason | string | unreadable, unresolvable, outside_output_dir, or unsafe_name |
items[].error_type | string | Exception class name, when an exception revealed the problem |
degraded is absent on an intact job, so its presence is the signal. Treat an entry that has it as a document to reprocess: ok is true and nothing else will flag it, because from the outside an empty content_list looks the same as a document that had no tables in it.
markdown is the exception. It is the document, so a job that cannot produce readable Markdown fails with ok: false rather than returning an empty string — a failed job can be retried, a successful one that quietly returned nothing cannot. The other three artifacts degrade.
Every item is also emitted as a response degraded warning on stdout, carrying the same fields plus the job_id, and counted as mineru.degraded.total when OpenTelemetry is configured. The field is the per-document signal — the only thing that says which document to reprocess. The log line and the counter are the fleet-wide ones, and they are what tells you the rate is non-zero at all.
Debug observability
Every response includes a top-level debug block with information that lets you correlate a parse to its environment without having to read worker logs:
{
"debug": {
"backend": "vlm-auto-engine",
"model_dir": "/root/.cache/huggingface/hub/models--opendatalab--MinerU2.5-Pro-2605-1.2B/snapshots/<hash>",
"gpu": {
"available": true,
"name": "NVIDIA RTX 4090",
"compute_capability": "8.9",
"total_memory_gb": 23.99
},
"phase_ms": {
"fetch_input": 12,
"mineru_parse": 18420,
"package": 95
}
}
}| Field | Notes |
|---|---|
backend | The backend that ran. Echoes the input or the default |
model_dir | Best-effort path to a cached MinerU VLM snapshot. It confirms a matching cache directory exists, not which model the selected backend loaded. Pipeline weights are baked separately. |
gpu | Card name, compute_capability (8.6 = Ampere, 8.9 = Ada, 9.0 = Hopper, 12.0 = Blackwell), VRAM. Helps debug "why did my job land on a different card than my pool config?" |
phase_ms | Per-phase timings: fetch_input (download/decode), mineru_parse (MinerU's aio_do_parse), package (tarball or inline assembly) |
On failure, debug still contains gpu, model_dir, and whatever phase_ms was collected before the error.
Failure response
When the handler raises or returns an error, the response sets ok: false and includes a top-level error key. RunPod marks the job FAILED in the dashboard based on the presence of that key. There is no results list on a failure response.
{
"error": "ValueError: must provide exactly one of file_url / file_b64 / volume_path",
"ok": false,
"elapsed_seconds": 0.1,
"mineru_version": "3.4.x",
"traceback": "Traceback (most recent call last):\n File ...",
"debug": {"gpu": {"...": "..."}, "model_dir": "...", "phase_ms": {"...": "..."}}
}| Field | Type | Notes |
|---|---|---|
error | string | Type name + message, e.g. ValueError: ... |
ok | bool | Always false on failure |
elapsed_seconds | float | Time before the error |
mineru_version | string | Version that was running |
traceback | string | Last 5 frames of the Python traceback; useful for debugging |
debug | object | Same shape as the success response — gpu, model_dir, and whatever phase_ms was collected before the failure |
error and traceback are normalized before they are returned: URLs inside them keep their scheme, host and path but drop credentials, query and fragment, and very long text is truncated. The same text goes to the worker's logs and, when OpenTelemetry export is configured, to the collector — so one failure reads the same in all three places.
Progress updates (streaming)
The handler posts {"status": "IN_PROGRESS", "output": ...} events during a parse, which is what /status and /stream report while the job runs. Phases:
{"phase": "fetching_input"}
{"phase": "parsing", "input_bytes": 1234567, "input_format": "pdf", "start_page": 0, "end_page": 99}Each update is awaited before the parse continues, rather than posted from a background thread the handler does not wait for. Progress travels to the same job-results endpoint as the final result, so an update still in flight at completion can be applied after the COMPLETED status and leave a finished job stuck IN_PROGRESS. Waiting for it is what keeps the two in order regardless of how fast the parse is — office inputs (DOCX/PPTX/XLSX) skip model inference and can finish in under 100 ms.
There is deliberately no packaging phase event either: it would carry no information a caller can act on, and no update is emitted after the parse.
With the RunPod SDK, submit asynchronously and stream from the returned job:
job = endpoint.run({"file_url": "https://example.com/report.pdf"})
for update in job.stream():
print(update)The HTTP equivalent is GET /v2/{endpoint_id}/stream/{job_id}. The
MineruClient wrapper only supports run_sync, so use the RunPod SDK or HTTP
directly when you need progress.
Validation behaviour
The handler validates input before doing any work:
- Field types and bounds via
runpod.serverless.utils.rp_validator. Wrong type, missing required field, out-of-range value, or unknown field name raisesValueError. - XOR source rule. Exactly one of
file_url,file_b64,volume_pathmust be set. Zero or two raisesValueError. - Basename safety. Every character in
basenamemust be alphanumeric,-, or_, and the whole stem at most 128 characters. Unicode letters and digits are accepted — and because filesystems bound a filename in bytes rather than characters, the stem plus the longest suffix the worker appends (_content_list_v2.json) must also stay within 255 bytes. That is 234 characters' worth of headroom for ASCII, but only 78 for three-byte characters such as CJK. - Inline file size. The worker rejects decoded
file_b64data over 20 MB — measured first on the encoded string, then on the decoded bytes. RunPod's whole-request limits are reached earlier because base64 expands the raw file; use the practical thresholds above. - Format detection. The first few bytes must match a known signature (PDF / image / OOXML); otherwise
ValueError. formatsmembership. Each entry must be one of["markdown", "content_list", "middle", "images"]; empty list is rejected; duplicates are silently collapsed.- URL fields.
file_urlandserver_urlmust behttp(s)with a host.file_urlmust also resolve to a publicly routable address unlessMINERU_ALLOW_LOCAL_FETCH=1. UnderMINERU_ENFORCE_TARGET_POLICY=1,server_url's host must appear inMINERU_ALLOWED_SERVER_HOSTS;MINERU_ALLOW_LOCAL_FETCHdoes not affect it. See URL inputs. volume_pathroots. The path must be absolute and, once resolved, sit under one of the worker's input roots. See Whatvolume_pathaccepts.langshape. A short script/language code: letters, digits,-or_, up to 32 characters.- Page range.
start_pagemust be>= 0; anend_pageof0or more must be>= start_page. If the endpoint setsMINERU_MAX_PAGES_PER_JOB, an explicit range larger than that is rejected — see Scaling.
All validation errors produce a failure response with error: "ValueError: ..." and the job is marked FAILED.
Source of truth
This page is regenerated by hand when the contract changes. The authoritative source is the docstring at the top of handler.py plus the INPUT_SCHEMA dict and validate_input function in worker/schema.py. If you see a discrepancy, the code wins; please open an issue so the docs page can be updated.
The formats values and the transport list are not spelled out in the schema: the formats are the keys of the output manifest in worker/harness.py, in declaration order, and the transports are whatever the runpod-doc-worker harness can pack. Response assembly — the results entries, the archive, the presigned URL — is that harness's; which files go into it is the manifest's.
Last updated on