New release: November 2026
On this page

Every endpoint, generated from the API specification. Parameters, bodies, responses and fields on this page are read from the same OpenAPI file you can download here.

Examples read the host from the environment. Hosted service: export OCR_BASE=https://api.ocr-software.com; your own server: export OCR_BASE=http://127.0.0.1:8080. A call marked “Key” needs Authorization: Bearer ocr_...; put your key in OCR_KEY. “No key” calls need none.

Every call can also answer 429 rate_limited or 503 with Retry-After: wait that long, then retry.

Download openapi.json

Let your agent set it up

One Markdown file holds the same API in text an agent reads. Agent guide · This page as Markdown · llms.txt

Service

GET /

Entry point

Authentication: none, open without an API key.

Fixed pointers to the public routes and how to get a key; no data of its own, no key needed. Links are absolute when the server knows its public address, else relative.

Responses

Status Description Body
200 Links application/json object

200 application/json

Field Type Required Description
service string
openapi string
catalog string
status string
health string
docs string
api_key string How to get a key (the same sentence as in the 401 answer).

Example

curl -s "$OCR_BASE/"
import os, urllib.request

url = os.environ["OCR_BASE"] + f"/"
req = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(req) as r:
    print(r.status, r.read().decode())

GET /healthz

Liveness

Authentication: none, open without an API key.

Responses

Status Description Body
200 ok application/json object

200 application/json

Field Type Required Description
status one of ok
revision string Commit the running build was made from; dev for a local build.

Example

curl -s "$OCR_BASE/healthz"
import os, urllib.request

url = os.environ["OCR_BASE"] + f"/healthz"
req = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(req) as r:
    print(r.status, r.read().decode())

GET /v1/status

Load of the service (public, no key)

Authentication: none, open without an API key.

How busy the service is and how long a new page waits, per class. Only counts and seconds; it names no key, job, address or file name. The wait uses the same estimate as queue in a job status. Limited per client address; Retry-After says when to retry.

Responses

Status Description Body
200 Load and queue per class application/json object
429 Too many status requests from this address. Retry-After says when to retry.

200 application/json

Field Type Required Description
load_percent integer (0 to 100) Occupied worker slots as a share of all slots.
classes map of object Keys paid and free.

Example

curl -s "$OCR_BASE/v1/status"
import os, urllib.request

url = os.environ["OCR_BASE"] + f"/v1/status"
req = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(req) as r:
    print(r.status, r.read().decode())

GET /openapi.json

This specification

Authentication: none, open without an API key.

Responses

Status Description Body
200 OpenAPI document

Example

curl -s "$OCR_BASE/openapi.json"
import os, urllib.request

url = os.environ["OCR_BASE"] + f"/openapi.json"
req = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(req) as r:
    print(r.status, r.read().decode())

Pipelines and usage

GET /v1/catalog

What the service offers (public, no key)

Authentication: none, open without an API key.

Pipelines with description, capabilities, formats, features and price per page, plus the query parameters of POST /v1/jobs. The same registry as GET /v1/pipelines, the same for everyone, cached for a minute. It names no host, revision, load or worker; a pipeline that cannot run right now has available false and one generic note. Limited per client address; Retry-After says when to retry.

In the guide

Responses

Status Description Body
200 The catalog application/json object
429 Too many catalog requests from this address. Retry-After says when to retry.

200 application/json

Field Type Required Description
schema integer Version of this layout.
pipelines list of Pipeline
formats list of string
features list of string
parameters list of object

Example

curl -s "$OCR_BASE/v1/catalog"
import os, urllib.request

url = os.environ["OCR_BASE"] + f"/v1/catalog"
req = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(req) as r:
    print(r.status, r.read().decode())

GET /v1/pipelines

List pipelines with factor, price and capabilities

Authentication: API key in the header Authorization: Bearer $OCR_KEY.

In the guide

Responses

Status Description Body
200 Pipelines application/json object
401 unauthorized application/json Errors
429 Codes: rate_limited, locked_out (too many wrong keys from this address or for this key prefix; a valid key from a locked address still passes). Retry-After says when to retry. application/json Errors

200 application/json

Field Type Required Description
pipelines list of Pipeline

Example

curl -s "$OCR_BASE/v1/pipelines" \
  -H "Authorization: Bearer $OCR_KEY"
import os, urllib.request

url = os.environ["OCR_BASE"] + f"/v1/pipelines"
req = urllib.request.Request(url, method="GET", headers={"Authorization": "Bearer " + os.environ["OCR_KEY"]})
with urllib.request.urlopen(req) as r:
    print(r.status, r.read().decode())

GET /v1/pipelines/{ref}

One pipeline

Authentication: API key in the header Authorization: Bearer $OCR_KEY.

In the guide

Parameters

Name In Type Required Description
ref path string yes name@version

Responses

