Upload Product Images
The Upload Product Images endpoint enables you to add new images to an existing product. Images are automatically uploaded to secure cloud storage, optimized for web delivery, and associated with the specified product. This endpoint supports multiple image uploads in a single request and maintains existing images while adding new ones.
Endpoint DetailsCopied!
-
Method:
POST
-
URL:
/api/v0/products/{id}/images
-
Content-Type:
multipart/form-data
-
Authentication: Required (API Key & Secret)
-
Rate Limiting: 100 requests per minute
-
Idempotency: Supported (recommended for upload operations)
Path ParametersCopied!
Parameter |
Type |
Required |
Description |
Example |
---|---|---|---|---|
|
string (UUID) |
Yes |
Unique product identifier |
|
Request ParametersCopied!
Form Data Fields
Field |
Type |
Required |
Description |
Limits |
---|---|---|---|---|
|
file[] |
Yes |
Image files to upload |
Max 10 files per request |
Image Requirements
Specification |
Requirement |
Recommendation |
---|---|---|
File Formats |
JPEG, PNG, WebP, GIF |
JPEG or PNG for best compatibility |
File Size |
Maximum 10MB per image |
2-5MB for optimal performance |
Dimensions |
Minimum 200x200px |
1200x1200px or larger |
Aspect Ratio |
Any ratio supported |
Square (1:1) recommended |
Color Space |
RGB, sRGB |
sRGB for web compatibility |
Quality |
Any |
85-95% JPEG quality |
Request ExamplesCopied!
Single Image Upload
curl -X POST "https://api.devdraft.com/api/v0/products/550e8400-e29b-41d4-a716-446655440000/images" \
-H "x-client-key: YOUR_CLIENT_KEY" \
-H "x-client-secret: YOUR_CLIENT_SECRET" \
-H "x-idempotency-key: $(uuidgen)" \
-F "images=@/path/to/product-image.jpg"
Multiple Images Upload
curl -X POST "https://api.devdraft.com/api/v0/products/550e8400-e29b-41d4-a716-446655440000/images" \
-H "x-client-key: YOUR_CLIENT_KEY" \
-H "x-client-secret: YOUR_CLIENT_SECRET" \
-H "x-idempotency-key: $(uuidgen)" \
-F "images=@/path/to/main-image.jpg" \
-F "images=@/path/to/detail-image.png" \
-F "images=@/path/to/lifestyle-image.jpg"
JavaScript/Node.js Examples
// Upload single image
const uploadProductImage = async (productId, imageFile) => {
const formData = new FormData();
formData.append('images', imageFile);
try {
const response = await fetch(`https://api.devdraft.com/api/v0/products/${productId}/images`, {
method: 'POST',
headers: {
'x-client-key': 'YOUR_CLIENT_KEY',
'x-client-secret': 'YOUR_CLIENT_SECRET',
'x-idempotency-key': generateUUID()
// Don't set Content-Type - browser will set it with boundary
},
body: formData
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const updatedProduct = await response.json();
return updatedProduct;
} catch (error) {
console.error('Error uploading image:', error);
throw error;
}
};
// Upload multiple images
const uploadMultipleImages = async (productId, imageFiles) => {
const formData = new FormData();
// Add all image files
imageFiles.forEach(file => {
formData.append('images', file);
});
try {
const response = await fetch(`https://api.devdraft.com/api/v0/products/${productId}/images`, {
method: 'POST',
headers: {
'x-client-key': 'YOUR_CLIENT_KEY',
'x-client-secret': 'YOUR_CLIENT_SECRET',
'x-idempotency-key': generateUUID()
},
body: formData
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Error uploading images:', error);
throw error;
}
};
// File input handler for web applications
const handleImageUpload = async (event, productId) => {
const files = Array.from(event.target.files);
// Validate files before upload
const validFiles = files.filter(file => {
// Check file type
if (!['image/jpeg', 'image/png', 'image/webp', 'image/gif'].includes(file.type)) {
console.warn(`Skipping unsupported file type: ${file.type}`);
return false;
}
// Check file size (10MB limit)
if (file.size > 10 * 1024 * 1024) {
console.warn(`Skipping large file: ${file.name} (${file.size} bytes)`);
return false;
}
return true;
});
if (validFiles.length === 0) {
throw new Error('No valid image files selected');
}
if (validFiles.length > 10) {
throw new Error('Maximum 10 images allowed per upload');
}
try {
const updatedProduct = await uploadMultipleImages(productId, validFiles);
console.log(`Successfully uploaded ${validFiles.length} images`);
return updatedProduct;
} catch (error) {
console.error('Upload failed:', error);
throw error;
}
};
// Usage examples
try {
// Single file upload
const fileInput = document.getElementById('imageInput');
const file = fileInput.files[0];
const result = await uploadProductImage('550e8400-e29b-41d4-a716-446655440000', file);
// Multiple files upload
const multipleFiles = Array.from(fileInput.files);
const multiResult = await uploadMultipleImages('550e8400-e29b-41d4-a716-446655440000', multipleFiles);
console.log('Images uploaded successfully');
} catch (error) {
console.error('Upload error:', error);
}
React Component Example
import React, { useState } from 'react';
const ProductImageUploader = ({ productId, onUploadComplete }) => {
const [uploading, setUploading] = useState(false);
const [dragActive, setDragActive] = useState(false);
const handleDrag = (e) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
handleFiles(Array.from(e.dataTransfer.files));
}
};
const handleFileInput = (e) => {
if (e.target.files && e.target.files[0]) {
handleFiles(Array.from(e.target.files));
}
};
const handleFiles = async (files) => {
setUploading(true);
try {
const updatedProduct = await uploadMultipleImages(productId, files);
onUploadComplete(updatedProduct);
} catch (error) {
console.error('Upload failed:', error);
alert('Upload failed: ' + error.message);
} finally {
setUploading(false);
}
};
return (
<div className={`upload-container ${dragActive ? 'drag-active' : ''}`}>
<div
className="upload-area"
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
>
<input
type="file"
id="image-upload"
multiple
accept="image/*"
onChange={handleFileInput}
disabled={uploading}
style={{ display: 'none' }}
/>
<label htmlFor="image-upload" className="upload-label">
{uploading ? (
<div>Uploading images...</div>
) : (
<div>
<p>Drop images here or click to select</p>
<p className="upload-hint">
Supports JPEG, PNG, WebP, GIF • Max 10MB per image • Up to 10 images
</p>
</div>
)}
</label>
</div>
</div>
);
};
Python Example
import requests
import uuid
from pathlib import Path
def upload_product_images(product_id, image_paths, client_key, client_secret):
"""Upload multiple images to a product"""
url = f"https://api.devdraft.com/api/v0/products/{product_id}/images"
headers = {
'x-client-key': client_key,
'x-client-secret': client_secret,
'x-idempotency-key': str(uuid.uuid4())
}
files = []
try:
# Prepare files for upload
for image_path in image_paths:
path = Path(image_path)
if not path.exists():
raise FileNotFoundError(f"Image file not found: {image_path}")
# Determine MIME type
suffix = path.suffix.lower()
mime_types = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.webp': 'image/webp',
'.gif': 'image/gif'
}
mime_type = mime_types.get(suffix, 'image/jpeg')
files.append(('images', (path.name, open(path, 'rb'), mime_type)))
# Upload images
response = requests.post(url, headers=headers, files=files)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise Exception(f"Upload failed: {e}")
finally:
# Close all file handles
for _, (_, file_handle, _) in files:
file_handle.close()
# Usage example
try:
image_paths = [
'/path/to/main-product.jpg',
'/path/to/detail-view.png',
'/path/to/lifestyle-shot.jpg'
]
updated_product = upload_product_images(
"550e8400-e29b-41d4-a716-446655440000",
image_paths,
"YOUR_CLIENT_KEY",
"YOUR_CLIENT_SECRET"
)
print(f"Uploaded {len(updated_product['images'])} total images")
print("New image URLs:")
for url in updated_product['images']:
print(f" - {url}")
except Exception as e:
print(f"Error: {e}")
Response FormatCopied!
Success Response (201 Created)
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Wireless Bluetooth Headphones",
"description": "Premium wireless headphones with active noise cancellation",
"price": 199.99,
"currency": "USD",
"productType": "PRODUCT",
"status": "ACTIVE",
"stockCount": 85,
"quantity": 100,
"weight": 0.5,
"unit": "kg",
"images": [
"https://devdraft-images.s3.amazonaws.com/products/existing-image-1.jpg",
"https://devdraft-images.s3.amazonaws.com/products/existing-image-2.jpg",
"https://devdraft-images.s3.amazonaws.com/products/uploaded-image-1-uuid.jpg",
"https://devdraft-images.s3.amazonaws.com/products/uploaded-image-2-uuid.png",
"https://devdraft-images.s3.amazonaws.com/products/uploaded-image-3-uuid.jpg"
],
"variations": null,
"paymentLink": "https://pay.devdraft.com/p/wireless-headphones",
"walletId": "abcd1234-5678-90ef-ghij-klmnopqrstuv",
"dateAdded": "2024-01-14T14:20:00.000Z",
"dateUpdated": "2024-01-20T10:15:00.000Z",
"wallet": {
"id": "abcd1234-5678-90ef-ghij-klmnopqrstuv",
"address": "0x742d35Cc6635C0532925a3b8d",
"blockchain": "ETHEREUM",
"type": "APP"
},
"transactions": []
}
Error ResponsesCopied!
Product Not Found (404 Not Found)
{
"statusCode": 404,
"message": "Product not found",
"error": "Not Found"
}
No Images Provided (400 Bad Request)
{
"statusCode": 400,
"message": "No image files provided",
"error": "Bad Request"
}
File Size Limit Exceeded (400 Bad Request)
{
"statusCode": 400,
"message": "Image file size exceeds 10MB limit",
"error": "Bad Request"
}
Unsupported File Type (400 Bad Request)
{
"statusCode": 400,
"message": "Unsupported file type. Only JPEG, PNG, WebP, and GIF are allowed.",
"error": "Bad Request"
}
Too Many Files (400 Bad Request)
{
"statusCode": 400,
"message": "Maximum 10 images allowed per upload",
"error": "Bad Request"
}
Authentication Error (401 Unauthorized)
{
"statusCode": 401,
"message": "Invalid or missing API credentials",
"error": "Unauthorized"
}
Rate Limit Error (429 Too Many Requests)
{
"statusCode": 429,
"message": "Rate limit exceeded. Maximum 100 requests per minute.",
"error": "Too Many Requests",
"retryAfter": 60
}
Upload Service Error (500 Internal Server Error)
{
"statusCode": 500,
"message": "Image upload service temporarily unavailable",
"error": "Internal Server Error"
}
Image Processing & StorageCopied!
Automatic Processing
-
Optimization: Images are automatically compressed for web delivery
-
Multiple Sizes: Various sizes generated (thumbnail, medium, large)
-
Format Conversion: WebP versions created for modern browsers
-
Quality Adjustment: Optimal quality settings applied automatically
Storage Details
-
Cloud Storage: Images stored in secure, globally distributed CDN
-
URL Structure: Unique URLs with UUID-based naming
-
Persistence: Images remain accessible even if product is deleted
-
Backup: Automatic backup and redundancy across multiple regions
Performance Features
-
CDN Delivery: Global content delivery network for fast loading
-
Lazy Loading: Images optimized for progressive loading
-
Responsive Images: Multiple sizes available for different screen sizes
-
Caching: Aggressive caching for improved performance
Advanced Use CasesCopied!
Image Validation & Processing
// Comprehensive image validation
const validateImages = (files) => {
const errors = [];
const validFiles = [];
// Check file count
if (files.length === 0) {
errors.push('No files selected');
return { errors, validFiles };
}
if (files.length > 10) {
errors.push('Maximum 10 images allowed');
return { errors, validFiles };
}
files.forEach((file, index) => {
const fileErrors = [];
// Check file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
if (!allowedTypes.includes(file.type)) {
fileErrors.push(`File ${index + 1}: Unsupported format (${file.type})`);
}
// Check file size (10MB limit)
const maxSize = 10 * 1024 * 1024;
if (file.size > maxSize) {
fileErrors.push(`File ${index + 1}: Size exceeds 10MB (${(file.size / 1024 / 1024).toFixed(1)}MB)`);
}
// Check minimum dimensions (if possible)
if (file.type.startsWith('image/')) {
validateImageDimensions(file).then(dimensions => {
if (dimensions.width < 200 || dimensions.height < 200) {
fileErrors.push(`File ${index + 1}: Minimum dimensions 200x200px`);
}
});
}
if (fileErrors.length === 0) {
validFiles.push(file);
} else {
errors.push(...fileErrors);
}
});
return { errors, validFiles };
};
// Check image dimensions
const validateImageDimensions = (file) => {
return new Promise((resolve) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(url);
resolve({ width: img.width, height: img.height });
};
img.onerror = () => {
URL.revokeObjectURL(url);
resolve({ width: 0, height: 0 });
};
img.src = url;
});
};
Batch Upload with Progress Tracking
// Upload images with progress tracking
const uploadImagesWithProgress = async (productId, files, onProgress) => {
const totalFiles = files.length;
let uploadedFiles = 0;
// Upload in smaller batches to avoid timeouts
const batchSize = 3;
const results = [];
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
try {
const batchResult = await uploadMultipleImages(productId, batch);
results.push(batchResult);
uploadedFiles += batch.length;
onProgress({
uploaded: uploadedFiles,
total: totalFiles,
percentage: Math.round((uploadedFiles / totalFiles) * 100),
currentBatch: Math.ceil((i + 1) / batchSize),
totalBatches: Math.ceil(totalFiles / batchSize)
});
} catch (error) {
console.error(`Batch ${Math.ceil((i + 1) / batchSize)} failed:`, error);
throw error;
}
// Small delay between batches to avoid rate limiting
if (i + batchSize < files.length) {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
return results[results.length - 1]; // Return final product state
};
// Usage with progress callback
const handleBatchUpload = async (productId, files) => {
try {
const result = await uploadImagesWithProgress(productId, files, (progress) => {
console.log(`Upload progress: ${progress.percentage}% (${progress.uploaded}/${progress.total})`);
// Update UI progress bar
updateProgressBar(progress.percentage);
});
console.log('All images uploaded successfully');
return result;
} catch (error) {
console.error('Batch upload failed:', error);
throw error;
}
};
Image Optimization Before Upload
// Client-side image optimization
const optimizeImage = (file, maxWidth = 1200, quality = 0.85) => {
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = () => {
// Calculate new dimensions
let { width, height } = img;
if (width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
}
// Set canvas size
canvas.width = width;
canvas.height = height;
// Draw and compress
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(resolve, 'image/jpeg', quality);
};
img.src = URL.createObjectURL(file);
});
};
// Optimize images before upload
const uploadOptimizedImages = async (productId, files) => {
const optimizedFiles = await Promise.all(
files.map(file => {
// Only optimize JPEG and PNG files
if (file.type === 'image/jpeg' || file.type === 'image/png') {
return optimizeImage(file);
}
return file;
})
);
return await uploadMultipleImages(productId, optimizedFiles);
};
Image Management Dashboard
// Complete image management solution
class ProductImageManager {
constructor(productId, apiClient) {
this.productId = productId;
this.apiClient = apiClient;
this.images = [];
}
async loadCurrentImages() {
const product = await this.apiClient.fetchProduct(this.productId);
this.images = product.images || [];
return this.images;
}
async uploadImages(files, options = {}) {
const { optimize = true, validate = true } = options;
// Validate files
if (validate) {
const { errors, validFiles } = validateImages(files);
if (errors.length > 0) {
throw new Error(`Validation failed: ${errors.join(', ')}`);
}
files = validFiles;
}
// Optimize if requested
if (optimize) {
files = await Promise.all(files.map(file => optimizeImage(file)));
}
// Upload images
const updatedProduct = await this.apiClient.uploadProductImages(this.productId, files);
this.images = updatedProduct.images;
return {
success: true,
totalImages: this.images.length,
newImages: files.length,
images: this.images
};
}
async reorderImages(newOrder) {
// Reorder images array based on new indices
const reorderedImages = newOrder.map(index => this.images[index]);
// Update product with new image order
const updatedProduct = await this.apiClient.updateProduct(this.productId, {
images: reorderedImages
});
this.images = updatedProduct.images;
return this.images;
}
async deleteImage(imageUrl) {
// Remove image from array
const filteredImages = this.images.filter(url => url !== imageUrl);
// Update product
const updatedProduct = await this.apiClient.updateProduct(this.productId, {
images: filteredImages
});
this.images = updatedProduct.images;
return this.images;
}
getImageInfo() {
return {
count: this.images.length,
maxAllowed: 10,
remaining: 10 - this.images.length,
urls: this.images
};
}
}
Integration PatternsCopied!
Error Recovery
// Robust upload with retry logic
const uploadWithRetry = async (productId, files, maxRetries = 3) => {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await uploadMultipleImages(productId, files);
} catch (error) {
lastError = error;
// Don't retry for client errors
if (error.message.includes('400') || error.message.includes('404')) {
throw error;
}
// Exponential backoff
const delay = Math.pow(2, attempt) * 1000;
console.warn(`Upload attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error(`Upload failed after ${maxRetries} attempts: ${lastError.message}`);
};
WebSocket Progress Updates
// Real-time upload progress via WebSocket
class RealTimeUploadTracker {
constructor(websocketUrl, productId) {
this.ws = new WebSocket(websocketUrl);
this.productId = productId;
this.listeners = new Set();
}
onProgress(callback) {
this.listeners.add(callback);
}
async uploadWithTracking(files) {
const uploadId = generateUUID();
// Listen for progress updates
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.uploadId === uploadId && data.type === 'upload_progress') {
this.listeners.forEach(callback => callback(data));
}
};
// Start upload with tracking ID
return await uploadMultipleImages(this.productId, files, {
uploadId,
trackProgress: true
});
}
}
Best PracticesCopied!
Performance Optimization
-
Compress images before upload to reduce transfer time
-
Upload in batches of 2-3 images to avoid timeouts
-
Use progress indicators for better user experience
-
Implement retry logic for network failures
User Experience
-
Show preview of images before upload
-
Validate files client-side before API calls
-
Provide clear error messages for validation failures
-
Support drag-and-drop for easier file selection
Security & Quality
-
Validate file types and sizes before upload
-
Scan uploaded images for malicious content
-
Implement rate limiting to prevent abuse
-
Use idempotency keys to prevent duplicate uploads
Mobile Considerations
-
Optimize for mobile networks with smaller batches
-
Support camera capture in addition to file selection
-
Compress images more aggressively on mobile devices
-
Provide offline queue for uploads when connection is poor
Next StepsCopied!
After uploading product images, you can:
-
Reorder Images: Arrange images in preferred display order
-
Update Product: Modify other product details if needed
-
Create Payment Links: Use updated product in checkout flows
-
Monitor Performance: Track how images affect conversion rates
-
Optimize SEO: Add alt text and descriptions for images
For more information, see: