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.
/api/v2/user-files/upload/streamDirect 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/api/v2/user-files/upload/stream-batchUpload 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.
/api/v2/uploads/request-presigned-urlGenerate 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/gifResponse
{ "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/api/v2/uploads/quota-checkCheck upload quota before starting (prevents failed uploads due to quota limits)
Request
// Query parameters:?file_count=10 // Required: number of files to uploadResponse
{ "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."}/api/v2/uploads/confirmConfirm 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/api/v2/uploads/check-duplicatesCheck 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.
/api/v2/uploads/batch-preparePrepare 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)/api/v2/uploads/batch-confirmConfirm 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)/api/v2/uploads/batch/{batch_id}/statusGet 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
/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
/api/v2/uploads/sessions/{session_id}/statusGet 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}"}/api/v2/uploads/sessions/{session_id}/resultsGet 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 }}/api/v2/uploads/sessions/{session_id}/cancelCancel 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/api/v2/uploads/sessionsList 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.
/api/v2/uploads/pending-batchesGet 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}/api/v2/uploads/batch/{batch_id}/auto-recoverAuto-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"}/api/v2/uploads/cleanup-stale-batchesClean 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."}/api/v2/uploads/stuck-uploadsFind 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 } ]}