Skip to main content
Building with an AI agent? POST /v1/preview is the fast inner loop: validate for free, glance at a frame or a whole-timeline contact sheet, then only call POST /v1/renders once it looks right.

How preview works

Preview reuses the exact same render pipeline and renderer as POST /v1/renders — same engine, same JSON schema — just restricted to a slice of the timeline and downscaled. What you see in a preview is exactly what the full render produces at that timestamp.
  1. Submit POST /v1/preview with a render_request composition.
  2. Choose what you want:
    • dry_run: true — validate only, no render, returns instantly.
    • at: <seconds> — a single frame.
    • window: { from, to } — a short clip, with audio.
    • contact_sheet: { ... } — a grid of frames sampled across the timeline.
  3. Get a response immediately200 for a dry run, no job (a contact-sheet dry run also includes the sampling plan, still with no job). Any non-dry-run preview — frame, window, or contact sheet — returns 202 with a job queued.
  4. Poll like any renderjob.id (prefix render_) works with GET /v1/renders/{id}. Preview jobs are free: excluded from your usage totals and from GET /v1/renders.

Free validation (dry_run)

Set dry_run: true to run Framelane’s linter without spending a render: duplicate element IDs, dangling or self-referencing transitions, an empty timeline, an element or caption that starts after the composition ends, an entrance motion timed later than the element it belongs to, an audio element hidden with visible: false (which drops the track rather than muting it), a track matte whose source layer is never on screen while the element it drives is, the canvas-space findings (off-frame boxes, colliding text, unreadable sizes and contrast, a drop shadow whose offset rounds to nothing), and — if you pass target_duration_sec — a timeline that misses your target length. A dry run also runs the full translator and reports every feature the real submit would reject — an unsupported field, a motion preset used on the wrong element type, a zero-offset shadow, a word_animation combined with motion[], a transition pointing at a non-video, a dangling group member or matte source. Each arrives as severity: "error" with the same message the 422 would carry, so ok: true means the composition will submit. Run it before every POST /v1/renders.
ok is false only when a violation is severity: "error" — a missed target_duration_sec, for example, is a "warning" and never blocks you.

Dry-run ok vs. submit-path ok

The same two fields appear in two different roles, and only one of them is a gate: The dry run is strictly the more informative of the two, because it runs the full translator: a feature the submit path rejects shows up there as an error, whereas on the submit path that same problem is a 422 and there is no body to attach it to. What the advisory lint adds is the handful of findings a strict translation does not cover — a self-referencing transition, a fully off-canvas element, a sub-pixel drop shadow — reported on the render you just paid for so you are not left comparing the artifact against your intent. If the linter itself fails, the submit still goes through and you get a single LINT_UNAVAILABLE warning with ok: true. That is a server-side fault, not a statement about your composition.

Frame and window previews

Pass at for a single frame, or window: { from, to } for a short clip (audio trimmed and muxed for free). Both render at a small draft width by default (480 px; raise width up to 3840 for a sharper look) and dispatch a real, cheap render job.
Poll job.id via GET /v1/renders/{id} exactly like a full render.
at and window are mutually exclusive. A window.to past the end of the composition is clamped down to the composition’s duration rather than rejected — you may get a shorter clip than requested, never an error. A window.from at or past the composition’s duration is rejected, with 422 invalid_request. An at past the end of the composition is clamped to the last frame, not rejected.

Contact sheets: see the whole edit in one image

Set contact_sheet to get back a single grid PNG sampling frames across the timeline — enough to check an entire edit in one glance (or one vision call), without downloading and scrubbing a video.
Framelane samples at meaningful moments — the first and last frame, where each element starts, each transition, and each caption’s first word — then fills any remaining budget uniformly (bare { "detail": "balanced" } on a 10-second composition budgets ~10 cells; max_cells caps that, shown here at 4 to keep the example short). The sheet field describes the plan immediately (even on a dry_run, before any render happens):
Combine with window to sample only part of the timeline instead of the whole thing. Once the render completes, job.output.url is a single PNG — columns × cellWidth pixels wide, rows × cellHeight tall — with each cell’s frame and burned-in label ("0:04 element_start") matching sheet.cells[i] in order. GET /v1/renders/{id}/poster redirects to a signed URL for the sheet PNG — the embeddable counterpart to /download.

Request reference

Endpoints

Good to know

  • Preview jobs cost nothing and are excluded from GET /v1/renders and GET /v1/workspace/usage — build as many as you need while iterating.
  • A preview is pixel-identical to the equivalent full render at the same timestamp, modulo resolution — the renderer takes no different code path for a preview, so what you approve (once scaled up) is what you’ll get.
  • Preview requests never accept Idempotency-Key — every call creates a new job.
  • If an element references media you haven’t finished uploading yet, a real preview (dry_run: false) returns 422 asset_not_ready; a dry_run: true validation tolerates it and lints the rest of the composition anyway.