Documentation

Uploads API Reference

Streaming uploads, presigned URLs, batch uploads, and session management

API Base URL

https://api.aionvision.tech/api/v2

Use Streaming Upload

For almost all use cases, use the streaming upload endpoints — they handle single files and batches in one request with no extra steps. Presigned URL workflows are documented below for edge cases only (files >100MB, browser-direct-to-S3 uploads).

Supported formats — Images: JPEG, PNG, WebP, GIF  ·  Documents: PDF, DOCX, TXT, MD

Streaming Upload

Recommended for Most Use Cases

Streaming upload is recommended for files up to 100MB. It processes files in-memory while uploading to S3 in parallel, reducing round-trips. Subject to memory limits: 2GB global, 500MB per tenant.

POST/api/v2/user-files/upload/stream

Direct streaming upload with parallel S3 upload and in-memory processing

Request

// multipart/form-data:
// Required:
// - file: The file to upload (up to 100MB)
// Optional form fields:
// - title: Custom title (max 255 chars)
// - tags: Comma-separated tags
// - auto_describe: Enable AI description (default: true)
// - skip_duplicates: Skip if file hash exists (default: false)
// - storage_target: "default" or "custom" (default: "default")
// Example using curl:
curl -X POST https://api.aionvision.tech/api/v2/user-files/upload/stream \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@photo.jpg" \
-F "title=Site Inspection" \
-F "tags=inspection,site" \
-F "skip_duplicates=false" \
-F "storage_target=default"

Response

{
"image_id": "550e8400-e29b-41d4-a716-446655440000",
"object_key": "uploads/{tenant_id}/{uuid}/photo.jpg",
"upload_method": "STREAMING",
"status": "completed", // "completed", "processing", or "skipped"
"processing_time_ms": 1250.5,
"s3_upload_completed": true,
"variants_queued": true,
"description_queued": true,
"skipped": false,
"skipped_existing_image_id": null, // Set when skipped=true (existing image UUID)
"storage_target": "default", // "default" or "custom"
"media_type": "image", // "image" or "document"
"document_type": null, // "pdf", "docx", "txt", "md" (for documents)
"text_extraction_status": null // "pending", "processing", "completed", "failed" (for documents)
}
// When skip_duplicates=true and file already exists:
{
"image_id": "550e8400-e29b-41d4-a716-446655440000",
"object_key": "uploads/{tenant_id}/{uuid}/photo.jpg",
"upload_method": "STREAMING",
"status": "skipped",
"processing_time_ms": 45.2,
"s3_upload_completed": true,
"variants_queued": false,
"description_queued": false,
"skipped": true,
"skipped_existing_image_id": "440e8400-e29b-41d4-a716-446655440099",
"storage_target": "default",
"media_type": "image",
"document_type": null,
"text_extraction_status": null
}
// Error: 429 Too Many Requests (backpressure)
// Response: {"detail": "Upload rejected: global_memory_limit. Please try again later."}
// Headers: Retry-After: 5
POST/api/v2/user-files/upload/stream-batch

Upload multiple files in a single request with server-managed concurrency

Request

// multipart/form-data:
// Required:
// - files: Multiple files to upload (up to tier limit)
// Optional form fields:
// - auto_describe: Enable AI descriptions (default: true)
// - skip_duplicates: Skip existing files by hash (default: false)
// - tags: Comma-separated tags for all files
// - intent: Processing intent (default: "describe") - "describe", "verify", "rules"
// - verification_level: "default", "quick", "standard", "thorough", "critical"
// - storage_target: "default" or "custom" (default: "default")
// Tier limits: FREE: 10, STARTER: 20, PROFESSIONAL: 20, ENTERPRISE: 50
curl -X POST https://api.aionvision.tech/api/v2/user-files/upload/stream-batch \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "files=@photo1.jpg" \
-F "files=@photo2.jpg" \
-F "skip_duplicates=false" \
-F "intent=describe" \
-F "storage_target=default"

