Skip to main content

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.

EndpointDescription
POST /pco/v1/jobsCreate a job from a folder, or return the existing one for the same spec; ?dry_run=true estimates
GET /pco/v1/jobsList jobs, newest first, cursor pagination
GET /pco/v1/jobs/{job_id}The job object
GET /pco/v1/jobs/{job_id}/filesPer-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}/reportThe NDJSON report, streamed
POST /pco/v1/jobs/{job_id}/retryRequeue failed files
POST /pco/v1/jobs/{job_id}/cancelStop the job
DELETE /pco/v1/jobs/{job_id}Forget the job record; the bucket is untouched
Webhook configuration and deliveriesSigning 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

fieldtyperequiredmeaning
namestringnoa label, echoed in the job object
input.folderfolder URIone of folder/filess3://bucket/path/, gs://bucket/path/, https://ACCOUNT.blob.core.windows.net/container/path/; trailing slash required
input.fileslist of object URIs, 1–10,000one of folder/filesan explicit list instead of a folder; output.folder is then required
input.recursivebool, default truenodescend into subfolders
input.include, input.excludelist of globsnonarrow the enumeration
output.folderfolder URInodefault: input.folder
output.layoutalongside | mirror | flatnodefault alongside, which requires output.folder to equal input.folder
output.on_existingskip | replacenodefault skip: a document whose .mmd already exists at its output URI is skipped
options.conversion_formatsobjectnothe hosted conversion_formats
options.extra_outputslistno.lines.json (always written anyway) and .lines.mmd.json
options.ocrobjectnoany v3/pdf option except the input fields; validated with the same error ids
policy.max_pages_per_documentint, default 5000no
policy.per_page_timeout_secondsint, default 15no
policy.max_retriesint, default 3noautomatic retries per document
policy.prioritylow | normal | highnodefault normal
callback_url, callback_events, callback_headersnothe hosted webhook fields; events job.completed (default), file.error, file.completed
client_referencestringnoyour reference; part of the job id, so two specs differing only here are two jobs

Query parameters

parametermeaning
dry_run=trueenumerate 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

parametermeaning
statusone of the job statuses
limitdefault 50
cursorfrom 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

parametermeaning
statusqueued, processing, completed, failed, skipped
limitdefault 100
cursorpagination

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

EndpointDescription
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/secretrotate; body {"force": true} to rotate again while a grace window is open, otherwise 409
POST /pco/v1/webhook-config/testbody {"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