Skip to main content

Polling Best Practices

Report generation is asynchronous. After calling POST /api/reports, you poll GET /api/reports/:id until the status reaches completed or failed. This guide covers how to do that reliably in production.

Status Lifecycle

Reports progress through these statuses:

pending → processing → completed
└→ failed
StatusMeaningAction
pendingQueued, not started yetKeep polling
processingAnalysis and report generation in progressKeep polling
completedReport ready, download URLs availableDownload PDF/DOCX
failedSomething went wrongRead progress.message for details

Progress Messages

While polling, the progress field provides updates on what's happening:

{
"status": "processing",
"progress": {
"message": "Starting analysis..."
}
}

Common progress messages in order:

  1. Starting analysis...
  2. Analysis complete. Building report documents...
  3. Report generation complete

Simple Polling: Fixed Interval

Poll every 5 seconds. Reports typically complete in 2-5 minutes.

import time

def wait_for_report(report_id, timeout=600, interval=5):
deadline = time.time() + timeout
while time.time() < deadline:
data = api("GET", f"/api/reports/{report_id}")["data"]
status = data["status"]
message = (data.get("progress") or {}).get("message", "")
print(f" [{status}] {message}")

if status == "completed":
return data
if status in ("failed", "cancelled"):
raise Exception(f"Report {status}: {message}")

time.sleep(interval)
raise Exception("Timed out waiting for report")

completed = wait_for_report(report_id)
pdf_url = completed["output"]["pdf_url"]

Production Polling: Exponential Backoff

Start fast, slow down over time. Reduces API calls on longer reports while still catching quick completions.

import time

def wait_for_report(report_id, timeout=600):
deadline = time.time() + timeout
interval = 3 # start at 3 seconds
max_interval = 15 # cap at 15 seconds

while time.time() < deadline:
data = api("GET", f"/api/reports/{report_id}")["data"]
status = data["status"]

if status == "completed":
return data
if status in ("failed", "cancelled"):
msg = (data.get("progress") or {}).get("message", "")
raise Exception(f"Report {status}: {msg}")

time.sleep(interval)
interval = min(interval * 1.5, max_interval)

raise Exception("Timed out waiting for report")

Handling Failures

When a report fails, the progress.message field explains why:

{
"status": "failed",
"progress": {
"message": "Delta-V calculation failed: insufficient image quality"
}
}

Common Failure Reasons

MessageCauseResolution
Delta-V calculation failedML couldn't analyze the photosUpload clearer or more damage photos
Biomechanics analysis failedOccupant data issueVerify occupant fields are complete
Report generation timed outProcessing took too longRetry — this is usually transient

Should You Retry?

Failure typeRetry?
Image quality issuesNo — upload better photos first
TimeoutYes — create a new report for the same case
Internal errorsYes — wait a minute, then retry

To retry, create a new report with the same case_id. You don't need to re-upload files.

# Retry: create a new report on the same case
report = api("POST", "/api/reports", json={
"case_id": case_id,
"type": "technical_report",
})
new_report_id = report["data"]["id"]

Downloading Results

Once status is completed, the output field contains signed download URLs:

{
"status": "completed",
"output": {
"pdf_url": "https://storage.silentwitness.ai/reports/...",
"docx_url": "https://storage.silentwitness.ai/reports/..."
}
}

Download URLs expire after 1 hour. If you need fresh URLs, call GET /api/reports/:id again — it generates new signed URLs each time.

for fmt in ("pdf", "docx"):
url = completed["output"][f"{fmt}_url"]
resp = requests.get(url)
with open(f"report.{fmt}", "wb") as f:
f.write(resp.content)
print(f"Saved report.{fmt} ({len(resp.content):,} bytes)")

Timeout Guidelines

ScenarioRecommended timeout
accident_only (crash analysis)5 minutes
accident_injury (crash + biomechanics)10 minutes
Testing with use_demo_data: true2 minutes

Next Steps