Response

{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"total_files": 3,
"accepted_files": 3,
"rejected_files": 0,
"status": "completed", // "processing", "partial", "rejected", "completed"
"immediate_results": [ // Present for small batches (<=5 files)
{
"image_id": "660e8400-e29b-41d4-a716-446655440001",
"filename": "photo1.jpg",
"object_key": "uploads/{tenant_id}/{uuid}/photo1.jpg",
"status": "completed", // "completed", "processing", "failed", "skipped"
"processing_time_ms": 1250.5,
"skipped": false,
"skipped_existing_image_id": null,
"error": null,
"storage_target": "default",
"media_type": "image", // "image" or "document"
"document_type": null, // "pdf", "docx", "txt", "md"
"text_extraction_status": null // "pending", "processing", "completed", "failed"
}
],
"status_url": "/api/v2/uploads/sessions/{session_id}/status",
"websocket_channel": "batch.{session_id}",
"rejections": null // List of rejected files, if any
}
// When files are rejected:
{
"session_id": "...",
"total_files": 3,
"accepted_files": 2,
"rejected_files": 1,
"status": "partial",
"immediate_results": [...],
"status_url": "/api/v2/uploads/sessions/.../status",
"websocket_channel": "batch...",
"rejections": [
{
"filename": "bad-file.xyz",
"index": 2,
"reason": "Unsupported file type",
"retryable": false
}
]
}

Presigned URL Workflow (advanced — files >100MB)

Use this 3-step flow only for files over 100MB or when uploading directly from a browser to S3. For most uploads, use the streaming endpoint above.

POST/api/v2/uploads/request-presigned-url

Generate presigned URL for direct S3 upload (expires in 10 minutes)

Request

{
"filename": "example.jpg",
"content_type": "image/jpeg",
"size_bytes": 2048576,
"purpose": "image_analysis", // Optional (default: "image_analysis")
"idempotency_key": "unique-retry-key-123", // Optional: prevents duplicate uploads on retry
"storage_target": "default" // Optional: "default" or "custom" (BYOB bucket)
}
// Allowed content_type values:
// image/jpeg, image/jpg, image/png, image/webp, image/gif

Response

{
"upload_url": "https://nyc3.digitaloceanspaces.com/bucket/presigned-url...",
"upload_method": "PUT", // "POST" or "PUT"
"upload_fields": null, // Form fields for POST uploads (null for PUT)
"upload_headers": { "Content-Type": "image/jpeg" }, // Headers for PUT uploads (null for POST)
"object_key": "uploads/{tenant_id}/{uuid}/example.jpg",
"expires_at": "2025-01-15T10:40:00",
"max_size_bytes": 104857600,
"storage_target": "default", // "default" or "custom"
"bucket_name": null // Set when using custom storage
}
// Note: upload_method determines which field to use:
// - PUT: Use upload_headers when making the PUT request
// - POST: Use upload_fields as form fields in the POST request
GET/api/v2/uploads/quota-check

Check upload quota before starting (prevents failed uploads due to quota limits)

Request

// Query parameters:
?file_count=10 // Required: number of files to upload

Response

{
"can_proceed": true,
"requested": 10,
"available": 990,
"monthly_limit": 1000,
"current_usage": 10,
"message": null
}
// When quota exceeded:
{
"can_proceed": false,
"requested": 50,
"available": 10,
"monthly_limit": 1000,
"current_usage": 990,
"message": "You have 10 of 1000 verifications remaining this month. Please upload 10 or fewer files, or upgrade your plan."
}
POST/api/v2/uploads/confirm

Confirm upload completion and register file (auto-describe is triggered automatically)

Request

{
"object_key": "uploads/{tenant_id}/{uuid}/example.jpg",
"size_bytes": 2048576,
"checksum": "sha256:abc123def456...", // Optional: for verification
"content_type": "image/jpeg" // Optional: MIME type (default: "image/jpeg")
}

Response

