Concepts

Jobs & Webhooks

Jobs & Webhooks

DeepRead processes documents as background jobs. When you submit a document for processing, you immediately receive a job id. Configure webhooks to receive notifications when jobs complete, or check job status manually.

Job lifecycle

Every job moves through a series of states:

queued → processing → completed
                   ↘ failed

Job states

  • queued: Job created, waiting to start processing
  • processing: Pipeline is actively running
  • completed: Processing finished successfully, results available in the document and extraction fields
  • failed: Processing encountered an unrecoverable error, see error field

failed is final. Transient problems on DeepRead's side (a provider outage, a worker restart, a network reset) never fail a job: it is retried automatically with backoff for up to 48 hours and keeps reporting queued or processing in the meantime. A job that reports failed will fail the same way if you resubmit the document unchanged — the error field tells you what to fix.

Creating a job

When you submit a document to a processing endpoint, the API creates a job and returns:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "queued"
}

The response is immediate (typically < 100ms). The actual processing happens in the background.

Webhooks

Webhooks are the recommended way to receive job completion notifications. Configure a webhook URL when creating a job, and DeepRead will send an HTTP POST request to your endpoint when the job completes or fails.

Webhooks are available from the Standard plan. Sending webhook_url on Free returns 402 with a body naming the plan that has it.

Configuring webhooks

Include a webhook_url in your job creation request:

See POST /v1/process for the exact request format.

When the job completes, DeepRead sends a POST request to your webhook URL with the job result. The webhook payload is identical to the GET /v1/jobs/{job_id} response (it carries schema_version: "dp02") — including status, document/extraction (for completed jobs), or error (for failed jobs). See API Reference - GET /v1/jobs/{job_id} for the complete payload structure.

Events

Every delivery says what it is, in the body's event field and in the X-DeepRead-Event header:

EventWhen
job.completedProcessing finished, results in the body
job.failedProcessing failed, error in the body
review.completed, review.updated, review.overdue, review.expired, review.cancelledJobs sent with reviewer_email; see Human review

Review events carry the job body as it stands at that moment, corrected values included.

Verifying deliveries

Every delivery carries X-DeepRead-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 over "<t>.<raw request body>" keyed by your account's signing secret. Read the secret at GET /dashboard/v1/webhooks/secret (it is created on first read) and rotate it at POST /dashboard/v1/webhooks/secret/rotate; deliveries sign with the new secret from the next one on. Verify over the exact bytes you received, compare in constant time, and reject a t older than five minutes:

import hmac, hashlib, time

def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = int(parts["t"])
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Even with a verified signature, treat the delivery as a notification and fetch the canonical result from GET /v1/jobs/{job_id} from your backend before acting on it.

Response structure

When the job is completed, the response includes a document object (OCR text + layout) and, if you provided a schema, an extraction.fields list. For documents with large OCR output (>1MB), the text may be stored externally in document.content.text_url. See API Reference - GET /v1/jobs/{job_id} for the complete response structure.

Manual status checking

If you don't use webhooks, or need to check job status manually, you can query the job status endpoint using GET /v1/jobs/{job_id}. See API Reference - GET /v1/jobs/{job_id} for complete endpoint documentation and examples.

Dashboard job history

You can also view all your jobs in the Dashboard Jobs page. It shows a unified history across all products (OCR, Forms, PII) with:

  • Search by file name
  • Filters by product type, status, and date range
  • Results — open OCR previews or download processed Forms/PII files directly

Rate limits

Every limit is per plan. A refused request answers 429 with a Retry-After header saying when to try again, except the hard maximum, which is 413.

LimitFreeStandardEnterprise
Submissions per minute (POST /v1/process)10100500
Pages in flight (queued + processing, added up)16200500
Polling per minute (GET /v1/jobs/{job_id})2060150
Pages per document501,0002,000
Hard maximum per document2,000 pages / 500 MB2,000 pages / 500 MB2,000 pages / 500 MB
  • Submissions per minute are counted per endpoint across every API server (POST /v1/form-fill, POST /v1/pii/redact and POST /v1/optimize each have their own count). Past the limit the request is refused with 429, a Retry-After header (seconds until the minute turns) and a body naming the limit. A refused request counts too.
  • Pages in flight is an admission limit, not reserved capacity: it bounds what one account can have waiting, it does not guarantee how fast the queue moves. A submission is admitted when you have nothing in flight (one large document is never refused for being large) or when its pages fit under the cap. Otherwise it is refused with 429, Retry-After: 30, and a body giving pages_in_flight, max_pages_in_flight and pages_requested. Nothing is queued: resubmit when a job finishes. OCR, PII redaction and form-fill jobs share the one budget, each counted at the pages it was admitted for.
  • Polling GET /v1/jobs/{job_id} past the plan's rate is refused with 429 and a Retry-After header. Webhooks are the way to avoid polling altogether.
  • Pages per document past the plan's limit are refused with 400 and page_count_exceeded, with page_count, max_pages and plan in the body. On Free and Standard a PDF that cannot be read (corrupted or password-protected) is refused with 400 pdf_parse_error, since its pages cannot be counted against the limit.
  • The hard maximum applies on every plan: no document over 2,000 pages or 500 MB is accepted, and the answer is 413 with the limit in the body. Contact us for larger documents.

Free also has a monthly allowance of 2,000 pages. It resets every month on the day the account signed up, and unused pages do not carry over; a document that would take the account past 2,000 is refused whole with 429 (no job is created) until the cycle resets. Every API response carries the standing:

X-RateLimit-Limit: 2000         # Monthly pages included in your plan
X-RateLimit-Remaining: 1850     # Pages remaining this cycle
X-RateLimit-Used: 150           # Pages used this cycle
X-RateLimit-Reset: 1704153600   # Unix timestamp of your own next reset, not the first of the month

Send an idempotency_key with any submission you retry after a 429, so the retry can never create a second job — see Retrying safely.

Error handling

When a job fails, the response (or webhook payload) includes an error field with a human-readable description of what went wrong. Common failure reasons include document corruption, poor image quality, processing timeouts, and schema validation failures. See Error Handling & Retry for detailed error handling strategies and recovery approaches.