Features Blog How It Works
EN DE FR ES
Sign in Get Started
← All articles Tutorials

Building a Virtual Closet App with Python and the FitInView API

2026-04-30 By FitInView Team 5 min read
Building a Virtual Closet App with Python and the FitInView API
Try it now · Live demo No signup · ~60 seconds

This tutorial shows how to build a simple virtual closet app in Python. Users upload garment photos, label each item by category, pick a full outfit, and submit a virtual try-on job through FitInView. The app then polls for completion, shows the result, and keeps a basic history of past looks.

What we're building

The app has four core parts. First, a wardrobe page where users add clothing photos and choose a category such as top, bottom, or outerwear. Second, an outfit builder that lets them combine garments into one look. Third, a try-on flow that sends the person image plus selected garment URLs to FitInView. Fourth, a results page that shows the final image and recent jobs.

This is intentionally small and practical. You can run it as a Flask app with any small DB for storage. The point is to keep the product shape clear, not to overbuild the first version.

Stack: Flask, any small DB, FitInView API

Use Flask for the web app, a small database for garments and job history, and FitInView for the virtual try-on call. For storage, keep garment image URLs in your own system or on your CDN, then store those URLs in the database. The FitInView API only needs the person photo URL and the garment image URLs.

Part Why it is used Notes
Flask Simple Python web app Good fit for a tutorial and a small wardrobe tool
Any small DB Store garments and job history Use the schema you are comfortable with
FitInView API Virtual try-on jobs Use POST /api/v1/tryon and GET /api/v1/jobs/{id}
Image hosting Serve person and garment images Use your own hosted URLs or CDN URLs

Before writing code, note a few public FitInView details. The API uses Bearer authentication, test keys begin with fiv_test_ and return mock responses, and duplicate try-on requests with the same Idempotency-Key within 24 hours return a cached response. Output resolution options are 1K, 2K, and 4K, and higher resolutions scale with resolution.

Step 1: Project setup

Create a small Flask project with one app file, one database file, and a templates folder. Install the only external library we need for the API call, requests.

mkdir virtual-closet
cd virtual-closet
python -m venv .venv
source .venv/bin/activate
pip install flask requests

Set two environment variables, one for your API key and one for the base URL.

export FITINVIEW_API_KEY='fiv_test_your_test_key'
export FITINVIEW_BASE_URL='https://api.fitinview.com'

Step 2: Database schema for garments

You only need a few fields to get started: an id, a user_id, an image_url, and a category. The category is chosen by the user during upload, which keeps the first version simple and avoids any need for automated item detection.

{
  "garments": {
    "id": "integer primary key",
    "user_id": "integer",
    "image_url": "text",
    "category": "text"
  },
  "tryon_jobs": {
    "id": "integer primary key",
    "user_id": "integer",
    "fitinview_job_id": "text",
    "status": "text",
    "result_url": "text",
    "created_at": "text"
  }
}

If you already have an accounts table, link garments to the current user. For a tutorial, you can treat user_id as a placeholder value and focus on the workflow.

Step 3: Upload and categorize garments

When the user uploads a photo, your app should save the image, create a public URL for it, and store that URL with the chosen category. The user selects the category in the form, for example top, bottom, or outerwear. This is enough for the outfit builder to assemble a valid try-on request later.

import os
import json
import sqlite3
from datetime import datetime
from flask import Flask, request, redirect, url_for, render_template_string, abort
import requests

app = Flask(__name__)
DB_PATH = "app.db"
FITINVIEW_BASE_URL = os.environ.get("FITINVIEW_BASE_URL", "https://api.fitinview.com")
FITINVIEW_API_KEY = os.environ.get("FITINVIEW_API_KEY", "")


def db():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn


def init_db():
    conn = db()
    conn.executescript("""
    create table if not exists garments (
      id integer primary key autoincrement,
      user_id integer not null,
      image_url text not null,
      category text not null
    );
    create table if not exists tryon_jobs (
      id integer primary key autoincrement,
      user_id integer not null,
      fitinview_job_id text not null,
      status text not null,
      result_url text,
      created_at text not null
    );
    """)
    conn.commit()
    conn.close()


@app.before_request
def _setup():
    init_db()

Step 4: Wardrobe gallery view

