How preview works
Preview reuses the exact same render pipeline and renderer asPOST /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.
- Submit
POST /v1/previewwith arender_requestcomposition. - 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.
- Get a response immediately —
200for 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 — returns202with a job queued. - Poll like any render —
job.id(prefixrender_) works withGET /v1/renders/{id}. Preview jobs are free: excluded from your usage totals and fromGET /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
Passat 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.
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
Setcontact_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.
{ "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/rendersandGET /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) returns422 asset_not_ready; adry_run: truevalidation tolerates it and lints the rest of the composition anyway.

