📖 Tutorial

Virtual Try-On API Tutorial —
Get On-Model Photos in 12 Seconds

⏱ 15 min read 🔧 curl · Python · Node.js 💡 Beginner friendly 📅 Updated March 2026

1. What is Virtual Try-On API

PixelAPI's Virtual Try-On API lets you dress a person photo in any garment — automatically, using AI. You send two images (a person and a garment), and in ~12 seconds you get back a realistic photo of that person wearing the garment.

Powered by our virtual try-on, the same technology used in high-end fashion AI platforms, now available as a simple REST API.

Use Cases

  • 🛍 Meesho / Flipkart catalog automation — Generate on-model product photos without hiring models
  • 👗 Fashion e-commerce — Show garments on diverse body types instantly
  • 🎨 Virtual fitting rooms — Let shoppers try clothes before buying
  • 📸 Influencer content — Scale photoshoot content with AI
  • 🏭 Wholesale suppliers — Create catalog images from factory samples

How It Works

👤
Input
Person Photo
+
👕
Input
Garment Image
→
🤖
AI Processing
our virtual try-on (~12s)
→
✨
Output
On-Model Photo
💡

Async API: The API uses a job-based system. You submit a job and poll for results. This allows for reliable processing even under high load.

2. Getting Started

Step 1: Create Your Account

Go to pixelapi.dev/app and sign up for a free account. You'll get free credits to start experimenting immediately.

Step 2: Get Your API Key

  1. Log in to your dashboard at pixelapi.dev/app
  2. Navigate to API Keys in the sidebar
  3. Click Generate New Key
  4. Copy your key — it starts with pk_
⚠️

Keep your API key secret! Never commit it to Git or expose it in client-side JavaScript. Use environment variables.

Step 3: Set Up Your Environment

# Set your API key as an environment variable
export PIXELAPI_KEY="pk_your_api_key_here"

# Verify it's set
echo $PIXELAPI_KEY
import os

# Load from environment variable
api_key = os.environ.get("PIXELAPI_KEY")
if not api_key:
    raise ValueError("Set PIXELAPI_KEY environment variable")

# Or install python-dotenv and use a .env file:
# pip install python-dotenv requests
# from dotenv import load_dotenv
# load_dotenv()
# api_key = os.getenv("PIXELAPI_KEY")
// Install: npm install node-fetch dotenv
// Create .env file with: PIXELAPI_KEY=pk_your_key

import 'dotenv/config';
const apiKey = process.env.PIXELAPI_KEY;
if (!apiKey) throw new Error('Set PIXELAPI_KEY in .env');

// Base URL
const BASE_URL = 'https://api.pixelapi.dev';

3. Your First Try-On

The Virtual Try-On API accepts two image URLs (person and garment) and a category parameter. It returns a job_id that you poll for results.

API Endpoint

POST https://api.pixelapi.dev/v1/virtual-tryon
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

Request Parameters

ParameterTypeRequiredDescription
person_imagestring (URL)✓URL of person photo
garment_imagestring (URL)✓URL of garment/clothing image
categorystring✓upper_body, lower_body, or full_body
n_samplesinteger✗Number of images to generate (default: 1, max: 4)
n_stepsinteger✗Diffusion steps (default: 20, higher = better quality)

Complete Code Examples

curl -X POST https://api.pixelapi.dev/v1/virtual-tryon \
  -H "Authorization: Bearer $PIXELAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "person_image": "https://example.com/person.jpg",
    "garment_image": "https://example.com/shirt.jpg",
    "category": "upper_body"
  }'

# Response:
# {
# "job_id": "job_abc123xyz",
# "status": "queued",
# "credits_used": 50
# }
import requests
import os

api_key = os.environ.get("PIXELAPI_KEY")

def submit_tryon(person_url: str, garment_url: str, category: str = "upper_body"):
    """Submit a virtual try-on job."""
    response = requests.post(
        "https://api.pixelapi.dev/v1/virtual-tryon",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        },
        json={
            "person_image": person_url,
            "garment_image": garment_url,
            "category": category
        }
    )
    response.raise_for_status()
    return response.json()

# Example usage
result = submit_tryon(
    person_url="https://example.com/person.jpg",
    garment_url="https://example.com/shirt.jpg",
    category="upper_body"
)
print(f"Job submitted: {result['job_id']}")
print(f"Credits used: {result['credits_used']}")
import 'dotenv/config';

