Usage

Error Handling & Retry

Error Handling & Retry

This guide covers error handling strategies for both job processing failures and webhook delivery issues.

Job processing errors

When a job fails during processing, the webhook payload (or manual status check) includes an error field with details.

Failed job payload

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "failed",
  "created_at": "2025-01-01T12:00:00Z",
  "completed_at": "2025-01-01T12:15:00Z",
  "error": "Unable to extract required fields from document"
}

Common error scenarios

Document issues:

  • Document is corrupted or unreadable
  • OCR processing failed due to poor image quality
  • Processing exceeded maximum time limit

Schema issues:

  • Schema validation failed
  • Required fields couldn't be extracted
  • Invalid schema format

Submission issues (HTTP errors on POST /v1/process, not job failures):

  • Invalid API key (401)
  • The API key could not be checked because our database did not answer (503, with Retry-After: 5) — retry after the header says; a bad key is always 401
  • Submissions per minute, pages in flight, or the monthly page allowance exceeded (429, with Retry-After) — retry after the header says
  • Service unavailable (5xx) — retry the request with backoff
  • Plan or credits do not cover the request, or the API key is at its monthly spend limit (402), file type not on the plan (415), document over the 2,000-page / 500 MB hard maximum (413), or an idempotency_key reused with a different file or options (409) — fix the request, retrying changes nothing

Handling job failures

A failed job is final. DeepRead already retries transient infrastructure errors internally — with backoff, for up to 48 hours — and while that happens the job simply reports queued or processing; you never see a failure that a retry could have fixed. Resubmitting the same document unchanged produces the same error; fix the cause it describes instead (an unreadable file, an unsupported layout, a schema that doesn't match the document).

Retry logic belongs at the submission step, where a 429 or 5xx response to POST /v1/process is worth retrying. Send an idempotency_key with every submission you might retry: a retry with the same key returns the job the first request created, with 200, instead of creating and charging a second one. The key is up to 255 characters and unique per account (case and surrounding whitespace ignored); the same key with a different file or options is refused with 409.

cURL Example
#!/usr/bin/env bash
# Submit a document, retrying only on transient failures (429 / 5xx).
set -euo pipefail
 
API_KEY="${DEEPREAD_API_KEY:?set DEEPREAD_API_KEY}"
FILE="invoice.pdf"
MAX_RETRIES=3
# One key per submission, sent on every retry: a retry that lands after a
# timeout returns the job the first attempt created instead of a second one.
IDEMPOTENCY_KEY="$(uuidgen)"
 
for attempt in $(seq 1 "$MAX_RETRIES"); do
# Write the body to a file so we can read the status code separately.
status=$(curl -sS -o /tmp/deepread-response.json -w '%{http_code}' \
-X POST https://api.deepread.tech/v1/process \
-H "X-API-Key: $API_KEY" \
-F "file=@$FILE" \
-F "pipeline=extract" \
-F "webhook_url=https://your-app.com/webhooks/deepread" \
-F "idempotency_key=$IDEMPOTENCY_KEY")
 
if [ "$status" = "200" ]; then
cat /tmp/deepread-response.json
exit 0
fi
 
# 4xx other than 429 is a client error — retrying will not help.
if [ "$status" != "429" ] && [ "$status" -lt 500 ]; then
echo "Request failed with $status (not retryable):" >&2
cat /tmp/deepread-response.json >&2
exit 1
fi
 
backoff=$((2 ** (attempt - 1)))
echo "Got $status, retrying in ${backoff}s (attempt $attempt/$MAX_RETRIES)..." >&2
sleep "$backoff"
done
 
echo "Failed after $MAX_RETRIES attempts" >&2
exit 1

Fallback to manual status checking

If webhooks are delayed or missed, use manual status checking as a fallback. You can also check the Dashboard Jobs page to see all job statuses and error messages at a glance.

cURL Example
#!/usr/bin/env bash
# Poll a job as a fallback when a webhook is delayed or missed.
set -euo pipefail
 
API_KEY="${DEEPREAD_API_KEY:?set DEEPREAD_API_KEY}"
JOB_ID="550e8400-e29b-41d4-a716-446655440000"
MAX_WAIT=300
INTERVAL=5
 
elapsed=0
while [ "$elapsed" -lt "$MAX_WAIT" ]; do
job=$(curl -sS "https://api.deepread.tech/v1/jobs/$JOB_ID" \
-H "X-API-Key: $API_KEY")
status=$(printf '%s' "$job" | jq -r '.status')
 
case "$status" in
completed)
printf '%s\n' "$job"
exit 0
;;
failed)
echo "Job failed: $(printf '%s' "$job" | jq -r '.error // "unknown error"')" >&2
exit 1
;;
esac
 
