Preview object
Canonical "render this in my UI" surface on every content container. One shape across slideshow, video, and image kinds.
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/:containerIdGET /v1/projects/:projectId/content(per row)GET /v1/projects/:projectId/top-performers- The
content.generatedwebhook payload
Schema
The canonical TypeScript type is ContentPreview, exported from @layers/shared-types (see ContentPreviewSchema for the Zod source of truth):
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
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.
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-assetendpoint 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
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
Content items
Containers are the unit of content. Each container holds media, captions, and scheduling metadata for one creative — generated by Layers or uploaded by you.
App media
Per-project brand assets — logo, app screenshots, demo videos, and end-card — the content pipeline uses so generated creative shows the customer's real product instead of hallucinated visuals.