POST /api/v1/storage/image/upload
API version: v1.2.4
Upload a starting image for MiniMax H3 Standard or Turbo image-to-video, or a
reference image for another supported workflow. Use the returned s3_url in
your generation request. Uploading a file and generating a video are separate
requests.
Endpoint
POST /api/v1/storage/image/uploadAuthentication
Required. Include your API key in the request header:
X-API-Key: your-api-key-hereRequest
Content Type
multipart/form-dataForm Fields
| Field | Type | Required | Description |
|---|---|---|---|
image | file | Yes | Image file to upload (JPG, PNG, WebP) |
Supported Formats
- JPEG/JPG: Recommended for photos
- PNG: Recommended for graphics with transparency
- WebP: Modern format with good compression
Image Requirements
- Published default file limit: 20 mebibytes (20,971,520 bytes). Account or service limits can differ; use the upload response when a file is rejected.
- Content: Use a nonempty JPEG, PNG or WebP image with a matching filename.
- Generation dimensions: Uploading does not set the video's output size.
Choose H3's
resolution_tierandaspect_ratioin the generation request; see video generation limits. - Preparation: Choose a clear source image with the framing you want to animate. Do any required resizing or color conversion before uploading; this endpoint does not promise automatic RGB conversion or background fills.
See upload limitations for storage constraints and import an image to start from a remote URL.
Response
Success (200 OK)
{
"file_id": "img-abc123def456",
"s3_url": "https://s3.amazonaws.com/bucket/images/abc123def456.jpg",
"filename": "my_image.jpg",
"message": "Image uploaded successfully"
}Response Fields
| Field | Type | Description |
|---|---|---|
file_id | string | Unique identifier for the uploaded file |
s3_url | string | Storage URL to use in /api/v1/inference/submit request |
filename | string | Original filename |
message | string | Success message |
Examples
Image Upload (Python)
import requests
MODEL_ID = "minimax/h3/image-to-video"
# For Turbo, use "minimax/h3/image-to-video/turbo".
# Upload image
with open("input_image.jpg", "rb") as f:
response = requests.post(
"https://phosor.ai/api/v1/storage/image/upload",
headers={"X-API-Key": "your-api-key"},
files={"image": ("input_image.jpg", f, "image/jpeg")},
timeout=60
)
response.raise_for_status()
result = response.json()
image_url = result["s3_url"]
print(f"Image uploaded: {image_url}")
# Use the image URL in video generation
video_response = requests.post(
"https://phosor.ai/api/v1/inference/submit",
headers={
"X-API-Key": "your-api-key",
"Content-Type": "application/json"
},
json={
"model": MODEL_ID,
"prompt": "The scene comes alive with gentle movement",
"image_url": image_url,
"duration": 5,
"resolution_tier": "480p",
"aspect_ratio": "16:9"
},
timeout=60
)
video_response.raise_for_status()
request_id = video_response.json()["request_id"]
print(f"Video generation started: {request_id}")Upload with Error Handling
import requests
import os
def upload_image(image_path, api_key):
"""
Upload an image for I2V generation.
Args:
image_path: Path to the image file
api_key: Your API key
Returns:
Storage URL of the uploaded image, or None if failed
"""
# Validate file exists
if not os.path.exists(image_path):
print(f"Error: File {image_path} not found")
return None
# Published default: 20 * 1024 * 1024 bytes. Adjust for your account limit.
file_size = os.path.getsize(image_path)
if file_size > 20 * 1024 * 1024:
print(f"Error: File too large ({file_size} bytes). Default limit: 20971520 bytes.")
return None
# Check file extension
valid_extensions = ['.jpg', '.jpeg', '.png', '.webp']
_, ext = os.path.splitext(image_path)
if ext.lower() not in valid_extensions:
print(f"Error: Invalid format {ext}. Supported: {valid_extensions}")
return None
# Determine content type
content_types = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.webp': 'image/webp'
}
content_type = content_types.get(ext.lower(), 'application/octet-stream')
# Upload
try:
with open(image_path, "rb") as f:
response = requests.post(
"https://phosor.ai/api/v1/storage/image/upload",
headers={"X-API-Key": api_key},
files={"image": (os.path.basename(image_path), f, content_type)},
timeout=60
)
if response.status_code == 200:
result = response.json()
print(f"Upload successful: {result['filename']}")
return result["s3_url"]
else:
print(f"Upload failed: {response.status_code} - {response.text}")
return None
except requests.exceptions.RequestException as e:
print(f"Network error: {e}")
return None
# Usage
image_url = upload_image("my_photo.jpg", "your-api-key")
if image_url:
print(f"Use this URL: {image_url}")Upload Multiple Images
import requests
import os
def upload_images_batch(image_paths, api_key):
"""Upload multiple images and return their URLs"""
results = []
for image_path in image_paths:
print(f"Uploading {image_path}...")
with open(image_path, "rb") as f:
response = requests.post(
"https://phosor.ai/api/v1/storage/image/upload",
headers={"X-API-Key": api_key},
files={"image": (os.path.basename(image_path), f)},
timeout=60
)
if response.status_code == 200:
result = response.json()
results.append({
"filename": os.path.basename(image_path),
"s3_url": result["s3_url"],
"file_id": result["file_id"]
})
print(f" ✓ Uploaded successfully")
else:
print(f" ✗ Failed: {response.text}")
results.append({
"filename": os.path.basename(image_path),
"error": response.text
})
return results
# Usage
image_files = ["image1.jpg", "image2.jpg", "image3.jpg"]
uploaded = upload_images_batch(image_files, "your-api-key")
for item in uploaded:
if "s3_url" in item:
print(f"{item['filename']}: {item['s3_url']}")cURL Example
curl -X POST https://phosor.ai/api/v1/storage/image/upload \
-H "X-API-Key: your-api-key" \
-F "image=@/path/to/image.jpg"JavaScript/Node.js Example
const fetch = require('node-fetch');
const FormData = require('form-data');
const fs = require('fs');
async function uploadImage(imagePath, apiKey) {
const formData = new FormData();
formData.append('image', fs.createReadStream(imagePath));
const response = await fetch('https://phosor.ai/api/v1/storage/image/upload', {
method: 'POST',
headers: {
'X-API-Key': apiKey
},
body: formData
});
if (response.ok) {
const result = await response.json();
console.log('Image uploaded:', result.s3_url);
return result.s3_url;
} else {
console.error('Upload failed:', await response.text());
return null;
}
}
// Usage
uploadImage('./input.jpg', 'your-api-key')
.then(url => console.log('URL:', url));Browser/React Example
import React, { useState } from 'react';
function ImageUploader({ apiKey, onUploadComplete }) {
const [uploading, setUploading] = useState(false);
const handleFileChange = async (event) => {
const file = event.target.files[0];
if (!file) return;
// Validate file
// Published default; adjust if your account uses another limit.
if (file.size > 20 * 1024 * 1024) {
alert('File too large. Default limit is 20,971,520 bytes.');
return;
}
if (!['image/jpeg', 'image/png', 'image/webp'].includes(file.type)) {
alert('Invalid file type. Please upload JPG, PNG, or WebP.');
return;
}
setUploading(true);
try {
const formData = new FormData();
formData.append('image', file);
const response = await fetch('https://phosor.ai/api/v1/storage/image/upload', {
method: 'POST',
headers: {
'X-API-Key': apiKey
},
body: formData
});
if (response.ok) {
const result = await response.json();
onUploadComplete(result.s3_url);
} else {
alert('Upload failed: ' + await response.text());
}
} catch (error) {
alert('Upload error: ' + error.message);
} finally {
setUploading(false);
}
};
return (
<div>
<input
type="file"
accept="image/jpeg,image/png,image/webp"
onChange={handleFileChange}
disabled={uploading}
/>
{uploading && <p>Uploading...</p>}
</div>
);
}
export default ImageUploader;Error Responses
Invalid File Format (400 Bad Request)
{
"error": "Invalid image file",
"message": "image field must be a file upload"
}Missing Image (400 Bad Request)
{
"error": "Image file required",
"message": "image field is required in form data"
}File Too Large
This upload endpoint returns HTTP 400 when an image is rejected for exceeding
the active file-size limit. Read the response's error and message fields;
the message can vary. Reduce the file size before trying again.
Unauthorized (401 Unauthorized)
Use a valid API key in X-API-Key. Authentication errors follow the
common API contract.
Rate Limited (429 Too Many Requests)
Wait for the Retry-After interval. The response headers describe the active
request limit; this page does not define a separate upload-only allowance.
After submitting a video, use its request_id to check
request status and retrieve the result.
Compare the H3 Standard and Turbo modes or open
Standard and
Turbo.
Image Processing Tips
Optimize Before Upload
from PIL import Image
import io
import requests
def optimize_image(image_path, max_size=(1024, 1024), quality=85):
"""Resize and compress image before upload"""
img = Image.open(image_path)
# Convert RGBA to RGB if needed
if img.mode == 'RGBA':
img = img.convert('RGB')
# Resize if too large
img.thumbnail(max_size, Image.Resampling.LANCZOS)
# Save to bytes
buffer = io.BytesIO()
img.save(buffer, format='JPEG', quality=quality, optimize=True)
buffer.seek(0)
return buffer
# Usage
optimized = optimize_image("large_image.png")
response = requests.post(
"https://phosor.ai/api/v1/storage/image/upload",
headers={"X-API-Key": "your-api-key"},
files={"image": ("optimized.jpg", optimized, "image/jpeg")},
timeout=60
)
response.raise_for_status()Check Image Dimensions
from PIL import Image
def validate_image(image_path):
"""Validate image before upload"""
try:
img = Image.open(image_path)
width, height = img.size
print(f"Image size: {width}x{height}")
print(f"Format: {img.format}")
print(f"Mode: {img.mode}")
# Check if aspect ratio is reasonable
aspect_ratio = width / height
if aspect_ratio < 0.5 or aspect_ratio > 2.0:
print("Warning: Unusual aspect ratio")
return True
except Exception as e:
print(f"Invalid image: {e}")
return False
# Usage
if validate_image("input.jpg"):
# Proceed with upload
pass