Status Description Body
200 Pipeline application/json Pipeline
401 unauthorized application/json Errors
404 not_found application/json Errors
429 Codes: rate_limited, locked_out (too many wrong keys from this address or for this key prefix; a valid key from a locked address still passes). Retry-After says when to retry. application/json Errors

Example

curl -s "$OCR_BASE/v1/pipelines/$REF" \
  -H "Authorization: Bearer $OCR_KEY"
import os, urllib.request

REF = "..."  # the ref of your call
url = os.environ["OCR_BASE"] + f"/v1/pipelines/{REF}"
req = urllib.request.Request(url, method="GET", headers={"Authorization": "Bearer " + os.environ["OCR_KEY"]})
with urllib.request.urlopen(req) as r:
    print(r.status, r.read().decode())

GET /v1/usage

Pages used by this key

Authentication: API key in the header Authorization: Bearer $OCR_KEY.

In the guide

Responses

Status Description Body
200 Usage application/json Usage
401 unauthorized application/json Errors
429 Codes: rate_limited, locked_out (too many wrong keys from this address or for this key prefix; a valid key from a locked address still passes). Retry-After says when to retry. application/json Errors

Example

curl -s "$OCR_BASE/v1/usage" \
  -H "Authorization: Bearer $OCR_KEY"
import os, urllib.request

url = os.environ["OCR_BASE"] + f"/v1/usage"
req = urllib.request.Request(url, method="GET", headers={"Authorization": "Bearer " + os.environ["OCR_KEY"]})
with urllib.request.urlopen(req) as r:
    print(r.status, r.read().decode())

Jobs

GET /v1/jobs

List the jobs of the calling key, newest first, in pages. Filter with client_ref and status; continue with next_cursor.

Authentication: API key in the header Authorization: Bearer $OCR_KEY.

In the guide

Parameters

Name In Type Required Description
client_ref query string Only jobs submitted with exactly this client_ref.
status query one of running, done Only jobs in this state.
limit query integer (1 to 1000, default 100) Jobs per page. Outside 1 to 1000: 400 invalid_request.
cursor query string The next_cursor of the previous page. Opaque; a bad cursor is 400 invalid_request.

Responses

Status Description Body
200 The jobs of this key application/json JobList
400 invalid_request application/json Errors
401 unauthorized application/json Errors

Example

curl -s "$OCR_BASE/v1/jobs" \
  -H "Authorization: Bearer $OCR_KEY"
import os, urllib.request

url = os.environ["OCR_BASE"] + f"/v1/jobs"
req = urllib.request.Request(url, method="GET", headers={"Authorization": "Bearer " + os.environ["OCR_KEY"]})
with urllib.request.urlopen(req) as r:
    print(r.status, r.read().decode())

POST /v1/jobs

Submit a document (request body = the file: PDF, PNG, JPEG or TIFF; each TIFF directory is one page). With dry_run=1 only the exact price is returned. Send an Idempotency-Key header to make a retry safe: the same key with the same document and parameters within 24 hours returns the existing job (200, header Idempotent-Replayed: true) and charges nothing again.

Authentication: API key in the header Authorization: Bearer $OCR_KEY.

In the guide

Parameters

Name In Type Required Description
Idempotency-Key header string (1-255 characters) 1 to 255 printable characters, chosen by the caller (a UUID is fine). Per key holder and valid for 24 hours. Same key, same document and same parameters: 200 with the existing job, no second job, no second charge. Same key with another document or other parameters: 409 idempotency_conflict. dry_run ignores the key.
dpi query one of 150, 200, 300 Resolution at which the pages of a PDF are rendered: 150, 200 or 300. Without it the service default applies (150). Images and TIFF are read at their own resolution and ignore it (the dry run warns with dpi_ignored). The page size limit applies at this resolution: a page that is too big at the wished dpi is refused with page_too_large, it is never rendered smaller. The resolution is part of the idempotency check, is returned as dpi on every page of the result, and does not change the price per page. Any other value: 400 invalid_dpi.
client_ref query string (200 characters) Your own reference (printable characters, at most 200). Stored with the job and returned in every job answer; GET /v1/jobs can filter by it.
pipeline query string name@version from /v1/pipelines. Optional only when exactly one pipeline exists. Values: scan-ctc@2, scan-ctc-tables@2, scan-vlm-ctc@2 (all pipelines)
formats query string Comma separated: json,text,md,hocr,pdf. A pre-check only: the call is refused with 422 capability_missing when the pipeline cannot deliver a format. It does not choose what the job stores; every format the pipeline can deliver is fetched later with GET /v1/jobs/{id}/result?format=.
features query string Comma separated: word_boxes,table_cells,layout_classes,header_footer. A pre-check only, like formats: 422 capability_missing when the pipeline does not deliver a feature; the result always holds what the pipeline delivers.
dry_run query one of 0, 1, true, false Return the exact price without running
job_budget query integer Millicents. Any value below the price of one page of the chosen pipeline (its minimum: 100 millicents at factor 1.0, see price_per_page_millicents in /v1/pipelines), 0 and negative values included, is refused with 422 budget_too_low; the message names the minimum. The job stops before a page that would exceed the budget; open pages are not_run and cost nothing.

