> 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.

# Preview a catalogue Variant revision

POST https://api.morphiaone.ai/v1/catalogue/variants/{variant_id}/revisions/{revision}/previews
Content-Type: application/json

Renders one `draft` or `approved` revision -- active or not -- on an uploaded
input, as a makeup job you poll like any other. Nothing about the variant moves:
its availability, `active_revision` and `generation` are what they were. The body
is `inputs` and, for a variant with more than one application, the
`application_id` to render. The job's `resolution` carries `purpose: "preview"`
and a `target` naming the revision and its `authored_intent_digest`.
A rejected revision answers `409 REVISION_DECIDED`; a retired variant
`409 VARIANT_RETIRED`. Both refuse a new preview only: a retry of one already
admitted replays it.
Send an `Idempotency-Key`: a retry with the same key and the same request
answers 200 with the same job. The preview's identity includes the revision's
authored intent, so the same key naming another revision, or a try-on, answers
`409 IDEMPOTENCY_KEY_REUSED`.
Requires the `catalogue.preview` scope, which your account operator provisions; no
other catalogue scope implies it, and it needs no `makeup.apply`. Served only where
the catalogue is enabled for your environment.

Reference: https://docs.morphiaone.ai/api-reference/endpoints/catalogue/variant-previews-create

## Authentication

- `X-API-Key` header (required) — API key for one environment of your account. Keep server-side; never expose in browser clients.

## Request

### Path parameters

- `variant_id` (string, required)
- `revision` (string, required)

### Headers

- `Idempotency-Key` (string, optional, nullable)

### Body (application/json)

This endpoint expects an object.

- `inputs` (list of V1CatalogueVariantsVariantIdRevisionsRevisionPreviewsPostRequestBodyContentApplicationJsonSchemaInputsItems, required)
- `application_id` (string, optional, nullable)

## Response

### 200

Idempotent replay of the job the first submission created

- `completed_at` (datetime, required, nullable)
- `created_at` (datetime, required)
- `error` (JobErrorV1, required, nullable)
- `expires_at` (datetime, required, nullable)
- `id` (string, required)
- `inputs` (list of JobInputRef, required)
- `operation` (enum, required) — What a job does: the operation's authorization scope. An operation is not its output's `type`: `OPERATION_OUTPUT_TYPE` is the one place the two meet.
  - Allowed values: `makeup.apply`, `skin.analyze`, `hair.try_on`
- `output` (JobV1Output, required, nullable)
- `status` (enum, required)
  - Allowed values: `queued`, `running`, `succeeded`, `failed`, `cancelled`
- `updated_at` (datetime, required)
- `livemode` (boolean, optional)
- `resolution` (MakeupResolution, optional) — `JobV1.resolution`: how a job's custom applications were resolved. Fixed at admission, so an exact retry and every later read answer the profile versions the job was admitted under, whatever the declaration says now.

### 202

Job accepted for asynchronous processing

- `completed_at` (datetime, required, nullable)
- `created_at` (datetime, required)
- `error` (JobErrorV1, required, nullable)
- `expires_at` (datetime, required, nullable)
- `id` (string, required)
- `inputs` (list of JobInputRef, required)
- `operation` (enum, required) — What a job does: the operation's authorization scope. An operation is not its output's `type`: `OPERATION_OUTPUT_TYPE` is the one place the two meet.
  - Allowed values: `makeup.apply`, `skin.analyze`, `hair.try_on`
- `output` (JobV1Output, required, nullable)
- `status` (enum, required)
  - Allowed values: `queued`, `running`, `succeeded`, `failed`, `cancelled`
- `updated_at` (datetime, required)
- `livemode` (boolean, optional)
- `resolution` (MakeupResolution, optional) — `JobV1.resolution`: how a job's custom applications were resolved. Fixed at admission, so an exact retry and every later read answer the profile versions the job was admitted under, whatever the declaration says now.

## Errors

### 400 Bad Request Error

Invalid request, inputs or Idempotency-Key

- `error` (ErrorDetail, required)

### 401 Unauthorized Error

Missing API key

- `error` (ErrorDetail, required)

### 402 Payment Required Error

Account credit balance is insufficient

- `error` (ErrorDetail, required)

### 403 Forbidden Error

API key lacks the catalogue.preview scope

- `error` (ErrorDetail, required)

### 404 Not Found Error

No such variant or revision, or input asset not found

- `error` (ErrorDetail, required)

### 409 Conflict Error

Variant retired, revision rejected, or idempotency conflict

- `error` (ErrorDetail, required)

### 410 Gone Error

Input asset expired

- `error` (ErrorDetail, required)

### 413 Content Too Large Error

Request body exceeds byte limit

