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

# Troubleshooting

## What to collect, always

```text
request_id     from the error body or the X-Request-Id header
job id         if a job was created (job_...)
asset id       if the problem is an upload or a download (asset_...)
operation      e.g. POST /v1/skin/analyses
HTTP status and error.code
timestamps     when you sent it, when you gave up
```

Never attach the image, the API key, or any fragment of the key. If a specific
photograph is genuinely necessary to reproduce a problem, say so and we will arrange
a path for it — do not paste it into a ticket.

## "My upload or submission is rejected"

| You see                        | Cause                                                                                     | Do                                                                                 |
| ------------------------------ | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `401 INVALID_API_KEY`          | header absent or misspelled, a wrong value, or a key that is revoked, expired or disabled | it is `X-API-Key`, on every call; check which base URL and which key you are using |
| `403 INSUFFICIENT_SCOPE`       | the key is usable and lacks this operation's scope                                        | reconfigure the key, do not retry                                                  |
| `413 PAYLOAD_TOO_LARGE`        | the upload is over the size limit                                                         | downscale before sending; you do not need full-resolution capture                  |
| `415 UNSUPPORTED_MEDIA_TYPE`   | not JPEG/PNG/WebP, or bytes that do not match the declared `Content-Type`                 | send the raw bytes with the matching `Content-Type`                                |
| `422 UNPROCESSABLE_IMAGE`      | a recognised image over the dimension or pixel limit, or undecodable                      | re-encode or downscale                                                             |
| `400 IDEMPOTENCY_KEY_REQUIRED` | a submission without `Idempotency-Key`                                                    | send one, derived from your own operation                                          |
| `400 INVALID_REQUEST`          | a malformed JSON body                                                                     | read `error.details[]`: each entry names the `field` and a `code`                  |
| `404 ASSET_NOT_FOUND`          | the asset id is not in this environment, or never existed                                 | check you are submitting the id `POST /v1/assets` returned                         |
| `410 ASSET_EXPIRED`            | the input asset passed its `expires_at`                                                   | upload again                                                                       |
| `402 INSUFFICIENT_CREDITS`     | out of credits                                                                            | top up; retrying will not help                                                     |

A `401` never says *why* the key is unusable, on purpose. If a key that worked
yesterday answers `401` today, check whether it was rotated or revoked on your side
before anything else.

## "The job never leaves queued or running"

First, check you are honouring `Retry-After` — a tight loop can be rate limited
into looking like a stall.

Then: a job stays `queued` when nothing is consuming the queue, and `running` when
the work is genuinely slow (large image, many requested concerns and
visualizations). Reduce `concerns` and `visualizations` to what you display and see
whether the duration changes.

Do **not** resubmit blindly. With the same `Idempotency-Key`, a resubmission returns
the same job and tells you nothing new; without one, it starts a second billable
analysis of the same image.

If a job stays `queued` well past its `Retry-After` hints, contact support with the
job id and the `request_id` of a poll; see [Support](/support).

## "The job failed with INPUT\_UNPROCESSABLE"

This is a job-level failure — HTTP `200`, `"status": "failed"`, `error.code` of
`INPUT_UNPROCESSABLE`: no usable face was found, or the image quality was too low.
Retry once, under a new `Idempotency-Key`, with a single direct, upright capture:

* no reflective glass or a reflected second face crossing the subject;
* the whole face outline visible, with margin at the sides and chin;
* not an edge-to-edge close crop.

Those conditions are supported by fixture evidence. **No numeric threshold exists**
for face size, lighting, pose or crop, and a clear frontal capture can still hit a
known detector gap.

If a compliant retry also fails, stop. Record the job id and `request_id` and ask.
Changing the image repeatedly until one passes produces an integration that works on
your test photo and fails on your customers'.

## "The result looks wrong"

First check the direction: a higher skin score means the concern is **less
visible**. A `9.1` for `acne` is a good reading. An inverted axis in a chart accounts for a good share of "the results look
backwards" reports.

For makeup, check the parity status before treating a difference from another
renderer as a defect: pixel parity is proven for no category today.
[Makeup try-on](/products/makeup)

Then record the job id, `request_id`, operation, the `products` or `concerns` you
requested, the `analysis_version`, and a safe reproduction reference.

## "The result download 404s"

On `GET /v1/assets/{asset_id}/content`, `404 ASSET_NOT_FOUND` means the asset expired,
is not in this environment, or never existed. A result asset expires with its job, at the
`expires_at` it carries, and the `href` is not storage. Fetch what you need promptly
and keep your own copy.

`404 ASSET_CONTENT_UNAVAILABLE` means you asked for the content of an **input** asset.
Only result assets serve content; you already have the input's bytes.

A job poll past the job's `expires_at` answers `404 JOB_NOT_FOUND` the same way.

A `503` on either route is not expiry: it means ask again, after its `Retry-After`.

## "I get 409 on a retry"

`409 IDEMPOTENCY_KEY_REUSED` means the key was reused with a different request body.
That is your code, not ours: the key is a claim that two requests are identical.
Version the key when the content changes. Do not retry it.

`409 IDEMPOTENCY_IN_PROGRESS` is different — it is retryable, it carries a
short `Retry-After` and no job id, and it clears.

## "Submitting a URL fails"

Expected. There is no remote-URL submission, and a submission names only an asset id.
Upload the bytes to `POST /v1/assets` from your own server.

## "I get 429 under load"

`429 TENANT_QUEUE_LIMIT_EXCEEDED` is your own environment's queue being full — submit
less, or spread it out. `429 RATE_LIMITED` is request rate. `503
QUEUE_CAPACITY_EXCEEDED` is ours, not yours.

All three are retryable with `Retry-After`. If you are hitting the first one
steadily, the fix is concurrency control on your side rather than more retries.