Request body application/pdf: the file itself

Request body image/png: the file itself

Request body image/jpeg: the file itself

Request body image/tiff: the file itself

Responses

Status Description Body
200 dry_run result; or, with an Idempotency-Key that already stands for a job, that job again (header Idempotent-Replayed: true) application/json DryRun
400 Codes: invalid_request, invalid_dpi (dpi is the only problem), empty_document application/json Errors
401 Codes: unauthorized application/json Errors
402 Codes: payment_required application/json Errors
409 Codes: idempotency_conflict (this Idempotency-Key was used within 24 hours for another document or other parameters) application/json Errors
408 Codes: upload_too_slow (the body arrived too slowly) application/json Errors
413 Codes: too_large (document over 100 MiB by default) application/json Errors
415 Codes: unsupported_type application/json Errors
422 Codes: invalid_document, password_required, too_many_pages (over 1000 pages by default), page_too_large (a page over 10 MiB: image file, TIFF page, or PDF file size divided by pages; or over the pixel limit), render_failed, timeout, resource_limit, capability_missing, budget_too_low application/json Errors
429 Codes: rate_limited, locked_out (Retry-After), too_many_uploads (too many uploads of this key in progress; retry later) application/json Errors
500 Codes: internal_error application/json Errors
503 Codes: busy (too many uploads in progress on the service; retry later), pipeline_unavailable (no worker serves the chosen pipeline right now; also for dry_run; nothing is booked; Retry-After says when to retry, GET /v1/catalog shows which pipelines are available) application/json Errors
501 Codes: not_implemented application/json Errors
202 Job accepted and running. Poll GET /v1/jobs/{id}; the Location header names it. application/json JobAccepted

Example

curl -s -X POST "$OCR_BASE/v1/jobs?pipeline=scan-ctc@2&job_budget=5000" \
  -H "Authorization: Bearer $OCR_KEY" \
  -H "Content-Type: application/pdf" \
  --data-binary @file.pdf
import os, urllib.request

url = os.environ["OCR_BASE"] + f"/v1/jobs?pipeline=scan-ctc@2&job_budget=5000"
req = urllib.request.Request(url, data=open("file.pdf", "rb").read(), method="POST", headers={"Authorization": "Bearer " + os.environ["OCR_KEY"], "Content-Type": "application/pdf"})
with urllib.request.urlopen(req) as r:
    print(r.status, r.read().decode())

GET /v1/jobs/{id}

State and counts of a job

Authentication: API key in the header Authorization: Bearer $OCR_KEY.

In the guide

Parameters

Name In Type Required Description
id path string yes
wait query integer (0 to 60, default 0) Long poll: hold the request up to this many whole seconds (0 to 60, default 0) until the job changes, then answer with the normal body. A finished job answers at once. When the service has too many waiters the answer comes at once, and the wait is shortened to the server write timeout minus 5 s (55 s by default). For bucket jobs only a change of state ends the wait at once; counted objects and pages end it after at least 3 s.

Responses

Status Description Body
200 Job application/json JobStatus
401 unauthorized application/json Errors
429 Codes: rate_limited, locked_out. Retry-After says when to retry. application/json Errors
404 not_found application/json Errors

Example

curl -s "$OCR_BASE/v1/jobs/$ID?wait=30" \
  -H "Authorization: Bearer $OCR_KEY"
import os, urllib.request

ID = "..."  # the id of your call
url = os.environ["OCR_BASE"] + f"/v1/jobs/{ID}?wait=30"
req = urllib.request.Request(url, method="GET", headers={"Authorization": "Bearer " + os.environ["OCR_KEY"]})
with urllib.request.urlopen(req) as r:
    print(r.status, r.read().decode())

DELETE /v1/jobs/{id}

Delete the stored result of a finished job now instead of after result_ttl (24 h by default). The job state and its counts stay readable; the bookings stay. Repeating the call is fine. A job that still runs is cancelled instead (202): its open pages end as not_run (counted in pages_not_run), finished pages stay booked, nothing more is charged, and the job ends with stop cancelled.

Authentication: API key in the header Authorization: Bearer $OCR_KEY.

In the guide

Parameters

Name In Type Required Description
id path string yes

Responses

Status Description Body
202 The job was still running and is being cancelled (state cancelling); poll GET /v1/jobs/{id} until state is done application/json JobCancelling
204 The result is gone
400 invalid_request application/json Errors
401 unauthorized application/json Errors
404 not_found application/json Errors

Example

curl -s -X DELETE "$OCR_BASE/v1/jobs/$ID" \
  -H "Authorization: Bearer $OCR_KEY"
import os, urllib.request

