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

# Skin analysis

> **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 `skin.analyze` scope, in an environment your account is provisioned
for; see [product availability](/products/availability).

`POST /v1/skin/analyses` submits an uploaded face image for cosmetic skin analysis.
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.skin.analyze({
  assetId: source.id,
  concerns: ["acne", "redness", "wrinkles"],
  visualizations: [{ concern: "acne", kind: "mask" }],
  idempotencyKey: "order-48213-skin",
});
const done = await client.jobs.wait(job.id);

if (done.output?.known && done.output.type === "skin.analysis") {
  const acne = done.output.concerns.find((item) => item.concern === "acne");
  console.log(acne?.status, acne?.score);
  const mask = done.output.visualizations.find((asset) => asset.role === "mask.acne");
  if (mask) {
    const image = await client.assets.getContent(mask);
    await writeFile("acne-mask.png", image.content);
  }
}
```

## Python SDK

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

```python
from pathlib import Path

from morphiaone import SkinAnalysisOutput

source = client.assets.upload("face.jpg")
job = client.skin.analyze(
    asset_id=source.id,
    concerns=["acne", "redness", "wrinkles"],
    visualizations=[{"concern": "acne", "kind": "mask"}],
    idempotency_key="order-48213-skin",
)
done = client.jobs.wait(job.id)

if isinstance(done.output, SkinAnalysisOutput):
    acne = next(item for item in done.output.concerns if item.concern == "acne")
    print(acne.status, acne.score)
    mask = next((asset for asset in done.output.visualizations if asset.role == "mask.acne"), None)
    if mask:
        image = client.assets.get_content(mask)
        Path("acne-mask.png").write_bytes(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 any 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: order-48213-skin" \
  -H "Content-Type: application/json" \
  -d '{
        "inputs": [{"role": "source", "asset_id": "asset_6pa8ad0ds2t7g1z6pg4tcr4zqp"}],
        "concerns": ["acne", "redness", "wrinkles"],
        "visualizations": [{"concern": "acne", "kind": "mask"}]
      }' \
  "${BASE_URL}/v1/skin/analyses"
```

`inputs` must name exactly one Asset with `role: "source"`. `concerns` is required and
is a set: order does not matter, and a duplicate is a `400 INVALID_REQUEST`.
`visualizations` is optional; each `(concern, kind)` pair must refer to a requested
concern, and `kind` is `mask` or `visual`.

Only requested concerns are analysed, and only requested images are rendered. Asking
for all twelve concerns and every visualization when you display two of them adds work
you wait for and discard.

The draft currently lists these twelve concerns:

```text
acne            dark_circle     eye_bag         redness
oiliness        wrinkles        firmness        moisture
pore            tear_trough     hyper_pigmentation   droopy_upper_eyelid
```

The launch set is still an open decision; none of these draft concern semantics is
release-ready.

## Reading the output

A succeeded job's `output.type` is `skin.analysis`:

```json
{
  "type": "skin.analysis",
  "analysis_version": "skin-2026-09-draft.1",
  "concerns": [
    {"concern": "acne", "status": "measured", "score": 8.7, "unavailable_reason": null},
    {"concern": "redness", "status": "measured", "score": 7.4, "unavailable_reason": null},
    {"concern": "wrinkles", "status": "unavailable", "score": null, "unavailable_reason": "not_measured"}
  ],
  "visualizations": [
    {
      "id": "asset_cnep5j6sw0kphca1v6gv6qwayc",
      "role": "mask.acne",
      "media_type": "image/png",
      "width": 1200,
      "height": 1600,
      "size_bytes": 55312,
      "expires_at": "2026-10-01T10:00:05Z",
      "href": "/v1/assets/asset_cnep5j6sw0kphca1v6gv6qwayc/content"
    }
  ]
}
```

* `concerns` has one entry per requested concern.
* A `measured` concern carries a `score` and a null `unavailable_reason`.
* An `unavailable` concern carries a null `score` and a reason: `not_measured` (the
  engine produced no value) or `not_measurable` (the value was outside its documented
  range). It is never a score of `0`.
* `analysis_version` names the scoring semantics. Compare scores only within one version.

## Scores: higher means less visible

Scores are from `0` to `10`, rounded to one decimal, and **higher means the concern is
less visible**—an `8.7` for `acne` is a good reading, not severe acne. The meaning is
per concern, not universal, and every concern's semantics are draft and not
release-ready. `pore` and `hyper_pigmentation` bottom out at `1` in practice,
`tear_trough` is scaled by face size and is not comparable across faces, and
`firmness` has no proven upper bound. The contract's `SkinConcern` schema carries the
note for each value.

This is cosmetic appearance analysis, not a clinical diagnosis.

## No demographic inferences

The V1 output schema carries no inferred skin tone, gender, age or race, and no request
field asks for them. It returns the concerns you named and the images you requested.

The input is still a photograph of an identifiable face, so your privacy notice and
retention rules have to cover it: [Privacy and retention](/concepts/privacy-retention).

## Result images

Each entry in `visualizations` is a result Asset. Its `role` is `mask.<concern>` or
`visual.<concern>`, and its `href` is `/v1/assets/{id}/content` relative to the API base
URL. Download it with your API key before `expires_at`; it expires with the job.
[Assets](/concepts/assets)

## Determinism

The internal Skin result contract specifies byte-identical results for the same image
and configuration, apart from expiry and job-scoped Asset paths. The V1 analysis
version remains draft, so store `analysis_version` alongside any score you keep.