Get started
TypeScript SDK
clothsy-ai wraps the HTTP API in a few typed methods, and takes care of idempotency keys, retries and waiting for results so you don't have to.
Install
npm install clothsy-ai- Runs on Node.js 18+, Deno, Bun, Vercel Edge and Cloudflare Workers — anywhere with a standard
fetch. - No dependencies. Nothing else is installed with it.
- Server only. Creating a client in a browser throws an error, because your API key would be visible to anyone. There is a
dangerouslyAllowBrowseroption for special cases such as internal tools on a trusted network — don't use it on a public site.
Building with Next.js? The same package includes a ready-made route handler and React button — see Next.js.
Create a client
import { Clothsy } from "clothsy-ai";
// Reads the key from process.env.CLOTHSY_API_KEY.
const clothsy = new Clothsy();
// Or configure it explicitly:
const custom = new Clothsy({
apiKey: process.env.CLOTHSY_API_KEY,
timeoutMs: 60_000,
maxRetries: 2,
});| Option | Type | Description |
|---|---|---|
apiKey | string | Your key. Defaults to process.env.CLOTHSY_API_KEY. |
baseUrl | string | Defaults to https://fabricvton-api.onrender.com/api/v1. You won't normally change it. |
timeoutMs | number | How long each HTTP request may take before it's abandoned. Default 60_000. |
maxRetries | number | How many times to retry a request that failed for a temporary reason. Default 2. See Retries. |
fetch | function | A fetch implementation to use instead of the global one — useful for tests, proxies or instrumentation. |
dangerouslyAllowBrowser | boolean | Allows creating a client in a browser. Default false. Your key would be public — avoid it. |
Where there's no process.env, pass the key in yourself:
import { Clothsy } from "clothsy-ai";
export default {
async fetch(request, env) {
// Workers have no process.env, so pass the secret in.
const clothsy = new Clothsy({ apiKey: env.CLOTHSY_API_KEY });
const credits = await clothsy.account.credits();
return Response.json({ credits });
},
};images.upload(data, options?)
Uploads a JPEG or PNG (up to 4 MB) with POST /images and resolves with { id, expiresAt }. The id works in any number of try-ons for 24 hours. Uploading is free.
import { readFile } from "node:fs/promises";
// From a file on disk (a Node.js Buffer is a Uint8Array):
const photo = await clothsy.images.upload(await readFile("shopper.jpg"), {
filename: "shopper.jpg",
contentType: "image/jpeg",
});
// From a form upload in a route handler (a File is a Blob):
const form = await request.formData();
const fromForm = await clothsy.images.upload(form.get("photo") as File);
console.log(photo.id, photo.expiresAt);| Argument | Type | Description |
|---|---|---|
data | Blob | bytes | The image: a Blob or File, or raw bytes such as a Uint8Array or Node.js Buffer. |
options.filename | string | A file name to send with the upload. |
options.contentType | string | image/jpeg or image/png. Worth setting when you pass raw bytes, which carry no type of their own. |
tryons.create(params)
Starts a try-on with POST /tryons and resolves straight away with { id, status: "pending", pollUrl }.
const tryon = await clothsy.tryons.create({
person: { imageId: photo.id }, // or { url: "https://…" }
garment: { url: "https://your-cdn.example.com/denim-jacket.jpg" }, // or { imageId: "img_…" }
title: "Cropped denim jacket",
consent: true,
idempotencyKey: requestId, // optional
});
tryon.id; // "7f3c2a9e-1d4b-4c1e-9a55-2b8f0c6d4e10"
tryon.status; // "pending"| Param | Type | Description |
|---|---|---|
person | { url } | { imageId } | The photo of the shopper: an HTTPS URL, or the id of an uploaded image. |
garment | { url } | { imageId } | The product image, in either form. |
title | string, optional | The product's name, up to 120 characters. Improves garment placement. |
consent | true | Required. Confirms the person is an adult who agreed, and that you have the rights to both images. |
idempotencyKey | string, optional | 8–128 letters, digits, _ or -. Generated for you if you leave it out. Pass your own when your code might call create twice for the same thing — for example, one key per shopper click. |
tryons.retrieve(id)
One check with GET /tryons/{id}. Resolves with { id, status, resultUrl, message? }, where status is pending, success or failed. A failed try-on is returned, not thrown.
const tryon = await clothsy.tryons.retrieve("7f3c2a9e-1d4b-4c1e-9a55-2b8f0c6d4e10");
if (tryon.status === "success") show(tryon.resultUrl);
if (tryon.status === "failed") console.log(tryon.message);tryons.waitFor(id, options?)
Polls until the try-on is finished and resolves with it once status is success. If the try-on fails it throws TryOnFailedError; if it's still pending when time runs out it throws TryOnTimeoutError. A timeout only means you stopped waiting — the try-on may still finish, and you can call waitFor again with the same id.
const controller = new AbortController();
const done = await clothsy.tryons.waitFor(tryon.id, {
timeoutMs: 120_000, // default 180_000
intervalMs: 3_000, // default 2_500
signal: controller.signal, // call controller.abort() to stop waiting
});
console.log(done.resultUrl);| Option | Type | Description |
|---|---|---|
timeoutMs | number | How long to keep waiting. Default 180_000 (3 minutes). |
intervalMs | number | Time between checks. Default 2_500. |
signal | AbortSignal | Stops waiting when aborted — for example, when the shopper closes the page. |
onStatus | function | Called with the latest try-on object ({ id, status, resultUrl, message }) each time the SDK checks on it, so you can report progress. |
tryons.run(params, waitOptions?)
Create and wait in one call. It uses POST /tryons/sync, which usually returns the finished image directly, and falls back to polling if it needs more time. It resolves with the finished try-on and throws the same errors as waitFor. Takes the same params as create and the same options as waitFor.
const done = await clothsy.tryons.run(
{
person: { imageId: photo.id },
garment: { url: "https://your-cdn.example.com/denim-jacket.jpg" },
title: "Cropped denim jacket",
consent: true,
},
{ timeoutMs: 120_000 }, // optional, same options as waitFor
);
console.log(done.resultUrl);run() is the simplest choice for scripts, jobs and any server code that can wait. Behind a storefront, where you want to answer the browser quickly, call create() and let the browser poll your server, which calls retrieve().account.credits()
Resolves with the number of credits left on the account, from GET /account.
const credits = await clothsy.account.credits(); // e.g. 42Idempotency and retries
- Every
create()andrun()sends anIdempotency-Key. If you don't pass one, the SDK makes one and reuses it for its own retries, so a retry can never start — or charge for — a second try-on. - Network errors and responses with status 429, 500, 502 or 503 are retried up to
maxRetriestimes, waiting 1 s, then 2 s, then 4 s. When the API sendsRetry-After, the SDK waits that long instead. - Other errors — a bad image, missing consent, no credits — are thrown at once, because retrying them unchanged won't help.
- Set
maxRetries: 0to turn retries off.
Errors
Everything the SDK throws for an API problem is a ClothsyError, with the HTTP status, the API's code and a message. More specific subclasses let you branch with instanceof:
| Class | Thrown when |
|---|---|
AuthenticationError | The key is missing, malformed or revoked (INVALID_API_KEY). |
InsufficientCreditsError | The account is out of credits (INSUFFICIENT_CREDITS). |
ValidationError | The request or one of its images was refused. Check error.code for the reason, such as INVALID_IMAGE_URL or PERSON_PHOTO_REJECTED. |
RateLimitError | Still rate limited after the retries. retryAfter says how many seconds to wait. |
ServerError | A 5xx response that didn't clear up after the retries. |
ConnectionError | The API couldn't be reached, or a request took longer than timeoutMs. |
TryOnFailedError | From waitFor() or run(): the try-on finished with failed. Its credit has been returned. |
TryOnTimeoutError | From waitFor() or run(): still pending when timeoutMs ran out. |
friendlyMessage(error) turns any of these into a short sentence that's safe to show a shopper, without exposing codes or internals.
import {
Clothsy,
ClothsyError,
InsufficientCreditsError,
RateLimitError,
TryOnFailedError,
TryOnTimeoutError,
ValidationError,
friendlyMessage,
} from "clothsy-ai";
const clothsy = new Clothsy();
export async function tryOn(params) {
try {
const done = await clothsy.tryons.run(params);
return { resultUrl: done.resultUrl };
} catch (error) {
if (error instanceof InsufficientCreditsError) {
await alertTheTeam("Try-on credits are used up");
} else if (error instanceof RateLimitError) {
console.warn(`Rate limited; retry in ${error.retryAfter} s`);
} else if (error instanceof ValidationError) {
console.info("Request refused:", error.code); // e.g. PERSON_PHOTO_REJECTED
} else if (error instanceof TryOnFailedError || error instanceof TryOnTimeoutError) {
console.info(error.message);
} else if (error instanceof ClothsyError) {
console.error(error.status, error.code, error.message);
} else {
throw error;
}
// Safe, friendly wording for the shopper, whatever went wrong:
return { message: friendlyMessage(error) };
}
}The full list of API codes is in Errors & limits.