ID = "..."  # the id of your call
url = os.environ["OCR_BASE"] + f"/v1/jobs/{ID}"
req = urllib.request.Request(url, method="DELETE", headers={"Authorization": "Bearer " + os.environ["OCR_KEY"]})
with urllib.request.urlopen(req) as r:
    print(r.status, r.read().decode())

GET /v1/jobs/{id}/report

Job report: how the pages of a job were read. Kept as long as the result (result_ttl).

Authentication: API key in the header Authorization: Bearer $OCR_KEY.

Names the pipeline file by its hash, shows every value of that file that changes the result (params, as the worker received them, with the render dpi the job chose), the model_rev the worker reported and the seconds per stage. Works for a running job (the pages read so far) and a finished one. It holds numbers, codes and hashes only, never text or file names.

Parameters

Name In Type Required Description
id path string yes

Responses

Status Description Body
200 The report application/json JobReport
401 unauthorized application/json Errors
429 Codes: rate_limited, locked_out. Retry-After says when to retry. application/json Errors
404 not_found (also after the retention time) application/json Errors

Example

curl -s "$OCR_BASE/v1/jobs/$ID/report" \
  -H "Authorization: Bearer $OCR_KEY"
import os, urllib.request

ID = "..."  # the id of your call
url = os.environ["OCR_BASE"] + f"/v1/jobs/{ID}/report"
req = urllib.request.Request(url, method="GET", headers={"Authorization": "Bearer " + os.environ["OCR_KEY"]})
with urllib.request.urlopen(req) as r:
    print(r.status, r.read().decode())

GET /v1/jobs/{id}/result

Result of a finished job. Results are kept for a limited time (result_ttl).

Authentication: API key in the header Authorization: Bearer $OCR_KEY.

In the guide

Parameters

Name In Type Required Description
id path string yes
format query one of json, text, md, hocr, pdf Default json. hocr needs a pipeline with the capability hocr (else capability_missing). pdf cannot be built by GET: it answers 422 source_required; use POST with the original document.

Responses

Status Description Body
200 The result application/json Result
text/plain string
text/markdown string
text/html string
400 invalid_request application/json Errors
401 unauthorized application/json Errors
429 Codes: rate_limited, locked_out. Retry-After says when to retry. application/json Errors
404 not_found (also after the retention time) application/json Errors
409 not_ready: the job is still running application/json Errors
422 capability_missing, or source_required for format=pdf (use POST with the original) application/json Errors

Example

curl -s "$OCR_BASE/v1/jobs/$ID/result?format=json" \
  -H "Authorization: Bearer $OCR_KEY" \
  -o response.out
import os, urllib.request

ID = "..."  # the id of your call
url = os.environ["OCR_BASE"] + f"/v1/jobs/{ID}/result?format=json"
req = urllib.request.Request(url, method="GET", headers={"Authorization": "Bearer " + os.environ["OCR_KEY"]})
with urllib.request.urlopen(req) as r:
    open("response.out", "wb").write(r.read())

POST /v1/jobs/{id}/result

Searchable PDF of a finished job. The body is the original document (the service does not keep it); the invisible text layer is placed at the level of the pipeline (words or lines). Nothing is stored.

Authentication: API key in the header Authorization: Bearer $OCR_KEY.

In the guide

Parameters

Name In Type Required Description
id path string yes
format query one of pdf yes

Request body application/octet-stream: the file itself

Responses

Status Description Body
200 The PDF. Pages that were not read carry a visible strip "OCR FAILED" or "OCR NOT RUN"; their numbers are in the header X-Pages-Unread. application/pdf the file itself
400 invalid_request application/json Errors
401 unauthorized application/json Errors
404 not_found (also after the retention time) application/json Errors
409 not_ready: the job is still running application/json Errors
422 capability_missing, source_mismatch (page count or size differs from the job; this is no checksum, another document of the same page count and sizes is not detected), pdf_unsupported, or an upload refusal of the intake: invalid_document, password_required, too_many_pages, page_too_large, render_failed, timeout, resource_limit application/json Errors
413 too_large application/json Errors
408 upload_too_slow (the body arrived too slowly) application/json Errors
415 unsupported_type application/json Errors
429 rate_limited, locked_out or too_many_uploads (Retry-After) application/json Errors
500 internal_error application/json Errors
503 busy: too many PDF builds or too many uploads at once (Retry-After) application/json Errors

Example

curl -s -X POST "$OCR_BASE/v1/jobs/$ID/result?format=pdf" \
  -H "Authorization: Bearer $OCR_KEY" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @body.json \
  -o response.out
import os, urllib.request

ID = "..."  # the id of your call
url = os.environ["OCR_BASE"] + f"/v1/jobs/{ID}/result?format=pdf"
req = urllib.request.Request(url, data=open("body.json", "rb").read(), method="POST", headers={"Authorization": "Bearer " + os.environ["OCR_KEY"], "Content-Type": "application/octet-stream"})
with urllib.request.urlopen(req) as r:
    open("response.out", "wb").write(r.read())

