๐Ÿ“– 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 OOTDiffusion, 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
OOTDiffusion (~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, dedicated GPU 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