---
title: "Preview or validate the project head"
method: POST
path: "/v1/projects/{project_id}/preview"
tags: ["projects"]
---

# Preview or validate the project head

`POST /v1/projects/{project_id}/preview`

## Request

- Base URL: `https://api.framelane.io`
- URL: `https://api.framelane.io/v1/projects/{project_id}/preview`
- Auth: HTTP bearer

Validate the project head cheaply, or preview it at a frame or window without a full render. Same free linter and faithful sliced preview as POST /v1/preview, against the stored head instead of an inline request.

## Path parameters

- `project_id` string, required

## Request body

- PreviewOptions: The knobs shared by a standalone preview and a stored-project preview. Everything except *which* composition to preview: an ``at`` frame or a ``window`` clip (mutually exclusive), the draft ``width``, ``dry_run`` for validate-only, and an optional target-duration check.
  - `at` number, nullable: Timeline seconds for a single-frame preview, snapped to the frame grid. Returns an unlabelled PNG, viewable inline via `get_render_preview`; a `window` preview is video and is not. Mutually exclusive with 'window' and 'contact_sheet'.
  - `window` PreviewWindow: A ``[from, to]`` time range, in timeline seconds.
    - `from` number, required: Window start in timeline seconds.
    - `to` number, required: Window end in timeline seconds.
  - `contact_sheet` ContactSheetOptions: Knobs for a contact-sheet preview. Range + cell width come from the parent ``PreviewOptions`` (``window`` / ``width``).
    - `detail` 'overview' | 'balanced' | 'detailed': Cell budget CEILING, not a target: overview 9, balanced 16, detailed 36. Actual count is ~1 cell/second of the sampled range (min 4), snapped up to fill a grid (4s->4, 6s->6), so all tiers match below ~10s.
    - `columns` integer, nullable: Grid columns. Defaults to a near-square layout.
    - `max_cells` integer, nullable: Hard cap on the number of cells, below the tier budget.
  - `width` integer, nullable: Preview width in px; height derives from the aspect ratio. Defaults to a small draft width.
  - `dry_run` boolean: Validate only: run the linter and return violations, no render.
  - `target_duration_sec` number, nullable: Optional target timeline length; emits TARGET_DURATION_MISSED when outside tolerance.
  - `target_tolerance_sec` number

## Response `200`

Validation result (dry-run).

