Uploading Evidence
This guide covers everything about uploading files to a case: file categories, auto-linking to vehicles, supported types, and best practices.
How It Works
Every file is uploaded individually via POST /api/files/upload as multipart form-data. You pass:
case_id(required) — which case this file belongs tofile_category— what type of file it isvehicle_role— which vehicle it belongs to (plaintiffordefendant)
The API automatically links files to the correct vehicle based on vehicle_role. No separate linking step is needed.
File Categories
| Category | Vehicle role required? | What it does |
|---|---|---|
vehicle_photo | Yes | Damage photo appended to that vehicle's image gallery |
edr_document | Yes | Sets as that vehicle's EDR file (replaces previous) |
tcr_document | No | Case-level police report, accident fields extracted automatically |
Vehicle Photos
Upload 4-8 damage photos per vehicle for best analysis results. Include front, rear, sides, and close-ups of damage areas.
- curl
- Python
- TypeScript
- Go
curl -X POST "https://api.silentwitness.ai/api/files/upload" \
-H "X-API-Key: $API_KEY" \
-F "file=@front_damage.jpg" \
-F "case_id=$CASE_ID" \
-F "file_category=vehicle_photo" \
-F "vehicle_role=plaintiff"
import requests
def upload_file(case_id, filepath, file_category, vehicle_role=None):
fields = {"case_id": case_id, "file_category": file_category}
if vehicle_role:
fields["vehicle_role"] = vehicle_role
with open(filepath, "rb") as f:
resp = requests.post(
f"{BASE_URL}/api/files/upload",
headers={"X-API-Key": API_KEY},
files={"file": (os.path.basename(filepath), f)},
data=fields,
)
resp.raise_for_status()
return resp.json()["data"]["file_id"]
# Upload plaintiff damage photos
for photo in ["front_damage.jpg", "rear_damage.jpg", "side_damage.jpg"]:
file_id = upload_file(case_id, photo, "vehicle_photo", "plaintiff")
print(f"Uploaded: {file_id}")
async function uploadFile(
caseId: string, filePath: string, fileCategory: string, vehicleRole?: string,
): Promise<string> {
const form = new FormData();
form.append("file", Bun.file(filePath));
form.append("case_id", caseId);
form.append("file_category", fileCategory);
if (vehicleRole) form.append("vehicle_role", vehicleRole);
const resp = await fetch(`${BASE_URL}/api/files/upload`, {
method: "POST",
headers: { "X-API-Key": API_KEY },
body: form,
});
if (!resp.ok) throw new Error(`Upload failed: ${resp.status}`);
const data = await resp.json();
return data.data.file_id;
}
// Upload plaintiff damage photos
for (const photo of ["front_damage.jpg", "rear_damage.jpg", "side_damage.jpg"]) {
const fileId = await uploadFile(caseId, photo, "vehicle_photo", "plaintiff");
console.log(`Uploaded: ${fileId}`);
}
func uploadFile(caseID, filePath, fileCategory, vehicleRole string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
part, _ := w.CreateFormFile("file", filepath.Base(filePath))
io.Copy(part, file)
w.WriteField("case_id", caseID)
w.WriteField("file_category", fileCategory)
if vehicleRole != "" {
w.WriteField("vehicle_role", vehicleRole)
}
w.Close()
req, _ := http.NewRequest("POST", baseURL+"/api/files/upload", &buf)
req.Header.Set("X-API-Key", apiKey)
req.Header.Set("Content-Type", w.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
return result["data"].(map[string]any)["file_id"].(string), nil
}
// Upload plaintiff damage photos
for _, photo := range []string{"front_damage.jpg", "rear_damage.jpg", "side_damage.jpg"} {
fileID, _ := uploadFile(caseID, photo, "vehicle_photo", "plaintiff")
fmt.Printf("Uploaded: %s\n", fileID)
}
Each upload appends to the vehicle's photo gallery. Upload as many as needed.
EDR Documents
Event Data Recorder reports must be PDF files. Each vehicle can have one EDR file — uploading a new one replaces the previous.
- curl
- Python
- TypeScript
- Go
curl -X POST "https://api.silentwitness.ai/api/files/upload" \
-H "X-API-Key: $API_KEY" \
-F "file=@edr_report.pdf" \
-F "case_id=$CASE_ID" \
-F "file_category=edr_document" \
-F "vehicle_role=plaintiff"
edr_id = upload_file(case_id, "edr_report.pdf", "edr_document", "plaintiff")
const edrId = await uploadFile(caseId, "edr_report.pdf", "edr_document", "plaintiff");
edrID, _ := uploadFile(caseID, "edr_report.pdf", "edr_document", "plaintiff")
Traffic Collision Reports (TCR)
Police reports are case-level — do not pass vehicle_role. The API automatically extracts accident details (date, time, location, description) from the PDF during report generation.
- curl
- Python
- TypeScript
- Go
curl -X POST "https://api.silentwitness.ai/api/files/upload" \
-H "X-API-Key: $API_KEY" \
-F "file=@police_report.pdf" \
-F "case_id=$CASE_ID" \
-F "file_category=tcr_document"
tcr_id = upload_file(case_id, "police_report.pdf", "tcr_document")
const tcrId = await uploadFile(caseId, "police_report.pdf", "tcr_document");
tcrID, _ := uploadFile(caseID, "police_report.pdf", "tcr_document", "")
Supported File Types
| Type | MIME | Extensions | Use case |
|---|---|---|---|
| JPEG | image/jpeg | .jpg, .jpeg | Vehicle photos |
| PNG | image/png | .png | Vehicle photos |
| GIF | image/gif | .gif | Vehicle photos |
| WebP | image/webp | .webp | Vehicle photos |
application/pdf | .pdf | EDR reports, police reports |
The server detects file type from the file's bytes, not the Content-Type header. Unsupported types are rejected with 400 Bad Request.
Constraints
- Maximum file size: 50MB per file
- Empty files: Not allowed
- EDR documents: Must be PDF
- TCR documents: Do not pass
vehicle_role(returns 400 if you do)
Upload Order
Files can be uploaded in any order and in parallel. The only requirement is that the case (and its vehicles) must exist before uploading. Since POST /api/cases creates vehicles in the same request, this is always satisfied.
POST /api/cases → creates case + vehicles
POST /api/files/upload (photo 1) ┐
POST /api/files/upload (photo 2) ├─ can run in parallel
POST /api/files/upload (EDR) ┤
POST /api/files/upload (TCR) ┘
POST /api/reports → all files already linked
Response
Each upload returns the file ID and status:
{
"success": true,
"data": {
"file_id": "file_abc123",
"file_name": "front_damage.jpg",
"status": "ready",
"download_url": "https://api.silentwitness.ai/api/files/file_abc123/download",
"url": "https://storage.silentwitness.ai/..."
}
}
You don't need to save or use the file_id — the report workflow automatically picks up all files linked to the case's vehicles.
Common Errors
| Error | Code | Resolution |
|---|---|---|
case_id is required | case_id_missing | Include case_id in the form data |
No file provided | file_missing | Include a file field in the multipart form |
File size exceeds maximum limit | file_too_large | File must be under 50MB |
EDR documents must be PDF files | unsupported_file_type | EDR uploads only accept PDF |
do not pass vehicle_role | vehicle_index_not_allowed | TCR is case-level, remove vehicle_role |
Next Steps
- Quick Start — Full workflow from case to report
- Upload API Reference — Endpoint details
- End-to-End Example — Complete working scripts