Web Integration
April 26, 20268 min read

How to Add AI Virtual Try-On to Your React Storefront

Add a dynamic fitting room experience to your React website. A complete step-by-step guide to uploading garments and triggering VTON generations using the native API.

How to Add AI Virtual Try-On to Your React Storefront

Adding a Virtual Try-On (VTON) experience to your React storefront can significantly boost shopper engagement and lower return rates. By directly calling the Snapmydesign (SMD) API, you can implement a completely customized try-on widget that blends seamlessly with your store's branding.

This guide walks you through building a custom React hook and component that connects to the SMD VTON API endpoints: POST /vton/upload and POST /vton/generate.

⚡ Direct API Integration in 3 Steps

1. Setup Your API Request Helpers

Since e-commerce applications typically call API services from a secure serverless function or proxy (to keep your API key hidden from the browser), let's write a reusable service helper:

javascript
// services/vtonApi.js
const API_BASE = "https://apisdk.snapmydesign.com/api/v1";

export async function uploadImage(userId, file, apiKey) {
  const formData = new FormData();
  formData.append("files", file);
  formData.append("userId", userId);

  const response = await fetch(`${API_BASE}/vton/upload`, {
    method: "POST",
    headers: {
      "X-API-Key": apiKey,
    },
    body: formData,
  });

  if (!response.ok) {
    throw new Error("Failed to upload image");
  }

  const data = await response.json();
  return data.uploaded[0].url; // Returns the uploaded asset URL
}

export async function generateTryOn(userId, clothesUrl, personUrl, apiKey) {
  const response = await fetch(`${API_BASE}/vton/generate`, {
    method: "POST",
    headers: {
      "X-API-Key": apiKey,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      userId,
      model_name: "medium", // Options: "fast" | "medium" | "quality"
      inputClothesImageUrls: [clothesUrl],
      inputPersonImageUrls: personUrl ? [personUrl] : [],
      prompt: "Place the garment on the model",
      version: 1.0,
    }),
  });

  if (!response.ok) {
    const err = await response.json();
    throw new Error(err.message || "Failed to generate try-on");
  }

  return await response.json();
}

2. Build the React Try-On Hook

Using React state hooks, we can manage the workflow state: idle, uploading, generating, success, or error.

javascript
// hooks/useVirtualTryOn.js
import { useState } from "react";
import { uploadImage, generateTryOn } from "../services/vtonApi";

export function useVirtualTryOn({ userId, apiKey }) {
  const [status, setStatus] = useState("idle"); // "idle" | "uploading" | "generating" | "success" | "error"
  const [resultUrl, setResultUrl] = useState(null);
  const [error, setError] = useState(null);

  const startTryOn = async (garmentFile, modelFile) => {
    setStatus("uploading");
    setError(null);
    try {
      // 1. Upload assets
      const clothesUrl = await uploadImage(userId, garmentFile, apiKey);
      let personUrl = null;
      if (modelFile) {
        personUrl = await uploadImage(userId, modelFile, apiKey);
      }

      // 2. Trigger generation
      setStatus("generating");
      const result = await generateTryOn(userId, clothesUrl, personUrl, apiKey);
      
      if (result.success && result.outputImageUrls?.length > 0) {
        setResultUrl(result.outputImageUrls[0]);
        setStatus("success");
      } else {
        throw new Error("No output image generated");
      }
    } catch (err) {
      setError(err.message);
      setStatus("error");
    }
  };

  return { startTryOn, status, resultUrl, error };
}

3. Create the Try-On Component UI

Let's design a styled React fitting room component using Tailwind CSS:

jsx
import React, { useState } from "react";
import { useVirtualTryOn } from "./hooks/useVirtualTryOn";

export default function FittingRoom({ garmentImageUrl }) {
  const [userPhoto, setUserPhoto] = useState(null);
  const { startTryOn, status, resultUrl, error } = useVirtualTryOn({
    userId: "your_user_id_here",
    apiKey: "smd_live_your_api_key_here",
  });

  const handleFileChange = (e) => {
    if (e.target.files && e.target.files[0]) {
      setUserPhoto(e.target.files[0]);
    }
  };

  const handleSubmit = async () => {
    try {
      const response = await fetch(garmentImageUrl);
      const blob = await response.blob();
      const garmentFile = new File([blob], "garment.png", { type: "image/png" });
      
      await startTryOn(garmentFile, userPhoto);
    } catch (err) {
      console.error(err);
    }
  };

  return (
    <div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
      <h3 className="text-lg font-bold text-gray-900 mb-4">Virtual Fitting Room</h3>
      
      <div className="space-y-4">
        {/* Upload Customer Photo */}
        <div>
          <label className="block text-sm font-semibold text-gray-700 mb-1">
            Upload Your Photo (Optional)
          </label>
          <input
            type="file"
            accept="image/*"
            onChange={handleFileChange}
            className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-secondary-50 file:text-secondary-700 hover:file:bg-secondary-100"
          />
        </div>

        {/* Action Button */}
        <button
          onClick={handleSubmit}
          disabled={status === "uploading" || status === "generating"}
          className="w-full rounded-lg bg-secondary-600 py-3 font-semibold text-white transition hover:bg-secondary-700 disabled:bg-gray-300"
        >
          {status === "uploading" && "Uploading photos..."}
          {status === "generating" && "Generating Try-On..."}
          {status !== "uploading" && status !== "generating" && "Try It On!"}
        </button>

        {/* Display Status or Result */}
        {status === "success" && resultUrl && (
          <div className="mt-6 overflow-hidden rounded-xl border border-gray-100">
            <img src={resultUrl} alt="Try On Result" className="w-full object-contain" />
          </div>
        )}

        {status === "error" && (
          <p className="mt-2 text-sm text-red-600 font-medium">Error: {error}</p>
        )}
      </div>
    </div>
  );
}

🎨 Endpoint Reference & Payload Details

Here is the parameter detail for /vton/generate requests:

ParameterTypeRequiredDescription
userIdstringYesYour account Developer User ID
model_namestringYesModel choice: fast, medium, or quality
inputClothesImageUrlsstring[]YesArray of uploaded garment image URLs (1 item max)
inputPersonImageUrlsstring[]NoArray of uploaded customer image URLs (1 item max)
promptstringNoCustom guidelines for VTON model output placement
versionnumberNoAPI rendering model version (e.g. 1.0 or 1.1)

💡 Best Practices for Production

Warning
API Key Protection: NEVER call the VTON API endpoints directly from your client-side React code in production. Shoppers could extract your API key from network logs. Always route your frontend requests through a secure server-side API Route / proxy helper.
Tip
Pre-Uploading Garments: Instead of converting the product garment image to a file and uploading it on every single click, pre-upload your catalogue garment assets when adding them to your PIM. Store the returned cloud URLs directly in your database to trigger try-on instantly.