# Revieve Developer Docs — full text This file is the entire site, concatenated. It is generated; do not edit. --- ## Revieve Developer Docs Source: src/content/docs/index.mdx > Computer-vision and product-recommendation APIs for beauty-tech partners. Skin analysis, makeup, hair, and recommendations — built for retail, brand, and consumer apps. import { Card, CardGrid, LinkCard } from "@astrojs/starlight/components"; ## What you can build Score 20+ skin attributes — wrinkles, redness, hyperpigmentation, pore dilation, skin age — from a single selfie. Drive product routines, regimen tracking, and before-and-after comparisons. Foundation RGB, undertone, Fitzpatrick type, and detected hair colour drive shade-finding experiences for any catalog. Combine the Skin Analysis and Product Recommendations APIs to build expert-style consultations that scale across territories and languages. Pipe analysis output into the Product Recommendations API to rank products against your taxonomy — with or without an image. ## The two APIs ## Looking for virtual try-on? For real-time, webcam-based makeup, hair-colour, or full-look try-on in the browser, use **Revieve Virtual Try-On** — a separate product with its own multi-hundred-point face tracking, optimised for live video on mobile and desktop, with on-device frame processing. ## Start here --- ## Introduction Source: src/content/docs/introduction.mdx > What Revieve provides, how the two APIs fit together, and who they are intended for. Includes when to use the Virtual Try-On product instead. import { Aside } from "@astrojs/starlight/components"; Revieve is a beauty-tech platform whose core capability is turning user images into structured, actionable data about skin, hair, and makeup — and then mapping that data onto your product catalog. This site documents the two APIs that surface that capability: - **Skin Analysis API** — a server-side computer-vision endpoint that accepts a JPEG and returns per-metric scores, regional measurements, masks, categorical findings, and quality diagnostics. - **Product Recommendations API** — a catalog-aware service that consumes analysis output (and optionally explicit user preferences) and returns ranked product picks for a given partner taxonomy. ## Who this is for The docs assume you are one of: - A **partner engineer** integrating Revieve into a brand or retailer's web or mobile app. - An **internal team** building Revieve-powered consumer experiences (concierge, regimen builders, in-store kiosks). - An **AI coding agent** acting on behalf of one of the above. (See the [AI agents page](/ai-agents/) for a structured ingestion route.) ## What good looks like A clean integration usually has three layers: 1. **Capture** — a camera/upload UI that produces a face-forward JPEG of at least 600×600 pixels. 2. **Analyze** — a single `POST` to the Skin Analysis API with the components your experience needs. 3. **Apply** — render results back to the user (visualization overlays, shade picks, product cards), optionally piped into Product Recommendations. The [Your first skin analysis](/guides/your-first-skin-analysis/) walk-through covers this end-to-end in about ten minutes. ## What's *not* documented here - **Onboarding and Partner ID provisioning.** Reach out to your Revieve account contact. The test ID `jmlv6b2qtS` works against `partner-test.revieve.com` for evaluation. - **Revieve Virtual Try-On** — the real-time webcam-based try-on product. See the [VTO overview](/products/virtual-try-on/) and the [product page on revieve.com](https://www.revieve.com/platform/makeup/makeup-virtual-try-on); integration is sales-led. - **The hosted concierge front-ends** (AI Skincare, Makeup, Haircare Advisors; AI Hair Color Artist) — products built *on top of* the APIs and the VTO engine. - **Internal-only model components.** Some attributes are computed server-side but intentionally not exposed in the public spec. ## Privacy posture The Skin Analysis API is **server-side**: the user's image is uploaded to Revieve for analysis, processed, and the response is returned. For real-time experiences (lipstick, eyeshadow, hair-colour try-on), use **Revieve VTO** instead — it runs in the browser on the user's own device because face tracking at video frame rate isn't feasible over a network hop. Webcam frames are not transmitted to Revieve during normal VTO operation, which can be a meaningful posture difference depending on the deployment. Compliance and data-handling specifics for your deployment are governed by your Revieve agreement. ## Versioning posture The Skin Analysis API uses a single base path and identifies itself by spec version (currently `3.x`). Backwards-incompatible changes are rare, signposted in the [Changelog](/changelog/), and accompanied by a [migration guide](/migrations/). New components are additive. --- ## APIs at a glance Source: src/content/docs/apis-overview.mdx > Side-by-side summary of the Skin Analysis and Product Recommendations APIs — what each accepts, what each returns, and when to call which. import { Tabs, TabItem } from "@astrojs/starlight/components"; import { Aside } from "@astrojs/starlight/components"; ## Two products, briefly This site documents the **Skin Analysis** and **Product Recommendations** APIs — both server-side, image- or data-in / JSON-out. For real-time webcam-based virtual try-on, use **[Revieve VTO](/products/virtual-try-on/)** — a separate browser-side product with its own face tracking. The split: | You want… | Use | | ---------------------------------------------------------- | -------------------------------- | | Per-metric skin analysis (wrinkles, redness, age, …) | Skin Analysis API (this site) | | Catalog-aware product recommendations | Product Recommendations API | | Real-time makeup try-on (lipstick, eyeshadow, full looks) | **Revieve VTO** | | Hair-colour try-on | **Revieve VTO** / AI Hair Color Artist | ## Skin Analysis API A single `POST /analyzeImage/` endpoint that accepts a multipart-encoded JPEG and a small set of query parameters. Returns a JSON document containing: - **`results`** — per-component findings (score, regional measurements, masks). - **`face_area`** — bounding box and face rotation. - **`status`** — informational, warning, and error codes describing image quality and per-metric outcomes. - **`inverse_color_balance_rb`** — inverse white-balance factors `[invR, invB, luminance]` for re-applying the image's colour cast to measured colours (see [Interpreting results](/guides/interpreting-results/#inverse_color_balance_rb)). You ask for what you need via the `components` query parameter. Common picks: | Use case | Components | | ----------------------- | -------------------------------------------------------------------------- | | Skin assessment | `wrinkles, redness, hyperpigmentation, acne, pore_dilation, skin_age` | | Foundation shade match | `makeup` | | Hair attribute readout | `hair` | See the full reference at [/api/skin-analysis/](/api/skin-analysis/). ## Product Recommendations API Catalog-aware ranked recommendations. You provide partner taxonomy and optionally pass through analysis output and user preferences. See the reference at [/api/product-recommendations/](/api/product-recommendations/). ## When to call which Capture image → Skin Analysis → render results → Product Recommendations using analysis output as input. This is the "expert consultation" pattern. Use **[Revieve VTO](/products/virtual-try-on/)**. Real-time try-on is not built on the Skin Analysis API. Capture image → Skin Analysis with `makeup` → read `Skin foundation RGB`, `Skin undertone`, `Skin Fitzpatrick type` from `sum_measures[]` → query Product Recommendations filtered by foundation taxonomy. No image. Send only user preferences and inventory filters to Product Recommendations. Lighter integration, lower personalisation ceiling. ## Hosting and environments | Environment | Base URL | When to use | | ------------- | ----------------------------------------------------- | --------------------------------------------- | | Test | `https://partner-test.revieve.com/api/3/` | Evaluation, integration, QA. Rate-limited. | | Production | Region-specific (EU, US, Asia) | Provided with your production Partner ID. | The Partner ID `jmlv6b2qtS` is wired up to test and is safe to commit. --- ## AI assistant integration Source: src/content/docs/ai-agents.mdx > Surfaces and reference material for grounding AI coding assistants in current Revieve API behavior — OpenAPI specs, llms.txt, MCP, SDK prompts, common mistakes to verify against. import { Aside, Card, CardGrid, LinkCard, Tabs, TabItem } from "@astrojs/starlight/components"; ## Overview If you're building a Revieve integration with help from an AI coding assistant (Claude, ChatGPT, Cursor, Copilot, an MCP-connected client), this page collects the surfaces that keep the assistant grounded in current API behavior — and the common mistakes worth verifying in any AI-generated code. ## Canonical references ## Ingestion surfaces Three ways to feed these docs into an AI assistant: [`/llms-full.txt`](/llms-full.txt) — every page concatenated, front-matter preserved. Best for one-shot context windows. Every doc page has a markdown sidecar at `.md`: - [`/guides/getting-started.md`](/guides/getting-started.md) - [`/concepts/redness.md`](/concepts/redness.md) - [`/api/skin-analysis/operations/analyzeimage.md`](/api/skin-analysis/operations/analyzeimage.md) *(if available)* Use these instead of scraping rendered HTML — they preserve front-matter, code blocks, and structure cleanly. Connect an MCP-compatible client (Claude Desktop, Continue, Cursor) to the docs as a server. See [MCP integration](/guides/mcp-integration/) for the config block. ## Endpoint summary ```http POST https://partner-test.revieve.com/api/3/analyzeImage/ Content-Type: multipart/form-data # Required query params ?skintone=<0-6> &gender= # Optional but useful &components= &hairtype=<-1..4> &skintone_rgb= # Form fields image= partner_id= ``` Response is JSON. Branch on `data.message` (`"OK"` / `"Warning"` / `"Error"`) before reading `data.results[]`. The `data.status[]` array is the first thing to check on any non-OK message. ## Common mistakes in AI-generated code When training data lags the current spec, AI assistants tend to make a recurring set of mistakes. Worth scanning generated code for these: | Wrong | Right | | --- | --- | | Recommending the CV API for real-time virtual try-on | Use **[Revieve VTO](/products/virtual-try-on/)** — a separate browser-side product with its own multi-hundred-point face tracker | | `Authorization: Bearer …` | `partner_id` form field on the multipart body | | `?components=` empty / unset to mean "no metrics" | Omitting means *all defaults*; pass an explicit list to slim | | Reading `value_raw` | Use `value` (normalized 0..1) | | Sending PNG / HEIC | JPEG only; ≥ 600×600 px | | `gender=other` / `gender=nonbinary` | Use `gender=unknown` | | Calling fields that aren't in the published spec | Use only the request fields documented in the [API reference](/api/skin-analysis/); reject invented fields in generated code | | Branching on HTTP 200 alone | Branch on `data.message` *and* `data.status[]` | | Treating `local_warnings[]` as a stable enum | Treat as advisory; code defensively per finding | | Hard-coding visualization intensity ceiling | Bounds widened in 3.29; normalize at runtime | | Confusing the `makeup` component with virtual makeup try-on | `makeup` *detects* shades for matching; *applying* virtual makeup is VTO | | Calling features in `visualization_data` "heatmap points" | They're spots, polylines, or polygon-area features — see [Visualization data](/concepts/visualization/) | ## Suggested implementation order 1. [Getting started](/guides/getting-started/) and [Authentication](/guides/authentication/). 2. Generate a thin client from the OpenAPI spec, or use an [SDK](/sdks/). 3. Use the call from [Your first skin analysis](/guides/your-first-skin-analysis/) as a fixture in integration tests. 4. Map status codes to user-facing copy per [Error handling](/guides/error-handling/). 5. For each metric your UI surfaces, read the corresponding [concept page](/concepts/) for value semantics and rendering notes. ## SDK system prompts Each first-party SDK ships a short system-prompt-shaped doc designed to be loaded into an assistant's context window before generating integration code: - [TypeScript — `prompts/SYSTEM.md`](https://github.com/revieve/api.revieve.com/blob/master/sdks/typescript/prompts/SYSTEM.md) - [Python — `prompts/SYSTEM.md`](https://github.com/revieve/api.revieve.com/blob/master/sdks/python/prompts/SYSTEM.md) - [Swift — `prompts/SYSTEM.md`](https://github.com/revieve/api.revieve.com/blob/master/sdks/swift/prompts/SYSTEM.md) - [Kotlin — `prompts/SYSTEM.md`](https://github.com/revieve/api.revieve.com/blob/master/sdks/kotlin/prompts/SYSTEM.md) ## When the assistant goes off-spec If generated code references a component, status code, or response field that doesn't appear in the OpenAPI document at this site, it doesn't exist in the API — re-prompt with `/llms-full.txt` or the relevant page sidecar in context, or paste the OpenAPI spec back in. --- ## Playground Source: src/content/docs/playground.mdx > Try the Skin Analysis API directly from the browser. Upload a JPEG, pick components, see the live response, copy the equivalent code. import { Aside } from "@astrojs/starlight/components"; import PlaygroundClient from "../../components/PlaygroundClient.astro"; Run a real call against `partner-test.revieve.com/api/3/analyzeImage/` from the browser. The playground talks directly to the test environment — no proxy, no backend. The shared test Partner ID `jmlv6b2qtS` is prefilled and rate-limited; use your own for sustained testing. ## What this is for - **Trying components without writing code.** Tick the boxes, upload a selfie, see what comes back. - **Generating reproducible calls.** "Show as code" panel emits the equivalent `curl`, JavaScript, or Python — paste into your client. - **Grounding AI integrators.** "Copy prompt" emits a self-contained prompt the user can hand to an LLM (Claude, ChatGPT, Cursor) along with the actual response. - **Spot-checking spec changes.** When a new component lands, this is the fastest way to see its real shape. ## Privacy Images are sent directly to `partner-test.revieve.com`. They are processed by Revieve's test inference and not retained beyond the request lifecycle. The page does not log to a third party. For sensitive imagery, use your own Partner ID and proxy through your backend instead. --- ## Virtual Try-On Source: src/content/docs/products/virtual-try-on.mdx > Revieve Virtual Try-On is a separate, real-time, webcam-based product for makeup, hair color, and look try-on — not part of the Skin Analysis API. import { Aside, LinkCard, CardGrid } from "@astrojs/starlight/components"; ## Overview Revieve's **Virtual Try-On (VTO)** is a separate product that lets shoppers try makeup, hair colour, and full looks in real time using their webcam — directly in the browser, on mobile and desktop. It is not part of the Skin Analysis API documented on this site. If you are integrating real-time try-on, **use VTO**. The Skin Analysis API documented here is for still-image analysis only — not the path for live AR. ## What VTO does, briefly - **Real-time, in-browser.** Webcam frames are processed by the VTO product directly in the browser; you don't post images to the CV API for try-on use cases. - **Multi-hundred-point face tracking.** Denser and finer-grained than what's needed for still-image analysis, optimised for live video. - **All makeup categories.** Face, lips, eyes, eyebrows. Single products, bundles, full-face looks. - **Inclusive by design.** Tracking optimised across ethnicities, demographics, ages, and genders. - **AR rendering with skin-tone and lighting adjustment.** True-to-product colour, intensity, and finish. - **Mobile and desktop.** Same product, different form factors. ## Why this matters for the CV API VTO and the CV API are complementary, not overlapping: | Use case | Use | | ------------------------------------------------------- | -------------- | | Real-time virtual try-on (lipstick, eyeshadow, looks) | **VTO** | | Hair-colour try-on | **VTO** / AI Hair Color Artist | | Diagnostic skin scoring (wrinkles, redness, age, …) | **Skin Analysis API** | | Personalised product recommendations | **Product Recommendations API** | | Foundation shade matching from a single still | Skin Analysis API (`makeup` component) | ## Why VTO runs on-device VTO processes tens of webcam frames per second to keep the AR overlay smooth. Round-tripping each frame to a server is impractical at that rate — the latency budget for a perceived-instant try-on is well under what a network hop allows, and bandwidth would balloon. So VTO does its face tracking and rendering locally in the browser. A useful side effect: webcam frames are not transmitted to Revieve during normal VTO operation. For deployments where this matters, that's a meaningful posture difference from the server-side Skin Analysis API (where a single still image *is* uploaded for analysis). Compliance and data-handling specifics for your deployment are governed by your Revieve agreement. ## Other Revieve consumer products VTO is one of several consumer-facing products built on the Revieve platform. Each has its own page on revieve.com; integration is sales-led: - **AI Skincare Advisor** — diagnostic skincare consultation built on the Skin Analysis API. - **AI Makeup Advisor** — makeup recommendations + try-on. - **AI Haircare Advisor** — diagnostic haircare consultation. - **AI Hair Color Artist** — virtual hair-colour try-on. These are products, not APIs. The Skin Analysis API and Product Recommendations API documented here are the underlying primitives. --- ## Getting started Source: src/content/docs/guides/getting-started.mdx > Make your first authenticated call to the Skin Analysis API in under five minutes using a JPEG and a Partner ID. import { Steps, Tabs, TabItem, Aside } from "@astrojs/starlight/components"; This guide gets you from zero to a successful round-trip against the test environment. It assumes nothing besides `curl` (or `fetch` / `requests`) and a frontal-face JPEG of at least 600×600 pixels. 1. **Pick or grab a test image.** Any frontal-face JPEG of at least 600×600 pixels works. Strong even lighting and a neutral background give the cleanest results. 2. **Send the image.** ```bash curl -X POST \ "https://partner-test.revieve.com/api/3/analyzeImage/?skintone=0&gender=unknown&components=wrinkles,redness,acne,skin_age" \ -F "partner_id=jmlv6b2qtS" \ -F "image=@./selfie.jpg" ``` ```js const form = new FormData(); form.append("partner_id", "jmlv6b2qtS"); form.append("image", fileInput.files[0]); const url = new URL("https://partner-test.revieve.com/api/3/analyzeImage/"); url.searchParams.set("skintone", "0"); url.searchParams.set("gender", "unknown"); url.searchParams.set("components", "wrinkles,redness,acne,skin_age"); const res = await fetch(url, { method: "POST", body: form }); const data = await res.json(); ``` ```python import requests with open("selfie.jpg", "rb") as f: res = requests.post( "https://partner-test.revieve.com/api/3/analyzeImage/", params={ "skintone": 0, "gender": "unknown", "components": "wrinkles,redness,acne,skin_age", }, data={"partner_id": "jmlv6b2qtS"}, files={"image": f}, ) data = res.json() ``` 3. **Read the top-level response.** ```json { "message": "OK", "face_area": { "bounding_box": [...], "rotation": -2.1 }, "status": [], "results": [ { "description": "wrinkles", "value": 0.34, "message": "Ok", "..." }, { "description": "redness", "value": 0.18, "message": "Ok", "..." } ] } ``` - `message: "OK"` confirms the call succeeded with no quality issues. - Each entry in `results[]` is one component you asked for. - `status[]` is empty here; if a code shows up, see [Interpreting results](/guides/interpreting-results/). 4. **Iterate on the `components` list.** Trim it to exactly what your UI uses. Fewer components → faster response, smaller payload. ## What's next - [Authentication](/guides/authentication/) — the Partner-ID model in detail. - [Your first skin analysis](/guides/your-first-skin-analysis/) — a longer walk-through that wires the response into a UI. - [Interpreting results](/guides/interpreting-results/) — how to render scores, masks, and overlays. - [API reference](/api/skin-analysis/) — every parameter, every response field. --- ## Authentication Source: src/content/docs/guides/authentication.mdx > Partner ID model, test vs production credentials, gateway enforcement, and where the credential rides on each request. import { Aside } from "@astrojs/starlight/components"; Revieve identifies callers by **Partner ID** — a short string issued per partner, per environment. There is no OAuth flow, no JWT, no session cookie, and no `Authorization` header. Authentication is a single multipart form field on every request. ## Where to put it The Partner ID rides as a form field in the multipart body of `POST /analyzeImage/`: ```http POST /api/3/analyzeImage/?skintone=0&gender=unknown HTTP/1.1 Content-Type: multipart/form-data; boundary=----X ----X Content-Disposition: form-data; name="partner_id" jmlv6b2qtS ----X Content-Disposition: form-data; name="image"; filename="selfie.jpg" Content-Type: image/jpeg ----X-- ``` That's it. Don't add `Authorization` headers; they aren't read. ## Test vs production | Environment | Partner ID | Base URL | | ----------- | -------------------------- | ----------------------------------------- | | Test | `jmlv6b2qtS` (shared demo) | `https://partner-test.revieve.com/api/3/` | | Production | Issued per partner | Region-specific, supplied at handoff | The test ID is rate-limited and shared. Treat it like any public demo key — fine for evaluation, never for real users. ## Storage and rotation Treat the Partner ID like a long-lived secret: - Never embed it in client-side bundles in production. Proxy through your own backend. - Don't commit production IDs to source control. Test ID is fine. - Rotate by request to your Revieve account contact. Old IDs continue to work until revoked. ## What the gateway enforces The CV service itself does not validate Partner ID — auth and rate-limiting live at the gateway. From the partner side this means: - **Quota** is per Partner ID, not per IP. - **Throttling** returns standard `429` responses with a `Retry-After` header. - **Region routing** is determined by the production base URL you receive — not by anything in the request. ## CORS and browsers The test environment permits browser-origin requests with `Access-Control-Allow-Origin: *`, which is convenient for prototyping but means you should still proxy through your own backend in production to avoid leaking the Partner ID into the client bundle. --- ## Your first skin analysis Source: src/content/docs/guides/your-first-skin-analysis.mdx > Ten-minute walk-through wiring a captured JPEG into the Skin Analysis API and rendering the response into a UI. import { Steps, Code, Aside } from "@astrojs/starlight/components"; This goes one step beyond [Getting started](/guides/getting-started/): capture an image in a browser, send it to the Skin Analysis API, and render the response into a small UI. The example uses plain HTML and `fetch` so it ports trivially into React, Vue, or any framework. About 60 lines of code, no build step. ## The full example ```html title="index.html" Revieve demo





