# Preview object (/docs/api/concepts/preview-object)



The `preview` object is the partner-facing render surface on every completed content container. Render directly off `preview` — never infer media kind from `assets[]` (which is the underlying file inventory and varies per generator).

Every endpoint that returns a container also returns its preview. Same shape across:

* [`GET /v1/content/:containerId`](/docs/api/reference/content/get-container)
* [`GET /v1/projects/:projectId/content`](/docs/api/reference/content/list-containers) (per row)
* [`GET /v1/projects/:projectId/top-performers`](/docs/api/reference/metrics/top-performers)
* The `content.generated` [webhook payload](/docs/api/operational/webhooks)

## Schema [#schema]

The canonical TypeScript type is `ContentPreview`, exported from `@layers/shared-types` (see `ContentPreviewSchema` for the Zod source of truth):

```ts
type ContentPreview = {
  kind: "slideshow" | "video" | "image";
  primaryUrl: string;            // always populated when preview is non-null
  thumbnailUrl: string | null;   // populated for slideshow + image; populated for video when poster is available
  imageUrls: string[];           // ordered slides for slideshow; [primaryUrl] for image; [] for video
  videoUrl: string | null;       // null for slideshow / image kinds
  hlsUrl: string | null;         // null until HLS pipeline ships
  durationMs: number | null;     // null for non-video kinds
  aspectRatio: string | null;    // colon-form e.g. "9:16"; null on legacy rows
};
```

`preview` is itself nullable on the container response — it returns `null` when the container has no media yet (status is `queued` / `processing`, or `failed` with no partial output). Partners should treat null as "media not ready — poll progress or wait for the webhook" rather than as an error.

| Field          | Type            | Notes                                                                                                                                                                                                       |
| -------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `kind`         | enum            | Discriminator. Pick rendering mode off this.                                                                                                                                                                |
| `primaryUrl`   | string          | The first/hero URL — fine to drop directly into `<img>` or `<video>` based on `kind`.                                                                                                                       |
| `thumbnailUrl` | string \| null  | Poster frame for videos; first slide for slideshows; the image URL itself for `image`. May be null on videos until the poster-frame pipeline ships.                                                         |
| `imageUrls`    | `string[]`      | Ordered slides when `kind = "slideshow"`. Single-element `[primaryUrl]` when `kind = "image"` (so a slideshow gallery component can render both kinds without branching). Empty array for `kind = "video"`. |
| `videoUrl`     | string \| null  | Direct MP4 URL. Populated when `kind = "video"`.                                                                                                                                                            |
| `hlsUrl`       | string \| null  | Adaptive-bitrate HLS manifest URL. Returns `null` until the HLS pipeline ships (post-v1).                                                                                                                   |
| `durationMs`   | integer \| null | Video duration in milliseconds. `null` for slideshow / image kinds.                                                                                                                                         |
| `aspectRatio`  | string \| null  | Ratio in colon-form (e.g. `"9:16"`). Hardcoded to `"9:16"` today — every Layers generator outputs vertical short-form. Becomes a per-container persisted field once square/landscape outputs ship.          |

## Per-kind field population [#per-kind-field-population]

The shape is uniform but only the relevant fields are non-null per `kind`. Defensive renderers should switch on `kind` and pull only the fields below.

| `kind`      | `primaryUrl` | `thumbnailUrl`           | `imageUrls`         | `videoUrl` | `hlsUrl`                 | `durationMs` | `aspectRatio`                    |
| ----------- | ------------ | ------------------------ | ------------------- | ---------- | ------------------------ | ------------ | -------------------------------- |
| `slideshow` | first slide  | first slide              | all slides, ordered | —          | —                        | —            | `"9:16"`                         |
| `video`     | MP4 URL      | poster frame (or `null`) | —                   | MP4 URL    | `null` (until HLS ships) | populated    | `"9:16"` typical, can be `"1:1"` |
| `image`     | image URL    | image URL                | `[primaryUrl]`      | —          | —                        | —            | `"9:16"` typical                 |

For `slideshow-builder` and `ugc-remix` outputs, `aspectRatio` is `"9:16"` (vertical) by canonical convention. Anything else is exceptional and flagged on the [content-items concept page](/docs/api/concepts/content-items).

## URL stability [#url-stability]

All URLs are long-lived CDN paths (R2-backed) — safe to embed in your own UI for the lifetime of the container. Specifically:

* They do **not** carry presigned-URL expirations. (If you ever need a short-lived signed URL, the [`get-asset`](/docs/api/reference/content/get-asset) endpoint can return one.)
* They are stable across re-fetches.
* They are removed when the container is soft-deleted or the project is archived.

## Rendering example [#rendering-example]

```tsx
function ContentTile({ preview }: { preview: Preview }) {
  switch (preview.kind) {
    case "image":
      return <img src={preview.primaryUrl} style={ratio(preview.aspectRatio)} />;
    case "slideshow":
      return (
        <div className="grid grid-cols-3 gap-1" style={ratio(preview.aspectRatio)}>
          {preview.imageUrls!.map((url) => <img src={url} key={url} />)}
        </div>
      );
    case "video":
      return (
        <video
          src={preview.videoUrl!}
          poster={preview.thumbnailUrl}
          controls
          style={ratio(preview.aspectRatio)}
        />
      );
  }
}
```

## See also [#see-also]

* [Content items](/docs/api/concepts/content-items)
* [`GET /v1/content/:containerId`](/docs/api/reference/content/get-container)
* [`GET /v1/projects/:projectId/content`](/docs/api/reference/content/list-containers)
* [Webhooks](/docs/api/operational/webhooks)