const BASE_URL = 'https://api.pixelapi.dev';
const apiKey = process.env.PIXELAPI_KEY;

async function submitTryOn(personUrl, garmentUrl, category = 'upper_body') {
  const response = await fetch(`${BASE_URL}/v1/virtual-tryon`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      person_image: personUrl,
      garment_image: garmentUrl,
      category: category
    })
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`API Error: ${error.detail || response.status}`);
  }

  return response.json();
}

// Usage
const job = await submitTryOn(
  'https://example.com/person.jpg',
  'https://example.com/shirt.jpg',
  'upper_body'
);
console.log('Job ID:', job.job_id);
console.log('Credits used:', job.credits_used);

4. Image Requirements

Person Photo Guidelines

RequirementRecommendedNotes
FormatJPEG, PNG, WebPJPEG recommended for speed
Resolution768×1024px or higherPortrait orientation works best
PoseStanding, front-facingArms slightly away from body
ClothingSimple, fitted outfitAvoid loose/baggy clothing
BackgroundClean, simpleWhite/neutral background ideal
LightingUniform, brightAvoid heavy shadows

Garment Image Guidelines

RequirementRecommendedNotes
TypeFlat-lay or ghost mannequinBoth work well
BackgroundWhite or transparentRemoves distractions
Resolution512×512px or higherHigher = better detail
LightingEven, no harsh shadowsStudio-style preferred
Category matchMust match category paramTop → upper_body etc.

What Works Well vs. What Doesn't

✅

Works great:

  • T-shirts, tops, shirts
  • Jeans, trousers, skirts
  • Dresses (full_body)
  • Jackets and hoodies
  • Clear front-facing poses
❌

May struggle with:

  • Heavily patterned garments
  • Extreme poses (side view)
  • Multiple overlapping garments
  • Very low resolution images
  • Heavy image compression

5. Polling for Results

The try-on API is asynchronous. After submitting a job, you poll the status endpoint until the job is complete (usually ~12 seconds).

Poll Endpoint

GET https://api.pixelapi.dev/v1/virtual-tryon/jobs/{job_id}
Authorization: Bearer YOUR_API_KEY

Job Status Values

StatusMeaning
queuedJob waiting in queue
processingAI is generating the image
completedDone! Result is available
failedAn error occurred (check error field)

Polling Code Examples

JOB_ID="job_abc123xyz"

# Poll until done (simple loop)
while true; do
  RESPONSE=$(curl -s \
    -H "Authorization: Bearer $PIXELAPI_KEY" \
    "https://api.pixelapi.dev/v1/virtual-tryon/jobs/$JOB_ID")
  
  STATUS=$(echo $RESPONSE | python3 -c "import sys,json; print(json.load(sys.stdin)['status'])")
  echo "Status: $STATUS"
  
  if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
    echo "$RESPONSE" | python3 -m json.tool
    break
  fi
  
  sleep 3
done
import requests
import time
import os

api_key = os.environ.get("PIXELAPI_KEY")

def poll_job(job_id: str, max_wait: int = 120, interval: int = 3):
    """Poll a job until completion or timeout."""
    elapsed = 0
    
    while elapsed < max_wait:
        response = requests.get(
            f"https://api.pixelapi.dev/v1/virtual-tryon/jobs/{job_id}",
            headers={"Authorization": f"Bearer {api_key}"}
        )
        response.raise_for_status()
        data = response.json()
        status = data["status"]
        
        print(f"[{elapsed}s] Status: {status}")
        
        if status == "completed":
            return data
        elif status == "failed":
            raise Exception(f"Job failed: {data.get('error', 'Unknown error')}")
        
        time.sleep(interval)
        elapsed += interval
    
    raise TimeoutError(f"Job did not complete within {max_wait} seconds")