```

## What it does, step by step



1. **The user picks a JPEG.** Anything ≥ 600×600 px works.

2. **We build the URL.** Required: `skintone` and `gender`. Optional: `components` (we ask for four). The full enum is in the [API reference](/api/skin-analysis/).

3. **We POST a multipart form.** The body has `partner_id` and `image`. Browsers set the `Content-Type` boundary automatically.

4. **We branch on the response shape.**
   - `data.message === "OK"` — clean run, render `data.results`.
   - `data.message === "Warning"` — usable results, optional UX nudge ("try better lighting").
   - `data.message === "Error"` — no results returned; surface `data.status[0]` to the user.

5. **We render `results`.** Each entry we asked for here has a `value` between 0 and 1 — multiply by 100 for percentage UI. Not every metric works this way: integer/categorical findings (e.g. `skin_age`, `skin_type`) leave `value` null and carry their result in `sum_measures[]` instead — see below.



## Where to go from here

- For richer rendering, the `measurement_locations[]` and `mask_shapes` fields drive overlay visualizations. See [Interpreting results](/guides/interpreting-results/).
- For integer and categorical findings (skin age, skin type, undertone, hair type, eye color), look at `sum_measures[]` rather than the top-level `value` — for these, `value` is null.
- For real-time AR try-on, use [Revieve VTO](/products/virtual-try-on/) — the Skin Analysis API is for still-image scoring only.

## Common quality issues

| Symptom                     | Likely cause                                  | Fix                                       |
| --------------------------- | --------------------------------------------- | ----------------------------------------- |
| `idx: 100` (input format)   | Sent PNG/HEIC, or file isn't readable.        | Convert to JPEG client-side.              |
| `idx: 101` (resolution)     | Image smaller than 600×600.                   | Recapture at higher resolution — don't upscale. |
| `idx: 301` (face too small) | Face narrower than 300 px in the frame.       | Prompt the user to move closer.           |
| `idx: 103/104` (lighting)   | Over- or under-exposed.                       | Reshoot in even lighting; warn the user.  |
| `idx: 201` (no face)        | Face is occluded or out of frame.             | Prompt the user to recentre.              |
| `idx: 308` (overexposure)   | Hot spot from direct flash.                   | Diffuse light or move away from window.   |

---
## Interpreting results
Source: src/content/docs/guides/interpreting-results.mdx
> How to read the Skin Analysis response — top-level fields, per-finding shape, masks, visualizations, and what to render.

import { Aside } from "@astrojs/starlight/components";

The Skin Analysis response is one JSON document with five top-level fields: `message`, `face_area`, `inverse_color_balance_rb`, `status[]`, and `results[]`. This page explains each and how to render it.



## The shape

```jsonc
{
  "message": "OK" | "Warning" | "Error",
  "face_area": {
    "bounding_box": [x_min, y_min, x_max, y_max],
    "rotation": -2.4
  },
  "inverse_color_balance_rb": [1.05, 0.98, 1.02],
  "status": [
    { "idx": 308, "description": "Image is overexposed at the face area. Find more diffuse lighting or reduce exposure.", "isError": 0 }
  ],
  "results": [
    {
      "description": "redness",
      "value": 0.23,
      "value_raw": 3.21,            // deprecated
      "message": "Ok",
      "local_warnings": [],
      "measurement_locations": [
        /* per-region scores; advanced visualization features live here,
           on each location's own visualization_data[] */
      ],
      "sum_measures": [/* derived/aggregate */],
      "visualization_data": null      // always null at finding level — see below
    }
  ]
}
```

## `message` — the global verdict

The first thing your UI should branch on:

- **`OK`** — clean run, results are reliable.
- **`Warning`** — results returned, but `status[]` flags one or more quality issues. Show the score, optionally nudge the user to retry for better fidelity.
- **`Error`** — no usable results. `status[]` explains why; show that and ask for a new image.

## `face_area` — geometry

`bounding_box` is `[x_min, y_min, x_max, y_max]` in pixels of the original image. `rotation` is degrees clockwise from vertical. Use them when overlaying landmarks or masks back onto the source frame.

## `status` — quality codes

Each entry has:

- **`idx`** — a numeric code (see [Status codes](/concepts/status-codes/) for the full reference).
- **`description`** — human-readable.
- **`isError`** — `1` blocks usable results, `0` is informational.

Common codes:

| `idx` | Meaning                                     | Treatment                          |
| ----- | ------------------------------------------- | ---------------------------------- |
| 100   | Image not in RGB color format               | Surface, ask for retry             |
| 101   | Image resolution too small                  | Surface, ask for retry             |
| 102   | Image aspect ratio too extreme              | Surface, ask for retry             |
| 103/104 | Image too dark / overexposed globally     | Optional nudge                     |
| 201   | No face detected                            | Surface, ask for retry             |
| 308   | Face area overexposed                       | Optional nudge                     |
| 401   | Skin masking failure — acne/hyperpigmentation results unreliable | Internal log; usable results |

The full list lives in the [API reference](/api/skin-analysis/).

## `results[]` — per-component findings

Each entry corresponds to one component you requested.

### `value` vs `value_raw`

`value` is normalized to `0..1` for nearly every metric — what you should display. `value_raw` is the pre-normalization value and is **deprecated**: ignore it for new integrations.

`skin_age` and `skin_type` are exceptions: they don't populate the top-level `value` at all — their integer result is carried in `sum_measures` (see below).

### `measurement_locations[]`

Where on the face the score was computed. Each entry includes:

- A `description` (`"left cheek"`, `"forehead"`, `"Global"`, …).
- A `value` for that region.
- AOI (area of interest) coordinates: `AOI_x_o`, `AOI_y_o`, `AOI_radius_o` (in original image pixels).
- `ut_zone` — `"u-zone"` | `"t-zone"` | `"none"` | `"mixed"`.
- Optional `mask_shapes` for visualization (polygons / ellipses).

### `sum_measures[]`

Aggregated or categorical sub-findings. Shape varies by metric:

- **Makeup** → face shape (1–6), eye color (1–3), skin undertone (−1 / 0 / +1), foundation RGB, Fitzpatrick type.
- **Hair** → color array, volume (0–1), type (0–4), frizziness (0–1).
- **Skin Age** → integer 0–100.
- **Skin Type** → integer 1–8 (normal, dry, oily, combination, sensitive variants).

### `visualization_data[]`

Optional per-metric advanced visualization — the third tier of [visualization data](/concepts/visualization/) (the first two being AOI circles and mask shapes). Returned only when the corresponding `_visualization` opt-in is in your `?components=...` query (e.g. `acne_normalized_visualization`, `wrinkles_visualization`, `redness_visualization`).

The field appears at two levels, and only one of them is ever populated: **read it from each `measurement_locations[]` entry.** The finding-level `visualization_data` is always `null` — don't look for features there.

```js
const features = (finding.measurement_locations ?? []).flatMap(
  (loc) => loc.visualization_data ?? []
);
```

Each feature is one of three implicit types — determined by the metric, not by a `kind` field:

- **Spot** — single point with intensity. Used by acne, freckles, dark spots, hyperpigmentation. `length: 1`, `x[0]` / `y[0]` is the centroid.
- **Polyline** — connected line of N points (one wrinkle, one fold). Used by wrinkles. `length: N`, `intensity` is an array of per-point intensities; `length_weighted_intensity` is the canonical "how bad is this wrinkle" composite.
- **Polygon area** — enclosed region whose `intensity` is a level index (`1`, `2`, `3`, …), not a normalized score. Used by redness, melasma, shine, skin type, texture, uneven skin tone, pore dilation. For the threshold-based metrics higher levels nest inside lower ones; `shine` and `skin_type` draw their levels from separate source fields and don't nest. `skin_sagging` also uses polygons but carries a continuous intensity instead of levels. See [Visualization data](/concepts/visualization/#intensity-ranges) for per-metric ranges.

All coordinates are in original-image pixel space. For paste-ready render code, see [Rendering recipes](/guides/rendering-recipes/).

## `inverse_color_balance_rb`

A 3-element array — **not** `[r, g, b]`. The elements are:

| Index | Meaning |
| ----- | ------- |
| `[0]` | inverse **red** white-balance multiplier (`1 / red_correction`) |
| `[1]` | inverse **blue** white-balance multiplier (`1 / blue_correction`) |
| `[2]` | inverse **luminance** factor (`1 / luminance_correction`) |

Green is the reference channel (multiplier `1`), so it has no entry — hence the `rb` ("red/blue") in the name, plus the luminance term. When white balance can't be estimated the array is `[1, 1, 1]` (a no-op).

### What it's for

The analysis runs in a **white-balanced ("true colour") space** — the engine internally removes the image's colour cast before measuring. These factors are the **inverse** of that correction, so they let you take a true-colour RGB value and **re-apply the original image's colour cast**. The point is efficiency: instead of colour-correcting the whole user photo (expensive, and a global approximation can make the photo itself look off), you leave the photo untouched and only re-bias the handful of colours you draw on top of it, so they sit naturally on the original image.

So use it when you composite a **CV-measured colour** (e.g. a foundation/skin-tone swatch the API returned in true-colour space) onto the **original, uncorrected photo**. Apply per channel:

```js
const [iR, iB, iL] = response.inverse_color_balance_rb;