{
"upload_id": "550e8400-e29b-41d4-a716-446655440000",
"object_key": "uploads/{tenant_id}/{uuid}/example.jpg",
"storage_path": "uploads/{tenant_id}/{uuid}/example.jpg",
"confirmed": true
}
// Note: After confirmation, auto-describe is queued automatically.
// Poll GET /api/v2/user-files/{image_id} to check description_status
POST/api/v2/uploads/check-duplicates

Check which file hashes already exist for this tenant before uploading

Request

{
"hashes": [
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592"
]
}
// hashes: SHA-256 content hashes (1-200 items)

Response

{
"duplicates": [
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
],
"unique": [
"d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592"
]
}

Batch Upload via Presigned URLs (advanced)

For most batch uploads, use /upload/stream-batch above. This presigned URL batch flow is limited to 5 files and requires multiple round-trips.

POST/api/v2/uploads/batch-prepare

Prepare batch upload with presigned URLs (max 5 files - use streaming for larger batches)

Request

{
"files": [
{
"filename": "image1.jpg",
"size_bytes": 1048576,
"content_type": "image/jpeg",
"idempotency_key": "file-1-retry-key", // Optional
"storage_target": "default" // Optional: "default" or "custom"
},
{
"filename": "document.pdf",
"size_bytes": 2097152,
"content_type": "application/pdf"
}
],
"intent": "describe", // "describe", "verify", "rules", or "document_extraction"
"verification_level": "standard", // Optional: "quick", "standard", "thorough", "critical"
"additional_params": { // Optional
"prompt": "Focus on safety equipment"
}
}
// Note: Limited to 5 files maximum per batch.
// For larger batches, use POST /api/v2/user-files/upload/stream-batch.
// Custom storage is not yet supported for batch uploads.

Response

{
"batch_id": "550e8400-e29b-41d4-a716-446655440000",
"upload_plan": {
"strategy": "parallel",
"max_concurrent": 5,
"retry_policy": { "max_attempts": 3, "backoff_ms": 1000 },
"timeout_per_upload": 180
},
"presigned_urls": [
{
"file_index": 0,
"filename": "image1.jpg",
"upload_url": "https://nyc3.digitaloceanspaces.com/...",
"upload_method": "PUT",
"upload_fields": null, // Form fields for POST uploads
"upload_headers": { "Content-Type": "image/jpeg" }, // Headers for PUT uploads
"object_key": "uploads/{tenant_id}/{uuid}/image1.jpg",
"expires_at": "2025-01-15T10:40:00",
"image_id": "660e8400-e29b-41d4-a716-446655440001" // Pre-created image ID
}
],
"total_size_bytes": 3145728,
"estimated_time_seconds": 45
}
// Error: 429 Too Many Requests (concurrent batch limit or quota exceeded)
POST/api/v2/uploads/batch-confirm

Confirm batch uploads and trigger processing (idempotent - safe to retry)

Request

{
"batch_id": "550e8400-e29b-41d4-a716-446655440000",
"confirmations": [
{
"object_key": "uploads/{tenant_id}/{uuid}/image1.jpg",
"success": true,
"file_size": 1048576,
"checksum": "sha256:abc123...", // Optional
"error_message": null, // Set if success=false
"media_type": "image" // Optional: "image" or "document"
}
],
"auto_process": true // Optional (default: true)
}

Response

{
"batch_id": "550e8400-e29b-41d4-a716-446655440000",
"successful_uploads": 2,
"failed_uploads": 0,
"processing_status": "queued", // "queued", "partial", "failed", or "already_processed"
"failed_files": null, // List of failed filenames if any
"message": "All 2 uploads confirmed successfully",
"stored_images": [
{
"image_id": "660e8400-e29b-41d4-a716-446655440001",
"object_key": "uploads/{tenant_id}/{uuid}/image1.jpg",
"filename": "image1.jpg",
"thumbnail_url": "https://...",
"variant_status": "pending",
"created_at": "2025-01-15T10:30:00Z"
}
]
}
// Error: 409 Conflict (another confirmation in progress for same batch)
GET/api/v2/uploads/batch/{batch_id}/status