sleep "$INTERVAL"
elapsed=$((elapsed + INTERVAL))
# Back off up to 30s so a long document doesn't hammer the API.
INTERVAL=$(( INTERVAL * 2 > 30 ? 30 : INTERVAL * 2 ))
done
 
echo "Job did not complete within ${MAX_WAIT}s" >&2
exit 1

Webhook delivery errors

How DeepRead retries

Each webhook is attempted up to 3 times, with a 30 second timeout per attempt and exponential backoff between them (1s, then 2s):

Your responseWhat DeepRead does
2xxSuccess — delivery is recorded, no further attempts
4xxPermanent failure — not retried. Treated as "this endpoint rejected the payload"
5xxRetried until the 3 attempts are used up
Timeout / connection errorRetried until the 3 attempts are used up

After the final attempt the outcome is stored on the job itself, so you can audit it: webhook.delivered, webhook.delivered_at and webhook.error on the GET /v1/jobs/{job_id} response.

Retries are not durable across a much longer outage — three attempts span a few seconds, not hours. If your endpoint is down longer than that, the delivery is lost and polling is the recovery path.

  • Poll GET /v1/jobs/{job_id} if you haven’t received a webhook within your SLO
  • Treat webhook payloads as notifications and fetch the canonical job result from the API

Handling webhook retries in your endpoint

Because a delivery can be attempted up to 3 times, your endpoint must be resilient to duplicates — a slow first attempt that times out on our side may still have been processed on yours. Deduplicate on the job id, the event and review.version together, so review.* deliveries that follow job.completed are not dropped as duplicates:

Python Example
import json
 
# In production, store this in a database/Redis (not in-memory) so the
# deduplication survives a restart and is shared across instances.
processed: set[tuple[str, str, int]] = set()
 
 
def handle_deepread_webhook_once(raw_body: bytes) -> None:
payload = json.loads(raw_body.decode("utf-8"))
review = payload.get("review") or {}
delivery = (payload["id"], payload["event"], review.get("version", 0))
 
# Deduplicate on (id, event, review.version) so handling is safe to repeat:
# a delivery is attempted up to 3 times, and a first attempt that timed out
# on our side may still have been processed on yours. review.* events for
# the same job arrive after job.completed and must not be dropped.
if delivery in processed:
return
processed.add(delivery)
 
print(f"Processing {delivery[1]} for job {delivery[0]}")

When to return error status codes

Return 200 OK when:

  • You successfully received the webhook (even if your own processing fails later)
  • The job was already processed (idempotency)
  • You want to stop retries for a specific delivery

Return 5xx when:

  • Your service is temporarily unavailable and you want the remaining attempts (503)
  • Your endpoint hit a transient error that a retry could clear

Avoid 4xx — including 401/403 — on requests that are ours. A 4xx is treated as a permanent rejection and stops retrying immediately, so an auth check that wrongly rejects our request costs you the delivery entirely. Authenticate deliveries by verifying the X-DeepRead-Signature header (see Handling Results); a request whose signature does not verify is not ours and can be rejected. After rotating your secret, deploy the new one straight away — deliveries sign with it from the next one on, and a stale secret would fail every verification. Whatever the signature says, confirm what you act on by fetching GET /v1/jobs/{job_id} with your own API key.

Monitoring and alerting

Set up monitoring for job failure rates, webhook delivery failures, processing times, and rate limit hits (429 responses).