Guide
Full examples
Three complete, copy-paste stores that do everything in the custom store guide: upload the shopper's photo, start the try-on, and relay polling to the browser.
The Node.js and Python servers expose the same two routes, so the browser code from the guide (tryon.js) works with either: POST /tryon with the photo, and GET /tryon/:id to poll. The Next.js example uses the package's own route and button instead.
Photos are uploaded with
POST /images, so there's no bucket to set up and nothing to clean up: an uploaded image's id stops working after 24 hours, and the file is deleted automatically within 35 days.Node.js (Express)
npm install express multer clothsy-ai
node --env-file=.env server.mjsCLOTHSY_API_KEY=clothsy_live_...// server.mjs — Node.js 20+
import express from "express";
import multer from "multer";
import { Clothsy, InsufficientCreditsError, friendlyMessage } from "clothsy-ai";
const app = express();
const upload = multer({ limits: { fileSize: 4 * 1024 * 1024 } }); // matches the API limit
const clothsy = new Clothsy(); // reads CLOTHSY_API_KEY
// Replace with your own catalogue lookup.
const PRODUCTS = {
"denim-jacket": { title: "Cropped denim jacket", imageUrl: "https://cdn.example.com/denim-jacket.jpg" },
};
function sendError(res, error) {
if (error instanceof InsufficientCreditsError) console.error("Clothsy credits are used up — top up.");
else console.error(error);
res.status(error.status ?? 502).json({ message: friendlyMessage(error) });
}
app.post("/tryon", upload.single("photo"), async (req, res) => {
const product = PRODUCTS[req.body.productId];
if (!product || !req.file) return res.status(400).json({ message: "Missing photo or product." });
if (!/^[A-Za-z0-9_-]{8,128}$/.test(req.body.requestId || "")) {
return res.status(400).json({ message: "Missing request id." });
}
try {
const photo = await clothsy.images.upload(req.file.buffer, {
filename: "photo.jpg",
contentType: "image/jpeg",
});
const tryon = await clothsy.tryons.create({
person: { imageId: photo.id },
garment: { url: product.imageUrl },
title: product.title,
consent: true,
idempotencyKey: req.body.requestId,
});
res.status(202).json({ id: tryon.id });
} catch (error) {
sendError(res, error);
}
});
app.get("/tryon/:id", async (req, res) => {
try {
const tryon = await clothsy.tryons.retrieve(req.params.id);
res.json({ status: tryon.status, resultUrl: tryon.resultUrl, message: tryon.message });
} catch (error) {
sendError(res, error);
}
});
app.use(express.static("public")); // your storefront, with tryon.js
app.listen(3000, () => console.log("Listening on http://localhost:3000"));Python (Flask)
No SDK needed: plain requests calls the HTTP API directly.
pip install flask requests
export CLOTHSY_API_KEY=clothsy_live_...
python app.py# app.py — Python 3.10+
import os, re
import requests
from flask import Flask, jsonify, request
API = "https://fabricvton-api.onrender.com/api/v1"
KEY = os.environ["CLOTHSY_API_KEY"]
app = Flask(__name__, static_folder="public", static_url_path="")
app.config["MAX_CONTENT_LENGTH"] = 5 * 1024 * 1024 # a 4 MB photo plus the form fields
# Replace with your own catalogue lookup.
PRODUCTS = {
"denim-jacket": {"title": "Cropped denim jacket", "imageUrl": "https://cdn.example.com/denim-jacket.jpg"},
}
def friendly_message(code=None):
if code == "PERSON_PHOTO_REJECTED":
return "Please use a clear photo of just you, facing the camera."
if code in ("IMAGE_TOO_LARGE", "UNSUPPORTED_IMAGE"):
return "Please use a JPEG or PNG under 4 MB."
if code == "RATE_LIMITED":
return "Lots of people are trying things on. Try again in a minute."
return "Virtual try-on isn't available right now. Please try again later."
def clothsy(method, path, **kwargs):
headers = {"Authorization": f"Bearer {KEY}", **kwargs.pop("headers", {})}
res = requests.request(method, API + path, headers=headers, timeout=60, **kwargs)
return res.status_code, res.json()
@app.post("/tryon")
def start_tryon():
product = PRODUCTS.get(request.form.get("productId", ""))
photo = request.files.get("photo")
request_id = request.form.get("requestId", "")
if not product or not photo or not re.fullmatch(r"[A-Za-z0-9_-]{8,128}", request_id):
return jsonify(message="Missing photo, product or request id."), 400
# 1. Upload the photo. Free, and the id works for 24 hours.
status, uploaded = clothsy("POST", "/images", files={"file": ("photo.jpg", photo.read(), "image/jpeg")})
if status >= 400:
return jsonify(message=friendly_message(uploaded.get("code"))), status
# 2. Start the try-on with the photo's id.
status, body = clothsy(
"POST", "/tryons",
headers={"Idempotency-Key": request_id},
json={
"personImageId": uploaded["id"],
"garmentImageUrl": product["imageUrl"],
"title": product["title"],
"consent": True,
},
)
if status >= 400:
if body.get("code") == "INSUFFICIENT_CREDITS":
app.logger.error("Clothsy credits are used up — top up.")
return jsonify(message=friendly_message(body.get("code"))), status
return jsonify(id=body["id"]), 202
@app.get("/tryon/<tryon_id>")
def poll_tryon(tryon_id):
status, body = clothsy("GET", f"/tryons/{tryon_id}")
if status >= 400:
return jsonify(message=friendly_message(body.get("code"))), status
return jsonify(status=body["status"], resultUrl=body.get("resultUrl"), message=body.get("message"))
if __name__ == "__main__":
app.run(port=3000)Next.js (App Router)
A product page with a working try-on button, in four files. The walkthrough is in the Next.js guide.
npx create-next-app@latest my-store
cd my-store
npm install clothsy-aiCLOTHSY_API_KEY=clothsy_live_...// Replace with your real catalogue: a database, a CMS or a commerce API.
export type Product = { id: string; title: string; price: string; imageUrl: string };
const PRODUCTS: Product[] = [
{
id: "denim-jacket",
title: "Cropped denim jacket",
price: "$79",
imageUrl: "https://cdn.example.com/denim-jacket.jpg",
},
{
id: "linen-shirt",
title: "Relaxed linen shirt",
price: "$45",
imageUrl: "https://cdn.example.com/linen-shirt.jpg",
},
];
export async function getProduct(id: string): Promise<Product | null> {
return PRODUCTS.find((product) => product.id === id) ?? null;
}import { createTryOnRoute } from "clothsy-ai/next";
import { getProduct } from "@/lib/catalog";
export const maxDuration = 60;
export const { POST, GET } = createTryOnRoute({
resolveProduct: async (productId) => {
const product = await getProduct(productId); // your catalogue
return product ? { imageUrl: product.imageUrl, title: product.title } : null;
},
});import { notFound } from "next/navigation";
import { TryOnButton } from "clothsy-ai/react";
import { getProduct } from "@/lib/catalog";
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const product = await getProduct(id);
if (!product) notFound();
return (
<main className="product">
<img src={product.imageUrl} alt={product.title} />
<div>
<h1>{product.title}</h1>
<p>{product.price}</p>
<TryOnButton
productId={product.id}
endpoint="/api/tryon"
label="Try it on"
className="product-tryon"
/>
<p className="small">
Try-on images are AI-generated. <a href="/privacy">How we use your photo</a>
</p>
</div>
</main>
);
}.product {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 48px;
max-width: 1100px;
margin: 48px auto;
padding: 0 16px;
}
.product img {
width: 100%;
border-radius: 12px;
}
.product-tryon {
--clothsy-accent: #111827;
}Run npm run dev and open http://localhost:3000/products/denim-jacket. When you deploy to Vercel, add CLOTHSY_API_KEY to the project's environment variables.