Get the unified status of a batch with aggregated counts

Response

{
"batch_id": "550e8400-e29b-41d4-a716-446655440000",
"tenant_id": "tenant_123",
"overall_status": "processing",
"completion_percentage": 60.0,
"counts": {
"total": 5,
"uploading": 0,
"confirming": 0,
"queued": 1,
"processing": 1,
"completed": 3,
"failed": 0,
"partially_completed": 0,
"stuck": 0
},
"description_counts": {
"pending": 1,
"processing": 1,
"completed": 3,
"failed": 0,
"skipped": 0
},
"error_summary": {
"count": 0,
"messages": []
},
"created_at": "2025-01-15T10:00:00Z",
"last_activity_at": "2025-01-15T10:01:30Z"
}

Upload Status

GET/api/v2/uploads/status/{image_id}

Get unified status for a single image by combining data from images, upload_intents, and task_intents

Response

{
"image_id": "550e8400-e29b-41d4-a716-446655440000",
"tenant_id": "tenant_123",
"batch_id": "660e8400-e29b-41d4-a716-446655440001",
"unified_status": "processing", // "uploading", "confirming", "queued", "processing",
// "completed", "failed", "partially_completed"
"component_statuses": {
"variant_status": "completed",
"description_status": "processing",
"upload_intent_status": "completed",
"task_intent_status": "processing"
},
"task_intent_ids": ["task_001", "task_002"],
"error_message": null,
"last_error_at": null,
"created_at": "2025-01-15T10:30:00Z",
"last_updated_at": "2025-01-15T10:30:45Z",
"completed_at": null,
"retry_count": 0,
"processing_duration_seconds": 45.2,
"is_stuck": false,
"is_terminal": false
}

Upload Session Tracking

GET/api/v2/uploads/sessions/{session_id}/status

Get current status and progress of an upload session

Response

{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "processing", // "pending", "uploading", "processing",
// "completed", "failed", "expired", "cancelled"
"total_files": 20,
"completed_files": 15,
"failed_files": 1,
"skipped_files": 2,
"pending_files": 2,
"progress_percentage": 90.0,
"created_at": "2025-01-15T10:30:00Z",
"started_at": "2025-01-15T10:30:00Z",
"completed_at": null,
"estimated_completion_time": null,
"recent_completions": [
{
"filename": "photo1.jpg",
"image_id": "660e8400-e29b-41d4-a716-446655440001",
"status": "completed",
"description": "A site inspection showing...", // First 100 chars
"processing_time_ms": null
}
],
"recent_errors": [
{
"filename": "bad-photo.jpg",
"index": 0,
"error": "Processing failed: invalid format",
"retryable": true
}
],
"results_url": "/api/v2/uploads/sessions/{session_id}/results",
"websocket_channel": "batch.{session_id}"
}
GET/api/v2/uploads/sessions/{session_id}/results

Get paginated results from an upload session

Request

// Query parameters:
?include_failed=true // Include failed files (default: true)
&offset=0 // Pagination offset (default: 0)
&limit=100 // Results per page (1-500, default: 100)

Response

{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"results": [
{
"image_id": "660e8400-e29b-41d4-a716-446655440001",
"filename": "photo1.jpg",
"object_key": "uploads/{tenant_id}/{uuid}/photo1.jpg",
"status": "completed", // "completed", "failed", "skipped"
"description": "Safety inspection showing...",
"visible_text": "EXIT sign visible...",
"tags": ["safety", "construction"],
"processing_time_ms": null,
"error_message": null,
"thumbnail_url": "https://...",
"created_at": "2025-01-15T10:30:05Z"
}
],
"total_count": 20,
"offset": 0,
"limit": 100,
"has_more": false,
"summary": {
"total_files": 20,
"completed": 17,
"failed": 1,
"skipped": 2
}
}
POST/api/v2/uploads/sessions/{session_id}/cancel

