# Upload and import your store's media

> Upload images and PDFs, use them on products and in emails, and copy a WordPress media library with the paths it had.

Source: https://www.coritan.com/docs/organizations/commerce/media/

In the dashboard:

- /dashboard/organizations/…/commerce/media: https://www.coritan.com/dashboard/organizations

**Media** holds the images and PDFs your store serves. Each file has an address that anyone can open and that never changes, so a product, an email or another site can link to it. Upload files from your computer, or copy a WordPress site's media library into the store with the paths its files had, so that links to the old site keep working once your storefront takes over its domain.

## Before you begin

- Every member of the organization can open **Media** and look through the files. Uploading files, changing their alt text, deleting them and importing from WordPress need the Admin or Owner role.
- You can upload JPEG, PNG, WebP, AVIF and GIF images of up to 25 MB, and PDFs of up to 50 MB. We read the type from the file itself, so renaming a file does not change it. SVG files cannot be uploaded.
- A store can upload 120 files an hour and start 3 WordPress imports in 24 hours. Its files can take 5 GB in all, unless support set a different limit, as [How much the store can hold](#how-much-the-store-can-hold) explains.

## Find a file

1. In the [dashboard](https://www.coritan.com/dashboard/organizations), open the organization, then **Commerce**, then **Products**, and choose **Media** in the row under it.
2. Search by name or path. Choose **All**, **Images** or **PDFs**, and **Any source**, **Uploaded here** or **From WordPress**.
3. To look in one folder, select it under **Folders**. The row above the files shows the folder you are in. Select **All files** to go back to the top.
4. Choose **Grid** for thumbnails, or **List** for a table with each file's type, size, dimensions, the date it was added and where it came from.
5. Select a file to open it in a panel over the list.

**Files** shows 48 files a page, newest first. The page address keeps the search, the filters, the folder, the layout and the page, so a reload or a shared link opens the list as you left it. **Folders** names only the folders that the files on the current page sit in. To reach another folder, search by its path.

## Upload files

1. Open the folder you want the files in. At **All files**, they go into this month's folder, such as `uploads/2026/10`. The description of the **Files** card names the folder.
2. Select **Upload files…** and choose one or more files, or drop them on the **Files** card.
3. **Uploads** shows each file as it goes up, one at a time. You can leave the page while they upload. Closing the tab stops them, and the browser asks you first. **Stop uploading** stops every file still waiting.
4. Each saved file shows its path. Select **Open** to see it.

A message then says how many files were uploaded, and **Uploads** says why for each file that was not. When the store refuses a file for a reason that applies to the rest, such as the hourly limit, the files after it are not sent. **Clear the list** empties **Uploads** once nothing is uploading.

### Folders and paths

A file's *path* is its folder and its name, such as `uploads/2026/10/front-view.jpg`. Its address is the **Address of the files**, under **Storage and address**, followed by a slash and the path.

- We clean each name the way WordPress does: spaces become hyphens, accents and characters such as `?`, `#` and `&` are dropped, and capital letters stay. `Front View.JPG` is saved as `Front-View.JPG`.
- The extension follows the file's real type, so a PNG image named `photo.jpg` is saved as `photo.png`.
- Paths are case-sensitive: `Photo.jpg` and `photo.jpg` are two files.
- When the folder already has a file with that name, the new one gets `-1`, then `-2` and so on before its extension, such as `front-view-1.jpg`.
- A path never changes, and neither does the file at it. To change a picture, upload the new one and point the products that show the old one at it.
- A folder exists while a file is in it. To upload into a new folder, name it in `folder` through the API ([With the API](#with-the-api)). A folder has at most 10 levels and 255 characters.

## Use a file

### On a product

In the product's **Images** card, select **Add image…**, then **Choose from media** to pick one of the store's images, or **Upload** to add one from your computer. [Add images](/docs/organizations/commerce/products/#add-images) has the steps. A PDF cannot be a product image.

### In an email or on a site

1. Open the file, and select **Copy the address** next to **Address**. **Open the file in a new tab** shows it as anyone will see it.
2. Paste the address where you need it: in an `<img>` tag in the **HTML** of one of the store's [emails](/docs/organizations/commerce/emails/#change-an-email), on your storefront or on another site.

Anyone can open the address without signing in. A PDF opens in the browser, which suits a size guide or a manual.

### Alt text

Alt text describes an image to someone who cannot see it. Open the image, enter its **Alt text**, up to 255 characters, and select **Save alt text**. When you add the image to a product, the product image copies the alt text, unless you give it a **Description** of its own. Changing the alt text later leaves the product images that copied it as they are. A file imported from WordPress brings the alt text it had there.

## Delete a file

1. Open the file. **Products that show it** lists every product whose images or thumbnail link to it.
2. While any product shows it, **Delete file…** is greyed out. Open each product and change or remove that image first.
3. Select **Delete file…**, then **Delete file** to confirm.

The file's address stops working everywhere it is linked, such as in emails and on other sites. Its path is not used again: a new upload with the same name gets a number added, and an import skips the path.

> [!CAUTION]
> Deleting a file cannot be undone. Upload it again to get it back, at a new path.

A product links to a file when one of its images has the file's address, or the file's path on your storefront's own address, such as `https://shop.example.com/wp-content/uploads/2024/05/front.jpg`. Your storefront's own addresses are its **Storefront address** and the **Sites that may call the Store API** in [Settings](/docs/organizations/commerce/settings/#store).

## How much the store can hold

**Storage and address**, under the files, shows how much of its limit the store's files take, and how many files it has. The limit is 5 GB, unless support set a different one for your store. Uploaded and imported files both count.

- At 90% of the limit, **Media** opens with **The store's media is almost full**. At the limit, it opens with **The store's media is full**.
- An upload past the limit is refused with `This file would take the store's media past its 5 GB limit. Remove files you no longer use, or ask support to raise the limit.`
- An import stops at the limit. The files it copied stay, and its report says why it stopped.
- When support lowers the limit below what the store holds, nothing is deleted. Uploads and imports stop until enough files are deleted.

To make room, delete files the store no longer uses. To ask for a higher limit, [open a ticket](https://www.coritan.com/dashboard/support).

## Import from WordPress

An import copies a WordPress site's media library into the store: each file, every smaller copy WordPress made of it, and the original of a large image WordPress scaled down. Each file keeps the path it had on the site, such as `wp-content/uploads/2024/05/photo.jpg`, so its new address ends the way its old one did.

### Before you import

- The site must be online at an `https://` address that anyone can reach. The import reads its media library through the WordPress REST API.
- An import can copy the files of drafts and private posts too. It then signs in to the site as a WordPress user who can edit media, with an application password you create for it.
- Large libraries take a while. The import works on our side, so you can close the dashboard while it runs.

### Create an application password

You need one only to include the files of drafts and private posts, or when the site shows its media library only to signed-in users.

1. Sign in to WordPress as a user who can edit media, such as an administrator.
2. Open Users > Profile, and find Application Passwords.
3. Give the new password a name you will recognise, such as `Coritan import`, and add it.
4. Copy the password. WordPress shows it only once.

We keep the password sealed while the import runs, and erase it when the import ends, however it ends. No page or API answer shows it. Revoke it in WordPress under Users > Profile > Application Passwords once the import has finished.

### Start the import

1. In **Media**, select **Import from WordPress…**.
2. Enter the **Site address**, such as `https://example.com`, or `https://example.com/blog` for a site in a folder.
3. Optionally, tick **Also scan the site's pages for files**. The import then also reads the home page and the pages the site's sitemap lists, 500 pages at most, and copies the files they link to that the media library does not list.
4. To include the files of drafts and private posts, tick **Include files attached to unpublished pages**. Enter the **WordPress user name** and the **Application password**.
5. Select **Start import**.

The import's report opens and follows the import on its own. While the import runs, **Media** shows its progress at the top. **See its progress** opens the report, and **Show the files it has copied** refreshes the list. A store runs one import at a time, so **Import from WordPress…** waits while another import runs.

### Read the report

**WordPress imports**, under the files in **Media**, lists the store's imports, newest first, each with its status: **Waiting to start**, **Importing**, **Finished**, **Failed** or **Cancelled**. Select one to open its report. **All media** goes back to the list.

While the import runs, the report shows **Waiting to start**, then **Copying the media library** with how many media items it has read, then **Scanning the site's pages** when the page scan is on. Each file shows in **Media** as soon as it is copied.

The counts are **So far** while the import runs, **What it did** once it has finished, and **What it did before it stopped** when it failed or was cancelled:

| Count | What it counts |
| --- | --- |
| **Media items read** | The items of the media library it has read. An item holds several files: the upload, each smaller copy and the original of a scaled image. |
| **Files copied** | The files it put in the store. |
| **Already in the store** | The files it skipped because the store has them already, from the same address or with the same content, and the files you deleted from the store. |
| **Could not copy** | The files it left out, listed under **Files it could not copy**. |
| **Copied in all** | The size of the files it copied. |
| **Pages scanned** | With the page scan, the pages it read. |
| **Pages it could not read** | With the page scan, the pages that did not answer. |

**Files it could not copy** lists each file it left out, with its address, the path it would have had and why. Choose a reason under **Every reason** to see one kind, search the list, or select **Download as CSV** to save all of them. The report lists the first 1,000 files and counts the rest.

| Reason | API `reason` | What it means |
| --- | --- | --- |
| **Not on the site** | `not_found` | The site has no file at that address. |
| **The site answered with an error** | `status` | The site answered with an error for that file. Run the import again later. |
| **Over 50 MB** | `too_large` | The file is larger than an import copies. |
| **Not a file the store takes** | `type` | The file is a video, a sound, a document other than a PDF, or a file that cannot be opened. |
| **Empty file** | `empty` | The site sent an empty file. |
| **The site was too slow** | `timeout` | The site took too long to send the file. Run the import again. |
| **The site could not be reached** | `unreachable` | The connection failed. Run the import again later. |
| **Sent to another address** | `redirect` | The file redirects to another site, or to an `http://` address. |
| **Private address** | `refused` | The file is on a private network, or on a port the import does not use. |
| **Path already taken** | `path_taken` | The store already has a different file at that path, and keeps it. |
| **Outside the uploads folder** | `outside_uploads` | The file is not in the site's `wp-content/uploads` folder. |
| **Path not allowed** | `bad_path` | The path is too long, or holds characters a path cannot hold. |

To get a file the import left out, upload it yourself.

### What the import copies and what it cannot see

The import copies every file the media library lists, with each smaller copy and the original of a scaled image, and the alt text each image has in WordPress. It copies files of up to 50 MB: images, PDFs and SVG images. An SVG image is served so that no script in it can run. It cannot see:

- The files of drafts and private posts, unless it signs in with an application password.
- Files that are in `wp-content/uploads` but not in the media library, such as those a plugin put there, unless the page scan is on and a page links to them. The scan reads the home page and the pages listed by the sitemap at `/wp-sitemap.xml`. When the site has no sitemap there, the scan reads only the home page, and the report says so.
- Anything outside `wp-content/uploads`, such as a theme's images. A media library item kept somewhere else, for example by a plugin that moves uploads to cloud storage, is listed as **Outside the uploads folder**.
- Files on another site. The import does not follow a file that redirects to another site, other than the same site with or without `www.`.

The page scan counts a link through Jetpack's image CDN or ExactDN, or a link to the site on `www.` or over `http://`, as a link to the file on the site, and copies the file from the site.

### Run it again or cancel it

- **Run the import again…**, on the report of an import that has ended, opens the import with the same settings. Enter the application password again, since we never show it. The new import copies only what is new: it skips each file the store has from the same address or with the same content, and a file you deleted stays deleted.
- **Cancel import…**, on the report of a running import, stops it. Select **Cancel import** to confirm, or **Keep importing**. The files it copied stay in **Media**, and running it again later copies the rest.

## Keep old links working

Once the store has your WordPress files, your storefront can serve them at their old addresses. Links to them in old emails, pages and feeds then keep working after your storefront takes over the site's domain.

1. In **Media**, copy the **Address of the files** under **Storage and address**, such as `https://api.coritan.com/media/stores/42`.
2. On your storefront, rewrite each request for a path under `/wp-content/uploads/` to that address, with the same path after it. Do the same for `/uploads/`, so the files you upload in **Media** show on your domain too.
3. Before you move the domain, open a few old file addresses on the storefront's current address, and check that each shows the same file as WordPress does.

With the rewrites, `https://example.com/wp-content/uploads/2024/05/photo.jpg` serves the file at `https://api.coritan.com/media/stores/42/wp-content/uploads/2024/05/photo.jpg`. A storefront on Next.js, such as the [starter](/docs/organizations/storefront/commerce-sdk/#start-from-the-next-js-starter), adds them to its `next.config.ts`:

```typescript
const MEDIA = "https://api.coritan.com/media/stores/42";

const nextConfig: NextConfig = {
  async rewrites() {
    return [
      { source: "/wp-content/uploads/:path*", destination: `${MEDIA}/wp-content/uploads/:path*` },
      { source: "/uploads/:path*", destination: `${MEDIA}/uploads/:path*` },
    ];
  },
};
```

Keep the WordPress site online at another address until the move is done, so you can run the import again for anything it missed.

## Result

- Each file you upload or import is in **Media**, at an address that anyone can open and that never changes.
- A product image chosen from **Media** shows on the storefront from that address.
- Each import's report shows what it copied, and lists every file it could not copy with the reason.

## Troubleshooting

**Upload files…** and **Import from WordPress…** are missing
: Your role is below Admin, so **Media** is read-only for you. Ask an owner or admin.

`SVG files cannot be uploaded.`
: Save the image as PNG or WebP and upload that. An import copies the SVG images a WordPress media library lists.

`That file type is not supported.`
: Upload a JPEG, PNG, WebP, AVIF or GIF image, or a PDF. We read the type from the file's content, so a file renamed to `.jpg` is refused when it is not an image.

`The image is 31.2 MB. An image can be at most 25 MB.`
: Make the image smaller and upload it again. A PDF can be up to 50 MB.

`A store can upload 120 files an hour. Try again later.`
: The store reached the hourly limit, and the files after it were not sent. Upload them again later.

`This file would take the store's media past its 5 GB limit.`
: Delete files the store no longer uses, or [open a ticket](https://www.coritan.com/dashboard/support) to ask for a higher limit, as [How much the store can hold](#how-much-the-store-can-hold) explains.

**Delete file…** is greyed out
: A product shows the file. **Products that show it** names each one: change or remove its image, then delete the file.

`No WordPress media library answered at that address. Check the site address.`
: Check the **Site address**. Give the address of the WordPress site itself, with its folder if it has one, such as `https://example.com/blog`.

`The site lists its media library only to a signed-in user.`
: Run the import again with **Include files attached to unpublished pages** ticked, and a user name and [application password](#create-an-application-password).

`WordPress refused the username and application password, or that user cannot edit media.`
: Check the user name and the password, and that the user can edit media. Create a new application password if you are not sure of it.

`The site sends its media library to example.org. Use https://example.org as the site address.`
: The site's address has changed. Start the import with the address the message gives.

`Another import is running. One import runs at a time; start this one when it finishes.`
: Wait for that import to end, or cancel it from its report.

`A store can start 3 media imports in 24 hours. Try again later.`
: Wait, then run the import again. It copies only what is new.

The import stopped at the store's limit
: Delete files the store no longer uses, or ask support to raise the limit, then select **Run the import again…**. It skips the files it has already copied.

An old link shows nothing after the move
: Check that the storefront rewrites the path to the **Address of the files**. Then search **Media** for the path: if the file is not there, look for it under **Files it could not copy** in the import's report.

## Related

- [Add and edit your store's products](/docs/organizations/commerce/products/)
- [Write the emails your store sends](/docs/organizations/commerce/emails/)
- [Import products, customers, promotions and orders](/docs/organizations/commerce/imports/)
- [Sell with the Commerce API](/docs/organizations/storefront/commerce-api/)

## With the API

Every route is under `https://api.coritan.com/api/v1/orgs/{org_slug}/commerce/media`. Reading the files and the imports takes the `commerce.catalog:read` scope, or any member's token. Uploading, changing alt text, deleting and importing take `commerce.media:write`, or an owner's or admin's token. `commerce.media:write` does not include reading, so a key that also lists the files needs both scopes.

Upload a file with `POST /commerce/media`, as `multipart/form-data` with the field `file`. `folder` is optional: without it, the file goes into this month's `uploads/YYYY/MM`, and an empty value puts it at the top. `alt` is optional too.

```bash
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/media" \
  -H "Authorization: Bearer $CORITAN_TOKEN" \
  -F "file=@front-view.jpg" \
  -F "folder=products/2026" \
  -F "alt=The front, with the two network ports on the left"
```

The answer is `201` with the file:

```json
{
  "media": {
    "id": "media_01j9z8q4n6xk3v7d2m5r8t1wqa",
    "path": "products/2026/front-view.jpg",
    "url": "https://api.coritan.com/media/stores/42/products/2026/front-view.jpg",
    "content_type": "image/jpeg",
    "bytes": 482113,
    "width": 1600,
    "height": 1200,
    "alt": "The front, with the two network ports on the left",
    "source": "upload",
    "source_url": null,
    "created_at": "2026-10-07T09:12:44Z"
  }
}
```

- `GET /commerce/media` lists the files, newest first. It takes `q` (the path holds this text, in any case), `type` (`image` or `pdf`), `source` (`upload` or `wordpress`), `folder` (that folder and the folders inside it), `limit` (1–200, 50 by default) and `offset`. The answer has `media`, `count`, `usage` with the store's `bytes`, `files` and `quota_bytes`, and `base_url`, the **Address of the files**.
- `GET /commerce/media/{media_id}` answers the file and `used_by`, the products whose images use it, each with `product_id` and `title`.
- `PATCH /commerce/media/{media_id}` with `{"alt": "..."}` changes the alt text. `null` or an empty value clears it. Nothing else about a file can change.
- `DELETE /commerce/media/{media_id}` deletes the file and answers `204`.
- `POST /commerce/products/{product_id}/images` takes `media_id` in place of `url`, as [Add products](/docs/organizations/storefront/commerce-api/#add-products) describes. The image then links to the file's `url`, and takes its width, height and alt text unless you send them.

Start an import with `POST /commerce/media/imports`. Send `username` and `application_password` together, or neither:

```bash
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/media/imports" \
  -H "Authorization: Bearer $CORITAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"source": "wordpress", "site_url": "https://example.com", "scan_pages": true,
       "username": "alex", "application_password": "'"$WP_APP_PASSWORD"'"}'
```

The answer is `202` with the `import`, whose `status` is `queued`. Follow it with `GET /commerce/media/imports/{import_id}` until `status` is `done`, `failed` or `canceled`. The import has the counts the report shows (`items_seen`, `files_copied`, `files_skipped`, `files_failed`, `bytes_copied`, `pages_scanned`, `pages_failed`), `error` when it failed, and `failures`, each with its `url`, `path`, `reason` and `message`. `failures_more` counts the failures beyond the first 1,000. `GET /commerce/media/imports` lists the imports without their failures, and takes `status`, `limit` (1–100, 20 by default) and `offset`. `POST /commerce/media/imports/{import_id}/cancel` stops a queued or running import.

| Status | `error` | When |
| --- | --- | --- |
| `413` | `quota_exceeded` | The file would take the store past its limit. `quota_bytes` and `used_bytes` give the numbers. |
| `413` | `payload_too_large` | An image is over 25 MB, or a PDF over 50 MB. |
| `415` | `unsupported_media_type` | The file is not a JPEG, PNG, WebP, AVIF, GIF or PDF, it is an SVG, or the body is not `multipart/form-data`. |
| `409` | `in_use` | Products use the file you are deleting. `products` lists them. |
| `409` | `import_in_progress` | Another import of the store is queued or running. `import_id` names it. |
| `409` | `import_ended` | The import you are cancelling has already ended. |
| `422` | `invalid` | A field the route cannot use, such as a `site_url` that is not `https://`. `field` names it. |
| `429` | `rate_limited` | The store uploaded 120 files in the last hour, or started 3 imports in 24 hours. `Retry-After` says when to try again. |
| `503` | `storage_unavailable` | Storage cannot take files right now. Try again in a few minutes. |

## API

- `GET /api/v1/orgs/{org_slug}/commerce/media`: The store's media, newest first (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-media)
- `POST /api/v1/orgs/{org_slug}/commerce/media`: Upload an image or a PDF to the store's media, and answer 201 with the file (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-media)
- `GET /api/v1/orgs/{org_slug}/commerce/media/imports`: The store's media imports, newest first, without their lists of failures (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-media-imports)
- `POST /api/v1/orgs/{org_slug}/commerce/media/imports`: Start media import (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-media-imports)
- `GET /api/v1/orgs/{org_slug}/commerce/media/imports/{import_id}`: One media import: its state, its counts and the files it could not copy (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-media-imports-import-id)
- `POST /api/v1/orgs/{org_slug}/commerce/media/imports/{import_id}/cancel`: Cancel a queued or running import, and answer it (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-media-imports-import-id-cancel)
- `GET /api/v1/orgs/{org_slug}/commerce/media/{media_id}`: One file of the store's media, and the products whose images use it (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-media-media-id)
- `PATCH /api/v1/orgs/{org_slug}/commerce/media/{media_id}`: Change a file's alt text, and answer the file (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-media-media-id)
- `DELETE /api/v1/orgs/{org_slug}/commerce/media/{media_id}`: Delete a file from the store's media, and answer 204 (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-delete-api-v1-orgs-org-slug-commerce-media-media-id)
