Build · Virtual Try-On

Build a virtual try-on feature for your app

In one sentence

Accept the product photo in your backend, POST it to /api/v1/tryon/apparel with a mannequin ID, poll the returned generation ID from a background worker (never the HTTP request thread) until status flips to completed, then persist output_url into your own object storage and render it in the UI.

  • Credits: 5 credits at 2K · 6 credits at 4K
  • Typical time: 1.5–4 min
  • Processing: async, polling

Every figure on this page comes from the Photta API docs · checked

A production-shaped recipe: customer uploads a product photo, your backend queues a Photta try-on job, a worker polls until it lands, the frontend shows the on-model result. No blocking requests, no leaked API keys, no fake SDK.

RESPONSE=$(curl -s -X POST https://api.photta.app/api/v1/tryon/apparel \
  -H "Authorization: Bearer $PHOTTA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "product_type": "top",
  "product_images": [
    "https://cdn.yourstore.com/products/shirt-front.jpg"
  ],
  "product_back_image": "https://cdn.yourstore.com/products/shirt-back.jpg",
  "product_category": "t_shirt",
  "mannequin_id": "mnk_f3a9b1",
  "pose_id": "pose_c2d4e5",
  "background": "https://cdn.yourstore.com/backgrounds/studio-loft.jpg",
  "lock_background": true,
  "resolution": "2K",
  "aspect_ratio": "3:4",
  "body_size": "M",
  "expression": "neutral",
  "user_instruction": "Keep the collar pattern crisp and centered."
}')
ID=$(echo "$RESPONSE" | jq -r '.data.id')

How it works

From upload to on-model image

Six concrete steps. Expect ten to fifteen minutes of integration work the first time.

  1. Step 1

    Sketch the data flow

    Customer device → your backend (upload) → Photta API (submit) → Photta API (poll) → your database (status mirror) → customer device (render). Draw this on paper before you write the first line — the boundary between your backend and Photta's is the only thing that matters.

  2. Step 2

    Generate and store an API key

    Live keys start with photta_live_. Put it in an environment variable. Never ship it to the browser. One key covers every Photta capability, so you don't need separate keys for try-on, jewelry or ghost mannequin.

  3. Step 3

    Wire up the upload endpoint

    Accept the customer's product photo into object storage first. Giving Photta a public URL is simpler than base64 and gives you an audit trail for every request.

  4. Step 4

    Submit the job from the server

    POST /api/v1/tryon/apparel with the product URL, mannequin_id, resolution and aspect_ratio. The API returns 202 Accepted + a generation ID. Store the ID in your database so your client can poll your own endpoint, not Photta's.

  5. Step 5

    Poll from a background worker

    Do not poll inside the HTTP request — the job takes 1.5 to 4 minutes, which exceeds most serverless timeouts. Use Trigger.dev, Inngest, BullMQ, Vercel Cron or your own queue to re-schedule a poll every 3 seconds until the job completes.

  6. Step 6

    Render the result

    When status flips to completed, copy data.output_url into your database and emit a realtime event (websocket, SSE or long-poll) to the customer's device. The UI swaps the loading skeleton for the image. Done.

Three code samples, end to end

Upload handler, job dispatcher, worker-side poller — every piece you need to ship the feature.

// POST /app/try-on → your backend upload handler
import { put } from "@vercel/blob";

export async function uploadProduct(file: File) {
  const { url } = await put(`products/${crypto.randomUUID()}.jpg`, file, {
    access: "public",
  });
  return url;
}
const res = await fetch("https://api.photta.app/api/v1/tryon/apparel", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.PHOTTA_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "product_type": "top",
    "product_images": [
      "https://cdn.yourstore.com/products/shirt-front.jpg"
    ],
    "product_back_image": "https://cdn.yourstore.com/products/shirt-back.jpg",
    "product_category": "t_shirt",
    "mannequin_id": "mnk_f3a9b1",
    "pose_id": "pose_c2d4e5",
    "background": "https://cdn.yourstore.com/backgrounds/studio-loft.jpg",
    "lock_background": true,
    "resolution": "2K",
    "aspect_ratio": "3:4",
    "body_size": "M",
    "expression": "neutral",
    "user_instruction": "Keep the collar pattern crisp and centered."
  }),
});
const { data } = await res.json(); // data.id, data.status
// Trigger.dev / Inngest / BullMQ task
const BASE = "https://api.photta.app/api/v1";

export const pollTryOnTask = task({
  id: "poll-try-on",
  run: async ({ generationId, attempt = 0 }) => {
    const res = await fetch(`${BASE}/tryon/apparel/${generationId}`, {
      headers: { Authorization: `Bearer ${process.env.PHOTTA_API_KEY}` },
    });
    const { data } = await res.json();
    if (data.status === "completed") return data.output_url;
    if (data.status === "failed") throw new Error(data.error_message);
    if (attempt >= 120) throw new Error("try-on timed out");
    // Re-schedule this same task after 3 seconds.
    return ctx.retry.after(3000, { generationId, attempt: attempt + 1 });
  },
});

Questions developers ask

Questions before you ship

Can I do this without a backend?

No. Your Photta API key must stay server-side. Even a thin Next.js route handler or a Cloudflare Worker counts as a backend for this purpose — the key just needs to live somewhere a user-agent can't read it.

How do I avoid hanging HTTP requests?

Never poll inside a request handler. Submit the job synchronously, return the generation ID to the client, and poll from a background worker. The client polls YOUR status endpoint, not Photta's — that way rate limits are your problem to manage, not each user's.

What happens if a job fails mid-flight?

Photta refunds the credits automatically on failure, and the response carries data.error_code + data.error_message. Surface a friendly message to the user and offer a retry button that kicks off a fresh job.

Can I show real progress?

Photta doesn't emit intermediate progress — status is either processing, completed or failed. What works well is a timed progress bar calibrated to the 1.5–4 minute window, or a simple skeleton loader. Don't lie about percentages.

Is the result URL permanent?

The CDN URL is stable, but your product shouldn't depend on it for long-term rendering. Download the bytes once and store them in your own S3/GCS/R2 bucket — then your PDP keeps working even if Photta's CDN moves.

Ship virtual try-on in an afternoon

Every code sample on this page matches the official Photta docs. Grab an API key and you'll have a working prototype before the coffee finishes brewing.