Bucket jobs

POST /v1/bucket-jobs

Start a job over an S3 bucket: every PDF under source.prefix is recognized, results and manifest.json are written under destination.prefix. The same job_id again resumes. Credentials live in RAM only.

Authentication: API key in the header Authorization: Bearer $OCR_KEY.

In the guide

Request body application/json: object

Field Type Required Description
job_id string yes
pipeline string
formats list of one of json, text, md, hocr, pdf Output files written per object: json (default), text, md, hocr (Extension .hocr), pdf (Extension .pdf: the text layer on the original PDF; a document that cannot get one ends partial with error code pdf_unsupported and gets its other files). Chosen here, unlike the document job where formats is only a pre-check. A page with status failed or not_run shows a status line in md; text stays plain text, the status is in json.
job_budget integer Millicents. Below the price of one page of the pipeline (0 and negative included): 422 budget_too_low, the message names the minimum. Omit for no budget.
max_objects integer
source object yes
destination object yes

Responses

Status Description Body
202 Accepted application/json object
400 invalid_request application/json Errors
401 unauthorized application/json Errors
402 payment_required application/json Errors
409 conflict: the job id is running application/json Errors
413 too_large application/json Errors
422 capability_missing, budget_too_low; access_denied, no_such_bucket, wrong_region, source_unreachable: the source prefix is listed before the 202, so wrong credentials or an unreachable store fail at once and nothing starts application/json Errors
503 pipeline_unavailable: no worker serves the chosen pipeline right now (Retry-After); nothing is booked application/json Errors

202 application/json

Field Type Required Description
job_id string
state string
pipeline string

Example

curl -s -X POST "$OCR_BASE/v1/bucket-jobs" \
  -H "Authorization: Bearer $OCR_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @body.json
import os, urllib.request

url = os.environ["OCR_BASE"] + f"/v1/bucket-jobs"
req = urllib.request.Request(url, data=open("body.json", "rb").read(), method="POST", headers={"Authorization": "Bearer " + os.environ["OCR_KEY"], "Content-Type": "application/json"})
with urllib.request.urlopen(req) as r:
    print(r.status, r.read().decode())

GET /v1/bucket-jobs/{id}

State and counts of a bucket job (no names)

Authentication: API key in the header Authorization: Bearer $OCR_KEY.

In the guide

Parameters

Name In Type Required Description
id path string yes
wait query integer (0 to 60, default 0) Long poll: hold the request up to this many whole seconds (0 to 60, default 0) until the job changes, then answer with the normal body. A finished job answers at once. When the service has too many waiters the answer comes at once, and the wait is shortened to the server write timeout minus 5 s (55 s by default). For bucket jobs only a change of state ends the wait at once; counted objects and pages end it after at least 3 s.

Responses

Status Description Body
200 Job application/json BucketJobStatus
401 unauthorized application/json Errors
404 not_found application/json Errors

Example

curl -s "$OCR_BASE/v1/bucket-jobs/$ID?wait=30" \
  -H "Authorization: Bearer $OCR_KEY"
import os, urllib.request

ID = "..."  # the id of your call
url = os.environ["OCR_BASE"] + f"/v1/bucket-jobs/{ID}?wait=30"
req = urllib.request.Request(url, method="GET", headers={"Authorization": "Bearer " + os.environ["OCR_KEY"]})
with urllib.request.urlopen(req) as r:
    print(r.status, r.read().decode())

Objects

The named objects that the calls above return or accept.

Pipeline

Field Type Required Description
pipeline string
description string
factor number Operator field, not used in the download edition.
price_per_page_millicents integer Operator field, not used in the download edition.
autoregressive boolean
capabilities map of boolean
available boolean false while the worker lacks a capability this pipeline needs or does not answer; a job, a dry run or a bucket job for it is then refused with 503 pipeline_unavailable and nothing is booked
unavailable_reason string Only on GET /v1/pipelines (with a key), only when available is false: which capability is missing. The public catalog never has it.
formats list of string
features list of string
orderable boolean Operator field, not used in the download edition. False for a preview pipeline: it exists in the service but is not offered for ordering yet. Clients that build a call offer only orderable pipelines as a normal choice.
preview_note string Operator field, not used in the download edition. Only when orderable is false; always the same sentence.
unavailable_note string Only when available is false; always the same sentence.

Usage

Field Type Required Description
balance_millicents integer Operator field, not used in the download edition.
spent_millicents integer Operator field, not used in the download edition.
pages_used integer
paid boolean Operator field, not used in the download edition.
pages_left integer or null Pages until the page limit of the key stops it; null for a key without that limit.

DryRun

Field Type Required Description
dry_run boolean
pipeline string
pages integer
factor number Operator field, not used in the download edition.
price_per_page_millicents integer Operator field, not used in the download edition.
price_millicents integer Operator field, not used in the download edition. Exact: pages times price per page
pages_covered integer Pages that run before job_budget stops the job
pages_warned integer Done pages that carry a warning (for example model_output_incomplete); a subset of pages_done. The warning itself stands on the page of the result.
pages_not_run integer
max_charge_millicents integer Operator field, not used in the download edition.
dpi integer Resolution the pages of the PDF would be rendered at (the dpi you asked for, else the default). Absent for images and TIFF.
balance_millicents integer Operator field, not used in the download edition. Only when the service bills
warnings list of DryRunWarning What a real job would meet: the worker cannot run the pipeline or job_budget or the balance covers fewer pages than the document has. Absent when there is nothing to say.

DryRunWarning

Field Type Required Description
code one of job_budget, dpi_ignored, balance_too_low, balance yes
message string yes

JobAccepted

Field Type Required Description
job_id string
state string
pipeline string
pages integer
price_millicents integer Operator field, not used in the download edition.
price_per_page_millicents integer Operator field, not used in the download edition.
max_charge_millicents integer Operator field, not used in the download edition. Most this job can charge: the pages job_budget allows times the page price (0 for a free key or when billing is off).
client_ref string Your client_ref, when you sent one.

JobCancelling

Answer of DELETE on a job that still runs. state is cancelling only here; GET /v1/jobs/{id} shows running until the job has ended, then done.

Field Type Required Description
job_id string
state one of cancelling

JobReport

Field Type Required Description
schema_version integer yes
job_id string yes
state one of running, done yes
pipeline string yes
pipeline_hash string yes sha256 of the pipeline file the job was accepted with; unknown for a job stored before the hash was kept.
pipeline_changed boolean True when the pipeline file has changed since the job was accepted; params still show the values the job was accepted with.
params object Every value of the pipeline file that changes the result, by step block (detector limits and thresholds, recognizer settings, model sha256 and precision, orientation, layout and table values, model run settings), as the worker received them. Paths of model files are not shown.
model_rev list of string yes The model_rev the worker reported, one entry per distinct value over the pages read.
models list of object yes The models the job was read with, from the params the job was accepted with: sha256 and precision per model. Empty for a job stored before the params were kept.
seconds object yes Seconds: scheduler_wait (waiting for a turn among all jobs), render (page image), queue (waiting before the worker), read (the whole request to the worker, retries included), worker (the time the worker itself reports for the page).
page_stages list of object yes
pages_read_with_other_pipeline_hash integer Pages read with another pipeline file than pipeline_hash (the service was restarted with a changed file during the job); left out when 0.
pages JobReportPages

JobReportPages

Field Type Required Description
total integer
done integer
failed integer
not_run integer
pending integer

JobStatus

Field Type Required Description
job_id string
client_ref string Your client_ref from the POST, when you sent one.
pipeline string
state one of running, done
pages integer
pages_done integer
pages_failed integer Pages that ended failed, none of them charged: the image could not be read (bad_image), the worker cannot run the pipeline (worker_unsupported), the engine failed on this page on every try (engine_error: the worker answered 500 three times), or the worker was too busy and refused the page for the whole waiting time (worker_busy: the worker was reachable but overloaded, nothing was computed), or the page of an autoregressive pipeline came back without one word although the model output was incomplete (model_output_empty: a page is never returned empty and done in silence). The job goes on with the other pages and ends as usual; a worker that is unreachable is a different case (state running, then stop worker_unavailable).
pages_not_run integer
pages_pending integer
stop string job_budget or balance when the job stopped early; cancelled after DELETE on a running job; worker_unavailable when the OCR worker stayed unreachable too long; worker_unsupported when the worker lost a capability while the job ran; pages_failed when the job ended with not one page read because every page failed
charged_millicents integer
queue QueueInfo

JobList

Field Type Required Description
jobs list of JobStatus
next_cursor string Present when more jobs follow; pass it as cursor. Absent on the last page.

QueueInfo

Where a running job stands among all jobs waiting for the OCR machine. Paid jobs run before free ones, jobs of different customers take turns, small backlogs go first. The job keeps running while it waits; the numbers change as other jobs come and go.

Field Type Required Description
class one of paid, free Service class of the job: paid jobs run before free ones.
paid_pages_ahead integer Free jobs only: pages of paid jobs that run before this job (part of pages_ahead).
position integer 1 = this job gets the next free slot.
jobs_total integer Jobs in the queue at this moment, so position reads as place x of y.
pages_ahead integer Pages that run before this job gets its next page.
estimated_wait_seconds number Estimated seconds until this job gets its next page. Absent until the service has measured a page. A guide, not a promise. For a free job it never exceeds max_wait_seconds.
waited_seconds number Free jobs only: seconds the job has waited for its next page.
max_wait_seconds number Free jobs only: seconds left until this job is served ahead of paid work (the free wait bound minus waited_seconds). Free jobs run on spare capacity and wait behind paid pages, but at most this long.
message string Free jobs only: one sentence on why the job waits (paid pages ahead, paid work goes first) and the latest time of its next page. For people, not for parsing.