# Full workflow: submit + poll
job = submit_tryon("https://example.com/person.jpg", "https://example.com/shirt.jpg")
result = poll_job(job["job_id"])
print("Done! Image data available.")
print(f"Output: {result['output'][:50]}...")
async function pollJob(jobId, maxWait = 120000, interval = 3000) {
  const start = Date.now();
  
  while (Date.now() - start < maxWait) {
    const response = await fetch(
      `${BASE_URL}/v1/virtual-tryon/jobs/${jobId}`,
      { headers: { 'Authorization': `Bearer ${apiKey}` } }
    );
    
    const data = await response.json();
    const elapsed = Math.round((Date.now() - start) / 1000);
    console.log(`[${elapsed}s] Status: ${data.status}`);
    
    if (data.status === 'completed') return data;
    if (data.status === 'failed') throw new Error(`Job failed: ${data.error}`);
    
    await new Promise(r => setTimeout(r, interval));
  }
  
  throw new Error(`Timeout after ${maxWait/1000}s`);
}

// Full workflow
const job = await submitTryOn('https://example.com/person.jpg', 'https://example.com/shirt.jpg');
const result = await pollJob(job.job_id);
console.log('Result ready!');

6. Handling the Result

When a job completes, the response contains a base64-encoded JPEG image in the output field.

# Save base64 result as JPEG
RESPONSE=$(curl -s \
  -H "Authorization: Bearer $PIXELAPI_KEY" \
  "https://api.pixelapi.dev/v1/virtual-tryon/jobs/$JOB_ID")

# Extract and decode the base64 image
echo $RESPONSE | python3 -c "
import sys, json, base64
data = json.load(sys.stdin)
img_b64 = data['output']
# Remove data URL prefix if present
if ',' in img_b64:
    img_b64 = img_b64.split(',')[1]
with open('result.jpg', 'wb') as f:
    f.write(base64.b64decode(img_b64))
print('Saved: result.jpg')
"
import base64
import os

def save_result(job_result: dict, output_path: str = "output.jpg"):
    """Save the try-on result image to disk."""
    img_b64 = job_result["output"]
    
    # Strip data URL prefix if present (e.g., "data:image/jpeg;base64,")
    if "," in img_b64:
        img_b64 = img_b64.split(",")[1]
    
    # Decode and save
    img_bytes = base64.b64decode(img_b64)
    with open(output_path, "wb") as f:
        f.write(img_bytes)
    
    print(f"Saved image: {output_path} ({len(img_bytes):,} bytes)")
    return output_path

# Complete end-to-end example
job = submit_tryon(
    person_url="https://example.com/model.jpg",
    garment_url="https://example.com/blue-shirt.jpg",
    category="upper_body"
)
result = poll_job(job["job_id"])
output_file = save_result(result, "tryon_result.jpg")
print(f"✅ Done! Saved to {output_file}")
import { writeFileSync } from 'fs';

function saveResult(jobResult, outputPath = 'output.jpg') {
  let imgB64 = jobResult.output;
  
  // Strip data URL prefix if present
  if (imgB64.includes(',')) {
    imgB64 = imgB64.split(',')[1];
  }
  
  const imgBuffer = Buffer.from(imgB64, 'base64');
  writeFileSync(outputPath, imgBuffer);
  console.log(`Saved: ${outputPath} (${imgBuffer.length.toLocaleString()} bytes)`);
  return outputPath;
}

// End-to-end
const job = await submitTryOn('https://example.com/model.jpg', 'https://example.com/shirt.jpg');
const result = await pollJob(job.job_id);
saveResult(result, 'tryon_output.jpg');
console.log('✅ Done!');
// Display in browser (React/vanilla JS)
function displayTryOnResult(jobResult) {
  const imgB64 = jobResult.output;
  
  // Create an image element
  const img = document.createElement('img');
  
  // If it's a raw base64 (no prefix), add the prefix
  if (imgB64.startsWith('/9j') || imgB64.startsWith('iVBOR')) {
    img.src = `data:image/jpeg;base64,${imgB64}`;
  } else {
    img.src = imgB64; // Already has data: prefix
  }
  
  img.style.maxWidth = '100%';
  img.alt = 'Virtual try-on result';
  
  document.getElementById('result-container').appendChild(img);
}

// Or in React:
function TryOnResult({ output }) {
  const src = output.includes(',') 
    ? output 
    : `data:image/jpeg;base64,${output}`;
  return <img src={src} alt="Try-on result" style={{maxWidth: '100%'}} />;
}

7. Advanced Usage

Batch Processing

Process multiple garments efficiently by submitting all jobs first, then polling them in parallel:

import asyncio
import aiohttp
import base64