- PreviewResponse
  - `editor_url` string, nullable: Open the previewed project in the authenticated console. Absent for an inline preview.
  - `kind` string, required: 'validation' (dry-run), 'frame', 'window', or 'contact_sheet'.
  - `ok` boolean, required: True when there are no error-severity violations.
  - `violations` Violation[]
    - `code` string, required: Stable machine code, e.g. 'DANGLING_TRANSITION'.
    - `message` string, required: Human-readable explanation.
    - `severity` 'error' | 'warning'
    - `path` string, nullable: Where it happened, e.g. 'elements[2]' or 'transitions[0].from_id'.
    - `pointer` string, nullable: The same place as an RFC 6901 JSON pointer into the request, e.g. '/elements/2/children/0/text_color'.
    - `target_id` string, nullable: Id of the element, frame, instance or group the finding is about; inside an instance, its expanded path (e.g. 'card1/title'). Null when it names none.
    - `bbox` number[], nullable: Canvas-space box [x, y, w, h] in pixels for layout findings (B1); null otherwise.
    - `time` number, nullable: Timeline second at which the finding applies; null for whole-timeline findings.
  - `job` JobOut: A render job or AI task.
    - `id` string, required: Unique job ID. Prefix indicates type: `render_` or `task_`.
    - `kind` 'render' | 'task', required
    - `is_preview` boolean: True for a cheap preview render. Previews are not billed and are excluded from the renders list.
    - `editor_url` string, nullable: Open the project this job was submitted from in the authenticated console. Absent for a job with no project.
    - `task_type` 'transcribe': The AI task types the API offers. Values: - `transcribe` — Speech-to-text transcription (via AssemblyAI) with optional analysis. Returns the transcript inline on the task's `result` object — there is no file artifact, so `output` is null and `/v1/tasks/{id}/download` returns 404.
    - `workspace_id` string, required: ID of the workspace that owns this job.
    - `status` 'ingesting' | 'queued' | 'processing' | 'completed' | 'failed' | 'cancelled', required: Values: - `ingesting` — External source files are being probed for duration before the render is queued. - `queued` — Accepted and waiting for a worker to pick it up. - `processing` — Actively rendering or processing. - `completed` — Finished successfully; the output artifact is available. - `failed` — Terminated with an error (see `error`). - `cancelled` — Cancelled by the caller before completion.
    - `progress_percent` integer: 0–100 progress indicator updated by the render engine.
    - `progress_stage` 'downloading' | 'compositing' | 'encoding' | 'uploading': Values: - `downloading` — Fetching source media. - `compositing` — Compositing the scene on the GPU. - `encoding` — Encoding the output video. - `uploading` — Uploading the finished artifact to storage.
    - `output` JobOutput
      - `url` string, nullable: CDN URL to the completed artifact.
      - `width` integer, nullable: Output video width in pixels.
      - `height` integer, nullable: Output video height in pixels.
      - `duration` number, nullable: Output duration in seconds.
      - `size_bytes` integer, nullable: Artifact file size in bytes.
    - `result` object, nullable: Structured JSON result for tasks that return data rather than a file — `transcribe` returns the transcript here (`text`, `words`, `utterances`, and any requested analysis). Populated once `status == completed`; `null` for renders and for any job whose only artifact is the file at `output.url`.
    - `error` JobError
      - `code` 'invalid_source' | 'source_not_found' | 'source_too_large' | 'asset_not_ready' | 'codec_unsupported' | 'decode_failed' | 'encode_failed' | 'timeout' | 'out_of_memory' | 'gpu_error' | 'upload_failed' | 'provider_error' | 'ingest_failed' | 'render_refused' | 'internal' | 'cancelled', required: Structured error codes surfaced on Job.error_code. Values: - `invalid_source` — A source URL was malformed or inaccessible. - `source_not_found` — A source URL returned 404. - `source_too_large` — A source file exceeded the maximum allowed size. - `asset_not_ready` — A referenced asset was still ingesting when the job ran. - `codec_unsupported` — A source used a codec the renderer cannot decode. - `decode_failed` — A source file could not be decoded. - `encode_failed` — The output could not be encoded. - `timeout` — Processing exceeded the time limit. - `out_of_memory` — The renderer ran out of GPU memory. - `gpu_error` — A GPU hardware/driver error occurred. - `upload_failed` — The output artifact failed to upload to storage. - `provider_error` — An upstream provider (e.g. AssemblyAI) returned an error. - `ingest_failed` — An external source could not be probed (unreachable, not a media file, or timed out). - `render_refused` — The renderer refused the composition by name: a runtime shader that did not compile or ran over its GPU time budget, or an operation, a component or a nested composition it could not apply. `error_message` carries the engine's words; change what it names and submit again. - `internal` — An unexpected internal error occurred. - `cancelled` — The job was cancelled.
      - `message` string, required: Human-readable error description.
    - `metadata` object: Caller-supplied key-value pairs echoed back on every webhook and response.
    - `created_at` string, date-time, required: ISO-8601 UTC timestamp when the job was created.
    - `updated_at` string, date-time, required: ISO-8601 UTC timestamp of the last status change.
    - `completed_at` string, date-time, nullable: ISO-8601 UTC timestamp when the job reached a terminal state.
  - `sheet` ContactSheetPlan: The sampling plan: which frames the sheet will show, and the grid shape. Returned immediately (even on ``dry_run``), so an agent gets the composition- aware sample plan for free before any render.
    - `columns` integer, required
    - `rows` integer, required
    - `width` integer, required: Per-cell width in px.
    - `window` PreviewWindow, required: A ``[from, to]`` time range, in timeline seconds.
      - `from` number, required: Window start in timeline seconds.
      - `to` number, required: Window end in timeline seconds.
    - `cells` ContactSheetCell[]
      - `index` integer, required: Position in the grid, row-major from 0.
      - `at` number, required: Timeline seconds of this frame (snapped to the frame grid).
      - `reason` 'first_frame' | 'element_start' | 'transition' | 'caption' | 'uniform' | 'last_frame', required: Why a contact-sheet cell was sampled -- what the frame is showing. Composition-aware: because we own the timeline, cells land on meaningful moments (an element entering, a transition, a caption starting) instead of blind scene detection, and each carries its reason so the agent knows what it is looking at.
  - `task` object, nullable: For a dry-run (not a contact sheet): the render task this composition translates to, the JSON the render engine parses. It is the lenient translation a preview dispatches: a feature a real submit would refuse is left out and reported in `violations`. Source URLs are as submitted. Absent otherwise.

## Other responses

- `202`: Preview render queued; poll via GET /v1/renders/{id}.
- `401`: Missing or invalid API key.
- `404`: Project not found.
- `422`: Unsupported source.

---

[API](https://skmtc.dev/framelane/apis/framelane-api.md) · [All operations](https://skmtc.dev/framelane/apis/framelane-api/llms.txt) · [OpenAPI document](https://skmtc.dev/framelane/apis/framelane-api/revisions/a223001ca8e5?raw)
