> ## Documentation Index
> Fetch the complete documentation index at: https://api.fanvue.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Working with Media

> Display Vault and chat media efficiently: request the right variants, avoid N+1 lookups, and handle short-lived media URLs the right way.

Almost every app that reads a creator's account needs to show their media: Vault folders, chat attachments, post images. Two things trip people up, and both have a supported answer.

1. The media URL you get back is short-lived, so a URL you stored yesterday may fail to load today.
2. Some list endpoints return media without image URLs, which tempts you into slow, repeated lookups.

This guide shows the intended patterns for both.

## How media and variants work

A media item has a stable `uuid` that never changes, and one or more **variants**. A variant is a specific rendition of the item at a specific size or treatment, each with its own URL.

| Variant             | Use it for                                     |
| ------------------- | ---------------------------------------------- |
| `thumbnail`         | Small previews in lists and grids              |
| `thumbnail_gallery` | Larger gallery previews                        |
| `main`              | The full-size image or video                   |
| `blurred`           | A locked or teaser state for unpurchased media |

The important rule: **you only get variant URLs when you ask for them.** List endpoints return a minimal shape (`uuid`, `status`) by default. Pass the `variants` query parameter to get the full object back, including a `variants` array where each entry has a `url`, `variantType`, `width`, `height`, and `displayPosition`.

<Note>Always filter to ready media with `status=ready`. An item that is still `processing` does not have usable variant URLs yet.</Note>

## Treat media URLs as short-lived

Variant URLs are signed and expire. They are designed to be fetched when you need them, not stored.

<Warning>Do not persist signed media URLs in your database and render them later. By the time a user opens the page, a stored URL may have expired and will return `403` or `404` from the CDN.</Warning>

Store the stable identifiers instead (`mediaUuid`, and for chat media the `messageUuid`), then resolve a fresh URL at display time. The pattern:

<Steps>
  <Step title="Store IDs, not URLs">
    Persist `mediaUuid` (and `messageUuid` for chat attachments). These never change.
  </Step>

  <Step title="Resolve on demand">
    When something scrolls into view, request the variants you need for those IDs.
  </Step>

  <Step title="Cache in memory, briefly">
    Hold the resolved URL in memory for the current view. Do not write it to durable storage.
  </Step>

  <Step title="Refetch on failure">
    If a render fails with `403` or `404`, resolve the URL again rather than retrying the dead one.
  </Step>
</Steps>

## Age verification for media access

Media is sensitive content. In regions where Fanvue is required to verify age, a user must complete **age verification** before your app can read their media. Until then, the media read endpoints return `403`.

This applies to the media read endpoints:

* `GET /media`
* `GET /media/{uuid}`
* `GET /media/bulk`
* `GET /vault/folders/{folderName}/media`

When verification is required and not yet complete, the request fails with `403` and a body that tells you where to send the user:

```json theme={null}
{
  "error": "age_verification_required",
  "message": "Age verification required before accessing this resource. Please visit: https://www.fanvue.com/oauth/age-verification?client_id=YOUR_CLIENT_ID",
  "ageVerificationUrl": "https://www.fanvue.com/oauth/age-verification?client_id=YOUR_CLIENT_ID"
}
```

The `ageVerificationUrl` points to the Fanvue-hosted verification the user completes themselves. Once they have verified, the media endpoints return normally.

<Note>This check is conditional. It only applies to users whose region requires age verification, and only until they have verified. Creators and already-verified users are never affected.</Note>

## Display Vault media efficiently

`GET /vault/folders/{folderName}/media` lists what is in a folder, but it returns lightweight items without variant URLs. Resolving each one by scanning the full media list is the slow path that causes hundreds of calls per folder view.