Cancel a pending or in-progress upload session (already-processed files keep their results)

Response

{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "cancelled",
"total_files": 20,
"completed_files": 10,
"failed_files": 0,
"skipped_files": 0,
"pending_files": 10,
"progress_percentage": 50.0,
"created_at": "2025-01-15T10:30:00Z",
"started_at": "2025-01-15T10:30:00Z",
"completed_at": "2025-01-15T10:31:00Z",
"estimated_completion_time": null,
"recent_completions": [],
"recent_errors": [],
"results_url": "/api/v2/uploads/sessions/{session_id}/results",
"websocket_channel": "batch.{session_id}"
}
// Error: 400 Bad Request if session is already completed/cancelled/expired
GET/api/v2/uploads/sessions

List upload sessions for the authenticated tenant

Request

// Query parameters:
?status=processing // Optional: filter by status
&upload_method=streaming // Optional: filter by method ("presigned" or "streaming")
&offset=0 // Pagination offset (default: 0)
&limit=20 // Results per page (1-100, default: 20)

Response

{
"sessions": [
{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"upload_method": "streaming",
"total_files": 20,
"completed_files": 18,
"failed_files": 1,
"skipped_files": 1,
"progress_percentage": 100.0,
"created_at": "2025-01-15T10:30:00Z",
"completed_at": "2025-01-15T10:32:00Z"
}
],
"total_count": 15,
"offset": 0,
"limit": 20,
"has_more": false
}

Admin / Recovery Endpoints

Recovery Endpoints

These endpoints help recover interrupted uploads and clean up stale batches. They are typically called automatically by the frontend on page load, but can also be called manually for troubleshooting.

GET/api/v2/uploads/pending-batches

Get batches that may need recovery (pending/uploading, created in last 2 hours, never confirmed)

Response

{
"batches": [
{
"batch_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"total_files": 5,
"confirmed_count": 0,
"created_at": "2025-01-15T10:30:00Z",
"age_seconds": 3600
}
],
"has_recoverable": true
}
POST/api/v2/uploads/batch/{batch_id}/auto-recover

Auto-recover an interrupted batch by verifying which files exist in S3 and confirming them

Response

{
"batch_id": "550e8400-e29b-41d4-a716-446655440000",
"recovered_files": 3,
"failed_files": 2,
"status": "recovered", // "recovered", "nothing_to_recover", "already_processed", "not_found"
"message": "Recovered 3 files, 2 files not found in storage"
}
POST/api/v2/uploads/cleanup-stale-batches

Clean up stale batches older than 30 minutes by marking them as expired

Response

{
"cleaned_count": 2,
"message": "Cleaned up 2 interrupted upload(s). Please re-upload if needed."
}
// When nothing to clean up:
{
"cleaned_count": 0,
"message": "No stale uploads found."
}
GET/api/v2/uploads/stuck-uploads

Find uploads stuck in non-terminal states for investigation

Request

// Query parameters:
?stuck_minutes=30 // Minutes before considering stuck (default: 30)
&limit=100 // Max results (default: 100)

Response

{
"stuck_count": 1,
"images": [
{
"image_id": "550e8400-e29b-41d4-a716-446655440000",
"tenant_id": "tenant_123",
"batch_id": "660e8400-e29b-41d4-a716-446655440001",
"unified_status": "processing",
"component_statuses": {
"variant_status": "completed",
"description_status": "processing",
"upload_intent_status": "completed",
"task_intent_status": "processing"
},
"task_intent_ids": ["task_001"],
"error_message": null,
"last_error_at": null,
"created_at": "2025-01-15T10:00:00Z",
"last_updated_at": "2025-01-15T10:00:30Z",
"completed_at": null,
"retry_count": 0,
"processing_duration_seconds": 1800.0,
"is_stuck": true,
"is_terminal": false
}
]
}