# Media API

The media library: upload images, PDFs and zips, keep alt text current, and delete what nothing should reference any more.

All endpoints need a bearer token (see [Overview & auth](/content/docs/api/index.html)).

## Endpoints

| Method & path | What it does |
| --- | --- |
| `GET /api/v1/media` | The library, newest first. Filters: `q` (file name and alt text), `kind` (`image`, `pdf` or `zip`), `limit` (up to 200; default 200), `offset` (skip that many, for paging through a library bigger than one response). |
| `POST /api/v1/media` | Upload a file as multipart form data: the file under `file`, optional `alt_text` alongside. Refused with `422` when no storage backend is configured, the type isn't an image, PDF or zip, or the file is over its ceiling (10 MB for images, 25 MB for files). |
| `GET /api/v1/media/:id` | One item: hosted URL, content type, kind, size, alt text. |
| `PATCH /api/v1/media/:id` | Update `alt_text` — what an image carries when inserted into email content. |
| `DELETE /api/v1/media/:id` | Deletes the stored file and the row. Already-sent emails referencing the URL keep the reference and lose the file behind it — delete only what nothing should point at any more. |
| `GET /api/v1/media/storage` | Where media serves from: `backend`, `uploads_enabled`, `public_base_url` and `configured`. Credentials never appear here — storage keys are entered in Settings and stay in the instance. |
| `POST /api/v1/media/storage/check` | Round-trips a probe file to prove media actually loads: uploads it, fetches it back over the public host, deletes it. `200` with `ok: true` on success, `422` with a `message` naming the step that broke otherwise. |

## Checking media hosting

A media URL that 404s is frozen into every email referencing it, and already-sent mail can't be repointed — so it's worth proving the host serves before a send rather than after one.

```
curl -X POST https://your-instance.example.com/api/v1/media/storage/check \
  -H "Authorization: Bearer $MIMEO_API_TOKEN"
```

```
{
  "ok": true,
  "message": "files.yourdomain.com served a test file — media hosting is working.",
  "url": "https://files.yourdomain.com/mimeo-host-check/… .txt",
  "storage": {
    "backend": "r2",
    "uploads_enabled": true,
    "public_base_url": "https://files.yourdomain.com",
    "configured": true
  }
}
```

On failure the `message` distinguishes the causes that look alike from the outside: a host that doesn't resolve yet, a certificate still issuing, public access switched off, or a domain connected to the wrong bucket.

## Uploading

```
curl -X POST https://your-instance.example.com/api/v1/media \
  -H "Authorization: Bearer $MIMEO_TOKEN" \
  -F "file=@hero.png" \
  -F "alt_text=The dashboard, mid-campaign"
```

The response is the stored item:

```
{
  "media": {
    "id": 12,
    "filename": "hero.png",
    "url": "https://media.example.com/3f9a.../hero.png",
    "content_type": "image/png",
    "kind": "image",
    "byte_size": 48213,
    "alt_text": "The dashboard, mid-campaign",
    "created_at": "2026-08-02T12:00:00Z"
  }
}
```

## Referencing media in email content

Email bodies are markdown; media inserts by URL. An image goes in as an inline image carrying its alt text, and a PDF or zip as a link:

```

[press-kit.zip](https://media.example.com/81c2.../press-kit.zip)
```

## Errors

| Status | When |
| --- | --- |
| `422` | No storage configured, no `file` part, an unsupported type, an over-ceiling file, or a failed alt-text save. The `error` names the fix. |
| `404` | No media item with that id. |