The wardrobe page should show all saved garments as cards. Group them by category so users can scan tops, bottoms, and outerwear quickly. For a first pass, a simple list or grid is enough.

@app.route("/")
def wardrobe():
    conn = db()
    garments = conn.execute(
        "select id, image_url, category from garments where user_id = ? order by id desc",
        (1,),
    ).fetchall()
    jobs = conn.execute(
        "select fitinview_job_id, status, result_url, created_at from tryon_jobs where user_id = ? order by id desc limit 20",
        (1,),
    ).fetchall()
    conn.close()
    return render_template_string("""
    <h1>Virtual Closet</h1>
    <p><a href='/upload'>Upload garment</a> | <a href='/builder'>Build outfit</a></p>
    <h2>Wardrobe</h2>
    {% for g in garments %}
      <div style='margin-bottom:12px'>
        <img src='{{ g.image_url }}' width='120'>
        <div>{{ g.category }} #{{ g.id }}</div>
      </div>
    {% endfor %}
    <h2>Recent try-ons</h2>
    {% for j in jobs %}
      <div>{{ j.created_at }} | {{ j.status }} | {% if j.result_url %}<a href='{{ j.result_url }}'>result</a>{% endif %}</div>
    {% endfor %}
    """, garments=garments, jobs=jobs)

Step 5: Outfit builder

The outfit builder can be a form with three selectors, one each for top, bottom, and optional outerwear. A user might choose only top and bottom, or add an extra layer when available. Store the selected garment ids in the browser form, then resolve them to image URLs on submit.

This is also a good place to let the user upload or choose the person photo used for the try-on. The person photo must be a URL that FitInView can access.

Step 6: Submit try-on and poll job status

FitInView accepts multiple garment_urls, so you can send a top, bottom, and optional outerwear together. The request returns an id and status. After that, poll GET /api/v1/jobs/{id} until the status becomes complete and a result_url is available.

Honest timing matters here. A typical experience is about 5 to 30 seconds depending on output size.

def garments_by_ids(ids):
    if not ids:
        return []
    conn = db()
    placeholders = ",".join(["?"] * len(ids))
    rows = conn.execute(
        f"select id, image_url, category from garments where id in ({placeholders}) and user_id = ?",
        (*ids, 1),
    ).fetchall()
    conn.close()
    by_id = {str(r["id"]): r for r in rows}
    return [by_id[i] for i in ids if i in by_id]


def submit_tryon(person_url, garment_urls, output_size="1K", prompt=""):
    url = f"{FITINVIEW_BASE_URL}/api/v1/tryon"
    headers = {
        "Authorization": f"Bearer {FITINVIEW_API_KEY}",
        "Content-Type": "application/json",
        "Idempotency-Key": f"wardrobe-{datetime.utcnow().timestamp()}"
    }
    payload = {
        "person_url": person_url,
        "garment_urls": garment_urls,
        "output_size": output_size,
        "prompt": prompt,
    }
    r = requests.post(url, headers=headers, json=payload, timeout=30)
    r.raise_for_status()
    return r.json()


def poll_job(job_id):
    url = f"{FITINVIEW_BASE_URL}/api/v1/jobs/{job_id}"
    headers = {"Authorization": f"Bearer {FITINVIEW_API_KEY}"}
    r = requests.get(url, headers=headers, timeout=30)
    r.raise_for_status()
    return r.json()


@app.route("/builder", methods=["GET", "POST"])
def builder():
    conn = db()
    garments = conn.execute("select id, image_url, category from garments where user_id = ?", (1,)).fetchall()
    conn.close()
    if request.method == "POST":
        person_url = request.form["person_url"]
        selected = [request.form.get("top"), request.form.get("bottom"), request.form.get("outerwear")]
        garment_rows = garments_by_ids([x for x in selected if x])
        garment_urls = [g["image_url"] for g in garment_rows]
        fit = submit_tryon(person_url, garment_urls, request.form.get("output_size", "1K"), request.form.get("prompt", ""))
        conn = db()
        conn.execute(
            "insert into tryon_jobs (user_id, fitinview_job_id, status, result_url, created_at) values (?, ?, ?, ?, ?)",
            (1, fit["id"], fit["status"], None, datetime.utcnow().isoformat()),
        )
        conn.commit()
        conn.close()
        return redirect(url_for("job_status", job_id=fit["id"]))
    return render_template_string("""
    <h1>Build outfit</h1>
    <form method='post'>
      <p>Person image URL <input name='person_url' required style='width:420px'></p>
      <p>Top <input name='top'></p>
      <p>Bottom <input name='bottom'></p>
      <p>Outerwear <input name='outerwear'></p>
      <p>Output size
        <select name='output_size'>
          <option>1K</option><option>2K</option><option>4K</option>
        </select>
      </p>
      <p>Prompt <input name='prompt' style='width:420px'></p>
      <button type='submit'>Try on</button>
    </form>
    <h2>Your garments</h2>
    {% for g in garments %}
      <div>#{{ g.id }} {{ g.category }} - {{ g.image_url }}</div>
    {% endfor %}
    """, garments=garments)
