> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.morphiaone.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.morphiaone.ai/_mcp/server.

# Makeup try-on

> **Info**
>
> **Release status.** API `1.0.0-rc.1` is a release candidate: request and response shapes may still change before the stable release. SDK `0.2.2` is published as a Developer Preview (`@morphiaone/sdk` on npm and `morphiaone` on PyPI).

Calls need a key with the `makeup.apply` scope, in an environment your account is provisioned
for; see [product availability](/products/availability).

`POST /v1/makeup/try-ons` renders products onto an uploaded face image. Upload the
image as an [Asset](/concepts/assets) first. Product requests run as asynchronous
[Jobs](/concepts/job-outputs).

## TypeScript SDK

Assume `client` is configured as shown in the [SDK guide](/sdks/overview):

```ts
import { readFile, writeFile } from "node:fs/promises";

const source = await client.assets.upload(await readFile("face.jpg"));
const job = await client.makeup.tryOn({
  assetId: source.id,
  products: [
    { category: "lip_liner", color: "#7B2E42", opacity: 0.8 },
    { category: "lipstick", color: "#C2185B" },
  ],
  idempotencyKey: "session-91c4-makeup",
});
const done = await client.jobs.wait(job.id);

if (done.output?.known && done.output.type === "makeup.try_on") {
  const image = await client.assets.getContent(done.output.image);
  await writeFile("makeup.png", image.content);
}
```

## Python SDK

Assume `client` is configured as shown in the [SDK guide](/sdks/overview):

```python
from morphiaone import MakeupTryOnOutput

source = client.assets.upload("face.jpg")
job = client.makeup.try_on(
    asset_id=source.id,
    products=[
        {"category": "lip_liner", "color": "#7B2E42", "opacity": 0.8},
        {"category": "lipstick", "color": "#C2185B"},
    ],
    idempotency_key="session-91c4-makeup",
)
done = client.jobs.wait(job.id)

if isinstance(done.output, MakeupTryOnOutput):
    image = client.assets.get_content(done.output.image)
    with open("makeup.png", "wb") as out:
        out.write(image.content)
```

`jobs.wait` returns only after success and raises `ProcessingError` for a failed job.
Pass a stable idempotency key for retries across processes; both SDKs generate a key
when omitted, but that protects only retries inside one call. See
[Idempotency](/concepts/idempotency).

## REST

Upload the image as raw bytes with `POST /v1/assets`, then submit its `asset_id`. The
request below returns a Job; persist its id, poll `GET /v1/jobs/{job_id}` until
`succeeded` or `failed`, and download the result Asset before it expires. See
[Assets](/concepts/assets) and [Jobs](/concepts/job-outputs) for those steps.

```bash
curl --fail-with-body --silent --show-error \
  -H "X-API-Key: ${API_KEY}" \
  -H "Idempotency-Key: session-91c4-makeup" \
  -H "Content-Type: application/json" \
  -d '{
        "inputs": [{"role": "source", "asset_id": "asset_6pa8ad0ds2t7g1z6pg4tcr4zqp"}],
        "products": [
          {"category": "lip_liner", "color": "#7B2E42", "opacity": 0.8},
          {"category": "lipstick", "color": "#C2185B"}
        ]
      }' \
  "${BASE_URL}/v1/makeup/try-ons"
```

## Products in the V1 draft

`products` is required and **ordered: its order is the layer order**. Reordering it
changes the request.

| `category`   | Product    |
| ------------ | ---------- |
| `foundation` | Foundation |
| `lipstick`   | Lipstick   |
| `lip_liner`  | Lip liner  |

These are the three categories in the current draft. Each product has a `category`, a
`color` as `#RRGGBB`, and optional `opacity` from `0` to `1` (default `1`, quantized to
`0.01`). Each category appears at most once per request, so a request holds at most
three products. A repeated category is a `400 INVALID_REQUEST` whose
`details[].code` is `duplicate_product_category`; an unknown category or malformed
colour is a `400 INVALID_REQUEST` naming the field. The draft launch set remains an
open decision.

## Reading the output

A succeeded job's `output.type` is `makeup.try_on`, and it carries one result image:

```json
{
  "type": "makeup.try_on",
  "revision": 1,
  "image": {
    "id": "asset_eyh67f8tc55kdrayf5zka7d7e7",
    "role": "result_image",
    "media_type": "image/png",
    "width": 1200,
    "height": 1600,
    "size_bytes": 932182,
    "expires_at": "2026-10-01T10:00:05Z",
    "href": "/v1/assets/asset_eyh67f8tc55kdrayf5zka7d7e7/content"
  }
}
```

Fetch `href` with your API key before `expires_at`. It is an authenticated download from
the API itself, not a CDN URL—see [Privacy and retention](/concepts/privacy-retention).

## Visual parity status

> **Warning**
>
> Pixel parity against the production browser renderer has not been verified for any
> category. Current reference authority is blocked by a confirmed output-capture defect;
> existing pixels are historical evidence only. A successful job is not evidence that its
> output matches an existing browser try-on experience; do not make that claim from this
> draft API.

## Interactive sessions

Each call is an independent submission with its own Job; there is no server-side
session that accumulates products. Debounce interactive controls before submitting,
and use a stable idempotency key when retrying the same logical try-on. Upload the face
once and reuse its `asset_id` while it remains unexpired.