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

# Assets

An Asset is the API's handle for an uploaded input image or a generated result image.
You create **input** Assets by uploading; the service creates **result** Assets when a
job succeeds. Product requests refer to Asset ids, so an image is uploaded once and can
be used by more than one request while it remains available.

Assets do not make an image public. Every call requires a valid API key, and an Asset
belongs to the environment (live or test) whose key created it. Keep the key on your
server. Do not expose Asset ids or content paths to a public client unless your own
application checks that user's access before returning them.

## Upload

Send the image bytes as the whole request body, with the matching `Content-Type`. No
multipart, no JSON wrapper, no base64. The SDKs do this in `assets.upload`.

```bash
curl -sS -X POST "$BASE_URL/v1/assets" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: image/jpeg" \
  --data-binary @face.jpg
```

```json
{
  "id": "asset_6pa8ad0ds2t7g1z6pg4tcr4zqp",
  "purpose": "input",
  "media_type": "image/jpeg",
  "size_bytes": 482113,
  "width": 1200,
  "height": 1600,
  "created_at": "2026-09-24T10:00:00Z",
  "expires_at": "2026-09-25T10:00:00Z"
}
```

The answer is `201` with `Location: /v1/assets/{asset_id}`.

* **Accepted types** are `image/jpeg`, `image/png` and `image/webp`. Another
  `Content-Type`, bytes that are not a recognised image, or bytes of a different type
  than declared are `415 UNSUPPORTED_MEDIA_TYPE`. A recognised image that cannot be
  used -- over the dimension or pixel limit, or undecodable -- is
  `422 UNPROCESSABLE_IMAGE`.
* **Too large** is `413 PAYLOAD_TOO_LARGE`, and it is checked before your key is: an
  oversized body answers 413 even with no valid key.
* **Metadata is removed.** EXIF, XMP and IPTC are stripped and the pixels are rotated to
  their displayed orientation before the asset is stored, so `width` and `height` are
  what a viewer shows.

Hair Color Try-On needs one input Asset, the source image; the API finds the hair
itself. A same-size grayscale hair mask from your application is an optional second
Asset that replaces the API's own segmentation. It does not create a hairstyle.

## Uploads are not deduplicated

`POST /v1/assets` takes no `Idempotency-Key`. Uploading the same bytes twice creates two
assets. That is deliberate: a retried upload after a timeout can leave an unreferenced
asset behind, which costs nothing and expires on its own. Deduplicate at the submit,
where the key is required, not at the upload.

## Reading an asset

`GET /v1/assets/{asset_id}` returns the metadata above for any asset in your
environment. An id that is not yours is `404 ASSET_NOT_FOUND`, whether or not it exists
elsewhere.

`GET /v1/assets/{asset_id}/content` returns the bytes of a **result** asset; the SDKs
call it `assets.getContent` and `assets.get_content`. An input asset answers
`404 ASSET_CONTENT_UNAVAILABLE`: you already have its bytes. The bytes come from the API
itself; the response is never a redirect to object storage, so your `X-API-Key` is the
only credential involved.

## Listing assets

`GET /v1/assets` returns your readable Assets, newest first, one page at a time. Filter
by `purpose` (`input` or `result`), `job_id`, or a `created_gte` / `created_lt` window,
and follow `next_cursor` for the next page. Listing is enabled per environment.

**`TypeScript`**

```ts title="TypeScript"
const page = await client.assets.list({ purpose: "input", limit: 50 });
for (const asset of page.data) console.log(asset.id, asset.expiresAt);

for await (const asset of client.assets.listAll({ purpose: "result", maxItems: 500 })) {
  console.log(asset.id);
}
```

**`Python`**

```python title="Python"
page = client.assets.list(purpose="input", limit=50)
for asset in page.data:
    print(asset.id, asset.expires_at)

for asset in client.assets.list_all(purpose="result", max_items=500):
    print(asset.id)
```

**`REST`**

```bash title="REST"
curl -sS -H "X-API-Key: $API_KEY" \
  "$BASE_URL/v1/assets?purpose=input&limit=50"
```

## Deleting an asset

`DELETE /v1/assets/{asset_id}` deletes one of your **input** Assets and erases its
bytes; it answers `204`. A job already submitted from it keeps its own copy until that
job is deleted or expires. A **result** Asset is deleted with its job, so deleting one
directly is `409 ASSET_OWNED_BY_JOB`: delete the job instead
([Jobs](/concepts/jobs#deleting-a-job)). Deletion is enabled per environment.

**`TypeScript`**

```ts title="TypeScript"
await client.assets.delete("asset_6pa8ad0ds2t7g1z6pg4tcr4zqp");
```

**`Python`**

```python title="Python"
client.assets.delete("asset_6pa8ad0ds2t7g1z6pg4tcr4zqp")
```

**`REST`**

```bash title="REST"
curl -sS -X DELETE -H "X-API-Key: $API_KEY" \
  "$BASE_URL/v1/assets/asset_6pa8ad0ds2t7g1z6pg4tcr4zqp"
```

## Expiry

Every asset carries `expires_at`. Treat it as the last moment you may rely on the asset:
fetch what you need before then, and store the bytes yourself if you need them longer. A
submit that names an expired asset answers `410 ASSET_EXPIRED`; upload again. Result
Assets expire with their Job.

## Keep image handling inside your retention policy

Face images and generated results can identify a person. Tell people how your
application uses them, limit copies to what your product needs, and delete what you no
longer need. Read [Privacy and retention](/concepts/privacy-retention) before storing
inputs or outputs. For Job output shapes, see [Job outputs](/concepts/job-outputs).