Usage

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:

Python Example
import json
import os
import 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 quickly

Webhook 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, and artifacts.preview_url when you sent preview=true
  • Failed: includes id, status, and error

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:

  1. Verify X-DeepRead-Signature over the raw body
  2. Read id from the webhook payload
  3. Fetch GET /v1/jobs/{job_id} from your backend using your X-API-Key
  4. 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.