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

# Authentication

Every published operation takes the same credential:

```http
X-API-Key: <your key>
```

There is one scheme and no alternative, which means there is no decision for you
to make and one code path for each of us to get right. The contract names it
`ApiKeyAuth`; the API reference on this site shows it on every operation.

## The one rule that matters more than the rest

**The key is a server-side secret.** It authenticates your account, it spends your
credits, and anything that holds it can do everything you can do.

So it must never reach a browser bundle, a mobile binary, a mobile app's
configuration, a public repository, a client-side environment variable, or a URL
query string. If you are building a front end, the supported shape is your own
backend holding the key and exposing a narrow endpoint of its own:
[Serving a browser or mobile client](/guides/bff-pattern).

We do not publish a browser snippet for this API. Plenty of API quickstarts do,
because it demos beautifully, and the result is keys in published bundles.

## Live and test keys

A key belongs to one environment of your account. A key that starts with `pk_live_`
runs real analyses and spends credits. A key that starts with `pk_test_` runs the same
lifecycle with fixture results and charges nothing; it can never read live jobs or
assets, and a live key can never read test ones. See [Test mode](/getting-started/test-mode).
Create and revoke keys on the **API keys** page of the
[Platform](https://platform.morphiaone.ai/login); see [Get access](/getting-started/access).

## Scopes

A key carries scopes. Each product submission requires that product's scope, and
cancelling or deleting a job requires the scope of the job's product. This table is
generated from the API contract:

| Operation                           | Requires                          |
| ----------------------------------- | --------------------------------- |
| `GET /v1/assets`                    | any valid key for the environment |
| `POST /v1/assets`                   | any valid key for the environment |
| `DELETE /v1/assets/{asset_id}`      | any valid key for the environment |
| `GET /v1/assets/{asset_id}`         | any valid key for the environment |
| `GET /v1/assets/{asset_id}/content` | any valid key for the environment |
| `GET /v1/credits/balance`           | any valid key for the environment |
| `POST /v1/hair/try-ons`             | `hair.apply`                      |
| `GET /v1/jobs`                      | any valid key for the environment |
| `DELETE /v1/jobs/{job_id}`          | the scope of the job's product    |
| `GET /v1/jobs/{job_id}`             | any valid key for the environment |
| `POST /v1/jobs/{job_id}/cancel`     | the scope of the job's product    |
| `POST /v1/makeup/try-ons`           | `makeup.apply`                    |
| `POST /v1/skin/analyses`            | `skin.analyze`                    |
| `GET /v1/usage`                     | any valid key for the environment |

A key's available scopes depend on its environment and the grants selected when it is
created. `INSUFFICIENT_SCOPE` means the key is valid but does not include the grant this
product operation requires.

Uploads, polling and result reads are authenticated but not scope-gated. A key that
could submit work but not read the result it created would be useless, and a scope
every key must hold is not a permission — it is noise.

## What a rejection tells you

| Status | Code                     | What it means                                                                   | Retry?                                                    |
| ------ | ------------------------ | ------------------------------------------------------------------------------- | --------------------------------------------------------- |
| 401    | `INVALID_API_KEY`        | no usable key: missing, unrecognised, revoked, expired, or its account disabled | no — fix the credential                                   |
| 403    | `INSUFFICIENT_SCOPE`     | valid key, not allowed this operation                                           | no — reconfigure                                          |
| 403    | `API_KEY_IP_NOT_ALLOWED` | valid key, called from an address outside its network allowlist                 | no — call from an allowed address or change the allowlist |
| 503    | `SERVICE_UNAVAILABLE`    | we could not check the key                                                      | **yes**, after `Retry-After`                              |

The first row is deliberately one code: a credential that cannot be used answers
`401 INVALID_API_KEY` whatever the reason, and only a usable key without the
operation's scope, or called from outside its allowlist, is a `403`. Branch on the status, and check the key you configured.

The last row is the one to handle deliberately. It is a `503` and not a `401`
because your credential was never judged — nothing is wrong with it. A client that
treated it as an authentication failure would discard a working key during an outage
of ours, which is precisely the wrong reaction.

Full list, generated from the code that raises them:
[Error codes](/api-reference/errors).

## Isolation

Assets and jobs belong to the environment whose key created them. A valid key from
another account, or from your own other environment, polling one of your job ids gets
`404 JOB_NOT_FOUND`, and reading one of your asset ids gets `404 ASSET_NOT_FOUND` — the
same answers as an id that never existed.

That is not vagueness; `403` there would confirm the resource exists and turn its id
into a lookup oracle.

## Rotation and hygiene

* Read the key from your secret manager or environment at process start; do not
  commit it, and do not bake it into an image.
* Rotate on any suspicion of exposure, and on staff changes. Plan for two keys to
  be valid at once during a rotation so you are never down to zero.
* Log the `request_id` from a failed call. Never log the key, and never log it
  partially — a prefix plus a length is a meaningful head start for an attacker.