The supported path is to list the folder's media directly from the media endpoint with variants included. `GET /media` accepts a `folderName` filter, a `variants` list, and `status`:

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.fanvue.com/media?folderName=Campaign%20assets&variants=thumbnail,main&status=ready&page=1&size=50" \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -H "X-Fanvue-API-Version: 2025-06-26"
  ```

  ```python Python theme={null}
  import requests

  def list_folder_media(token, folder, page=1, size=50):
      r = requests.get(
          "https://api.fanvue.com/media",
          params={
              "folderName": folder,
              "variants": "thumbnail,main",
              "status": "ready",
              "page": page,
              "size": size,
          },
          headers={
              "Authorization": f"Bearer {token}",
              "X-Fanvue-API-Version": "2025-06-26",
          },
      )
      r.raise_for_status()
      return r.json()  # { "data": [...], "pagination": {...} }
  ```

  ```javascript Node theme={null}
  async function listFolderMedia(token, folder, page = 1, size = 50) {
    const qs = new URLSearchParams({
      folderName: folder,
      variants: "thumbnail,main",
      status: "ready",
      page: String(page),
      size: String(size),
    });
    const r = await fetch(`https://api.fanvue.com/media?${qs}`, {
      headers: {
        Authorization: `Bearer ${token}`,
        "X-Fanvue-API-Version": "2025-06-26",
      },
    });
    if (!r.ok) throw new Error(`media list failed: ${r.status}`);
    return r.json();
  }
  ```
</CodeGroup>

Each item in `data` comes back with its `variants` array, so you get thumbnail and full-size URLs in one paged response instead of one lookup per item. Page through with `page` and `size` to load a folder.

<Tip>Pick the smallest variant that fits the surface. Render grids and lists with `thumbnail`, and only resolve `main` when the user opens an item full-screen.</Tip>

### The `{folderName}` path segment is a name, not an id

On the folder-scoped endpoints, `{folderName}` is the folder's **exact display name**, URL-encoded, not a folder id or uuid. This includes:

* `GET /vault/folders/{folderName}/media` — list a folder's media
* `DELETE /vault/folders/{folderName}/media/{mediaUuid}` — detach one media item from a folder

The name is matched exactly: it is case-sensitive and scoped to the owning creator. URL-encode any spaces or special characters, so the folder `Folder 1` becomes `Folder%201`. Here `{mediaUuid}` is the media UUID.

<Warning>A `Folder not found` 404 means no folder with that exact name exists for that creator. Check the casing and encoding of the name, not a folder id.</Warning>

## Resolve specific media by ID

When you already hold a set of media UUIDs (for example, IDs you stored earlier), resolve them in a batch rather than one request each.

`GET /media/bulk` takes a comma-separated `mediaUuids` list and a `variants` list:

```bash theme={null}
curl "https://api.fanvue.com/media/bulk?mediaUuids=UUID_A,UUID_B,UUID_C&variants=thumbnail,main" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "X-Fanvue-API-Version: 2025-06-26"
```

This is the right tool for re-resolving fresh URLs for media you are about to show.

## Display chat media reliably

Chat attachments follow the same short-lived-URL rule. Sync and store each message's stable IDs (`messageUuid`, `mediaUuid`), then resolve fresh URLs only for the media a user is actually viewing.

`GET /chats/{userUuid}/messages/{messageUuid}/media` resolves attachments for a single message. It takes `mediaUuids` (comma-separated, up to 20) and an optional `variants` list:

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.fanvue.com/chats/$USER_UUID/messages/$MESSAGE_UUID/media?mediaUuids=UUID_A,UUID_B&variants=thumbnail,main" \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -H "X-Fanvue-API-Version: 2025-06-26"
  ```

  ```python Python theme={null}
  def resolve_chat_media(token, user_uuid, message_uuid, media_uuids, variants="thumbnail,main"):
      r = requests.get(
          f"https://api.fanvue.com/chats/{user_uuid}/messages/{message_uuid}/media",
          params={"mediaUuids": ",".join(media_uuids), "variants": variants},
          headers={
              "Authorization": f"Bearer {token}",
              "X-Fanvue-API-Version": "2025-06-26",
          },
      )
      r.raise_for_status()
      return r.json()
  ```
</CodeGroup>

Call this lazily as media scrolls into view, in batches of up to 20 UUIDs, and cache each resolved URL in memory until you navigate away. If an image 403s, resolve it again.

## Putting it together

* Store IDs, never signed URLs.
* Ask for the `variants` you need, and filter with `status=ready`.
* Use `folderName` on `GET /media` to load a Vault folder with thumbnails in one call per page.
* Use `GET /media/bulk` or the per-message media endpoint to re-resolve URLs on demand.
* Treat `403` and `404` on a media URL as "resolve again", not as a hard error.

Stay mindful of [rate limits](/docs/authentication/rate-limits): resolving lazily, in batches, and only for visible media keeps you well clear of the per-user limit.
