Layers
Partner APIConcepts

Preview object

Canonical "render this in my UI" surface on every content container. One shape across slideshow, video, and image kinds.

View as Markdown

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:

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.

FieldTypeNotes
kindenumDiscriminator. Pick rendering mode off this.
primaryUrlstringThe first/hero URL — fine to drop directly into <img> or <video> based on kind.
thumbnailUrlstring | nullPoster frame for videos; first slide for slideshows; the image URL itself for image. May be null on videos until the poster-frame pipeline ships.
imageUrlsstring[]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".
videoUrlstring | nullDirect MP4 URL. Populated when kind = "video".
hlsUrlstring | nullAdaptive-bitrate HLS manifest URL. Returns null until the HLS pipeline ships (post-v1).
durationMsinteger | nullVideo duration in milliseconds. null for slideshow / image kinds.
aspectRatiostring | nullRatio 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.

kindprimaryUrlthumbnailUrlimageUrlsvideoUrlhlsUrldurationMsaspectRatio
slideshowfirst slidefirst slideall slides, ordered"9:16"
videoMP4 URLposter frame (or null)MP4 URLnull (until HLS ships)populated"9:16" typical, can be "1:1"
imageimage URLimage 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-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

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

On this page