- `error` (ErrorDetail, required)

### 429 Too Many Requests Error

Admission limit exceeded

- `error` (ErrorDetail, required)

### 503 Service Unavailable Error

Service capacity or dependency unavailable

- `error` (ErrorDetail, required)

## Types

### V1CatalogueVariantsVariantIdRevisionsRevisionPreviewsPostRequestBodyContentApplicationJsonSchemaInputsItems

One job input: which uploaded asset, in which role.

- `asset_id` (string, required)
- `role` ("source", required)

### JobErrorV1

- `code` (enum, required)
  - Allowed values: `INPUT_UNPROCESSABLE`, `INPUT_EXPIRED`, `PROCESSING_FAILED`, `EXECUTION_ENVIRONMENT_UNAVAILABLE`
- `message` (string, required)

### JobInputRef

One input as a job reports it: an open role and a tolerant shape.

- `asset_id` (string, required)
- `role` (string, required)

### JobV1Output

- `type`: `hair.try_on` (HairTryOnOutput)
  - `applied` (HairTryOnApplied, required)
  - `image` (ResultAsset, required) — One named part of a job output, served through the V1 asset content route.
  - `revision` (integer, optional, default: 1)
- `type`: `makeup.try_on` (MakeupTryOnOutput)
  - `image` (ResultAsset, required) — One named part of a job output, served through the V1 asset content route.
  - `revision` (integer, optional, default: 1)
- `type`: `skin.analysis` (SkinAnalysisOutput)
  - `concerns` (list of SkinConcernResult, required)
  - `analysis_version` (string, optional, default: skin-2026-09-draft.1)
  - `visualizations` (list of ResultAsset, optional, default: [])

### MakeupResolution

`JobV1.resolution`: how a job's custom applications were resolved. Fixed at admission, so an exact retry and every later read answer the profile versions the job was admitted under, whatever the declaration says now.

- `applications` (list of MakeupResolvedApplication, required)
- `look` (MakeupResolutionLook, optional) — The catalogue look revision a look request expanded (P3 F3.2).
- `purpose` ("preview", optional)
- `target` (MakeupResolutionTarget, optional) — The exact catalogue revision a preview rendered (P3 F3.3; F0.2 §8): the revision's identity and the digest of what was authored at it.

### ErrorDetail

- `code` (string, required)
- `message` (string, required)
- `request_id` (string, required)
- `details` (ErrorDetailDetails, optional)
- `doc_url` (string, optional, nullable)
- `retryable` (boolean, optional, nullable)

### HairTryOnApplied

- `color_gradient` (HairAppliedColorGradient, required)

### ResultAsset

One named part of a job output, served through the V1 asset content route.

- `expires_at` (datetime, required)
- `href` (string, required)
- `id` (string, required)
- `media_type` (enum, required)
  - Allowed values: `image/png`, `image/webp`
- `role` (string, required)
- `size_bytes` (integer, required)
- `height` (integer, optional, nullable)
- `width` (integer, optional, nullable)

### SkinConcernResult

- `concern` (enum, required)
  - Allowed values: `acne`, `dark_circle`, `eye_bag`, `redness`, `oiliness`, `wrinkles`, `firmness`, `moisture`, `pore`, `tear_trough`, `hyper_pigmentation`, `droopy_upper_eyelid`
- `score` (double, required, nullable)
- `status` (enum, required)
  - Allowed values: `measured`, `unavailable`
- `unavailable_reason` (enum, required, nullable)
  - Allowed values: `not_measured`, `not_measurable`

### MakeupResolvedApplication