// trueColor = [R, G, B] in 0..255 (a colour the API measured in true-colour space)
function toImageCast([R, G, B]) {
  return [
    R * iR * iL,   // red channel: inverse red WB × luminance
    G *      iL,   // green channel: luminance only (green is the reference)
    B * iB * iL,   // blue channel: inverse blue WB × luminance
  ];
}
```

The result is how that colour appears under the photo's own lighting/cast, so it blends with the un-corrected image.





## A minimal renderer

```js
function summarize(response) {
  if (response.message === "Error") {
    return { ok: false, reason: response.status?.[0]?.description };
  }

  const scores = Object.fromEntries(
    response.results
      .filter((r) => typeof r.value === "number")
      .map((r) => [r.description, Math.round(r.value * 100)])
  );

  return { ok: true, scores, warnings: response.status ?? [] };
}
```

That's enough for a percentage-bar dashboard. Mask overlays and AR rendering are documented in their respective concept pages.



---
## Error handling
Source: src/content/docs/guides/error-handling.mdx
> HTTP- and document-level error semantics, retry strategy, and a defensive client implementation for the Skin Analysis API.

import { Aside } from "@astrojs/starlight/components";

The Skin Analysis API has two layers of failure: HTTP-level (transport, auth, rate-limiting) and document-level (image quality, per-metric model confidence). Each calls for different handling.



## HTTP-level

| Status | Cause                                    | What to do                                              |
| ------ | ---------------------------------------- | ------------------------------------------------------- |
| 200    | Success — but check `data.message`       | Branch on `message`, then `status[]`.                   |
| 400    | Malformed request                        | Fix the call. Almost always a missing `image` field.    |
| 413    | Image too large                          | Resize client-side; aim for < 5 MB.                     |
| 429    | Rate limited                             | Honor `Retry-After`. Back off exponentially.            |
| 5xx    | Transient infra issue                    | Retry up to 2 times with jitter.                        |

## Document-level

A `200 OK` HTTP response can still report problems via `data.message` and `data.status[]`. The handling matrix:

| `message` | Meaning                                      | UX                                                        |
| --------- | -------------------------------------------- | --------------------------------------------------------- |
| `OK`      | Clean run                                    | Render results.                                           |
| `Warning` | Results usable, quality issue noted          | Render results. Optionally surface advice from `status[]`. |
| `Error`   | No usable results                            | Hide result UI. Surface the first `status[].description`. |

Per-finding `local_warnings[]` arrays apply at the metric level — e.g. pore dilation may fail because face resolution wasn't sufficient even when the overall call succeeded. Hide that one card, keep the rest.

## A defensive client

```ts
async function analyze(file: File) {
  const url = new URL("https://partner-test.revieve.com/api/3/analyzeImage/");
  url.searchParams.set("skintone", "0");
  url.searchParams.set("gender", "unknown");

  const form = new FormData();
  form.append("partner_id", PARTNER_ID);
  form.append("image", file);

  for (let attempt = 0; attempt < 3; attempt++) {
    const res = await fetch(url, { method: "POST", body: form });

    if (res.status === 429) {
      const wait = Number(res.headers.get("Retry-After") ?? 1) * 1000;
      await new Promise((r) => setTimeout(r, wait));
      continue;
    }

    if (res.status >= 500 && res.status < 600) {
      await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
      continue;
    }

    if (!res.ok) {
      throw new ApiError(`HTTP ${res.status}`, res.status);
    }

    const data = await res.json();

    if (data.message === "Error") {
      throw new AnalysisError(
        data.status?.[0]?.description ?? "Analysis failed",
        data.status?.[0]?.idx,
      );
    }

    return data;
  }

  throw new Error("Exhausted retries");
}
```

## Surface to users, log to ops

The `status[]` array is partner-facing copy. The model-internal codes (`local_warnings[]` per finding, raw `idx` numbers) are useful for your observability stack but not for end-user copy. Translate before rendering.

A copy library that maps `idx` → friendly message keeps your UX consistent and translatable; treat the descriptions in the spec as a starting point, not a final UI string.

---
## MCP integration
Source: src/content/docs/guides/mcp-integration.mdx
> Connect Claude Desktop, Continue, Cursor, or any MCP-compatible client to the Revieve docs as a server, plus the API itself via the OpenAPI spec.

import { Tabs, TabItem, Aside } from "@astrojs/starlight/components";

## Overview

The [Model Context Protocol](https://modelcontextprotocol.io) (MCP) lets AI assistants and IDEs pull live context from external sources. Two ways to wire Revieve in:

1. **Docs server** — point an off-the-shelf docs MCP server at `https://api.revieve.com`. The assistant can then ground answers in the canonical docs without any custom code.
2. **API server** — generate an MCP server from the OpenAPI spec so the assistant can *call* the API itself, not just read about it.

Both are config-only; you don't need to write code to set either up.

## Docs server (read-only)


  
    Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):

    ```json
    {
      "mcpServers": {
        "revieve-docs": {
          "command": "npx",
          "args": [
            "-y",
            "@modelcontextprotocol/server-fetch",
            "https://api.revieve.com/llms-full.txt"
          ]
        }
      }
    }
    ```

    Restart Claude Desktop. Claude will treat the docs as a queryable context source.
  
  
    In `~/.continue/config.json`:

    ```json
    {
      "contextProviders": [
        {
          "name": "url",
          "params": {
            "url": "https://api.revieve.com/llms-full.txt"
          }
        }
      ]
    }
    ```
  
  
    Cursor reads docs from any URL via the `@docs` provider. Add `https://api.revieve.com/llms-full.txt` in **Settings → Features → Docs**.
  


## API server (read + call)

Generate an MCP server from the OpenAPI spec and wire it into the same client config. Several open-source generators do this; one example using `@modelcontextprotocol/openapi-mcp-server`:

```json
{
  "mcpServers": {
    "revieve-api": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/openapi-mcp-server",
        "--spec",
        "https://api.revieve.com/openapi/skin-analysis.yaml",
        "--base-url",
        "https://partner-test.revieve.com/api/3"
      ],
      "env": {
        "REVIEVE_PARTNER_ID": "jmlv6b2qtS"
      }
    }
  }
}
```

The MCP server exposes each OpenAPI operation as an MCP tool the assistant can call directly — no SDK install required.



## What you get

- **Grounded answers.** The assistant cites specific doc pages when explaining components.
- **Up-to-date code.** The assistant generates calls against the current spec (3.29.1), not 2023's pattern-matched suggestions.
- **Live API exploration.** With the API server, the assistant can run a real `analyzeImage` call against the test environment as part of an answer.

## Discovery shortcuts

