> For the complete documentation index, see [llms.txt](https://docs.photoroom.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.photoroom.com/tutorials/how-to-add-a-virtual-try-on-to-an-e-commerce-app.md).

# How to add a Virtual Try-On to an e-commerce app

{% columns %}
{% column %}
{% embed url="<https://youtube.com/shorts/E7mCBsvM-48>" %}

{% endcolumn %}

{% column %}

{% endcolumn %}
{% endcolumns %}

## Overview

Letting shoppers see a garment on *themselves*, rather than on a model, is one of the most direct uses of the Virtual Try-On feature of the Image Editing API. This tutorial shows how to build that experience in a native iOS app: the shopper picks a product, takes a selfie (or chooses a full-body photo), enters their height, and gets back a photo of themselves wearing the piece.

The screenshots and code samples come from a SwiftUI reference implementation; the same approach applies to any mobile or web app.

## What the flow looks like

1. The shopper opens a product and taps **Try on me**.
2. They provide a photo of themselves, either from the photo library or with an in-app camera that fires after a three-second countdown.
3. They enter their height in centimetres.
4. They tap **Generate try-on**. A single call to the Image Editing API returns the try-on image.
5. They can switch to another product and generate again with the same photo and height.

|                                                                                                       *1. Pick a product*                                                                                                       |                                                                                                                                                                                                                  *2. Add a photo and height*                                                                                                                                                                                                                 |                                                                                                      *3. Generate the try-on*                                                                                                      |
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| ![](https://2855892273-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1SYxn7dWbQYsNtUdJE3f%2Fuploads%2FRolwp2IcCO1g5Ne5IWhP%2Ftry-on-01-product.png?alt=media\&token=8fc22fd5-4202-4b41-bcb7-6d2d6ad0597a) | ![](https://2855892273-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1SYxn7dWbQYsNtUdJE3f%2Fuploads%2Fl4oHiogkno9nnhAKW2kV%2Ftry-on-02-height.png?alt=media\&token=1b2c97ca-c606-4e81-b164-1af0083687ea)![](https://2855892273-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1SYxn7dWbQYsNtUdJE3f%2Fuploads%2Ff4Xi8knfz78u0NuMQFbm%2Ftry-on-04-review.png?alt=media\&token=0817136b-23d5-446b-9c8a-95a5ea5cb87c) | ![](https://2855892273-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1SYxn7dWbQYsNtUdJE3f%2Fuploads%2FAc1xH0XAak0zPZ0gibnI%2Ftry-on-08-comparison.png?alt=media\&token=a04cd8cb-0a3b-4491-ac01-91c8434ef5a5) |

{% hint style="info" %}
The whole flow is **one API call per product**. Everything else (camera, countdown, height validation, image preparation) runs on the device. The photo and height are kept in memory only and are never persisted to disk.
{% endhint %}

## Before you start

You need an iPhone running iOS 17 or later (the Simulator works for everything except the camera) and a Photoroom API key. If you don't have one yet, here are the steps to create yours. Store the key in the device Keychain, never in the app bundle or in logs.

{% hint style="info" %}
Use Sandbox mode while you develop: prepend `sandbox_` to your API key and calls are free (results are watermarked).
{% endhint %}

## How it works

### Step 1: Take the selfie

A good try-on starts with a good input photo: one person, full body, good lighting, clothing clearly visible. A small dedicated camera screen, rather than the system picker, helps the shopper get there:

* It defaults to the **front camera**, so a shopper can frame themselves, and offers a flip button.
* The shutter starts a **three-second countdown** so the shopper has time to step back and stand naturally. Each tick is also announced to VoiceOver. The countdown can be cancelled, and is cancelled automatically when the app becomes inactive.
* The capture uses the `.photo` session preset with `photoQualityPrioritization = .speed`, which is plenty for an upload that will be downscaled anyway, and keeps the shutter feeling instant.
* The front-camera photo is captured **mirrored**, so that the review screen matches what the shopper saw in the preview.
* After capture, a **Retake / Use photo** review screen lets the shopper check their pose before anything is uploaded.

|                                                                                                            *Countdown*                                                                                                            |                                                                                                     *Review before upload*                                                                                                     |
| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| ![](https://2855892273-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1SYxn7dWbQYsNtUdJE3f%2Fuploads%2FdgwPYGJwMzllr2m17YF2%2Ftry-on-03-countdown.png?alt=media\&token=438e3b83-d38f-4052-9257-1bd5c4e7d732) | ![](https://2855892273-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1SYxn7dWbQYsNtUdJE3f%2Fuploads%2Ff4Xi8knfz78u0NuMQFbm%2Ftry-on-04-review.png?alt=media\&token=0817136b-23d5-446b-9c8a-95a5ea5cb87c) |

```swift
func shutter() {
    guard ready, !capturing, remaining == nil else { return }
    countdown.start { second in
        remaining = second
        UIAccessibility.post(notification: .announcement, argument: String(second))
    } capture: {
        remaining = nil
        capturing = true
        camera.capture(angle: captureAngle, mirrored: device?.position == .front)
    }
}
```

Alternatively, **Choose a photo** opens a `PhotosPicker` for an existing full-body photo. Both paths end up calling `model.setPerson(image)`.

### Step 2: Ask for the shopper's height

Height is the one piece of information the photo cannot provide: the same person can fill the frame from close up or far away, so the model has no reliable scale. Telling the API the shopper's height gives it a reference for garment length and drape.

Treat height as **required**, validated as a whole number between 50 and 250 cm, and only enable **Generate try-on** once both a photo and a valid height are present:

```swift
var heightCM: Int? {
    guard let value = Int(heightText.trimmingCharacters(in: .whitespacesAndNewlines)),
          (50...250).contains(value) else { return nil }
    return value
}
var canGenerate: Bool { person != nil && heightCM != nil && !isGenerating }
```

A detail worth copying: use a `UITextField` configured once with `keyboardType = .numberPad`, autocorrection, predictions and Writing Tools turned off, and an empty input-assistant bar. Numeric entry never needs the predictive-text UI, and configuring the traits before the field becomes first responder avoids keyboard flicker.

Changing the height, the photo, or the garment should cancel any in-flight request and clear the previous result, so a displayed image always corresponds to the inputs on screen.

### Step 3: Prepare the images for upload

Before the API call, both the selfie and the product image go through the same preparation, on a dedicated background queue so the UI never stalls:

* decode with `CGImageSourceCreateThumbnailAtIndex` and `kCGImageSourceCreateThumbnailWithTransform`, which **bakes in the EXIF orientation** (a selfie taken in portrait would otherwise arrive sideways);
* downscale to at most **1,400 pixels** on the longest edge;
* draw onto an opaque white canvas, which strips any alpha channel and metadata;
* encode as **JPEG at quality 0.9**.

```swift
let ratio = min(1, 1400 / max(image.size.width, image.size.height))
let size = CGSize(width: image.size.width * ratio, height: image.size.height * ratio)
let format = UIGraphicsImageRendererFormat()
format.scale = 1; format.opaque = true
let normalized = UIGraphicsImageRenderer(size: size, format: format).image { context in
    UIColor.white.setFill()
    context.fill(CGRect(origin: .zero, size: size))
    image.draw(in: CGRect(origin: .zero, size: size))
}
let data = normalized.jpegData(compressionQuality: 0.9)
```

This keeps uploads small (typically a few hundred kilobytes) without affecting the result, since the output resolution is decided by the API, not by the input.

### Step 4: Call the Image Editing API

The try-on is a single `POST /v2/edit` request. The **product image is the main `imageFile`**, and the **shopper's photo is passed as the custom model** with `virtualModel.model.custom.imageFile`. The prompt in `virtualModel.prompt` is assembled from three parts: instructions to keep the shopper's identity, pose, background and framing unchanged; the shopper's height; and, when your catalogue has them, the garment's measurements (length, flat chest width, shoulder width and sleeve length, in cm). Together they give the model a scale for the person *and* for the garment, so the hem and sleeves land in a plausible place. The equivalent curl call is:

```bash
curl --request POST \
  --url https://image-api.photoroom.com/v2/edit \
  --header 'x-api-key: YOUR_API_KEY' \
  --form imageFile=@product.jpg \
  --form virtualModel.model.custom.imageFile=@selfie.jpg \
  --form virtualModel.mode=ai.auto \
  --form 'virtualModel.prompt=Dress the person in the reference photo in the supplied garment. Preserve the original person'"'"'s face, identity, body shape, pose, background and framing. Change only the clothing, retaining realistic fabric details and drape. The person in the reference photo is 180 cm tall. Use this height as context for realistic garment length, drape and proportions on this person. Preserve their identity and body shape; do not stretch or resize their body. Estimated garment measurements for this item: length from high shoulder point beside the neck to hem, excluding collar/hood: 125 cm; flat chest width from armpit to armpit: 56 cm (not circumference); shoulder width across the garment: 46 cm; sleeve length from shoulder to cuff: 64 cm. Use these fixed garment dimensions as approximate guidance for hem position, sleeve reach, ease and drape. Do not rescale the garment dimensions to fit the person or alter the person'"'"'s body to fit the garment. Preserve the garment design from the product image.' \
  --form removeBackground=false \
  --form export.format=webp \
  --output try-on.webp
```

And in Swift, with a small multipart helper:

```swift
let photoPrompt = "Dress the person in the reference photo in the supplied garment. Preserve the original person's face, identity, body shape, pose, background and framing. Change only the clothing, retaining realistic fabric details and drape."
let heightPrompt = "The person in the reference photo is \(heightCM) cm tall. Use this height as context for realistic garment length, drape and proportions on this person. Preserve their identity and body shape; do not stretch or resize their body."
// `m` holds the garment's measurements; skip this part when you don't have them.
let measurementsPrompt = "Estimated garment measurements for this item: length from high shoulder point beside the neck to hem, excluding collar/hood: \(m.lengthCM) cm; flat chest width from armpit to armpit: \(m.chestWidthCM) cm (not circumference); shoulder width across the garment: \(m.shoulderWidthCM) cm; sleeve length from shoulder to cuff: \(m.sleeveLengthCM) cm. Use these fixed garment dimensions as approximate guidance for hem position, sleeve reach, ease and drape. Do not rescale the garment dimensions to fit the person or alter the person's body to fit the garment. Preserve the garment design from the product image."

var request = URLRequest(url: URL(string: "https://image-api.photoroom.com/v2/edit")!, timeoutInterval: 180)
request.httpMethod = "POST"
request.setValue(apiKey, forHTTPHeaderField: "x-api-key")
request.setValue("image/jpeg, image/png, image/webp", forHTTPHeaderField: "Accept")

var form = MultipartForm()
form.file("imageFile", filename: "product.jpg", data: productData)
form.file("virtualModel.model.custom.imageFile", filename: "model.jpg", data: personData)
form.field("virtualModel.mode", value: "ai.auto")
form.field("virtualModel.prompt", value: [photoPrompt, heightPrompt, measurementsPrompt].joined(separator: " "))
form.field("removeBackground", value: "false")
form.field("export.format", value: "webp")
request.setValue("multipart/form-data; boundary=\(form.boundary)", forHTTPHeaderField: "Content-Type")
request.httpBody = form.finish()
```

A few points about these parameters:

* **No scene or pose override.** The Virtual Try-On feature accepts `virtualModel.scene` and `virtualModel.pose`, but this flow deliberately omits them: the goal is to show the garment on the shopper *as they are*, in their own photo, so the prompt asks to preserve the original background and pose instead.
* **`removeBackground=false`** keeps the shopper's own background in the output. The Virtual Try-On documentation also recommends `referenceBox=originalImage` when keeping the background; add it if you notice the framing changing.
* **Height is guidance, not a measurement.** It helps the model choose a plausible garment length and drape; it does not guarantee a physically exact fit. Keep the wording in the prompt explicit about *not* resizing the person.
* **Garment measurements are guidance too.** Take them from your size chart and spell out how each one is defined (flat width, not circumference; length from the high shoulder point, excluding the collar), so the model interprets the numbers the way you intend. If a product has no measurements, simply leave this part of the prompt out rather than inventing values.
* **`export.format=webp`** keeps the download small on mobile networks; `UIImage` decodes it directly.
* To request a 2K output, add `virtualModel.quality=advanced`. This is an Enterprise plan option; without it you get the default 1K output.

The response body is the image itself. Check the status code and the `Content-Type` before decoding:

```swift
let session = URLSession(configuration: .ephemeral)   // keeps photos out of URL caches
defer { session.invalidateAndCancel() }
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse else { throw TryOnError.provider(0) }
guard (200..<300).contains(http.statusCode) else { throw TryOnError.provider(http.statusCode) }
guard http.mimeType?.hasPrefix("image/") == true else { throw TryOnError.invalidImage }
```

Using an **ephemeral `URLSession`** is a small but worthwhile privacy measure: the shopper's photo and the generated image never touch the on-disk URL cache.

### Step 5: Show the result and let the shopper keep shopping

Run the request in a `Task` tagged with a revision ID, so that a late response from a cancelled request can never overwrite a newer result. While the request is in flight, show the shopper's photo blurred under an "Applying your look" card that repeats the height being used, with a **Cancel generation** button. Decode the returned image at up to 4,096 pixels for display and the save/share screen.

|                                                                                                         *While generating*                                                                                                         |                                                                                                            *Result*                                                                                                            |
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| ![](https://2855892273-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1SYxn7dWbQYsNtUdJE3f%2Fuploads%2F9oVPyJPfXXpyRvqYmbXD%2Ftry-on-06-generating.png?alt=media\&token=bf80ce6d-1d01-4e76-975a-e1bd4e75d406) | ![](https://2855892273-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1SYxn7dWbQYsNtUdJE3f%2Fuploads%2FQGjfOOX6xG7L9PbORL51%2Ftry-on-07-result.png?alt=media\&token=a0cec199-cfaf-475b-883a-66ee1865df20) |

Because the photo and height live in the view model, the shopper can expand the product panel and pick another piece: reuse the prepared selfie and the same prompt structure and make one more API call. Backgrounding the app or closing the fitting room should cancel any request in progress.

## Handling errors

Map HTTP status codes to short, actionable messages:

```swift
case 401, 403: "The service rejected your credentials. Check your API key in Settings."
case 402:      "This API account needs more credits."
case 429:      "The service is busy. Please wait a moment and try again."   // default limit: 60 images/min
default:       "The try-on service could not finish (HTTP \(code)). Please try again."
```

Set the request timeout to 180 seconds: Virtual Try-On is a generative feature and can take longer than a plain background removal, especially at higher output resolutions.

## Adapting the flow to your app

* **Your own catalogue.** Flat packshots on a clean background work best as the product image, and you can pass extra angles with `virtualModel.additionalProductImages[].imageFile`. If you have size-chart data, attach it to each product so it flows into the prompt.
* **Remember the shopper.** This tutorial keeps the photo and height in memory for the lifetime of the view. In a real app you may want to store them (with consent) so a returning shopper can try on new arrivals in one tap.
* **Server-side calls.** Shipping an API key inside a mobile app exposes it to anyone who inspects the binary. For production, move the request behind your own backend and keep the camera, height input and image preparation on the device. The API call is a plain multipart HTTP request and translates directly to any language; see Code Samples.
* **A studio look instead of the shopper's photo.** If you want a polished editorial result rather than the shopper's own background, set `virtualModel.scene.preset.name` and `virtualModel.pose=standing`, and drop the "preserve background and framing" wording from the prompt. The Virtual Model page lists the available scenes and poses.

{% hint style="info" %}
Cost per try-on: **1** Image Editing API call per product. Taking a new photo or changing the height does not cost anything until the shopper taps **Generate try-on** again.
{% endhint %}

## Book a demo

If you're interested to use this feature, feel free to [contact our team to book a demo](https://www.photoroom.com/contact-sales?utm_source=apidocumentation\&utm_medium=enterprisegated).

<a href="https://www.photoroom.com/contact-sales?utm_source=apidocumentation&#x26;utm_medium=enterprisegated" class="button primary">Book a demo</a>&#x20;

## Going further

* Feature reference: Virtual Try-On, Virtual Model.
* Related tutorial: How to create listing images for clothing and apparel.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.photoroom.com/tutorials/how-to-add-a-virtual-try-on-to-an-e-commerce-app.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
