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

id

string (UUID)

Yes

Unique product identifier

550e8400-e29b-41d4-a716-446655440000

Request ParametersCopied!

Form Data Fields

Field

Type

Required

Description

Limits

images

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:

  1. Reorder Images: Arrange images in preferred display order

  2. Update Product: Modify other product details if needed

  3. Create Payment Links: Use updated product in checkout flows

  4. Monitor Performance: Track how images affect conversion rates

  5. Optimize SEO: Add alt text and descriptions for images

For more information, see: