OCR API reference: endpoints, fields, error codes
On this page
- Service
- GET /
- GET /healthz
- GET /v1/status
- GET /openapi.json
- Pipelines and usage
- GET /v1/catalog
- GET /v1/pipelines
- GET /v1/pipelines/{ref}
- GET /v1/usage
- Jobs
- GET /v1/jobs
- POST /v1/jobs
- GET /v1/jobs/{id}
- DELETE /v1/jobs/{id}
- GET /v1/jobs/{id}/report
- GET /v1/jobs/{id}/result
- POST /v1/jobs/{id}/result
- Bucket jobs
- POST /v1/bucket-jobs
- GET /v1/bucket-jobs/{id}
- Objects
- Pipeline
- Usage
- DryRun
- DryRunWarning
- JobAccepted
- JobCancelling
- JobReport
- JobReportPages
- JobStatus
- JobList
- QueueInfo
- BucketJobStatus
- Result
- ResultBox
- ResultCell
- ResultLine
- ResultModel_run
- ResultPage
- ResultPage_error
- ResultRegion
- ResultTable
- ResultWarning
- ResultWord
- Errors
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 Resulttext/plain stringtext/markdown stringtext/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.
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.
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.
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. |