What one custom application was resolved as (P1.5 §9): the profile, the shade and the options with their defaults filled. The explicit product it compiled to is internal (the job's admitted plan) and never published.

- `category` (enum, required) — Public makeup categories enabled in the V1 draft, in native type order.
  - Allowed values: `foundation`, `blush`, `contour`, `eyeshadow`, `eyeliner`, `highlighter`, `mascara`, `lipstick`, `lip_liner`, `concealer`, `bronzer`
- `color` (string, required)
- `index` (integer, required)
- `options` (map from string to string, required)
- `profile` (MakeupResolutionProfile, required) — The profile version a custom application resolved under.
- `components` (map from string to map from string to string, optional)
- `design` (MakeupResolutionDesign, optional) — The design version a profile implements (Foundation V1 D10).
- `placement` (string, optional)
- `variant` (MakeupResolutionVariant, optional) — The catalogue variant revision a variant application resolved as (P2.3).
- `warnings` (list of MakeupResolutionWarning, optional, default: [])

### MakeupResolutionLook

The catalogue look revision a look request expanded (P3 F3.2).

- `id` (string, required)
- `revision` (integer, required)

### MakeupResolutionTarget

The exact catalogue revision a preview rendered (P3 F3.3; F0.2 §8): the revision's identity and the digest of what was authored at it.

- `authored_intent_digest` (string, required)
- `id` (string, required)
- `kind` (enum, required)
  - Allowed values: `variant_revision`, `look_revision`
- `revision` (integer, required)

### ErrorDetailDetails

- map from string to any
- list of map from string to any

### HairAppliedColorGradient

- `dark` (HairAppliedColorStop, required)
- `light` (HairAppliedColorStop, required)

### MakeupResolutionProfile

The profile version a custom application resolved under.

- `digest` (string, required)
- `id` (string, required)
- `version` (integer, required)

### MakeupResolutionDesign

The design version a profile implements (Foundation V1 D10).

- `digest` (string, required)
- `id` (string, required)
- `version` (integer, required)

### MakeupResolutionVariant

The catalogue variant revision a variant application resolved as (P2.3).

- `id` (string, required)
- `revision` (integer, required)

### MakeupResolutionWarning

- `code` (string, required)
- `field` (string, required)

### HairAppliedColorStop

- `color` (string, required)
- `opacity` (double, required)

## Examples

### Catalogue_variantPreviewsCreate_example

**Request**

```json
{
  "inputs": [
    {
      "asset_id": "string",
      "role": "string"
    }
  ]
}
```

**Response**

```json
{
  "completed_at": null,
  "created_at": "2026-09-24T10:00:05Z",
  "error": null,
  "expires_at": "2026-09-25T10:00:05Z",
  "id": "job_bw7ydhvq1h5937xtam03ddbtmt",
  "inputs": [
    {
      "asset_id": "asset_6pa8ad0ds2t7g1z6pg4tcr4zqp",
      "role": "source"
    }
  ],
  "operation": "makeup.apply",
  "output": null,
  "status": "queued",
  "updated_at": "2026-09-24T10:00:05Z"
}
```

**SDK Code**

```python Catalogue_variantPreviewsCreate_example
import requests

url = "https://api.morphiaone.ai/v1/catalogue/variants/variant_id/revisions/revision/previews"

payload = { "inputs": [
        {
            "asset_id": "string",
            "role": "string"
        }
    ] }
headers = {
    "X-API-Key": "<apiKey>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript Catalogue_variantPreviewsCreate_example
const url = 'https://api.morphiaone.ai/v1/catalogue/variants/variant_id/revisions/revision/previews';
const options = {
  method: 'POST',
  headers: {'X-API-Key': '<apiKey>', 'Content-Type': 'application/json'},
  body: '{"inputs":[{"asset_id":"string","role":"string"}]}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go Catalogue_variantPreviewsCreate_example
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.morphiaone.ai/v1/catalogue/variants/variant_id/revisions/revision/previews"

	payload := strings.NewReader("{\n  \"inputs\": [\n    {\n      \"asset_id\": \"string\",\n      \"role\": \"string\"\n    }\n  ]\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("X-API-Key", "<apiKey>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby Catalogue_variantPreviewsCreate_example
require 'uri'
require 'net/http'

url = URI("https://api.morphiaone.ai/v1/catalogue/variants/variant_id/revisions/revision/previews")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<apiKey>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"inputs\": [\n    {\n      \"asset_id\": \"string\",\n      \"role\": \"string\"\n    }\n  ]\n}"

response = http.request(request)
puts response.read_body
```

```java Catalogue_variantPreviewsCreate_example
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.morphiaone.ai/v1/catalogue/variants/variant_id/revisions/revision/previews")
  .header("X-API-Key", "<apiKey>")
  .header("Content-Type", "application/json")
  .body("{\n  \"inputs\": [\n    {\n      \"asset_id\": \"string\",\n      \"role\": \"string\"\n    }\n  ]\n}")
  .asString();
```

```php Catalogue_variantPreviewsCreate_example
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.morphiaone.ai/v1/catalogue/variants/variant_id/revisions/revision/previews', [
  'body' => '{
  "inputs": [
    {
      "asset_id": "string",
      "role": "string"
    }
  ]
}',
  'headers' => [
    'Content-Type' => 'application/json',
    'X-API-Key' => '<apiKey>',
  ],
]);

echo $response->getBody();
```

```csharp Catalogue_variantPreviewsCreate_example
using RestSharp;

var client = new RestClient("https://api.morphiaone.ai/v1/catalogue/variants/variant_id/revisions/revision/previews");
var request = new RestRequest(Method.POST);
request.AddHeader("X-API-Key", "<apiKey>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"inputs\": [\n    {\n      \"asset_id\": \"string\",\n      \"role\": \"string\"\n    }\n  ]\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift Catalogue_variantPreviewsCreate_example
import Foundation

let headers = [
  "X-API-Key": "<apiKey>",
  "Content-Type": "application/json"
]
let parameters = ["inputs": [
    [
      "asset_id": "string",
      "role": "string"
    ]
  ]] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.morphiaone.ai/v1/catalogue/variants/variant_id/revisions/revision/previews")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### Example 2

**Request**

```json
{
  "inputs": [
    {
      "asset_id": "string",
      "role": "string"
    }
  ]
}
```

**Response**

```json
{
  "completed_at": "2024-01-15T09:30:00Z",
  "created_at": "2024-01-15T09:30:00Z",
  "error": {
    "code": "INPUT_UNPROCESSABLE",
    "message": "string"
  },
  "expires_at": "2024-01-15T09:30:00Z",
  "id": "string",
  "inputs": [
    {
      "asset_id": "string",
      "role": "string"
    }
  ],
  "operation": "makeup.apply",
  "output": {
    "type": "makeup.try_on",
    "image": {
      "expires_at": "2024-01-15T09:30:00Z",
      "href": "string",
      "id": "string",
      "media_type": "image/png",
      "role": "string",
      "size_bytes": 1,
      "height": 1,
      "width": 1
    },
    "revision": 1
  },
  "status": "queued",
  "updated_at": "2024-01-15T09:30:00Z",
  "livemode": true,
  "resolution": {
    "applications": [
      {
        "category": "foundation",
        "color": "string",
        "index": 1,
        "options": {},
        "profile": {
          "digest": "string",
          "id": "string",
          "version": 1
        },
        "components": {},
        "design": {
          "digest": "string",
          "id": "string",
          "version": 1
        },
        "placement": "string",
        "variant": {
          "id": "string",
          "revision": 1
        },
        "warnings": [
          {
            "code": "string",
            "field": "string"
          }
        ]
      }
    ],
    "look": {
      "id": "string",
      "revision": 1
    },
    "purpose": "string",
    "target": {
      "authored_intent_digest": "string",
      "id": "string",
      "kind": "variant_revision",
      "revision": 1
    }
  }
}
```

**SDK Code**

```python
import requests

url = "https://api.morphiaone.ai/v1/catalogue/variants/variant_id/revisions/revision/previews"

payload = { "inputs": [
        {
            "asset_id": "string",
            "role": "string"
        }
    ] }
headers = {
    "X-API-Key": "<apiKey>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript
const url = 'https://api.morphiaone.ai/v1/catalogue/variants/variant_id/revisions/revision/previews';
const options = {
  method: 'POST',
  headers: {'X-API-Key': '<apiKey>', 'Content-Type': 'application/json'},
  body: '{"inputs":[{"asset_id":"string","role":"string"}]}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.morphiaone.ai/v1/catalogue/variants/variant_id/revisions/revision/previews"

	payload := strings.NewReader("{\n  \"inputs\": [\n    {\n      \"asset_id\": \"string\",\n      \"role\": \"string\"\n    }\n  ]\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("X-API-Key", "<apiKey>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.morphiaone.ai/v1/catalogue/variants/variant_id/revisions/revision/previews")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<apiKey>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"inputs\": [\n    {\n      \"asset_id\": \"string\",\n      \"role\": \"string\"\n    }\n  ]\n}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.morphiaone.ai/v1/catalogue/variants/variant_id/revisions/revision/previews")
  .header("X-API-Key", "<apiKey>")
  .header("Content-Type", "application/json")
  .body("{\n  \"inputs\": [\n    {\n      \"asset_id\": \"string\",\n      \"role\": \"string\"\n    }\n  ]\n}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.morphiaone.ai/v1/catalogue/variants/variant_id/revisions/revision/previews', [
  'body' => '{
  "inputs": [
    {
      "asset_id": "string",
      "role": "string"
    }
  ]
}',
  'headers' => [
    'Content-Type' => 'application/json',
    'X-API-Key' => '<apiKey>',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.morphiaone.ai/v1/catalogue/variants/variant_id/revisions/revision/previews");
var request = new RestRequest(Method.POST);
request.AddHeader("X-API-Key", "<apiKey>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"inputs\": [\n    {\n      \"asset_id\": \"string\",\n      \"role\": \"string\"\n    }\n  ]\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "X-API-Key": "<apiKey>",
  "Content-Type": "application/json"
]
let parameters = ["inputs": [
    [
      "asset_id": "string",
      "role": "string"
    ]
  ]] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.morphiaone.ai/v1/catalogue/variants/variant_id/revisions/revision/previews")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```