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.

Next steps