Skip to navigation

Skin analysis

Request cosmetic concern scores and optional result images.

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.

POST /v1/skin/analyses submits an uploaded face image for cosmetic skin analysis. Upload the image as an Asset first. Product requests run as asynchronous Jobs.

TypeScript SDK

Assume client is configured as shown in the SDK guide:

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:

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.

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 and Jobs for those steps.

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:

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:

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

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

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.