@app.route('/jobs/<job_id>')
def job_status(job_id):
    fit = poll_job(job_id)
    conn = db()
    conn.execute(
        "update tryon_jobs set status = ?, result_url = ? where fitinview_job_id = ? and user_id = ?",
        (fit.get('status', ''), fit.get('result_url'), job_id, 1),
    )
    conn.commit()
    conn.close()
    return render_template_string("""
    <h1>Job {{ job.id }}</h1>
    <p>Status: {{ job.status }}</p>
    {% if job.result_url %}
      <p><img src='{{ job.result_url }}' width='360'></p>
      <p><a href='{{ job.result_url }}'>Open result</a></p>
    {% else %}
      <p>Refresh this page in a few seconds.</p>
    {% endif %}
    <p><a href='/'>Back to wardrobe</a></p>
    """, job=fit)

Step 7: Display result and history

Once the job is complete, show the result image at the top of the page and keep the older jobs below it. That gives users a basic history of their looks and makes the app feel more like a wardrobe tool than a one-off demo.

You can also add filters later, for example by category, date, or output size. For now, showing the last 20 jobs is enough.

Production: switch to webhooks and host garment images on your CDN

Polling is fine for a tutorial, but production apps are usually smoother with webhooks. FitInView can send a completion callback to your HTTPS webhook_url. The callback includes X-FitInView-Signature, which is an HMAC-SHA256 of the raw request body. Verify it with hmac.compare_digest before accepting the payload.

import hmac
import hashlib


def verify_signature(raw_body: bytes, header_value: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header_value or "")

For production, host garment images on your CDN or another stable public image host so FitInView can fetch them reliably. If you want a better user experience, submit the try-on request with webhook_url and update the job record as soon as the callback lands.

Comparison notes

Service Public pricing shape Webhooks Multi-garment support Resolution notes
FitInView Public dev packs shown on the developers page Yes Yes 1K, 2K, 4K, scales with resolution
FASHN.ai Subscription with monthly credits and top-ups Not publicly listed in the facts provided Not publicly listed Up to 4K image generation
FitRoom Pay-as-you-go plus subscription tiers, dollar pricing gated Not mentioned on public pricing page Yes Up to 2048px
Segmind PAYG entry and subscription, varies by model Not publicly listed Not publicly listed Per GPU-second on serverless models
tryon-api.com Free Starter and paid tiers, session allowance based Yes Not publicly listed Not publicly listed

For teams comparing options, the main questions are simple. Can you send multiple garments in one request, can you receive completion callbacks, and is pricing public enough to plan ahead. FitInView makes those basics clear on its developer page, while some competitors gate parts of pricing behind purchase or do not publicly break out per-call costs.

Practical next step

Start with the polling version first, because it is easiest to understand and debug. Then add webhooks once the core wardrobe flow works end to end. After that, move garment images to a stable public host, add better filtering in the gallery, and let users save favorite looks from their history.

Ready to try it yourself?

AI virtual try-on, wardrobe management, and daily outfit ideas. Free to start.

Get Started Free →

No credit card · Free forever plan available

More from the blog

Comparisons

Best Virtual Try-On Apps in 2026: Compared & Ranked

Read more →
Comparisons

Best AI Wardrobe Apps in 2026: Complete Guide

Read more →
Comparisons

Best AI Outfit Generator Apps in 2026

Read more →
Guides

How to Try On Clothes From Your Own Closet Virtually

Read more →
Try it yourself Free AI Try-On Sign Up Free