Handling Results
Handling Results
This guide shows how to handle DeepRead results in production using webhooks without duplicating the API reference. For background concepts, see Jobs & Webhooks.
Overview
When you provide a webhook_url in your processing request, DeepRead sends an HTTP
POST request to your endpoint when the job completes or fails.
Your endpoint should:
- Respond quickly with
200 OK(then process asynchronously) - Be idempotent (safe to handle duplicates)
- Treat the webhook as a notification, and fetch the canonical result from the API
using
GET /v1/jobs/{job_id}before taking action
Building a webhook endpoint
Basic endpoint structure
Your webhook endpoint should accept POST requests and respond quickly:
import jsonimport osimport urllib.request def handle_deepread_webhook(raw_body: bytes) -> None: # 1) Parse what DeepRead sends to your webhook payload = json.loads(raw_body.decode("utf-8")) job_id = payload["id"] # 2) Treat the webhook as a notification. Fetch the canonical job result. api_key = os.environ["DEEPREAD_API_KEY"] req = urllib.request.Request( f"https://api.deepread.tech/v1/jobs/{job_id}", headers={"X-API-Key": api_key}, method="GET", ) with urllib.request.urlopen(req, timeout=10) as resp: job = json.loads(resp.read().decode("utf-8")) if job.get("status") == "completed": fields = (job.get("extraction") or {}).get("fields") or [] print(f"Job {job_id} completed") print(f"Extracted {len(fields)} fields") elif job.get("status") == "failed": print(f"Job {job_id} failed: {job.get('error')}") # Your web server should:# - read the raw request body bytes# - call handle_deepread_webhook(raw_body)# - respond 2xx quicklyWebhook payload
The webhook payload is identical to the GET /v1/jobs/{job_id} response and
carries schema_version: "dp02". For example:
- Completed: includes
id,status, and (for completed jobs)document,extraction,review, andartifacts.preview_urlwhen you sentpreview=true - Failed: includes
id,status, anderror
Example payload (completed)
{ "id": "550e8400-e29b-41d4-a716-446655440000", "event": "job.completed", "status": "completed", "schema_version": "dp02" }
For the authoritative schema and full response shape, see GET /v1/jobs/{job_id}.
Verify the signature
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"])
Read the raw body bytes before any JSON parsing — a re-serialised body will not match. Rotating the secret takes effect from the next delivery, so deploy the new secret straight away.
Then fetch the job
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. The safest pattern is:
- Verify
X-DeepRead-Signatureover the raw body - Read
idfrom the webhook payload - Fetch
GET /v1/jobs/{job_id}from your backend using yourX-API-Key - Use the API response as the source of truth (not the webhook body)
Handling large text responses
For documents with large OCR output (>1MB), the text may be stored externally. When document.content.text_url is present, fetch the full text from that URL instead of using document.content.text directly. See Jobs & Webhooks for more details on response structure.
Dashboard job history
You can also view and track all your jobs from the Dashboard Jobs page — filter by product (OCR, Forms, PII), status, date range, and access results directly.
Error handling
Delivery is attempted up to 3 times with backoff, but a 4xx from your endpoint
stops retries immediately and a longer outage will outlast all three attempts. If
you don’t receive a webhook, use GET /v1/jobs/{job_id} to check status and
retrieve results. See Error Handling & Retry
for the full retry table and fallback patterns.