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 processingprocessing: Pipeline is actively runningcompleted: Processing finished successfully, results available in thedocumentandextractionfieldsfailed: Processing encountered an unrecoverable error, seeerrorfield
failedis 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 reportingqueuedorprocessingin the meantime. A job that reportsfailedwill fail the same way if you resubmit the document unchanged — theerrorfield 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:
| Event | When |
|---|---|
job.completed | Processing finished, results in the body |
job.failed | Processing failed, error in the body |
review.completed, review.updated, review.overdue, review.expired, review.cancelled | Jobs 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.
| Limit | Free | Standard | Enterprise |
|---|---|---|---|
Submissions per minute (POST /v1/process) | 10 | 100 | 500 |
| Pages in flight (queued + processing, added up) | 16 | 200 | 500 |
Polling per minute (GET /v1/jobs/{job_id}) | 20 | 60 | 150 |
| Pages per document | 50 | 1,000 | 2,000 |
| Hard maximum per document | 2,000 pages / 500 MB | 2,000 pages / 500 MB | 2,000 pages / 500 MB |
- Submissions per minute are counted per endpoint across every API server (
POST /v1/form-fill,POST /v1/pii/redactandPOST /v1/optimizeeach have their own count). Past the limit the request is refused with429, aRetry-Afterheader (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 givingpages_in_flight,max_pages_in_flightandpages_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 with429and aRetry-Afterheader. Webhooks are the way to avoid polling altogether. - Pages per document past the plan's limit are refused with
400andpage_count_exceeded, withpage_count,max_pagesandplanin the body. On Free and Standard a PDF that cannot be read (corrupted or password-protected) is refused with400pdf_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
413with 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.