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)

Terminal
npm install express multer clothsy-ai
node --env-file=.env server.mjs
.env
CLOTHSY_API_KEY=clothsy_live_...
server.mjs
// 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.

Terminal
pip install flask requests
export CLOTHSY_API_KEY=clothsy_live_...
python app.py
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.

Terminal
npx create-next-app@latest my-store
cd my-store
npm install clothsy-ai
.env.local
CLOTHSY_API_KEY=clothsy_live_...
lib/catalog.ts
// 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;
}
app/api/tryon/route.ts
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;
  },
});
app/products/[id]/page.tsx
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>
  );
}
app/globals.css (add to the end)
.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.