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
| Status | Meaning | Action |
|---|---|---|
pending | Queued, not started yet | Keep polling |
processing | Analysis and report generation in progress | Keep polling |
completed | Report ready, download URLs available | Download PDF/DOCX |
failed | Something went wrong | Read 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:
Starting analysis...Analysis complete. Building report documents...Report generation complete
Simple Polling: Fixed Interval
Poll every 5 seconds. Reports typically complete in 2-5 minutes.
- Python
- TypeScript
- Go
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"]
async function waitForReport(reportId: string, timeoutMs = 600_000, intervalMs = 5_000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const { data } = await api("GET", `/api/reports/${reportId}`);
console.log(` [${data.status}] ${data.progress?.message ?? ""}`);
if (data.status === "completed") return data;
if (data.status === "failed" || data.status === "cancelled") {
throw new Error(`Report ${data.status}: ${data.progress?.message}`);
}
await new Promise((r) => setTimeout(r, intervalMs));
}
throw new Error("Timed out waiting for report");
}
const completed = await waitForReport(reportId);
const pdfUrl = completed.output.pdf_url;
func waitForReport(reportID string, timeout time.Duration) (map[string]any, error) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
resp, err := doRequest("GET", "/api/reports/"+reportID, nil)
if err != nil {
return nil, err
}
var result struct {
Data struct {
Status string `json:"status"`
Progress map[string]any `json:"progress"`
Output map[string]any `json:"output"`
} `json:"data"`
}
json.Unmarshal(resp, &result)
fmt.Printf(" [%s] %v\n", result.Data.Status, result.Data.Progress["message"])
switch result.Data.Status {
case "completed":
return result.Data.Output, nil
case "failed", "cancelled":
return nil, fmt.Errorf("report %s", result.Data.Status)
}
time.Sleep(5 * time.Second)
}
return nil, fmt.Errorf("timed out waiting for report")
}
output, err := waitForReport(reportID, 10*time.Minute)
pdfURL := output["pdf_url"].(string)
Production Polling: Exponential Backoff
Start fast, slow down over time. Reduces API calls on longer reports while still catching quick completions.
- Python
- TypeScript
- Go
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")
async function waitForReport(reportId: string, timeoutMs = 600_000) {
const deadline = Date.now() + timeoutMs;
let interval = 3_000;
const maxInterval = 15_000;
while (Date.now() < deadline) {
const { data } = await api("GET", `/api/reports/${reportId}`);
if (data.status === "completed") return data;
if (data.status === "failed" || data.status === "cancelled") {
throw new Error(`Report ${data.status}: ${data.progress?.message ?? ""}`);
}
await new Promise((r) => setTimeout(r, interval));
interval = Math.min(interval * 1.5, maxInterval);
}
throw new Error("Timed out waiting for report");
}
func waitForReport(reportID string, timeout time.Duration) (map[string]any, error) {
deadline := time.Now().Add(timeout)
interval := 3 * time.Second
maxInterval := 15 * time.Second
for time.Now().Before(deadline) {
resp, err := doRequest("GET", "/api/reports/"+reportID, nil)
if err != nil {
return nil, err
}
var result struct {
Data struct {
Status string `json:"status"`
Progress map[string]any `json:"progress"`
Output map[string]any `json:"output"`
} `json:"data"`
}
json.Unmarshal(resp, &result)
switch result.Data.Status {
case "completed":
return result.Data.Output, nil
case "failed", "cancelled":
return nil, fmt.Errorf("report %s", result.Data.Status)
}
time.Sleep(interval)
interval = time.Duration(float64(interval) * 1.5)
if interval > maxInterval {
interval = maxInterval
}
}
return nil, fmt.Errorf("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
| Message | Cause | Resolution |
|---|---|---|
Delta-V calculation failed | ML couldn't analyze the photos | Upload clearer or more damage photos |
Biomechanics analysis failed | Occupant data issue | Verify occupant fields are complete |
Report generation timed out | Processing took too long | Retry — this is usually transient |
Should You Retry?
| Failure type | Retry? |
|---|---|
| Image quality issues | No — upload better photos first |
| Timeout | Yes — create a new report for the same case |
| Internal errors | Yes — wait a minute, then retry |
To retry, create a new report with the same case_id. You don't need to re-upload files.
- Python
- TypeScript
- Go
# 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"]
// Retry: create a new report on the same case
const report = await api("POST", "/api/reports", {
case_id: caseId,
type: "technical_report",
});
const newReportId = report.data.id;
// Retry: create a new report on the same case
resp, _ := doRequest("POST", "/api/reports", map[string]any{
"case_id": caseID,
"type": "technical_report",
})
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.
- Python
- TypeScript
- Go
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)")
for (const fmt of ["pdf", "docx"] as const) {
const url = completed.output[`${fmt}_url`];
const resp = await fetch(url);
await Bun.write(`report.${fmt}`, resp);
console.log(`Saved report.${fmt}`);
}
for _, ext := range []string{"pdf", "docx"} {
url := output[ext+"_url"].(string)
resp, _ := http.Get(url)
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
os.WriteFile("report."+ext, body, 0644)
fmt.Printf("Saved report.%s (%d bytes)\n", ext, len(body))
}
Timeout Guidelines
| Scenario | Recommended timeout |
|---|---|
accident_only (crash analysis) | 5 minutes |
accident_injury (crash + biomechanics) | 10 minutes |
Testing with use_demo_data: true | 2 minutes |
Next Steps
- Quick Start — Full workflow overview
- End-to-End Example — Complete scripts with polling built in
- Errors — Error codes reference