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

# Job outputs

## Submitting

A submit names its input assets and its product parameters, and carries an
`Idempotency-Key`.

```bash
curl -sS -X POST "$BASE_URL/v1/skin/analyses" \
  -H "X-API-Key: $API_KEY" \
  -H "Idempotency-Key: 5d0e1f0c-3b53-4d1f-9d0a-7f4f4c1d2e11" \
  -H "Content-Type: application/json" \
  -d '{
        "inputs": [{"role": "source", "asset_id": "asset_6pa8ad0ds2t7g1z6pg4tcr4zqp"}],
        "concerns": ["acne", "redness", "wrinkles"],
        "visualizations": [{"concern": "acne", "kind": "mask"}]
      }'
```

A makeup try-on has the same `inputs` and an ordered `products` list instead. The order
is the layer order, and each category appears at most once:

```json
{
  "inputs": [{"role": "source", "asset_id": "asset_6pa8ad0ds2t7g1z6pg4tcr4zqp"}],
  "products": [
    {"category": "lip_liner", "color": "#7B2E42", "opacity": 0.8},
    {"category": "lipstick", "color": "#C2185B"}
  ]
}
```

The answer is `202` with the job, `Location: /v1/jobs/{job_id}` and `Retry-After`.

### The key

* A missing key is `400 IDEMPOTENCY_KEY_REQUIRED`; a malformed one is
  `400 INVALID_IDEMPOTENCY_KEY`.
* The same key with the same body answers the **same** job, so a retry after a timeout
  is safe.
* The same key with a different body is `409 IDEMPOTENCY_KEY_REUSED`. A retry that
  arrives while the first use is still being admitted is `409 IDEMPOTENCY_IN_PROGRESS`
  with a short `Retry-After`: send the same request again after it.

Generate one key per logical request and keep it with the request until you have a job
id. [Idempotency](/concepts/idempotency) explains why, and applies unchanged.

## Polling

`GET /v1/jobs/{job_id}` until `status` is `succeeded` or `failed`. Wait the
`Retry-After` the previous answer gave you between reads. Send the previous `ETag` in
`If-None-Match` and an unchanged job answers `304` with no body.

The job's fields obey fixed invariants, and a client can rely on them:

| `status`            | `completed_at` | `output` | `error` |
| ------------------- | -------------- | -------- | ------- |
| `queued`, `running` | null           | null     | null    |
| `succeeded`         | set            | set      | null    |
| `failed`            | set            | null     | set     |

A failed job still answers HTTP `200`. Read `status` before `output`. `error.code` on a failed job is one of `INPUT_UNPROCESSABLE`,
`INPUT_EXPIRED` or `PROCESSING_FAILED`. The first needs a different image, the second
a fresh upload, and the third may succeed on a retry under a new `Idempotency-Key`.

## Decoding the output

`output.type` names the schema. Three types exist today:

* **`makeup.try_on`** carries `revision` and one `image`, a result asset reference.
* **`skin.analysis`** carries `analysis_version`, one entry per requested concern, and
  the requested `visualizations` as result asset references.
* **`hair.try_on`** carries one result `image` Asset plus `applied.color_gradient` with
  normalized `dark` and `light` colors and opacities. See the
  [Hair color try-on guide](/products/hair-try-on) for its inputs and supported
  options.

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

A concern `score` runs from 0 to 10 and **higher means less visible**: 8.7 for acne is a
good reading, not a bad one. A concern the service could not measure says so with
`status: unavailable` and a reason, and never with a score of 0.

Only what you asked for comes back. A skin analysis returns exactly the concerns and
visualizations the request named.

### The union is open

`output` is an open union. A later product adds a new `type`, and your code must keep
working when one arrives: branch on the types you know and keep the rest whole rather
than failing. `inputs[].role` is an open string for the same reason, and any response
object may gain fields.

### Result images

Each result reference's `href` is `/v1/assets/{id}/content` for its own `id`, relative
to the API's base URL. Download it with your `X-API-Key`, as described in
[Assets](/concepts/assets). It expires with the job.

## Usage

`GET /v1/usage?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` reports distinct accepted jobs
per UTC day and operation over `[start_date, end_date)`. A submission that never
reached acceptance is not counted. These are operational counts, not an invoice.