- `https://api.revieve.com/.well-known/openapi.yaml` redirects to the canonical spec.
- `https://api.revieve.com/.well-known/ai-plugin.json` is a manifest pointing to the spec.
- `https://api.revieve.com/llms.txt` is the page index in [llmstxt.org](https://llmstxt.org) format.
- `https://api.revieve.com/llms-full.txt` is every page concatenated.
- Every doc page has a markdown sidecar at `.md` (for example, `/guides/getting-started.md`).

---
## Concepts
Source: src/content/docs/concepts/index.mdx
> What each skin, makeup, hair, and AR metric actually measures, what the API returns, and how to render it.

import { LinkCard, CardGrid } from "@astrojs/starlight/components";

This section is the *visual* counterpart to the OpenAPI reference. The reference tells you the field names; concept pages tell you what those fields *mean* — clinically, visually, and as data on a face.

Every concept page follows the same shape:

1. **Overview** — definition in beauty / dermatology terms.
2. **Machine-readable summary** — one fenced YAML block with request, response path, value range, related components.
3. **Reference imagery** — real face with the metric's mask or feature overlay applied, where imagery is available.
4. **Rendering notes** — how to turn data into a UI affordance.
5. **Edge cases and `local_warnings[]`** — known footguns and the metric-specific status codes.

## Start with


  


## Skin


  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  


## Composite findings


  
  


## Visualization & AR plumbing


  
  


## Reference


  


---
## Wrinkles
Source: src/content/docs/concepts/wrinkles.mdx
> Per-region wrinkle severity (0..1) returned as polyline features — point-by-point coordinates per detected line — with a length × intensity composite for ranking.

import ConceptOverlay from "../../../components/ConceptOverlay.astro";

## Overview

Wrinkles measures the visible depth, count, and distribution of lines on the face. The `value` is a normalized severity score; `wrinkles_visualization` returns one **polyline feature per detected line** — `x[]` / `y[]` arrays of point coordinates that draw the actual line back onto the source image, plus per-point and aggregate intensity values.

```yaml
component: wrinkles
request:
  query: "components=wrinkles,wrinkles_visualization"
response_path: "results[].description == \"wrinkles\""
value_range: [0, 1]
also_request:
  - wrinkles_visualization      # polyline features, one per detected line
warnings: []                    # no wrinkles-specific codes; see local_warnings below
```

## Sub-region locations

Within the single `wrinkles` finding, `measurement_locations[]` carries one entry per face sub-region (**12 in total**) — partners see these descriptions verbatim and can style different sub-regions differently:

| `measurement_locations[].description` | Region |
|---|---|
| `forehead worry lines` | Horizontal lines across the forehead. |
| `between-brows frown lines` | Glabellar / "11" lines. |
| `noseline bunny lines` | Wrinkles across the nose bridge. |
| `left side eye crow's feet` / `right side eye crow's feet` | Lines radiating from the outer corner of the eye. |
| `left side tearline` / `right side tearline` | Lines below the inner corner of the eye. |
| `left side nasolabial line` / `right side nasolabial line` | Nasolabial folds. |
| `left side marionette line` / `right side marionette line` | Marionette lines from mouth corners. |
| `vertical lip lines` | Lines around the lips. |

These are descriptions on the same finding, **not** separate top-level metrics — there's no `nasolabial_folds` or `crows_feet` component to request.

Each entry also carries its own per-region score — `value` (0..1 severity for that sub-region), a deprecated `value_raw`, and a `Wrinkle intensity` sub-measure in `measures[]` — so you can surface region-level scores, not just the aggregate `value` on the finding.

## Rendering notes

For a numeric UI:

```js
const w = response.results.find((r) => r.description === "wrinkles");
const score = Math.round(w.value * 100);
```

For drawing the lines themselves, request `wrinkles_visualization` and iterate. Each `visualization_data[]` feature is a **polyline** — `x[]` and `y[]` are arrays of length N covering the points along one detected line:

```js
for (const loc of w.measurement_locations ?? []) {
  // The location's description (e.g. "left side nasolabial line")
  // tells you which face region this group of lines belongs to —
  // useful for region-specific styling.
  for (const line of loc.visualization_data ?? []) {
    // line.x and line.y are length-N arrays
    // line.intensity is an array of per-point intensities
    // line.length_weighted_intensity is a composite ranking value
    // line.length is the point count; line.mean_intensity / line.median_intensity are summary stats
    drawPolyline(line.x, line.y);
  }
}
```

Three details that matter for polylines:

1. **Decimate dense lines.** A wrinkle feature can carry hundreds of points. Keep every Nth point above a threshold — every 3rd above 20 is a workable default. Visually identical, much cheaper.
2. **`length_weighted_intensity`** is the canonical "how bad is this wrinkle" composite — combines line length with per-point intensity. Sort features descending by it to surface the worst N findings.
3. **Stroke style.** A 1–2 px stroke at low opacity reads better than a hard line.

Paste-ready code: [Rendering recipes → Polylines](/guides/rendering-recipes/#recipe-2--polylines).

## Edge cases

- **Male users.** Beard-area sub-regions return `value: null` (with null `visualization_data` and a null `Wrinkle intensity`) — beard area of skin isn't analysed. Skip null entries when iterating `measurement_locations[]`.
- **Smiling expressions** dynamically increase apparent wrinkle count. Capture in a neutral expression for trend tracking.
- **Heavy makeup** reduces wrinkle visibility. The API does not detect whether makeup is worn, so show a standing "based on visible skin" disclaimer rather than conditioning it on a makeup signal.
- **Low resolution** reduces line-finding precision. `wrinkles` adds no threshold of its own beyond the API-wide resolution floors — see [Image quality → Resolution](/guides/image-quality/#resolution).

## `local_warnings[]`

The `wrinkles` metric emits no metric-specific `local_warnings[]` codes. It inherits the global image-quality codes (100–308); a skin-masking failure upstream (`401`, a shared code) can also make results unreliable. See [Status codes](/concepts/status-codes/).

## Related

- **[Visualization data](/concepts/visualization/)** — Cat D (AOI + mask + advanced); polyline-type features with per-point intensities and the `length_weighted_intensity` composite.
- **[Rendering recipes → Polylines](/guides/rendering-recipes/#recipe-2--polylines)** — paste-ready renderer with the dense-point decimation pattern.
- **[Sub-area divisions](/concepts/sub-areas/)** — wrinkles uses a custom division (the sub-regions listed above), not the standard primary or alternative face splits.

---
## Redness
Source: src/content/docs/concepts/redness.mdx
> Erythema scoring (0..1) per face region, with mask shapes and a polygon-area visualization channel for AR rendering.

import ConceptOverlay from "../../../components/ConceptOverlay.astro";
import { Aside } from "@astrojs/starlight/components";

## Overview

Redness metric responds to variations of the amount of red color (caused by e.g. local inflammation) in the skin disregarding the base skin tone.

```yaml
component: redness
request:
  query: "components=redness"
response_path: "results[].description == \"redness\""
value_range: [0, 1]              # normalized severity
also_request:
  - redness_visualization        # polygon-area features, 3 intensity levels by default
  - redness_absolute             # tone-adjusted variant for cross-user comparison
warnings: []                   # redness emits no metric-specific codes
```

## What the API returns

Request the component:

```http
?components=redness
```

The corresponding entry in `results[]`:

```jsonc
{
  "description": "redness",
  "value": 0.18,                  // 0..1, normalized severity
  "message": "Ok",
  "local_warnings": [],
  "measurement_locations": [
    { "description": "left cheek",  "value": 0.21, "AOI_x_o": 412, "AOI_y_o": 580, "AOI_radius_o": 90, "ut_zone": "u-zone" },
    { "description": "right cheek", "value": 0.19, "..." },
    { "description": "forehead",    "value": 0.11, "..." }
    // …11 regions in total (cheeks, forehead, nose, chin, around-mouth, around/under-eye).
    // A region returns value -1 when its mask is unavailable.
    // The overall score is the top-level `value` — there is no "Global" region entry.
  ],
  "sum_measures": []
}
```

If you also request `redness_visualization`, you get a `visualization_data[]` array of **polygon-area features** with 3 intensity levels by default (per-feature `intensity` is a category integer `1..3`, not a continuous score). Higher-intensity polygons sit *inside* lower-intensity ones — render with low opacity and the layering produces a heatmap-like gradient. See [Rendering recipes → Polygon areas](/guides/rendering-recipes/#recipe-3--polygon-areas).

For a tone-adjusted variant intended for comparison across users, request `redness_absolute` instead.

## Reference imagery



## Rendering notes

For a numeric UI:

```js
const redness = response.results.find((r) => r.description === "redness");
const score = Math.round(redness.value * 100);   // 0..100
```

For a regional breakdown:

```js
const regions = redness.measurement_locations
  .filter((m) => m.value >= 0)              // skip unavailable regions (value -1)
  .sort((a, b) => b.value - a.value);
```

For a heatmap-style overlay:

1. Request `redness_visualization` alongside `redness`.
2. Sort `visualization_data[]` ascending by `intensity` (categorical 1..3).
3. For each feature, fill the polygon defined by its `x[]` / `y[]` arrays with a warm color, opacity ≈ 0.45.
4. Higher-intensity polygons naturally render on top, producing a layered gradient.
5. The polygon colours here are ones you choose, so colour balance doesn't apply to them. Only if you also overlay a CV-measured colour (e.g. a skin/foundation swatch) do you re-bias it with [`inverse_color_balance_rb`](/guides/interpreting-results/#inverse_color_balance_rb) so it matches the original photo.

Paste-ready code: [Rendering recipes → Polygon areas](/guides/rendering-recipes/#recipe-3--polygon-areas).

## Edge cases

- **Heavy makeup** can mask redness, so the score may understate true erythema when coverage products are worn. The API has no makeup-coverage signal to gate on, so surface a "based on visible skin" disclaimer rather than trying to auto-hide.

## `local_warnings[]` for this metric

The `redness` metric emits no metric-specific `local_warnings[]` codes; it inherits the global image-quality codes. When a region's mask can't be generated, that region simply returns `value: -1` (no status code). See [Error handling](/guides/error-handling/).

---
## Hyperpigmentation
Source: src/content/docs/concepts/hyperpigmentation.mdx
> Legacy aggregate pigmentation score (0..1) — the larger of a diffuse-area and a dark-spot sub-score — kept for backwards compatibility; prefer the standalone melasma, freckles, and dark_spots metrics. Includes an adjusted variant and a per-spot visualization channel.

import ConceptOverlay from "../../../components/ConceptOverlay.astro";
import { Aside } from "@astrojs/starlight/components";

## Overview



Hyperpigmentation measures areas of skin that are darker than the surrounding tissue — sun damage, post-inflammatory marks, melasma overlap, age spots. The `value` is the **larger** of two sub-scores — a diffuse `area` component and a discrete dark-`spot` component (both returned in `sum_measures`) — so it reflects the worse of the two, not their sum. The base `hyperpigmentation` score is normalized within the user's own skin tone; `hyperpigmentation_adjusted` rescales for comparability across skin tones. The visualization channel returns **spot-type features** — one feature per detected pigmented spot, suitable for marker overlays.

```yaml
component: hyperpigmentation
request:
  query: "components=hyperpigmentation,hyperpigmentation_adjusted,hyperpigmentation_visualization"
response_path: "results[].description == \"hyperpigmentation\""
value_range: [0, 1]
also_request:
  - hyperpigmentation_adjusted       # cross-tone-comparable variant
  - hyperpigmentation_visualization  # spot-type features, intensity range [0..400]
warnings: [401]
```

## Reference imagery



## When to use which variant

For new integrations, prefer the standalone `melasma` / `freckles` / `dark_spots` metrics above. If you're maintaining an existing `hyperpigmentation` integration:

| Goal                                                        | Use                           |
| ----------------------------------------------------------- | ----------------------------- |
| User-relative dashboard ("your level vs. last month")       | `hyperpigmentation`           |
| Cross-user comparisons / cohort analytics                   | `hyperpigmentation_adjusted`  |
| Per-spot marker rendering on the face                       | `hyperpigmentation_visualization` |

## Rendering notes

```js
const hp = response.results.find((r) => r.description === "hyperpigmentation");
const score = Math.round(hp.value * 100);
```

Beyond the top-level `value`, the finding exposes two `sum_measures` — `area` (diffuse pigmentation) and `spot` (discrete dark spots), each `0..1`. Each `measurement_locations[]` entry carries the same `area` / `spot` measures plus its own `value` and `mask_shapes`.

When a region's mask can't be sampled, that region signals unavailability in two different ways — guard for both:

- its `area` and `spot` **measures** come back as **`-1`**;
- the region's own **`value`** comes back as **`null`**, not `-1`.

Either way it means "region unavailable," not a real low score. (The standalone [`melasma`](/concepts/melasma/), [`freckles`](/concepts/freckles/) and [`dark_spots`](/concepts/dark-spots/) metrics are simpler here — their region `value` is `-1` in the same situation.)

For per-spot markers, iterate each measurement location's `visualization_data[]` (each feature is a single spot — `length: 1`, `x[0]` / `y[0]` is the centroid, `intensity` is the feature's severity in `[0..400]`). The marker colours are ones you choose, so colour balance doesn't apply to them; only re-bias a CV-measured colour with [`inverse_color_balance_rb`](/guides/interpreting-results/#inverse_color_balance_rb) if you overlay one. Paste-ready: [Rendering recipes → Spots](/guides/rendering-recipes/#recipe-1--spots).

## `local_warnings[]`

Inherits global image-quality codes. Region-mask failures surface as `idx: 401`.

## Related

- **Recommended instead:** [`melasma`](/concepts/melasma/), [`freckles`](/concepts/freckles/), [`dark_spots`](/concepts/dark-spots/) — the standalone pigmentation metrics this legacy aggregate is built from.
- **[How metrics connect](/concepts/metric-relationships/)** — the pigmentation family (`hyperpigmentation`, `dark_spots`, `freckles`, `melasma`) shares one analysis; `hyperpigmentation_adjusted` combines `shine` + `dark_spots` + `melasma`.
- **[Visualization data](/concepts/visualization/)** — Cat D (AOI + mask + advanced); spot-type features, intensity range `[0..400]`.
- **[Rendering recipes → Spots](/guides/rendering-recipes/#recipe-1--spots)** — paste-ready dot / box / cross renderers.

---
## Acne
Source: src/content/docs/concepts/acne.mdx
> Per-lesion detection and severity (0..1) with normalized and legacy visualization scales.

import ConceptOverlay from "../../../components/ConceptOverlay.astro";
import { Aside } from "@astrojs/starlight/components";

## Overview

Acne measures the presence and severity of inflammatory and non-inflammatory lesions — papules, pustules, comedones — across the face. The `value` is a normalized score; `acne_visualization` returns per-lesion intensities suitable for marker overlays.

```yaml
component: acne
request:
  query: "components=acne,acne_normalized_visualization"
response_path: "results[].description == \"acne\""
value_range: [0, 1]
also_request:
  - acne_normalized_visualization   # preferred for new code
  - acne_visualization              # legacy intensity scale
warnings: [401, 3000]
```



## Reference imagery



## Rendering notes

```js
const acne = response.results.find((r) => r.description === "acne");
const score = Math.round(acne.value * 100);

// Per-lesion markers — visualization_data lives on each measurement_locations[]
// entry, not on the finding directly. Each feature is a "spot" type:
// length-1 x[] and y[] arrays, intensity is a single number.
const lesions = (acne.measurement_locations ?? []).flatMap(
  (loc) => loc.visualization_data ?? []
);
```

Spot-type features render naturally as a dot marker. A common style is **two concentric circles** (a 6 px filled disc inside a 12 px stroke ring); square outlines or crosses also work. The response also carries a `Spots number` sum-measure (raw count of detected spots), but avoid surfacing it as a clinical "active acne" count — the `value` is a severity score, not a lesion count, and the count itself is sensitive to image quality.

Paste-ready code: [Rendering recipes → Spots](/guides/rendering-recipes/#recipe-1--spots).

## Edge cases

- **`acne` aborts on some global image-quality warnings.** Unlike other metrics, it will not return a score when the image carries `204`, `312`, `313`, `314` or `319` — see the `local_warnings[]` section below.

## `local_warnings[]`

`idx: 3000` (error) — acne measurement aborted due to image quality concerns.

`acne` is stricter than the other metrics about image quality. Five codes that are only *warnings* at the response level cause acne to abort: `204` (face detection very uncertain), `312` (unsymmetric exposure), `313` (hard shadows), `314` (eyeglasses detected) and `319` (face too tightly framed). When one of them is present:

- acne returns no score — `value` is `null` and `message` is `"Error"`;
- `3000` is added to the finding's `local_warnings[]`;
- the triggering code is **also** copied into `local_warnings[]` with its `isError` raised to `1`, even though the same code appears in the top-level `status[]` as a warning (`isError: 0`).

So don't be surprised to see the same `idx` with a different `isError` in the two places — inside the acne finding it means "this blocked acne", at the top level it means "this is a quality warning for the image". Every other requested metric still returns normally.

## Related

- **[Visualization data](/concepts/visualization/)** — Cat D (AOI + mask + advanced); spot-type features. Two intensity scales: `[0..1]` normalized and `[0..2500]` legacy.
- **[Rendering recipes → Spots](/guides/rendering-recipes/#recipe-1--spots)** — paste-ready dot / box / cross renderers.

---
## Texture
Source: src/content/docs/concepts/texture.mdx
> Surface roughness (texture, 0..1) with a polygon-area visualization channel.

import ConceptOverlay from "../../../components/ConceptOverlay.astro";
import { Aside } from "@astrojs/starlight/components";

## Overview

Texture measures small-scale surface roughness — fine lines, rough patches, tactile irregularity. It is an independent surface-roughness metric computed from its own analysis.

```yaml
component: texture
request:
  query: "components=texture,texture_visualization"
response_path: "results[].description == \"texture\""
value_range: [0, 1]
also_request:
  - texture_visualization        # polygon-area features, 2 intensity levels by default
warnings: []                   # texture emits no metric-specific codes
```

## Reference imagery



## Rendering notes

```js
const tex = response.results.find((r) => r.description === "texture");
```

For a "what's wrong with my skin" diagnostic, surface `texture` directly. For an improvement-over-time, "good number goes up" UI, use `smoothness` — a separate metric (the inverse of `uneven_skin_tone`, not of texture) documented on [Uneven skin tone and smoothness](/concepts/uneven-skin-tone/).

Beyond the top-level `value`, `texture` returns a per-region breakdown in `measurement_locations[]` — one entry per face region, each with its own `value` (`0..1`), `mask_shapes`, and polygon-area `visualization_data`. A region scores **`-1`** when its mask couldn't be sampled — treat that as "region unavailable," not a real low score.

## Edge cases

- Recommend high-quality capture since texture analysis relies on high image resolution and low amount of filtering.

## `local_warnings[]`

The `texture` metric emits no metric-specific `local_warnings[]` codes; it inherits the global image-quality codes. An unsamplable region returns `value: -1` (no status code).

## Related

- **[How metrics connect](/concepts/metric-relationships/)** — `texture` is standalone; `smoothness` (a separate metric) is the inverse of `uneven_skin_tone`.
- **[Visualization data](/concepts/visualization/)** — Cat D (AOI + mask + advanced); 2 intensity levels by default.
- **[Rendering recipes → Polygon areas](/guides/rendering-recipes/#recipe-3--polygon-areas)** — paste-ready renderer.



---
## Shine
Source: src/content/docs/concepts/shine.mdx
> Surface shine — combined diffuse sheen and specular highlights — per zone (0..1), with a polygon-area visualization channel for oiliness and glossiness UX.

import ConceptOverlay from "../../../components/ConceptOverlay.astro";

## Overview

Shine measures visible surface shine on the face — the glossiness from sebum, sweat, or product. It reflects both **diffuse sheen** and **specular highlights**, which are also reported separately in `sum_measures`. Higher values typically indicate the T-zone (forehead, nose), but partner UIs often surface per-zone values to drive routine recommendations.

```yaml
component: shine
request:
  query: "components=shine,shine_visualization"
response_path: "results[].description == \"Skin shine\""   # NOTE: request component is `shine`, response description is `Skin shine`
value_range: [0, 1]
also_request:
  - shine_visualization        # polygon-area features, 4 categories: low-diffuse, high-diffuse, low-specular, high-specular
warnings: []                 # shine emits no metric-specific codes
```

## Reference imagery



## Rendering notes

```js
// Request component is `shine`, but the response finding's description is "Skin shine".
const shine = response.results.find((r) => r.description === "Skin shine");

// Per-zone entries are "t-zone" and "u-zone". The "Global" entry has value: null —
// it only carries whole-face visualization data. The whole-face score is shine.value.
const byZone = Object.fromEntries(
  shine.measurement_locations
    .filter((m) => m.description !== "Global")
    .map((m) => [m.description, m.value]),
);
```

Beyond the top-level `value`, the finding exposes three `sum_measures` — `Diffuse shine`, `Specular shine`, and `Specular shine intensity` — so you can separate matte sheen from sharp specular hotspots when driving recommendations.

The `ut_zone` field on each measurement location distinguishes `t-zone` from `u-zone` — useful for routine personalization (oily T-zone vs. dry U-zone).

## Edge cases

- **Direct flash** dramatically inflates shine scores. Diffuse light is critical.
- **Sweat or recently-applied product** reads as shine.

## `local_warnings[]`

The shine metric emits no metric-specific `local_warnings[]` codes; it inherits the global image-quality codes.

## Related

- **[Visualization data](/concepts/visualization/)** — Cat D (AOI + mask + advanced); 4 fixed categories with special meanings: low/high diffuse shine, low/high specular shine.
- **[Rendering recipes → Polygon areas](/guides/rendering-recipes/#recipe-3--polygon-areas)** — paste-ready renderer.

---
## Pore dilation
Source: src/content/docs/concepts/pore-dilation.mdx
> Visible pore size and density (0..1) with a polygon-area visualization. Requires sufficient face area resolution.

import ConceptOverlay from "../../../components/ConceptOverlay.astro";
import { Aside } from "@astrojs/starlight/components";

## Overview

Pore dilation measures the visible size and density of skin pores — a function of sebum production, age, sun damage, and skin type. The metric is highly sensitive to image resolution; the model returns a status warning when the face area is too small to resolve individual pores reliably.

```yaml
component: pore_dilation
request:
  query: "components=pore_dilation,pore_dilation_visualization"
response_path: "results[].description == \"pore_dilation\""
value_range: [0, 1]                # value is null when analysis is aborted (see below)
also_request:
  - pore_dilation_visualization
warnings: [2000, 2001]
face_width_px:
  minimum: 600                     # below this -> 2000 (error), analysis aborted, value null
  full_quality: 1200               # 600-1199 -> 2001 (warning); >=1200 -> no warning
```



## Reference imagery



## Rendering notes

```js
const pores = response.results.find((r) => r.description === "pore_dilation");

// The finding is present whenever requested, but `value` is null when the
// analysis was aborted (face under 600 px). Guard on the value, not the finding.
if (!pores || pores.value == null) {
  // hide the pores card; surface the resolution warning (2000 / 2001) if present
}
```

The visualization channel returns polygon-area features with **2 intensity levels by default** (the count follows your `visualization_thresholds`; default `[0.0, 0.5]`). Render them like any polygon-area metric — draw each feature's coordinate path, low intensity to high. Paste-ready code: [Rendering recipes → Polygon areas](/guides/rendering-recipes/#recipe-3--polygon-areas).

### Regional breakdown

`measurement_locations[]` carries one entry per face zone — `left cheek`, `right cheek`, `forehead left`, `forehead center`, `forehead right`, `nose`, `left lower face`, `right lower face`, `chin` — each with its own `value`, AOI, and mask shapes. Note the global `value` is the **average of only four zones** (`left cheek`, `right cheek`, `forehead left`, `forehead right`); `nose`, `chin`, the lower-face zones, and `forehead center` are still returned but do **not** feed the headline score.

On faces analysed as male, the three lower-face zones — `left lower face`, `right lower face` and `chin` — are skipped and return **`value: null`**, since beard area can't be measured reliably. The entries are still present, so guard per-region as well as at finding level. Note this is `null`, not the `-1` that the pigmentation metrics use for an unavailable region.

## Edge cases

- **Face width under 600 px** → status `2000`, analysis aborted, `value: null`; treat as "data unavailable" rather than zero.
- **Face width 600–1199 px** → status `2001`; scores are returned but flagged unreliable.
- **Smoothing / beauty filters applied client-side** remove the fine detail the metric measures, so scores drop toward zero regardless of the user's actual pores.
- **Heavy foundation** fills and conceals pores, lowering the score.

## `local_warnings[]`

- `2000` (error) — face area resolution too low (face width under 600 px); pore analysis aborted, `value` is `null`.
- `2001` (warning) — face area resolution low (face width 600–1199 px); results returned via an upsizing fallback and may be unreliable.

These are the only codes the metric emits. (The canonical `2000` text reads "…at minimum 1200 pixels wide," which the spec marks as outdated — the effective floor is 600 px.)

## Related

- **[Visualization data](/concepts/visualization/)** — Cat D (AOI + mask + advanced); 2 intensity levels by default.
- **[Rendering recipes → Polygon areas](/guides/rendering-recipes/#recipe-3--polygon-areas)** — paste-ready polygon-area renderer.

---
## Melasma
Source: src/content/docs/concepts/melasma.mdx
> Detection of patchy, hormonally- and UV-driven hyperpigmentation — larger diffuse patches, distinct from discrete dark spots. 0..1 normalized score with mask overlays.

import ConceptOverlay from "../../../components/ConceptOverlay.astro";

## Overview

Melasma is a specific type of hyperpigmentation that presents as larger, irregular patches — typically on cheeks, forehead, and upper lip.

```yaml
component: melasma
request:
  query: "components=melasma,melasma_visualization"
response_path: "results[].description == \"melasma\""
value_range: [0, 1]
also_request:
  - melasma_visualization     # polygon-area features, 3 intensity levels by default
warnings: []                  # melasma emits no metric-specific codes
```

## Reference imagery



## Rendering notes

```js
const m = response.results.find((r) => r.description === "melasma");
```

When both `hyperpigmentation` and `melasma` register, surface them as separate findings — they trigger different product categories (sunscreen + tranexamic acid for melasma vs. vitamin C / retinoid for general pigment).

Melasma also returns a per-region breakdown in `measurement_locations[]` — one entry per face region, each with its own `value` (`0..1`), `mask_shapes`, and (when `melasma_visualization` is requested) polygon-area `visualization_data`. A region scores **`-1`** when its mask couldn't be sampled — treat that as "region unavailable," not a real low score.

## `local_warnings[]`

The `melasma` metric emits no metric-specific `local_warnings[]` codes. It inherits global image-quality codes; a skin-mask sampling failure is reported as `401` on the `hyperpigmentation` finding (which shares the same analysis), not on `melasma`.

## Related

- **[Visualization data](/concepts/visualization/)** — Cat D (AOI + mask + advanced); 3 intensity levels by default (the count follows your `visualization_thresholds`; default `[1.5, 6.0, 10.0]`). Per-feature `intensity` is a level index `1..3`, not a continuous score.
- **[Rendering recipes → Polygon areas](/guides/rendering-recipes/#recipe-3--polygon-areas)** — paste-ready renderer.

---
## Freckles
Source: src/content/docs/concepts/freckles.mdx
> Discrete small-pigment-spot detection (0..1 score) with per-spot coordinates for marker rendering.

import ConceptOverlay from "../../../components/ConceptOverlay.astro";

## Overview

Freckles measures discrete, small, round pigment spots — distinct from larger melasma patches and considered generally smaller than dark spots.

```yaml
component: freckles
request:
  query: "components=freckles,freckles_visualization"
response_path: "results[].description == \"freckles\""
value_range: [0, 1]
also_request:
  - freckles_visualization     # per-spot coordinates and intensities
warnings: []                  # freckles emits no metric-specific codes
```

## Reference imagery



## Rendering notes

```js
const fr = response.results.find((r) => r.description === "freckles");

// visualization_data lives on each measurement_locations[] entry.
// Each feature is a "spot": length-1 x[]/y[] arrays, intensity is a number.
const markers = (fr.measurement_locations ?? []).flatMap(
  (loc) => loc.visualization_data ?? []
);
```

Use small dots for markers — large overlays read as alarming on what is often a desirable trait. Consider opt-in toggling rather than always-on display. Paste-ready: [Rendering recipes → Spots](/guides/rendering-recipes/#recipe-1--spots).

Each `measurement_locations[]` entry also carries its own per-region `value` (`0..1`) alongside its `mask_shapes` and spot `visualization_data`. A region scores **`-1`** when its mask couldn't be sampled — treat that as "region unavailable," not a real low score.

## Edge cases

- **Cultural/aesthetic context matters** — many users do not consider freckles a "concern." Default to neutral copy in the UI.

## `local_warnings[]`

The `freckles` metric emits no metric-specific `local_warnings[]` codes. It inherits global image-quality codes; a skin-mask sampling failure is reported as `401` on the `hyperpigmentation` finding (which shares the same analysis), not on `freckles`.

## Related

- **[Visualization data](/concepts/visualization/)** — Cat D (AOI + mask + advanced); spot-type features, intensity range `[0..400]`.
- **[Rendering recipes → Spots](/guides/rendering-recipes/#recipe-1--spots)** — concentric-circle dots are the recommended marker style.

---
## Dark spots
Source: src/content/docs/concepts/dark-spots.mdx
> Larger, distinct, deeper-pigmented spots (0..1) — distinct from freckles and from diffuse hyperpigmentation. Includes an adjusted variant.

import ConceptOverlay from "../../../components/ConceptOverlay.astro";

## Overview

Dark spots picks up deeper-pigmented marks — sun spots, age spots, post-inflammatory hyperpigmentation. Larger than freckles; more discrete than melasma patches.

```yaml
component: dark_spots
request:
  query: "components=dark_spots,dark_spots_adjusted,dark_spots_visualization"
response_path: "results[].description == \"dark_spots\""
value_range: [0, 1]
also_request:
  - dark_spots_adjusted              # cross-tone-comparable variant
  - dark_spots_visualization         # spot-type features, intensity range [0..400]
warnings: []                       # dark_spots emits no metric-specific codes
```

## Reference imagery



## Rendering notes

```js
const ds = response.results.find((r) => r.description === "dark_spots");

// visualization_data lives on each measurement_locations[] entry, not on the
// top-level finding. Each feature is a "spot": length-1 x[]/y[] arrays.
const markers = (ds.measurement_locations ?? []).flatMap(
  (loc) => loc.visualization_data ?? []
);
```

For "treat these specific spots" UIs, render those markers prominently. For routine recommendations, the aggregate `value` is enough.

Each `measurement_locations[]` entry also carries its own per-region `value` (`0..1`) alongside its `mask_shapes`. A region scores **`-1`** when its mask couldn't be sampled — treat that as "region unavailable," not a real low score.

## Edge cases

- Distinction between **Freckles** and darks spots can be partially overlapping in some cases.

## `local_warnings[]`

The `dark_spots` metric emits no metric-specific `local_warnings[]` codes. It inherits global image-quality codes; a skin-mask sampling failure is reported as `401` on the `hyperpigmentation` finding (which shares the same analysis), not on `dark_spots`.

## Related

- **[Visualization data](/concepts/visualization/)** — Cat D (AOI + mask + advanced); spot-type features, intensity range `[0..400]`.
- **[Rendering recipes → Spots](/guides/rendering-recipes/#recipe-1--spots)** — box outlines are the recommended marker; dot or cross styles also work.

---
## Dull skin and radiance
Source: src/content/docs/concepts/dull-skin.mdx
> Dullness severity (0..1) derived from shine, plus the related radiance glow score (a smoothness + shine combination) and an adjusted radiance variant for cross-tone use.

## Overview

Dull skin measures the absence of natural luminosity. The score is derived from the `shine` analysis.

```yaml
components:
  - dull_skin
  - radiance
  - radiance_adjusted
request:
  query: "components=dull_skin,radiance,radiance_adjusted"
response_path:
  - "results[].description == \"dull_skin\""
  - "results[].description == \"radiance\""
  - "results[].description == \"radiance_adjusted\""
value_range: [0, 1]            # dull_skin can be -1; radiance / radiance_adjusted can be null when unavailable
warnings: []                   # these metrics emit no metric-specific codes
```

## When to use which

- **`dull_skin`** — diagnostic UIs ("here's what's reducing your glow").
- **`radiance`** — improvement-tracking UIs ("your radiance is up").
- **`radiance_adjusted`** — cross-user analytics or cohort studies.

## Rendering notes

```js
const dull   = response.results.find((r) => r.description === "dull_skin");
const radiant = response.results.find((r) => r.description === "radiance");
```

These metrics don't have a visualization channel — they're whole-face scores. Surface as a single dial or score, not a heatmap.

When the underlying `shine` analysis is unavailable, these fall back to a sentinel instead of a real score: `dull_skin` returns `-1`, while `radiance` and `radiance_adjusted` return `null`. Guard for both (e.g. `dull.value >= 0`, `radiant.value != null`) and show an "n/a" treatment rather than a bogus dial.

## Edge cases

- **Foundation and finishing products** change how much light the skin surface returns, so they move these scores — the reading reflects the product on the skin as much as the skin itself. Ask users to capture bare-faced for trend tracking.

## `local_warnings[]`

These metrics emit no metric-specific `local_warnings[]` codes; they inherit the global image-quality codes.

## Related

- **[How metrics connect](/concepts/metric-relationships/)** — `dull_skin` is derived from `shine`; `radiance` combines `smoothness` + `shine`.
- **[Visualization data](/concepts/visualization/)** — Cat A (no spatial output). Same category as `skin_firmness`. The metric is a derived/inverted whole-face score; no per-feature data, no AOIs, no mask shapes. UI should be a single dial / number, never a face overlay.

---
## Uneven skin tone and smoothness
Source: src/content/docs/concepts/uneven-skin-tone.mdx
> Variation in skin colour across the face (uneven_skin_tone, 0..1) and its inverse smoothness score, with a polygon-area visualization for "where is the unevenness.

import ConceptOverlay from "../../../components/ConceptOverlay.astro";

## Overview

Uneven skin tone measures how much facial skin colour varies. Analysis includes variations in darkness-lightness and variations of the chromatic nature (blue-yellow / red-green). The metric is sensitive to a large spectrum of spatial scales from the smaller details to smoother trends across larger distances. However, the smallest details covered by the texture metric are not included to the uneven skin tone analysis. Smoothness is its inverse, normalized for "good number goes up" UIs. Both are computed from the same underlying analysis.

```yaml
components:
  - uneven_skin_tone
  - smoothness
request:
  query: "components=uneven_skin_tone,smoothness,uneven_skin_tone_visualization"
response_path:
  - "results[].description == \"uneven_skin_tone\""
  - "results[].description == \"smoothness\""
value_range: [0, 1]
also_request:
  - uneven_skin_tone_visualization
warnings: []                   # these metrics emit no metric-specific codes
```

## When to use which

- **`uneven_skin_tone`** — diagnostic UIs ("where does the tone vary").
- **`smoothness`** — improvement-tracking UIs ("your tone is more even — score increases as condition improves").

Don't surface both in the same UI; users find the inversion confusing.

## Reference imagery



## Rendering notes

```js
const ust = response.results.find((r) => r.description === "uneven_skin_tone");
const sm  = response.results.find((r) => r.description === "smoothness");
```

For an improvement-over-time UI, prefer `smoothness`. For a "where is my tone uneven" diagnostic, prefer `uneven_skin_tone`. This is a complement to `hyperpigmentation` and `dark_spots` — surface them together for a complete pigmentation story. Avoid using all of them as separate dials in the same UI; pick one summary score and put the others in a "details" panel.

Beyond the top-level `value`, each finding carries two `sum_measures` — `Luminance variation` and `Chroma variation` (on `smoothness` these read inverted, so a higher number is the better outcome) — plus a per-region breakdown in `measurement_locations[]` (one entry per face region, each with its own `value` and polygon-area `visualization_data`). A region scores **`-1`** when its mask couldn't be sampled — treat that as "region unavailable," not a real low score.

## Edge cases

- **Lighting gradients** (window light from one side) materially elevate this score. Encourage front-on diffuse lighting.

## `local_warnings[]`

These metrics emit no metric-specific `local_warnings[]` codes; they inherit the global image-quality codes. An unsamplable region returns `value: -1` (no status code).

## Related

- **[How metrics connect](/concepts/metric-relationships/)** — `smoothness` is the inverse of `uneven_skin_tone` (same analysis).
- **[Visualization data](/concepts/visualization/)** — Cat D (AOI + mask + advanced); 2 intensity levels by default (categorical `1..2`).
- **[Rendering recipes → Polygon areas](/guides/rendering-recipes/#recipe-3--polygon-areas)** — paste-ready renderer.
- **`smoothness` shares this metric's advanced visualization data.** The API has no separate `smoothness_visualization` switch — request `uneven_skin_tone_visualization` and the response carries advanced viz on both metrics.

---
## Sagging and firmness
Source: src/content/docs/concepts/skin-sagging.mdx
> Skin laxity (sagging, 0..1) and the inverse firmness score, with a regional visualization channel.

import ConceptOverlay from "../../../components/ConceptOverlay.astro";

## Overview

Sagging measures visible loss of skin elasticity — most prominent at the jawline, jowls, lower cheek, and brow. Firmness is its inverse. Both are computed from the same analysis. Visualization markers identify the regions contributing most to the score.

```yaml
components:
  - skin_sagging
  - skin_firmness
request:
  query: "components=skin_sagging,skin_firmness,skin_sagging_visualization"
response_path:
  - "results[].description == \"skin_sagging\""
  - "results[].description == \"skin_firmness\""
value_range: [0, 1]
also_request:
  - skin_sagging_visualization
warnings: [3001, 3002, 4000]   # 3001/3002 -> skin_sagging, 4000 -> skin_firmness
```

## When to use which

- **`skin_sagging`** — diagnostic UI for anti-aging routines.
- **`skin_firmness`** — improvement-over-time UI ("your firmness improved").

## Reference imagery



## Rendering notes

```js
const sag = response.results.find((r) => r.description === "skin_sagging");
const firm = response.results.find((r) => r.description === "skin_firmness");
```

`skin_sagging` returns a single `measurement_locations[]` entry, described as `Global`. It is **not** a region score — its `value` is `null`, and it exists to carry the whole-face `visualization_data`. The score you display is the finding's top-level `value`. The entry is present on every successful run, with an empty `visualization_data` when `skin_sagging_visualization` wasn't requested.

This is a slow-changing metric — month-over-month tracking is appropriate; week-over-week noise will dominate signal.

## Edge cases

- **`value` can be `null` for males in the lower-face / beard region.** Beard analysis isn't reliable, so the API returns `null` rather than guessing — this behavior is fixed, not configurable. Render skin sagging with an "n/a" treatment when `value` is null. (In rare cases, some analysis areas may not be visible and will return null exceptionally.)

## `local_warnings[]`

- `3001` (error, on `skin_sagging`) — Cannot analyze skin sagging since eyebag analysis had failed.
- `3002` (error, on `skin_sagging`) — Cannot analyze skin sagging since wrinkle analysis from required locations had failed.
- `4000` (error, on `skin_firmness`) — Skin firmness not analyzed due to error in skin sagging analysis.

## Related

- **[How metrics connect](/concepts/metric-relationships/)** — `skin_firmness` is the inverse of `skin_sagging`; sagging combines `wrinkles` + `eyes`.
- **[Visualization data](/concepts/visualization/)** — Cat D− (advanced visualization structure but with simpler outputs — relevant finding-area data only). Polygon-area type, intensity range `[0..1]`.
- **[Rendering recipes → Polygon areas](/guides/rendering-recipes/#recipe-3--polygon-areas)** — paste-ready renderer; a neutral white overlay reads better here than the warm-color ramp used elsewhere — the metric is more "regions contributing to the score" than a severity heatmap.

---
## Skin age
Source: src/content/docs/concepts/skin-age.mdx
> Estimated apparent skin age as an integer 0..100. Distinct from chronological age — derived from visible skin features.

import { Aside } from "@astrojs/starlight/components";

## Overview

Skin age estimates the apparent age of the user's skin from visible signs of aging in the face image. It is **not** chronological age — two people of the same chronological age can receive different estimates.

```yaml
component: skin_age
request:
  query: "components=skin_age"
response_path: "results[].description == \"skin_age\""
value: null                      # top-level value is unused
sum_measures:
  - skin_age                     # integer apparent skin age (~0..100)
warnings: [7000]
```



## Rendering notes

```js
const age = response.results.find((r) => r.description === "skin_age");
// The integer is in sum_measures (description "skin_age"), not the top-level value.
const years = age?.sum_measures.find((m) => m.description === "skin_age")?.value;  // integer ~0..100, or null

// The sum-measure is present even when the analysis failed (status 7000),
// carrying value: null. Guard on the value, not on the measure existing.
if (years == null) {
  // hide the skin-age card
}
```

UX considerations:

- **Surface with care.** Many users find a high estimate distressing. Pair with copy that emphasizes lifestyle factors and improvement potential.
- **Treat it as an estimate, not a measurement.** The result can shift between captures of the same person as lighting, expression and framing change. Establish your own expected variation on your capture flow before you present it as a trackable number.
- **Show a range, not a point.** "Your skin reads as 32–35" reads better than "34."

## `local_warnings[]`

- `7000` (error) — Skin Age analysis aborted due to image quality concerns or internal error.

When `7000` is present the finding is still returned, with `message: "Error"` and the `skin_age` sum-measure carrying `value: null`. Treat the null as "unavailable" rather than assuming the measure is missing.

---
## Skin type
Source: src/content/docs/concepts/skin-type.mdx
> Categorical skin-type classifier returning an integer 1..8 (in sum_measures) across normal, dry, oily, combination, and sensitive variants.

import { Aside } from "@astrojs/starlight/components";

## Overview

Skin type returns a categorical classification — not a `0..1` score. The classification is an integer `1..8` mapping to dermatological skin types and their sensitive variants, useful for routine recommendations. It is returned in **`sum_measures[]`** (description `"skin_type"`) — the finding's top-level `value` is unused. A per-zone breakdown (`t-zone` / `u-zone`) is also returned in `measurement_locations[]`.

```yaml
component: skin_type
request:
  query: "components=skin_type,skin_type_visualization"
response_path: 'results[].description == "skin_type"'
value: null                        # top-level value is unused
sum_measures:
  - skin_type                      # integer category 1..8 (see mapping below)
measurement_locations:
  - t-zone                         # per-zone skin_type + contributing scores
  - u-zone
also_request:
  - skin_type_visualization        # composite polygon-area features:
                                   #   levels 1-4 = shine data,
                                   #   levels 5-6 = pore dilation data,
                                   #   levels 7-9 = redness data
warnings: []                       # no skin_type-specific status codes
```

## Value mapping

| Value | Type                   |
| ----- | ---------------------- |
| 1     | Normal                 |
| 2     | Dry                    |
| 3     | Oily                   |
| 4     | Combination            |
| 5     | Sensitive              |
| 6     | Dry sensitive          |
| 7     | Oily sensitive         |
| 8     | Combination sensitive  |



## Rendering notes

Read the integer from `sum_measures[]`, not from `value`:

```js
const SKIN_TYPE = {
  1: "Normal", 2: "Dry", 3: "Oily", 4: "Combination", 5: "Sensitive",
  6: "Dry sensitive", 7: "Oily sensitive", 8: "Combination sensitive",
};

const t = response.results.find((r) => r.description === "skin_type");
const skinTypeId = t?.sum_measures.find((m) => m.description === "skin_type")?.value;
const label = SKIN_TYPE[skinTypeId];
```

For routine recommendations, surface the label and pair with `shine`, `texture`, and `pore_dilation` for nuance.

### Per-zone breakdown

`measurement_locations[]` carries one entry per face zone — `t-zone` and `u-zone` — each with its own `skin_type` integer (same `1..8` mapping) plus the contributing per-zone scores (`Skin shine`, `pore_dilation`, `redness`, `dull_skin`) in its `measures[]`, and the zone's mask shapes. Use these when you want to show, e.g., an oily T-zone alongside a normal U-zone rather than a single whole-face label.

## Edge cases

- **Recently-applied product** can shift the classifier toward "oily" or "combination" depending on finish.
- **Sensitive variants** are inferred from redness and visible irritation patterns; they are conservative — many users with sensitive skin will be classified as their non-sensitive base type.

## `local_warnings[]`

The `skin_type` metric emits no metric-specific `local_warnings[]` codes (it sets `message: "Ok"`). It inherits the global image-quality codes; its contributing metrics (`shine`, `redness`, `pore_dilation`) carry their own codes if requested.

## Related

- **[How metrics connect](/concepts/metric-relationships/)** — `skin_type` composites `shine`, `pore_dilation`, `redness`, and `dull_skin`.
- **[Visualization data](/concepts/visualization/)** — `skin_type_visualization` is a composite Type-2 stream: levels 1-4 carry shine data, 5-6 pore dilation, 7-9 redness. Each level is a polygon-area feature.
- **[Rendering recipes → Polygon areas](/guides/rendering-recipes/#recipe-3--polygon-areas)** — same renderer as the underlying metrics.

---
## Eye area
Source: src/content/docs/concepts/eyes.mdx
> Periorbital concerns — under-eye darkness (dark circles) and eyebag prominence — surfaced via the eyes component, both per eye and as an aggregate.

import ConceptOverlay from "../../../components/ConceptOverlay.astro";

## Overview

The `eyes` component analyses the periorbital region — the under-eye area and surrounding skin. It returns an aggregate severity score plus two sub-scores: **eyebags** (puffiness / under-eye edges) and **dark circles** (under-eye darkness). The sub-scores come back both globally (in `sum_measures[]`) and per eye (in `measurement_locations[]`), suitable for driving eye-cream and concealer recommendations.

```yaml
component: eyes
request:
  query: "components=eyes"
response_path: "results[].description == \"eyes\""
value_range: [0, 1]               # aggregate = mean of the two per-eye scores
sum_measures:
  - eyebags             # 0..1
  - dark circles        # 0..1  (note: space, not snake_case)
measurement_locations:
  - left eye            # per-eye eyebags + dark circles, with mask shapes
  - right eye
warnings: []            # the eyes metric emits no metric-specific status codes
```

## Reference imagery



## Rendering notes

The two sub-scores are in `sum_measures[]` — note the descriptions are space-separated, not snake_case:

```js
const eyes = response.results.find((r) => r.description === "eyes");

const eyebags     = eyes.sum_measures.find((m) => m.description === "eyebags")?.value;
const darkCircles = eyes.sum_measures.find((m) => m.description === "dark circles")?.value;
```

For a per-eye breakdown, read `measurement_locations[]` — one entry per eye, each carrying its own `eyebags` and `dark circles` measures (and mask shapes for overlays):

```js
const perEye = eyes.measurement_locations.map((loc) => ({
  side: loc.description,                                          // "left eye" | "right eye"
  eyebags: loc.measures.find((m) => m.description === "eyebags")?.value,
  darkCircles: loc.measures.find((m) => m.description === "dark circles")?.value,
}));
```

Surface the two sub-scores separately rather than as a single aggregate — users have very specific eye-area concerns and product preferences.

## Edge cases

- **Glasses** make this analysis unreliable. Eyeglasses are flagged globally in `status[]` as `314` ("Eyeglasses detected. Remove eyeglasses before taking the photo. Results are unreliable."), not as an eyes-specific warning.

## `local_warnings[]`

The `eyes` metric does not emit any metric-specific `local_warnings[]` codes. Watch the global `status[]` instead — in particular `314` (eyeglasses detected) degrades eye-area results. Note also that if you request `skin_sagging`, a failed eyebag analysis surfaces there as `3001` ("Cannot analyze skin sagging since eyebag analysis had failed."), since sagging depends on the eye-area analysis.

---
## Makeup
Source: src/content/docs/concepts/makeup.mdx
> Composite makeup analysis — face shape, skin undertone, foundation RGB, Fitzpatrick type, eye color — for shade-finding and try-on UX.

import { Aside } from "@astrojs/starlight/components";

## Overview

The `makeup` component bundles several categorical attributes useful for cosmetics personalization: detected face shape, skin undertone, the user's foundation colour as RGB, Fitzpatrick skin type, and eye color. Most values come back via `sum_measures[]` rather than as a numeric `value`.



```yaml
component: makeup
request:
  query: "components=makeup"
response_path: "results[].description == \"Makeup\""   # NOTE: request component is `makeup`, response description is `Makeup`
value_range: null              # composite — read sum_measures[]
sum_measures:
  - Face shape                 # integer 1..6
  - Eye color                  # integer 1..3, or -1 when unavailable
  - Skin undertone             # integer -1, 0, +1, or null when unavailable
  - Skin foundation RGB        # [[r, g, b], ""]
  - Skin Fitzpatrick type      # integer 1..6, or null when unavailable
warnings: [1000, 1101, 1102, 1111, 1112, 1121, 1122, 1191, 1192, 1200, 1201]
```

## Value mappings

| `Face shape` | Shape    |
| ------------ | -------- |
| 1            | Diamond  |
| 2            | Heart    |
| 3            | Long     |
| 4            | Oval     |
| 5            | Round    |
| 6            | Square   |

| `Skin undertone` | Undertone |
| ---------------- | --------- |
| -1               | Cool      |
| 0                | Neutral   |
| +1               | Warm      |

| `Eye color` | Color |
| ----------- | ----- |
| -1          | Unavailable — eye-colour analysis did not complete |
| 1           | Brown |
| 2           | Blue  |
| 3           | Green |

| `Skin Fitzpatrick type` | Type            |
| ----------------------- | --------------- |
| 1                  | Always burns    |
| 2                  | Usually burns   |
| 3                  | Sometimes burns |
| 4                  | Rarely burns    |
| 5                  | Very rarely     |
| 6                  | Never burns     |

`Skin undertone` and `Skin Fitzpatrick type` come back as `null` when the analysis they depend on did not complete — guard for that rather than assuming a number is always present.



## Rendering notes

```js
// Request component is `makeup`, but the response finding's description is "Makeup".
const m = response.results.find((r) => r.description === "Makeup");
const measure = (d) => m?.sum_measures.find((s) => s.description === d)?.value;

const faceShape   = measure("Face shape");        // 1..6
const eyeColor    = measure("Eye color");         // 1..3
const undertone   = measure("Skin undertone");    // -1 | 0 | +1
const fitzpatrick = measure("Skin Fitzpatrick type");  // 1..6

// "Skin foundation RGB" is [[r, g, b], ""] — check the status first.
const [fdnRgb, fdnStatus] = measure("Skin foundation RGB") ?? [];
const fdnUsable = fdnStatus !== "Foundation rgb is invalid.";
```

Match foundation by combining RGB and undertone; match lipstick by undertone + Fitzpatrick; match brow products by hair colour (request `hair` separately).

### `measurement_locations[]` are mask carriers

The finding returns nine cosmetic-application areas — `Lips`, `Liplines`, `Eyelines`, `Eyeshadow`, `Eyebrows`, `Nose`, `Left cheek`, `Right cheek`, `Skin`. They carry **no score**: `value` is `null` on all of them and there are no AOI coordinates. Their only content is `mask_shapes`, which is returned solely when you also request the `masks` component:

```http
?components=makeup,masks
```

Request `makeup` on its own and the nine entries still appear, but stripped of their shapes — empty by design, not a fault. If you aren't drawing the areas, read `sum_measures[]` and ignore `measurement_locations[]` entirely. See [Masks](/concepts/masks/).

## Edge cases

- **Existing makeup** affects the output — the values are sampled from the visible skin surface, so if foundation is worn the sampled shade reflects the product rather than the user's bare skin. The component does **not** detect or report whether makeup is present.

## `local_warnings[]`

**Skin tone input:**
- `1000` (error) — skin tone input not defined. Unable to proceed.

**Eye area — eye state:**
- `1101` (error) — eyes look too much closed. Keep eyes open. Eye color analysis aborted.
- `1102` (warning) — eyes look quite closed. Keep eyes open.

**Eye area — lighting:**
- `1111` (error) — eyes region is too dark. Improve lighting on the eyes. Eye color analysis aborted.
- `1112` (warning) — eyes region is quite dark. Improve lighting on the eyes.

**Eye area — color cast:**
- `1121` (error) — eyes whites have a heavy color cast. Correct white balance settings on the camera. Eye color analysis aborted. _(obsolete)_
- `1122` (warning) — eyes whites have a significant color cast. Correct white balance settings on the camera.

**Eye color processing errors:**
- `1191` (error) — eye color detection processing error 1. Eye area has no color information.
- `1192` (error) — eye color detection processing error 2. Eye area is too dark.

**Foundation color:**
- `1200` (warning) — color balance not corrected due to eye area low image quality. Skin foundation color analysis might be unreliable.
- `1201` (warning) — skin foundation estimation failed. Foundation rbg output is invalid.

---
## Hair
Source: src/content/docs/concepts/hair.mdx
> Hair colour detection (with named palette), volume, type (coily/curly/straight/wavy), and frizziness.

import { Aside } from "@astrojs/starlight/components";

## Overview

The `hair` component returns four sub-findings: detected hair colour (or colours, for highlights and balayage), volume, type, and frizziness. Each detected colour is matched against a named palette of roughly 140 hair shades, and the result carries both that palette entry's name and its RGB — so partner UIs can show a swatch and a recognisable name together. Note the RGB is the palette shade's, not a raw pixel sample, so the swatch and the name always agree with each other.

```yaml
component: hair
request:
  query: "components=hair"
response_path: "results[].description == \"hair\""
value_range: null
sum_measures:
  - Hair color              # [color_structure, message]; color_structure = [[index, name, prevalence, rgb], ...]
  - Hair volume             # 0..1
  - Hair type               # 0..4 (categorical; 1 deprecated)
  - Hair frizziness         # label: 0 (low) / 1 (high)
  - Hair frizziness ranged  # continuous 0..1
input_hint:
  hairtype_query: "?hairtype=<-1|0|1|2|3|4>"
warnings: [5000, 5002, 5003]
```

## Value mappings

| `hair_type` | Type    |
| ----------- | ------- |
| 0           | Coily   |
| 1           | DEPRECATED |
| 2           | Curly   |
| 3           | Straight |
| 4           | Wavy    |

The optional `hairtype` query parameter (`-1` for auto, `0`–`4` for explicit) **biases** the model toward the type you supply — useful when partner apps already capture the user's hair type via questionnaire. It is a hint, not an override: a confident detection can still disagree with the hint, so don't assume the returned `Hair type` equals what you passed in.



## Rendering notes

```js
const hair = response.results.find((r) => r.description === "hair");

// "Hair color" value is [colorStructure, message]; the entries are in [0].
const hairColor = hair.sum_measures.find((m) => m.description === "Hair color")?.value;
const colors = hairColor?.[0];   // [[index, name, prevalence, [r,g,b]], ...]

const volume    = hair.sum_measures.find((m) => m.description === "Hair volume")?.value;
const hairType  = hair.sum_measures.find((m) => m.description === "Hair type")?.value;
const frizziness = hair.sum_measures.find((m) => m.description === "Hair frizziness")?.value;              // label: 0 (low) / 1 (high)
const frizzinessRanged = hair.sum_measures.find((m) => m.description === "Hair frizziness ranged")?.value; // continuous 0..1
```

For multi-colour hair (highlights, balayage, grown-out roots), `colors[]` carries one entry per detected colour, ordered by `prevalence` descending, with prevalence summing to 1 across the entries. Up to four colours can come back. How many you actually get depends on the image, so read the array rather than assuming a count — surfacing the top one or two is usually enough for a UI.

## Edge cases

- **Tied-up hair** can register low volume incorrectly. Capture with hair down for accurate volume scoring.
- **Wet hair** reads as straighter and darker. Capture dry for routine recommendations.

## `local_warnings[]`

- `5000` — hair analysis aborted due to image quality concerns or internal error.
- `5002` — no hair detected. Hair analysis aborted.
- `5003` — not enough hair detected. Hair analysis aborted.

---
## Masks
Source: src/content/docs/concepts/masks.mdx
> Tier-2 visualization data — exact region boundaries described as a small drawing program. Returned only when the masks component is requested.

import { Aside } from "@astrojs/starlight/components";

## Overview

`masks` is the **second tier** of [visualization data](/concepts/visualization/) — between the cheap [AOI circles](/concepts/visualization/#tier-1--aoi) and the rich [advanced/concern visualization](/concepts/visualization/#tier-3--advanced-concern-visualization). It returns the *exact* region boundary that a metric was measured over, encoded as a small drawing program: an ordered list of shapes to add to (or cut out of) a buffer, optionally followed by an outer mask the result is multiplied against.

It's the right tier when you need pixel-accurate region rendering — diagnostic dashboards, before/after panels, "highlight the cheek when discussing redness" UX. For markers/spots/lines on top of the photo, use Tier 3 instead. For a coarse "this is roughly the area" zoom, AOI circles are enough.



## Requesting masks

`masks` is a flag, not a metric. Add it to your `?components=...` query alongside the metrics whose region shapes you want:

```http
?components=masks,redness,wrinkles
```

Each finding's `measurement_locations[]` then carries a `mask_shapes` block. Without `masks`, you only get the AOI circles.

### Two places to look

`mask_shapes` appears at **two levels**, and several metrics populate both:

- **Per measurement location** — the boundary of that one region. This is the common case.
- **On the finding itself** — a whole-metric boundary, next to `value` and `sum_measures`. `redness`, `texture`, `uneven_skin_tone`, `smoothness` and `shine` use it for the full sampled skin area; `hair` uses it for the hair outline.

For `hair` the finding-level block is the **only** mask — hair has no `measurement_locations` at all, so a renderer that walks locations will find nothing. Check both levels.



## The mask shape schema

Each `mask_shapes` block has two parts:

```jsonc
{
  "coordinates": "original",        // pixels of the input image
  "shapes": [                       // ordered draw program
    { "color": 1, "type": "polygon", "params": [[x0, y0], [x1, y1], ...] },
    { "color": 0, "type": "ellipse", "params": [cx, cy, rx, ry, rot, a1, a2] }
  ],
  "mask": {                         // optional outer mask, multiplied at the end
    "color": 1,
    "type": "polygon",
    "params": [[x0, y0], [x1, y1], ...]
  }
}
```

- **`coordinates: "original"`** — all `params` values are in the input-image pixel coordinate system (origin top-left, X-right, Y-down).
- **`shapes[]`** — the draw program. Each entry has `color`, `type`, `params`.
- **`mask`** — optional. If present, the final result is multiplied by this single shape (so anything outside it gets zeroed out). It's a full shape object (`color`, `type`, `params`) — same vocabulary as `shapes[]`.

### Shape types

Four shape primitives. Each uses a different `params` layout:

| `type` | `params` shape | Notes |
|---|---|---|
| **`polygon`** | `[[x0, y0], [x1, y1], ...]` | Guaranteed convex. **Connect first point to last explicitly when drawing** — the API omits the closing edge. |
| **`unrestricted_polygon`** | `[[x0, y0], [x1, y1], ...]` | Not guaranteed convex. Same closing-edge requirement. |
| **`ellipse`** | `[cx, cy, rx, ry, rotation, startAngle, endAngle]` | Standard 7-parameter ellipse — see [OpenCV's `ellipse` function](https://docs.opencv.org/4.x/d6/d6e/group__imgproc__draw.html#ga28b2267d35786f5f890ca167236cbc69) for the canonical interpretation. |
| **`polyline`** | `[[x0, y0], [x1, y1], ...]` | A *line*, not an enclosed area. **Don't connect first to last.** |

### `color` — add or cut

Each shape's `color` is an integer:

- `color: 1` — **add** the shape's pixels to the buffer (set them to 1).
- `color: 0` — **cut** the shape's pixels from the buffer (set them to 0).

Order matters: shapes are applied left-to-right, each on top of the result so far. So `[ellipse@1, polygon@0]` means "draw the ellipse, then erase the polygon's interior from it."

## Drawing a mask — the buffer-A / buffer-B walkthrough

Two single-channel image buffers A and B, both starting empty (all zeros), both the size of the input image.

1. **Buffer A — apply the `shapes[]` program.**
   For each shape in order, draw its primitive onto A using the shape's `color` (1 fills with 1, 0 fills with 0).
2. **Buffer B — draw the outer `mask` shape (if present).**
   If `mask` is omitted, treat B as all-ones. Otherwise draw the single `mask` shape onto B with its color.
3. **Final mask = A × B (elementwise).**
   A pixel ends up `1` only if both buffer A *and* buffer B mark it as 1.

Pseudocode:

```js
function buildMask({ shapes, mask }, width, height) {
  const A = new Uint8Array(width * height);     // zeros
  for (const { type, color, params } of shapes) {
    drawShape(A, type, params, color);
  }

  const B = new Uint8Array(width * height).fill(mask ? 0 : 1);
  if (mask) {
    drawShape(B, mask.type, mask.params, mask.color);
  }

  const final = new Uint8Array(width * height);
  for (let i = 0; i < final.length; i++) final[i] = A[i] && B[i] ? 1 : 0;
  return final;
}
```

`drawShape` is wherever your renderer's primitives live — `` 2D context, OpenCV's `polylines` / `fillPoly` / `ellipse`, an SVG path builder, etc. Python + `matplotlib.pyplot` or OpenCV is a common choice for server-side / batch rendering.

## Why most metrics' masks look the same

Most metrics' `mask_shapes` start from the same shared origin — the facial skin area — and then add or cut metric-specific regions. So if you compare `mask_shapes` across `redness`, `texture`, and `uneven_skin_tone`, large parts of their `shapes[]` arrays will be identical. That's expected; it's the shared face-area scaffold.

## Edge cases

- **Without `masks`, fall back to AOIs.** You can still draw approximate regions from each measurement location's `AOI_x_o` / `AOI_y_o` / `AOI_radius_o` — circles around each area of interest. Less precise, much smaller payload.
- **`local_warnings[]`** — a skin-mask sampling failure surfaces as `idx: 401` on `hyperpigmentation` and `acne` (the metrics that sample masks). Other mask-using metrics instead return `value: -1` for the affected region rather than a code. Treat as advisory; the rest of the metric's output is still usable.

## Related

- **[Visualization data](/concepts/visualization/)** — the three-tier model and which metrics support which tier.
- **[Rendering recipes](/guides/rendering-recipes/)** — paste-ready mask renderers for canvas / SVG / WebGL.
- **[Status codes](/concepts/status-codes/)** — `401` and other masking diagnostics.

---
## Status codes
Source: src/content/docs/concepts/status-codes.mdx
> Complete reference for the idx codes returned in status[] and per-finding local_warnings[]. Image-quality, face detection, per-metric failures.

import { Aside } from "@astrojs/starlight/components";

## Overview

Every `analyzeImage` response carries a `status[]` array of `{ idx, description, isError }` entries describing image-quality and pipeline-level issues. Per-finding `local_warnings[]` carry the same shape but scoped to one metric. This page lists every code, grouped by stage.



## General (99)

| `idx` | `isError` | Description |
| ----- | --------- | ----------- |
| 99    | 1         | [custom context] undefined failure. |

## Image preprocessing (100–104)

| `idx` | `isError` | Description |
| ----- | --------- | ----------- |
| 100   | 1         | Unable to process: image must be in RGB color format. |
| 101   | 1         | Unable to process: Image resolution too small. |
| 102   | 1         | Unable to downsample image resolution: Image aspect ratio too extreme. |
| 103   | 0         | Image too dark. Increase exposure. |
| 104   | 0         | Image is overexposed. Find more diffuse lighting. |

## Face detection (201–206)

| `idx` | `isError` | Description |
| ----- | --------- | ----------- |
| 201   | 1         | Unable to detect a face |
| 202   | 0         | Multiple faces detected. Only the best fit face is analyzed. |
| 203   | 0         | Face detection is uncertain. Analysis quality is compromized. |
| 204   | 0         | Face detection is very uncertain. Analysis quality is unreliable. |
| 205   | 0         | More margin space around the face needed. Results might be compromised. Take the picture a bit further away. _(obsolete — no longer emitted; `319` covers tight framing)_ |
| 206   | 1         | Facial landmarks are invalid. Aborting processing. |

## Face normalization / quality (301–319)

| `idx` | `isError` | Description |
| ----- | --------- | ----------- |
| 301   | 1         | The detected face resolution is too small. Take a photo closer to the target. |
| 302   | 1         | Face is too much tilted to the side. Face straight at the camera. |
| 303   | 0         | Face is possibly tilted to the side. Face straight at the camera. |
| 304   | 1         | Face is too much tilted down. Face straight at the camera. |
| 305   | 0         | Face is possibly tilted down. Face straight at the camera. |
| 306   | 0         | Face is possibly tilted up. Face straight at the camera. |
| 307   | 0         | Image too dark at the face area. Increase exposure/light to the face. |
| 308   | 0         | Image is overexposed at the face area. Find more diffuse lighting or reduce exposure. |
| 309   | 0         | The face area blue channel is clipping (overexposed). Reduce exposure or adjust color balance. |
| 310   | 0         | The face area green channel is clipping (overexposed). Reduce exposure or adjust color balance. |
| 311   | 0         | The face area red channel is clipping (overexposed). Reduce exposure or adjust color balance. |
| 312   | 0         | The face has unsymmetric exposure. Find even illumination. |
| 313   | 0         | The face area has hard shadows. Find more diffuse lighting. |
| 314   | 0         | Eyeglasses detected. Remove eyeglasses before taking the photo. Results are unreliable. |
| 315   | 1         | Forehead is partially outside the image. Make sure the whole face fits the image. Aborting processing. |
| 316   | 1         | Lower face is partially outside the image. Make sure the whole face fits the image. Aborting processing. |
| 317   | 1         | Face right side is partially outside the image. Make sure the whole face fits the image. Aborting processing. |
| 318   | 1         | Face left side is partially outside the image. Make sure the whole face fits the image. Aborting processing. |
| 319   | 0         | Face is too tightly framed in the image. Leave more space around the face. |

## Skin masking (401)

| `idx` | `isError` | Description |
| ----- | --------- | ----------- |
| 401   | 0         | Unable to sample skin. Acne/Hyperpigmentation results are unreliable. |

## Aggregate (500)

| `idx` | `isError` | Description |
| ----- | --------- | ----------- |
| 500   | 1         | All metrics had an error. |

## Makeup analysis (1000–1201)

| `idx` | `isError` | Description |
| ----- | --------- | ----------- |
| 1000  | 1         | Skin tone input not defined. Unable to proceed. |
| 1101  | 1         | Eyes look too much closed. Keep eyes open. Eye color analysis aborted. |
| 1102  | 0         | Eyes look quite closed. Keep eyes open. |
| 1111  | 1         | Eyes region is too dark. Improve lighting on the eyes. Eye color analysis aborted. |
| 1112  | 0         | Eyes region is quite dark. Improve lighting on the eyes. |
| 1121  | 1         | Eyes whites have a heavy color cast. Correct white balance settings on the camera. Eye color analysis aborted. _(obsolete)_ |
| 1122  | 0         | Eyes whites have a significant color cast. Correct white balance settings on the camera. |
| 1191  | 1         | Eye color detection processing error 1. Eye area has no color information. |
| 1192  | 1         | Eye color detection processing error 2. Eye area is too dark. |
| 1200  | 0         | Color balance not corrected due to eye area low image quality. Skin foundation color analysis might be unreliable |
| 1201  | 0         | Skin foundation estimation failed. Foundation rbg output is invalid. |

## Pore dilation (2000–2001)

| `idx` | `isError` | Description |
| ----- | --------- | ----------- |
| 2000  | 1         | Image resolution for the face area is too low. Face at the image has to be at minimum 1200 pixels wide. |
| 2001  | 0         | Image resolution for the face area is low. |

## Acne (3000)

| `idx` | `isError` | Description |
| ----- | --------- | ----------- |
| 3000  | 1         | Acne measurement aborted due to image quality concerns. |

## Skin sagging (3001–3002)

| `idx` | `isError` | Description |
| ----- | --------- | ----------- |
| 3001  | 1         | Cannot analyze skin sagging since eyebag analysis had failed. |
| 3002  | 1         | Cannot analyze skin sagging since wrinkle analysis from required locations had failed. |

## Skin firmness (4000)

| `idx` | `isError` | Description |
| ----- | --------- | ----------- |
| 4000  | 1         | Skin firmness not analyzed due to error in skin sagging analysis. |

## Hair (5000–5003)

| `idx` | `isError` | Description |
| ----- | --------- | ----------- |
| 5000  | 1         | Hair analysis aborted due to image quality concerns or internal error. |
| 5002  | 1         | No hair detected. Hair analysis aborted. |
| 5003  | 1         | Not enough hair detected. Hair analysis aborted. |

## Skin age (7000)

| `idx` | `isError` | Description |
| ----- | --------- | ----------- |
| 7000  | 1         | Skin Age analysis aborted due to image quality concerns or internal error. |

## Use in code

```js
const FATAL = new Set([
  99, 100, 101, 102,
  201, 206,
  301, 302, 304, 315, 316, 317, 318,
  500,
  1000, 1101, 1111, 1121, 1191, 1192,
  2000,
  3000, 3001, 3002, 4000,
  5000, 5002, 5003,
  7000,
]);

function describe(status) {
  return status
    .filter((s) => s.isError === 1)
    .map((s) => ({ code: s.idx, fatal: FATAL.has(s.idx), description: s.description }));
}
```

For the most current listing, the OpenAPI spec is canonical: see [`/openapi/skin-analysis.yaml`](/openapi/skin-analysis.yaml).

---
## SDKs
Source: src/content/docs/sdks.mdx
> First-party SDKs for the Revieve Skin Analysis API in TypeScript, Python, Swift, and Kotlin. Currently preview / v0.1.0.

import { Card, CardGrid, LinkCard, Aside } from "@astrojs/starlight/components";

First-party clients for the Revieve Skin Analysis API. All four SDKs share the same shape: one `SkinAnalysisClient`, one `analyzeImage` method, typed components and responses.



## Choose your SDK


  
  
  
  


## Feature matrix

| Capability                       | TypeScript | Python | Swift | Kotlin |
| -------------------------------- | :--------: | :----: | :---: | :----: |
| `analyzeImage`                   | ✓          | ✓      | ✓     | ✓      |
| Strong typing for components     | ✓          | ✓      | ✓     | ✓      |
| Strong typing for response       | ✓          | partial (TypedDict planned) | ✓ | ✓ |
| Async/await                      | ✓ (native) | sync only (v0.1) | ✓     | ✓ (suspend) |
| Browser support                  | ✓          | —      | —     | —      |
| Mobile support                   | (via PWA)  | —      | ✓     | ✓ (Android) |
| Test fixture / mock injection    | ✓ (`fetch`)| ✓ (`http_client`) | ✓ (`URLSession`) | ✓ (`HttpClient`) |
| `prompts/SYSTEM.md` for LLMs     | ✓          | ✓      | ✓     | ✓      |

## What's *not* in the SDKs

- The Product Recommendations API has no first-party SDK yet — call it via plain HTTP for now or generate a thin client from [its OpenAPI spec](/openapi/product-recommendations.yaml).
- The SDKs cover exactly the components and request fields in the published [OpenAPI spec](/openapi/skin-analysis.yaml) — nothing outside that surface.

## Generate from spec instead

If you'd rather not depend on a Revieve-maintained package, every modern code generator builds a working client from the OpenAPI document:

- `openapi-typescript` for type-only TypeScript.
- `openapi-python-client` for typed `httpx` Python.
- Apple `swift-openapi-generator` for Swift.
- OpenAPI Generator's `kotlin` template for Kotlin / Java.
- `oapi-codegen` for Go.
- `OpenAPI.Generator` for C# / .NET.

Spec URL: [`/openapi/skin-analysis.yaml`](/openapi/skin-analysis.yaml).

---
## TypeScript SDK
Source: src/content/docs/sdks/typescript.mdx
> First-party TypeScript SDK for the Revieve Skin Analysis API — strong types, browser and Node, no extra dependencies.

import { Tabs, TabItem, Aside } from "@astrojs/starlight/components";

`@revieve/sdk` — a thin, typed client for `analyzeImage`. Source lives in [`sdks/typescript/`](https://github.com/revieve/api.revieve.com/tree/master/sdks/typescript) in this repo.

## Install



```bash
npm install @revieve/sdk
```

Requires Node 20+ or any modern browser. Uses the global `fetch`.

## Quick start


  
    ```ts
    import { readFile } from "node:fs/promises";
    import { SkinAnalysisClient } from "@revieve/sdk";

    const revieve = new SkinAnalysisClient({ partnerId: "jmlv6b2qtS" });
    const image = await readFile("./selfie.jpg");

    const result = await revieve.analyzeImage({
      image,
      components: ["wrinkles", "redness", "skin_age"],
    });

    console.log(result.message, result.results);
    ```
  
  
    ```ts
    import { SkinAnalysisClient } from "@revieve/sdk";

    const revieve = new SkinAnalysisClient({ partnerId: "jmlv6b2qtS" });

    document.getElementById("file").addEventListener("change", async (e) => {
      const result = await revieve.analyzeImage({
        image: (e.target as HTMLInputElement).files![0],
        components: ["wrinkles", "redness", "skin_age"],
      });
      console.log(result);
    });
    ```
  




## Errors

```ts
import { SkinAnalysisClient, RevieveAnalysisError } from "@revieve/sdk";

try {
  await revieve.analyzeImage({ image });
} catch (err) {
  if (err instanceof RevieveAnalysisError) {
    console.log(err.firstErrorIdx, err.message);
  } else {
    throw err;
  }
}
```

## Test fixture injection

Pass a custom `fetch` to the constructor — useful for unit tests or for environments with custom transport requirements:

```ts
const revieve = new SkinAnalysisClient({
  partnerId: "test",
  fetch: async (url, init) => new Response(JSON.stringify(fixtureResponse)),
});
```

## Source

- [`src/index.ts`](https://github.com/revieve/api.revieve.com/blob/master/sdks/typescript/src/index.ts)
- [`src/client.ts`](https://github.com/revieve/api.revieve.com/blob/master/sdks/typescript/src/client.ts)
- [`src/types.ts`](https://github.com/revieve/api.revieve.com/blob/master/sdks/typescript/src/types.ts)
- [`src/errors.ts`](https://github.com/revieve/api.revieve.com/blob/master/sdks/typescript/src/errors.ts)
- [`README.md`](https://github.com/revieve/api.revieve.com/blob/master/sdks/typescript/README.md)

---
## Python SDK
Source: src/content/docs/sdks/python.mdx
> First-party Python SDK for the Revieve Skin Analysis API — typed, mypy-strict-clean, httpx-backed.

import { Aside } from "@astrojs/starlight/components";

`revieve-sdk` — a thin, typed client for `analyze_image`. Source lives in [`sdks/python/`](https://github.com/revieve/api.revieve.com/tree/master/sdks/python).

## Install



```bash
pip install revieve-sdk
```

Requires Python 3.10+. Depends on `httpx`.

## Quick start

```python
from revieve import SkinAnalysisClient

revieve = SkinAnalysisClient(partner_id="jmlv6b2qtS")

with open("selfie.jpg", "rb") as f:
    result = revieve.analyze_image(
        image=f,
        components=["wrinkles", "redness", "skin_age"],
        gender="unknown",
        skintone=0,
    )

print(result["message"], len(result["results"]))
```

## Errors

```python
from revieve import RevieveAnalysisError, RevieveNetworkError

try:
    result = revieve.analyze_image(image=f)
except RevieveAnalysisError as err:
    print(err.first_error_idx, err)
except RevieveNetworkError:
    raise
```

## Async / sync

The v0.1 client is synchronous. An `AsyncSkinAnalysisClient` is on the roadmap; in the meantime, run sync calls in a thread pool from your async app:

```python
import asyncio
import functools

async def analyze(image_path: str):
    return await asyncio.to_thread(
        functools.partial(revieve.analyze_image, image=open(image_path, "rb"))
    )
```

## Test fixture injection

Pass a configured `httpx.Client` for unit testing, retries, or proxies:

```python
import httpx

mock = httpx.Client(transport=httpx.MockTransport(handler))
revieve = SkinAnalysisClient(partner_id="test", http_client=mock)
```



## Source

- [`src/revieve/__init__.py`](https://github.com/revieve/api.revieve.com/blob/master/sdks/python/src/revieve/__init__.py)
- [`src/revieve/client.py`](https://github.com/revieve/api.revieve.com/blob/master/sdks/python/src/revieve/client.py)
- [`src/revieve/errors.py`](https://github.com/revieve/api.revieve.com/blob/master/sdks/python/src/revieve/errors.py)
- [`README.md`](https://github.com/revieve/api.revieve.com/blob/master/sdks/python/README.md)

---
## Swift SDK
Source: src/content/docs/sdks/swift.mdx
> First-party Swift SDK for the Revieve Skin Analysis API — Codable models, Swift Concurrency, iOS 15+ / macOS 12+ / visionOS 1+.

import { Aside } from "@astrojs/starlight/components";

`Revieve` — a thin, typed client for `analyzeImage`. Source lives in [`sdks/swift/`](https://github.com/revieve/api.revieve.com/tree/master/sdks/swift).

## Install



In `Package.swift`:

```swift
.package(url: "https://github.com/revieve/revieve-sdk-swift", from: "0.1.0")
```

In Xcode: **File → Add Package Dependencies…** and paste the same URL.

Targets iOS 15+, macOS 12+, visionOS 1+.

## Quick start

```swift
import Revieve

let revieve = try SkinAnalysisClient(partnerId: "jmlv6b2qtS")

let result = try await revieve.analyzeImage(
    .init(
        image: imageData,
        components: [.wrinkles, .redness, .skin_age],
        gender: .unknown,
        skintone: 0
    )
)

print(result.message, result.results.count)
```

## Errors

```swift
do {
    let result = try await revieve.analyzeImage(.init(image: imageData))
    render(result)
} catch let error as RevieveError {
    if case .analysis(let message, _) = error {
        showToast(message)
        log(error.firstErrorIdx)
    } else {
        throw error
    }
}
```

## Test injection

Pass a custom `URLSession` for mocking. Combined with `URLProtocol`-based fakes this gives full control over the request/response cycle.

```swift
let mockConfig = URLSessionConfiguration.ephemeral
mockConfig.protocolClasses = [MockURLProtocol.self]
let session = URLSession(configuration: mockConfig)

let revieve = try SkinAnalysisClient(
    partnerId: "test",
    baseURL: URL(string: "https://example.invalid/api/3/")!,
    session: session
)
```

## Source

- [`Sources/Revieve/Client.swift`](https://github.com/revieve/api.revieve.com/blob/master/sdks/swift/Sources/Revieve/Client.swift)
- [`Sources/Revieve/Models.swift`](https://github.com/revieve/api.revieve.com/blob/master/sdks/swift/Sources/Revieve/Models.swift)
- [`README.md`](https://github.com/revieve/api.revieve.com/blob/master/sdks/swift/README.md)

---
## Kotlin SDK
Source: src/content/docs/sdks/kotlin.mdx
> First-party Kotlin SDK for the Revieve Skin Analysis API — typed enums, suspend functions, JVM 17+ and Android.

import { Aside } from "@astrojs/starlight/components";

`com.revieve:revieve-sdk` — a thin, typed client for `analyzeImage`. Source lives in [`sdks/kotlin/`](https://github.com/revieve/api.revieve.com/tree/master/sdks/kotlin).

## Install



```kotlin
// build.gradle.kts
dependencies {
    implementation("com.revieve:revieve-sdk:0.1.0")
}
```

Targets JVM 17+ and Android (minimum API level depends on your Ktor configuration; OkHttp engine works back to API 21).

## Quick start

```kotlin
import com.revieve.*

SkinAnalysisClient(partnerId = "jmlv6b2qtS").use { revieve ->
    val result = revieve.analyzeImage(
        image = imageBytes,
        components = listOf(Component.WRINKLES, Component.REDNESS, Component.SKIN_AGE),
        gender = Gender.UNKNOWN,
        skintone = 0,
    )
    println(result.message)
}
```

## Errors

```kotlin
try {
    val result = revieve.analyzeImage(image = imageBytes)
    render(result)
} catch (err: RevieveException.Analysis) {
    showToast(err.message ?: "Analysis failed")
    log(err.firstErrorIdx)
} catch (err: RevieveException.Network) {
    log(err)
    throw err
}
```

## Test injection

Pass a mock `HttpClient` to swap the underlying engine for unit tests:

```kotlin
val mock = HttpClient(MockEngine) {
    install(ContentNegotiation) { json() }
    engine {
        addHandler { request -> respond(fixtureBody, HttpStatusCode.OK) }
    }
}
val revieve = SkinAnalysisClient(partnerId = "test", http = mock)
```

## Coroutines

`analyzeImage` is `suspend`. Call from a coroutine scope; on Android use `viewModelScope` or `lifecycleScope`. Kotlin Multiplatform support is on the roadmap but not v0.1.

## Source

- [`src/main/kotlin/com/revieve/Client.kt`](https://github.com/revieve/api.revieve.com/blob/master/sdks/kotlin/src/main/kotlin/com/revieve/Client.kt)
- [`src/main/kotlin/com/revieve/Models.kt`](https://github.com/revieve/api.revieve.com/blob/master/sdks/kotlin/src/main/kotlin/com/revieve/Models.kt)
- [`README.md`](https://github.com/revieve/api.revieve.com/blob/master/sdks/kotlin/README.md)

---
## Changelog
Source: src/content/docs/changelog/index.mdx
> Human-readable release notes for the Skin Analysis and Product Recommendations APIs.

Release notes for the Skin Analysis and Product Recommendations APIs. The full per-version OpenAPI history lives in the [API reference](/api/skin-analysis/); this page summarises what changed and why.

For breaking changes, see [Migrations](/migrations/).

## Skin Analysis

- [Version history (3.20.0 → 3.29.1)](/changelog/version-history/) — current release: v3.29.1

_Versions 3.20.0 → 3.29.1 are summarized on the [version history page](/changelog/version-history/). Older OpenAPI documents ship as historical files in the [legacy archive](https://github.com/revieve/api.revieve.com/tree/master/legacy/openapi-archive/cv) and are no longer served as live doc pages._

## Product Recommendations

- v0.9.0 — current

_No notable behaviour changes since the last release. See the [API reference](/api/product-recommendations/)._

---
## 3.25 → 3.29
Source: src/content/docs/migrations/3-25-to-3-29.mdx
> Migration guide for upgrading the Skin Analysis API integration from 3.25.0 to 3.29.1.

The 3.25 → 3.29 transition is **additive** — no payload field that existed in 3.25.0 changes shape or disappears. New components and a new status code arrive; one visualisation scale shifts.

## TL;DR

- Three new components you may want to opt into: `skin_age` (already documented in 3.25), `skin_type`, `hyperpigmentation_adjusted`.
- One new status code: **`99`** (custom-context, undefined failure).
- The `acne_visualization` intensity range widened. If your UI clamps to a hard-coded ceiling, raise it.
- Treat `local_warnings[]` codes as advisory, not contractual: render whatever findings come back, hide the cards that don't.

## Step by step

### 1. Update the spec reference

If you generate clients from the OpenAPI document, point your generator at the new spec:

- Live URL: `https://api.revieve.com/openapi/skin-analysis.yaml`
- Or pin to a commit in this repo.

### 2. Handle the new status code

Add `99` to your status-code map. Treat it as transient and offer the user a retry, the same way you'd handle a 5xx.

```ts
const FATAL_CODES = new Set([100, 101, 102, 201, 206, 99]);
```

### 3. Opt into new components (optional)

Add to your `components` query string only what your UI surfaces:

```diff
-?components=wrinkles,redness,acne,hyperpigmentation
+?components=wrinkles,redness,acne,hyperpigmentation,hyperpigmentation_adjusted,skin_age,skin_type
```

Each new component adds work for the model. Don't request what you don't render.

### 4. Adjust visualization rendering

The `acne_visualization` intensity values are no longer clipped to the previous ceiling. If your renderer normalises intensities, update the divisor; if it clamps to a fixed max, raise that max or switch to `acne_normalized_visualization`.

### 5. Code defensively against `local_warnings[]`

The `local_warnings[]` array is advisory, not a stable contract. Render whatever findings come back; hide the cards whose `local_warnings[]` indicates an unusable result for that metric. Don't hard-fail the whole UI on any specific code.

## What hasn't changed

- The endpoint path and authentication model.
- The `image` requirements (JPEG, ≥ 600×600).
- The shape of `face_area`, top-level `status`, and `inverse_color_balance_rb`.
- The `value` normalisation (`0..1`) on numeric findings.
- The deprecation of `value_raw`.

## Rollback

Set your client back to the 3.25.0 spec. The 3.29 server remains compatible with 3.25-shaped requests; newer components won't appear in the response.