Private Cloud OCR: batch jobs
These endpoints exist on a Private Cloud OCR deployment,
not on api.mathpix.com. There is no app_id/app_key; requests are plain HTTP to your
deployment's host. The walkthrough is the batch jobs guide.
| Endpoint | Description |
|---|---|
| POST /pco/v1/jobs | Create a job from a folder, or return the existing one for the same spec; ?dry_run=true estimates |
| GET /pco/v1/jobs | List jobs, newest first, cursor pagination |
| GET /pco/v1/jobs/{job_id} | The job object |
| GET /pco/v1/jobs/{job_id}/files | Per-file listing, filterable by status |
| GET /pco/v1/jobs/{job_id}/files/{document_id} | One file, with per-page errors |
| GET /pco/v1/jobs/{job_id}/report | The NDJSON report, streamed |
| POST /pco/v1/jobs/{job_id}/retry | Requeue failed files |
| POST /pco/v1/jobs/{job_id}/cancel | Stop the job |
| DELETE /pco/v1/jobs/{job_id} | Forget the job record; the bucket is untouched |
| Webhook configuration and deliveries | Signing secret, rotation, test delivery, delivery state |
Errors use the hosted envelope, {"error": "...", "error_info": {"id": "...", "message": "..."}}.
Submission-time ids: input_folder_required (no trailing slash), input_folder_or_files,
input_folder_unreadable, output_folder_unwritable, invalid_layout, invalid_options (the
nested error_info carries the /v3/pdf id), invalid_policy, invalid_callback,
unsupported_uri_scheme. Later: job_not_found, job_not_retryable.
POST /pco/v1/jobs
Create a job. Returns 201 with the job object, or 200 with the existing job
when an identical spec (input, output folder, layout, options, client_reference) is already
known. The job id is a hash of those fields, so a resubmission attaches rather than duplicates.
Request body
| field | type | required | meaning |
|---|---|---|---|
name | string | no | a label, echoed in the job object |
input.folder | folder URI | one of folder/files | s3://bucket/path/, gs://bucket/path/, https://ACCOUNT.blob.core.windows.net/container/path/; trailing slash required |
input.files | list of object URIs, 1–10,000 | one of folder/files | an explicit list instead of a folder; output.folder is then required |
input.recursive | bool, default true | no | descend into subfolders |
input.include, input.exclude | list of globs | no | narrow the enumeration |
output.folder | folder URI | no | default: input.folder |
output.layout | alongside | mirror | flat | no | default alongside, which requires output.folder to equal input.folder |
output.on_existing | skip | replace | no | default skip: a document whose .mmd already exists at its output URI is skipped |
options.conversion_formats | object | no | the hosted conversion_formats |
options.extra_outputs | list | no | .lines.json (always written anyway) and .lines.mmd.json |
options.ocr | object | no | any v3/pdf option except the input fields; validated with the same error ids |
policy.max_pages_per_document | int, default 5000 | no | |
policy.per_page_timeout_seconds | int, default 15 | no | |
policy.max_retries | int, default 3 | no | automatic retries per document |
policy.priority | low | normal | high | no | default normal |
callback_url, callback_events, callback_headers | no | the hosted webhook fields; events job.completed (default), file.error, file.completed | |
client_reference | string | no | your reference; part of the job id, so two specs differing only here are two jobs |
Query parameters
| parameter | meaning |
|---|---|
dry_run=true | enumerate and estimate without creating anything; the response is the estimate below |
Example
curl -s http://pco.internal:8080/pco/v1/jobs -H 'Content-Type: application/json' -d '{"input": {"folder": "s3://example-docs/batch1/"}, "options": {"conversion_formats": {"md": true}}}'
Dry-run response
{
"dry_run": true,
"files": {"discovered": 3, "excluded": 1, "already_processed": 0, "by_type": {"pdf": 2, "docx": 1}},
"pages": {"files_sampled": 2, "estimated_total": 4},
"estimated_gpu_hours": null,
"deployment": {"gpu_workers": 1, "measured_pages_per_minute": null}
}
already_processed is what on_existing: skip would skip; estimated_gpu_hours is null until
the deployment has processed enough pages to have a measured rate.
Job object
Returned by POST, GET, retry and cancel; the list endpoint omits spec.
{
"job_id": "j_7f3a9c1e4b",
"name": "arxiv-2024-q3",
"status": "processing",
"created_at": "2026-09-11T14:02:11Z",
"started_at": "2026-09-11T14:02:12Z",
"finished_at": null,
"enumeration": {"status": "completed", "files_listed": 18240, "files_excluded": 12},
"files": {"discovered": 18240, "queued": 11200, "processing": 24, "completed": 6900, "failed": 96, "skipped": 20},
"pages": {"completed": 231540, "failed": 412},
"throughput_pages_per_minute": 1840.5,
"eta_seconds": 5400,
"output": {"folder": "s3://example-docs/arxiv/2024-q3/", "layout": "alongside",
"manifest_uri": "s3://example-docs/arxiv/2024-q3/_mathpix/j_7f3a9c1e4b/manifest.json",
"report_uri": "s3://example-docs/arxiv/2024-q3/_mathpix/j_7f3a9c1e4b/report.jsonl"},
"versions": {"ocr_version": "SuperNet-200", "pdf_version": "SuperNet-200p2"},
"client_reference": null,
"spec": {"input": {"...": "as submitted, defaults filled in"}}
}
status is one of enumerating, processing, completed, completed_with_failures,
cancelled, error (with error_info). files.discovered equals the sum of the other five
counters once enumeration is complete.
GET /pco/v1/jobs
Query parameters
| parameter | meaning |
|---|---|
status | one of the job statuses |
limit | default 50 |
cursor | from the previous page's next_cursor |
{"jobs": [{"job_id": "j_7f3a9c1e4b", "status": "processing", "files": {"...": "..."}, "pages": {"...": "..."}}], "next_cursor": null}
GET /pco/v1/jobs/{job_id}
The job object. 404 job_not_found once the record has been deleted or has
expired (records live 30 days in Redis; the manifest and report in the bucket remain).
GET /pco/v1/jobs/{job_id}/files
Query parameters
| parameter | meaning |
|---|---|
status | queued, processing, completed, failed, skipped |
limit | default 100 |
cursor | pagination |
File object
{
"document_id": "3c2a9f7e1b8d4e6fa0c1b2d3e4f5a6b7",
"key": "arxiv/2024-q3/2401.01235.pdf",
"status": "completed",
"pages": 14,
"pages_failed": 1,
"attempts": 1,
"error_id": null,
"error_message": null,
"outputs": {"mmd": "s3://example-docs/arxiv/2024-q3/2401.01235.mmd", "md": "s3://example-docs/arxiv/2024-q3/2401.01235.md", "lines.json": "s3://example-docs/arxiv/2024-q3/2401.01235.lines.json"},
"started_at": "2026-09-11T14:20:01Z",
"finished_at": "2026-09-11T14:21:37Z"
}
document_id is deterministic per job and key. A completed file with pages_failed above 0
finished with a page-level error inside it. The response is {"files": [...], "next_cursor": ...}.
GET /pco/v1/jobs/{job_id}/files/{document_id}
The file object plus page_errors ([{"page": 7, "error_id": "...", "error_message": "..."}]),
conversion_status per format when formats were requested, and versions.
GET /pco/v1/jobs/{job_id}/report
Content-Type: application/x-ndjson, one file object per line for every file in
a terminal state, the same bytes as report.jsonl in the bucket, streamed. ?status= filters.
This is a read, not a drain.
POST /pco/v1/jobs/{job_id}/retry
Optional body {"error_ids": ["pdf_content_type"]} to requeue only failures with those ids; no
body requeues every failed file. Returns the job object with status back to processing and a
top-level requeued count. 409 job_not_retryable while the job is enumerating or processing.
POST /pco/v1/jobs/{job_id}/cancel
No body. Enumeration stops, queued files are marked skipped with error_id: cancelled,
in-flight files finish and are recorded, and the job ends cancelled. Cancelling a terminal job
returns it unchanged. To pick a cancelled job back up, resubmit the same spec with
on_existing: skip.
DELETE /pco/v1/jobs/{job_id}
204. Removes the job record and per-file status from the deployment's Redis. Nothing in your
bucket is touched, including the manifest and report.
Webhook configuration and deliveries
| Endpoint | Description |
|---|---|
GET /pco/v1/webhook-config | {"secret": "...", "previous_secret_expires_at": ...}: the per-deployment signing secret and, during a rotation, when the previous one stops signing |
POST /pco/v1/webhook-config/secret | rotate; body {"force": true} to rotate again while a grace window is open, otherwise 409 |
POST /pco/v1/webhook-config/test | body {"callback_url": "...", "callback_headers": {...}}; sends one signed test delivery and reports the receiver's answer |
GET /pco/v1/deliveries?job_id=... | {"deliveries": [...]} with status, attempts and last HTTP status per notification |
Payloads and headers are the hosted API's: job.completed carries job_id, status,
file_count, files_completed, files_errored, num_pages_sent, num_pages_completed;
file.error and file.completed carry file_id (the document id), custom_id (the storage
key), job_id, status, num_pages, num_pages_completed and, on error, error_info.
Signature: Mathpix-Signature: t=<unix_ts>,v1=<hex> over {timestamp}.{body} with HMAC-SHA256,
dual-signed for 24 hours after a rotation. See webhooks.
See also
- Batch jobs guide
- Operator endpoints
- Process Documents, which a deployment serves unchanged