BucketJobStatus

Field Type Required Description
job_id string
pipeline string
state one of running, complete, incomplete, failed, interrupted
stop string Why the job stopped early: job_budget, balance, max_objects, job_time (run time limit) or page_cap (a page limit set for the key or its order)
error one of output_prefix_conflict, access_denied, no_such_bucket, wrong_region, source_unreachable, destination_unreachable, job_timeout, run_failed Why a job failed or ended early. job_timeout: the run reached its time limit; the job is incomplete and the same job_id continues it. access_denied: the bucket refused the credentials or the anonymous read. no_such_bucket. wrong_region: the bucket lives in another region than the one given. source_unreachable and destination_unreachable: network, timeout or service error on that side. output_prefix_conflict: the output prefix holds another job's manifest, or source and output overlap. run_failed: anything else.
objects integer
pages_done integer
pages_failed integer
pages_not_run integer
updated string
started string
objects_listed integer PDF objects returned by the listing so far. The bucket is not pre-counted (the listing is streamed while objects run), so this is a lower bound until listing_complete is true.
listing_complete boolean
objects_total integer Present only once listing_complete is true: the final object count.
percent_objects number Present only once listing_complete is true: share of objects handled (processed or skipped as already done), 0 to 100.
pages_per_min number Pages per minute over the last 5 minutes.
pages_per_min_overall number Pages per minute since started.
current_count integer
current_objects list of string Object keys (the customer's own names) being processed now. Shown only here and in the manifest, never logged or stored in the service registry.
queue QueueInfo
error_side one of source, destination Which bucket caused the error, when a bucket did.

Result

Field Type Required Description
not_delivered list of string yes
pages list of ResultPage yes
pipeline string yes
schema_version yes

ResultBox

ResultCell

Field Type Required Description
box ResultBox
col integer (0 to 499) yes
colspan integer (1 to 500) yes
row integer (0 to 499) yes
rowspan integer (1 to 500) yes
words list of ResultWord yes

ResultLine

Field Type Required Description
mark one of #, ##, ###, ####, #####, ######, -
words list of ResultWord yes

ResultModel_run

Field Type Required Description
blocks integer (at least 0) yes
ctc_only integer (at least 0) yes
ctc_only_chars integer (at least 0) yes
finish_reason string (32 characters)
max_tokens integer (at least 0) yes
tokens integer (at least 0) yes
truncated boolean yes

ResultPage

Field Type Required Description
dpi integer (1 to 1200)
error ResultPage_error
height integer (1 to 100000)
layout list of ResultRegion
lines list of ResultLine yes
model_run ResultModel_run
page integer (1 to 1000000) yes
rotation one of 0, 90, 180, 270
rotation_checked boolean
safe_ok boolean yes Always present; today always false. It is not calibrated and promises nothing in either direction.
status one of done, failed, not_run yes
tables list of ResultTable
warnings list of ResultWarning
width integer (1 to 100000)

ResultPage_error

Field Type Required Description
code string yes
message string (1-500 characters) yes

ResultRegion

Field Type Required Description
box ResultBox yes
box_source one of model model: the box of the language model's block. ctc: the box built from the character OCR cells.
class one of title, section_header, text, list_item, table, figure, caption, footnote, formula, page_header, page_footer, page_number, other yes
confidence number or null (0 to 1) yes
native string (1-64 characters) yes

ResultTable

Field Type Required Description
box ResultBox yes
box_source one of model, ctc model: the box of the language model's block. ctc: the box built from the character OCR cells.
cells list of ResultCell yes
cols integer (1 to 500) yes
rows integer (1 to 500) yes
words_unplaced integer (at least 1)

ResultWarning

Field Type Required Description
code one of table_not_found, table_words_unplaced, model_output_incomplete yes
message string (300 characters) yes
table integer (at least 0)

ResultWord

Field Type Required Description
box list of number (0 to 100000)
confidence number (0 to 1) yes
confidence_min number (0 to 1)
ctc_text string (1-1000 characters)
flag one of ok, differs, punct_differs, low_conf, sup, no_ctc, markup, not_in_model
source one of textlayer, ctc, vlm yes
text string (1-1000 characters) yes
verified boolean yes Always present; the pipelines do not set it yet, so read it as false. Judge by confidence.

Errors

Every error is JSON {"error":{"code","message"}}. Branch on code, not on message.

Code HTTP Cause What to do
invalid_request 400 A query parameter is unknown, repeated or has an invalid value, the pipeline name is unknown, or the body is not valid. On POST /v1/jobs, details lists every problem found. Fix the request. Sending it again unchanged fails again.
invalid_dpi 400 POST /v1/jobs: dpi is not 150, 200 or 300 (and nothing else is wrong with the request; otherwise invalid_request). Send dpi as 150, 200 or 300, or leave it out for the default (150). Retrying the same value fails again.
empty_document 400 The request body is empty. Send the file itself as the request body.
unauthorized 401 The key is missing or wrong. Check the Authorization header (Bearer ocr_...) and the key. Do not try variants: ten wrong keys within ten minutes lock the client address (service defaults).
payment_required 402 The free pages of this key are used up (a key made without -paid stops there), or the balance of a paid key does not cover one page. Ask whoever runs the service: a key made with -paid has no page limit. Do not retry.
not_found 404 Unknown job or pipeline, or the result has expired (result_ttl, 24 hours by default). Check the id. Fetch results soon after the job is done; an expired result cannot be restored, send the document again.
not_ready 409 The result was requested while the job still runs. Keep polling GET /v1/jobs/{id} until state is done, then fetch the result.
conflict 409 A bucket job with this job_id is already running. Poll the running bucket job instead of starting it again, or use a new job_id.
idempotency_conflict 409 POST /v1/jobs: this Idempotency-Key was already used within the last 24 hours for another document or other parameters (pipeline, formats, features, job_budget, client_ref, dpi). Use a new key for a new job. Send the same key only with the identical document and parameters.
method_not_allowed 405 Wrong HTTP method for this path. Use the method shown in the reference.
too_large 413 The file is larger than the size limit. Send a smaller file, or split it into several. The default limit is 100 MiB (max_body_bytes); the operator of the service may set another.
unsupported_type 415 The content is not PDF, PNG, JPEG or TIFF (the type is detected from the content, not from the header). Convert the file to one of these four formats.
invalid_document 422 The document is broken or cannot be read. Open the file yourself to check it, repair or re-create it.
password_required 422 The PDF is encrypted. Send an unlocked copy.
too_many_pages 422 The document has more pages than the page limit. Split the document. The default limit is 1000 pages (intake.limits.max_pages); the operator of the service may set another.
page_too_large 422 A page is too large to render. Scan or export at a lower resolution.
render_failed 422 A page could not be rendered. Check that the page opens in a viewer; re-export the file.
timeout 422 Processing took too long. Try again once with a smaller file; if it repeats, contact support with the job id. Without a support contract there is no claim to an answer.
resource_limit 422 A resource limit was hit while reading the document. Split the document or lower its resolution.
capability_missing 422 The pipeline cannot deliver a format or feature you asked for in formats or features. missing names what is missing, pipelines lists those that deliver everything asked. Pick another pipeline from pipelines (your choice, not an automatic switch), or drop the format or feature.
budget_too_low 422 job_budget is below the price of one page of the chosen pipeline (0 and negative values too). The message names the minimum. Send a job_budget of at least the minimum named in the message.
source_required 422 GET .../result?format=pdf: a searchable PDF cannot be built from the stored result alone. Use POST /v1/jobs/{id}/result?format=pdf with the original document as the body.
source_mismatch 422 POST .../result for a PDF: page count or page sizes differ from the job. There is no checksum, so another document with the same page count and sizes is not detected. Send the same document that made the job.
pdf_unsupported 422 The document cannot get a text layer (searchable PDF). Use the json, text or md result instead.
access_denied 422 Bucket job: the bucket refused the credentials, or the anonymous read. Nothing starts. Check the access key, secret key and the bucket policy.
no_such_bucket 422 Bucket job: the bucket does not exist. Check bucket name, endpoint and path_style.
wrong_region 422 Bucket job: the bucket lives in another region than the one given. Give the region the bucket lives in.
source_unreachable 422 Bucket job: network, timeout or service error on the source side. Check that the endpoint is reachable from the internet; start again with the same job_id.
rate_limited 429 Too many requests, or too many jobs at once, for this key. Retry-After is short (seconds). Wait the Retry-After seconds, then retry. Poll with ?wait=N instead of fast polling.
locked_out 429 Lockout of the client address after repeated wrong keys (Retry-After 900 by default). Only wrong keys are counted and refused; a valid key always gets through. Do not wait and retry in a loop. Stop, fix the key, and try again after the lock.
upload_too_slow 408 The body arrived too slowly. Retry on a faster connection, or with a smaller file.
too_many_uploads 429 Too many uploads of this key are in progress at once. Retry after Retry-After, with backoff; send fewer documents in parallel.
busy 503 Too many uploads or PDF builds are in progress on the whole service. The error carries limit, limit_value and current. Retry after Retry-After, with backoff.
forbidden 403 The key or the client address is blocked. Ask whoever runs the service.
unavailable 503 No worker is available for the chosen pipeline right now. Retry after Retry-After.
internal_error 500 Server error. Retry with backoff. If it repeats, contact support with the time and the job id.
not_implemented 501 The service has no worker configured for jobs. Nothing to fix on your side; contact support.