async def submit_batch(person_url: str, garments: list[dict]) -> list[str]:
    """Submit multiple try-on jobs concurrently."""
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }
    
    async with aiohttp.ClientSession() as session:
        tasks = []
        for garment in garments:
            payload = {
                "person_image": person_url,
                "garment_image": garment["url"],
                "category": garment.get("category", "upper_body")
            }
            tasks.append(session.post(
                "https://api.pixelapi.dev/v1/virtual-tryon",
                headers=headers,
                json=payload
            ))
        
        responses = await asyncio.gather(*tasks)
        job_ids = []
        for resp in responses:
            data = await resp.json()
            job_ids.append(data["job_id"])
        
        return job_ids

# Usage
garments = [
    {"url": "https://example.com/red-tshirt.jpg", "category": "upper_body"},
    {"url": "https://example.com/blue-jeans.jpg", "category": "lower_body"},
    {"url": "https://example.com/white-dress.jpg", "category": "full_body"},
]

job_ids = asyncio.run(submit_batch("https://example.com/model.jpg", garments))
print(f"Submitted {len(job_ids)} jobs: {job_ids}")

Categories Reference

CategoryUse ForExamples
upper_bodyTop half clothingT-shirts, shirts, jackets, hoodies, sweaters
lower_bodyBottom half clothingJeans, trousers, skirts, shorts, leggings
full_bodyFull outfitDresses, jumpsuits, sarees, full suits

Quality Parameters

{
  "person_image": "https://example.com/person.jpg",
  "garment_image": "https://example.com/garment.jpg",
  "category": "upper_body",
  "n_samples": 1,
  "n_steps": 30
}

Higher n_steps (up to 40) improves quality but increases processing time. Default 20 is a good balance.

8. Pricing & Limits

₹3 per image

= 50 credits per virtual try-on job

Free Trial Available

No credit card required to start

RATE LIMIT
10 req/min
MAX FILE SIZE
10 MB
INFERENCE TIME
~12 seconds
OUTPUT SIZE
768×1024px JPEG
💰

Need high volume? Enterprise plans available with volume discounts, priority queues, and SLAs. Contact us.

9. FAQ

Why did I get an "OpenPose failed" error? ▼

OpenPose is used to detect body keypoints (pose) in the person photo. Common causes:

  • Person is too small in the frame — ensure the person fills at least 60% of the image
  • Extreme poses or non-standard body orientations
  • Heavy occlusion (person partially hidden behind objects)
  • Very low resolution — try at least 512×512px

Fix: Use a clear, front-facing, well-lit person photo with the person filling most of the frame.

How long does processing take? ▼

Typically 10–20 seconds for a single image. During peak hours or with higher n_steps, it may take up to 45 seconds. Jobs are queued and processed in order. Consider submitting batch jobs and polling concurrently.

What image formats are supported? ▼

JPEG, PNG, and WebP are all supported. Images must be accessible via HTTPS URLs. We recommend JPEG for the best quality-to-size ratio. Images are downloaded by our servers — URLs must be publicly accessible (not behind auth).

Can I use local files instead of URLs? ▼

Currently the API accepts URLs only. To use local files, you can:

  1. Upload to S3, Cloudinary, or any CDN and use the URL
  2. Use a temporary file hosting service like transfer.sh or 0x0.st
  3. Base64-encode and host via a data URL endpoint on your server

Multipart form upload (direct file upload) is coming soon!

The output image looks unrealistic. How do I improve it? ▼
  • Use higher resolution images (1024×1024 or larger)
  • Increase n_steps to 30–40 for better quality
  • Use a cleaner background for the person photo
  • Make sure the garment image has a white/transparent background
  • Ensure the category matches the garment type
How do I handle rate limit errors? ▼

If you receive a 429 Too Many Requests error, implement exponential backoff:

import time

def submit_with_retry(person_url, garment_url, max_retries=3):
    for attempt in range(max_retries):
        try:
            return submit_tryon(person_url, garment_url)
        except requests.HTTPError as e:
            if e.response.status_code == 429 and attempt < max_retries - 1:
                wait = 2 ** attempt # 1s, 2s, 4s
                print(f"Rate limited. Waiting {wait}s...")
                time.sleep(wait)
            else:
                raise

PixelAPI offers more AI-powered image processing endpoints to complete your workflow:

Ready to Build? 🚀

Get started with free credits. No credit card required.
Generate your first on-model photo in under 2 minutes.

Start Free Trial →    View Full API Docs