Quickstart
Run your first virtual try-on in minutes — with the OpenAI SDK, raw HTTP, or async jobs.
Introduction
TryOn-API is the OpenRouter for virtual try-on — one OpenAI-compatible API, one key, many models.
Pick a model with a model parameter, exactly like OpenRouter's image-generation chat/completions. Under the hood the gateway routes to dedicated try-on engines (Kling) and image models repurposed for try-on (Gemini, OpenAI gpt-image-1, FAL FLUX Kontext), with automatic fallback, per-model credit pricing, streaming, and async jobs.
There are three ways to call the API. Pick whichever fits your stack:
| Approach | Endpoint | Best for |
|---|---|---|
| OpenAI SDK drop-in | POST /api/v1/chat/completions |
Already using OpenAI/OpenRouter — swap base_url + key + model slug. |
| Raw HTTP | POST /api/v1/tryon |
Ergonomic native try-on — multipart file upload or JSON, no SDK. |
| Async jobs | mode: "async" + GET /api/v1/tryon/:id |
Long-running generations — submit, then poll or subscribe via WebSocket. |
Get an API key
Create a key on the API Keys page. Keys look like tryon_... and are shown in full only once, at creation — store it somewhere safe.
Send your key on every request in the Authorization header:
Authorization: Bearer tryon_...
Never ship a tryon_ key in client-side code. Call the API from your backend, or use a short-lived demo session for browser demos.
Your first request
Send a person image and a garment image to POST /api/v1/chat/completions. Tag each image with tryon_role ("person" or "garment"). Result images come back on choices[0].message.images[].
curl -X POST https://tryon-api.com/api/v1/chat/completions \
-H "Authorization: Bearer tryon_..." \
-H "Content-Type: application/json" \
-d '{
"model": "google/gemini-2.5-flash-image",
"modalities": ["image"],
"messages": [{
"role": "user",
"content": [
{ "type": "text", "text": "put the garment on the person" },
{ "type": "image_url", "image_url": { "url": "https://example.com/person.jpg" }, "tryon_role": "person" },
{ "type": "image_url", "image_url": { "url": "https://example.com/shirt.jpg" }, "tryon_role": "garment" }
]
}]
}'
const res = await fetch("https://tryon-api.com/api/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": "Bearer tryon_...",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "google/gemini-2.5-flash-image",
modalities: ["image"],
messages: [{
role: "user",
content: [
{ type: "text", text: "put the garment on the person" },
{ type: "image_url", image_url: { url: "https://example.com/person.jpg" }, tryon_role: "person" },
{ type: "image_url", image_url: { url: "https://example.com/shirt.jpg" }, tryon_role: "garment" },
],
}],
}),
});
const data = await res.json();
// Result image(s) on data.choices[0].message.images[]
console.log(data.choices[0].message.images[0].image_url.url);
image_url.url accepts a data: URL or an http(s) URL (fetched server-side). The MIME type is sniffed from magic bytes.
Using the OpenAI SDK
TryOn-API is OpenAI-compatible, so the official openai package works as a drop-in. Point baseURL at https://tryon-api.com/api/v1, use your tryon_ key, and pass a TryOn-API model slug.
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://tryon-api.com/api/v1", // 1. base URL
apiKey: "tryon_...", // 2. your TryOn-API key
});
const resp = await client.chat.completions.create({
model: "google/gemini-2.5-flash-image", // 3. a TryOn-API model slug
modalities: ["image"],
messages: [{
role: "user",
content: [
{ type: "text", text: "put the garment on the person" },
{ type: "image_url", image_url: { url: PERSON_URL }, tryon_role: "person" },
{ type: "image_url", image_url: { url: GARMENT_URL }, tryon_role: "garment" },
],
}],
});
// Result image(s) on resp.choices[0].message.images[]
console.log(resp.choices[0].message.images[0].image_url.url);
Browse every slug and its credit cost on the Models page. Use "auto" to let the gateway pick any capable model.
Migrate from OpenRouter in 3 lines
Already calling OpenRouter's image-generation chat completions? Point the OpenAI SDK at this gateway — swap (1) the base URL, (2) the key, and (3) the model slug. The request body is unchanged; tryon_role on image parts is an optional extension.
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://tryon-api.com/api/v1", // 1. base URL
apiKey: "tryon_...", // 2. your TryOn-API key
});
const resp = await client.chat.completions.create({
model: "google/gemini-2.5-flash-image", // 3. a TryOn-API model slug
modalities: ["image"],
messages: [{
role: "user",
content: [
{ type: "text", text: "put the garment on the person" },
{ type: "image_url", image_url: { url: PERSON_URL }, tryon_role: "person" },
{ type: "image_url", image_url: { url: GARMENT_URL }, tryon_role: "garment" },
],
}],
});
// Result image(s) on resp.choices[0].message.images[]
# Same body you already send to OpenRouter — just change the host, key and slug.
curl -X POST https://tryon-api.com/api/v1/chat/completions \
-H "Authorization: Bearer tryon_..." \
-H "Content-Type: application/json" \
-d '{
"model": "google/gemini-2.5-flash-image",
"modalities": ["image"],
"messages": [{
"role": "user",
"content": [
{ "type": "text", "text": "put the garment on the person" },
{ "type": "image_url", "image_url": { "url": "https://example.com/person.jpg" }, "tryon_role": "person" },
{ "type": "image_url", "image_url": { "url": "https://example.com/shirt.jpg" }, "tryon_role": "garment" }
]
}]
}'
Native /tryon endpoint
Prefer not to use the chat schema? POST /api/v1/tryon is the ergonomic native endpoint. It accepts multipart/form-data (upload files directly) or application/json (image URLs), auto-detected from the Content-Type.
curl -X POST https://tryon-api.com/api/v1/tryon \
-H "Authorization: Bearer tryon_..." \
-F "model=kling/kolors-v1-5" \
-F "person_images=@person.jpg" \
-F "garment_images=@dress.jpg"
const res = await fetch("https://tryon-api.com/api/v1/tryon", {
method: "POST",
headers: {
"Authorization": "Bearer tryon_...",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "kling/kolors-v1-5",
person_images: ["https://example.com/person.jpg"],
garment_images: ["https://example.com/dress.jpg"],
category: "apparel",
}),
});
const data = await res.json();
// data.images[0].url · data.usage.credits · data.status
console.log(data.images[0].url);
Async mode
For long-running generations, add mode: "async". The submit returns 202 immediately with a job handle; the generation runs in a per-job Durable Object. Billing is pre-deducted at submit and refunded on a refundable failure.
# 1. Submit (mode:"async") → 202 { jobId, status, statusUrl, wsUrl }
curl -X POST https://tryon-api.com/api/v1/tryon \
-H "Authorization: Bearer tryon_..." \
-H "Content-Type: application/json" \
-d '{
"model": "kling/kolors-v1-5",
"mode": "async",
"person_images": ["https://example.com/person.jpg"],
"garment_images": ["https://example.com/dress.jpg"]
}'
# 2. Poll status until completed | failed
curl https://tryon-api.com/api/v1/tryon/gen_... \
-H "Authorization: Bearer tryon_..."
// 1. Submit
const submit = await fetch("https://tryon-api.com/api/v1/tryon", {
method: "POST",
headers: {
"Authorization": "Bearer tryon_...",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "kling/kolors-v1-5",
mode: "async",
person_images: ["https://example.com/person.jpg"],
garment_images: ["https://example.com/dress.jpg"],
}),
});
const { jobId } = await submit.json(); // 202 { jobId, status, statusUrl, wsUrl }
// 2a. Poll
async function poll(id) {
const r = await fetch(`https://tryon-api.com/api/v1/tryon/${id}`, {
headers: { "Authorization": "Bearer tryon_..." },
});
return r.json(); // { status: "queued|processing|completed|failed", images, ... }
}
// 2b. Or subscribe to live updates over WebSocket
const ws = new WebSocket(`wss://tryon-api.com/api/v1/tryon/${jobId}/ws`);
ws.onmessage = (e) => console.log(JSON.parse(e.data)); // { status, images, ... }
Lifecycle: queued → processing → completed | failed. The WebSocket sends the latest cached status on connect and the final job status on completion, then closes.