Video preview
1. Fetch a single frame
The preview endpoint draws one timeline timestamp through the same pipeline that renders the export — clips, keyframes, effects, captions, colour grade, all of it. What you see is what the MP4 will contain at that moment.
curl "https://api.vidmoat.com/v1/projects/$ID/preview?format=image&at=2.5" \
-H "Authorization: Bearer $VIDMOAT_KEY" \
-o frame.jpgrender.read, not render.write — a preview consumes no export quota, so a read-only key can verify a composition it is not allowed to render.| Query | Meaning |
|---|---|
format | json (default), image or html — they answer different questions, see below. |
at | Timeline timestamp in seconds. Required for format=image; on format=json it adds one rendered frame to the response. |
resolution | full or half. Half is noticeably faster and enough to check layout. |
2. Embed the live composition
format=html returns the whole composition as one self-contained document: your clips, text, captions, effects and keyframes, driven by a single GSAP timeline. No video file is produced and no render quota is touched. Serve it in an iframe and you have a scrubbable preview of the finished video.
It arrives paused at 0. Clip visibility is driven by the timeline, so until you tell the document what time it is, nothing is on screen. That is deliberate — it is what makes deterministic seeking possible — but it means an un-driven composition looks like a black rectangle rather than like a bug.
fontSize and y live — passing them at the top level is silently ignored and every element lands centred on top of everything else.{
"commands": [
{
"op": "addClip",
"type": "video",
"src": "/fixtures/v1/video.mp4",
"start": 0,
"duration": 6,
"trackIndex": 0
},
{
"op": "addTextClip",
"text": "Shipped from the API",
"start": 0.4,
"duration": 3,
"trackIndex": 1,
"style": {
"fontSize": 72
},
"patch": {
"y": -220
}
},
{
"op": "addTextClip",
"text": "No editor. No timeline. One POST.",
"start": 1.2,
"duration": 4.2,
"trackIndex": 1,
"style": {
"fontSize": 34
},
"patch": {
"y": -110
}
},
{
"op": "addCaptions",
"words": [
{
"text": "Every",
"start": 3.4,
"end": 3.7
},
{
"text": "frame",
"start": 3.7,
"end": 4
},
{
"text": "you",
"start": 4,
"end": 4.2
},
{
"text": "see",
"start": 4.2,
"end": 4.45
},
{
"text": "here",
"start": 4.45,
"end": 4.75
},
{
"text": "is",
"start": 4.75,
"end": 4.9
},
{
"text": "real",
"start": 4.9,
"end": 5.4
}
],
"trackIndex": 2
}
]
}const win = document.querySelector('iframe').contentWindow;
win.setCompositionTime(2.5); // seek, and pin every video to its own in-clip time
win.playComposition(); // or let it runsetCompositionTime rather than poking the GSAP timeline directly.tl.seek() suppresses events by default, so the callback that re-evaluates which clips are on screen never fires and anything not already visible at 0 stays hidden — and GSAP has no idea a <video> has a playhead of its own, so the frame under your text would be wrong even when the text was right.format=html is served with Cache-Control: private, no-store and is the right tool for a signed-in web UI you are building on top of Vidmoat — and the wrong one for a server-side pipeline. Use approach 1 there.What the frames actually look like
The four moments below are the same composition at four timestamps, captured with setCompositionTime. They are the frames worth sampling in any project: where something appears, where two elements share the screen, where one track hands over to another, and the last frame before a clip ends.

at=0.8Title only. Check it clears the top safe area.
at=2Title and subtitle together — the overlap moment.
at=4.3Captions running, titles gone.
at=5.8Last frame before the clip ends.3. Preview inline with the edit (MCP)
If you are driving Vidmoat from an agent over https://api.vidmoat.com/api/mcp, do not preview as a separate step. Pass previewAt on the edit itself and the frames come back as images in the same response, so the model sees the consequence of its own edit in one turn:
{
"name": "edit_project",
"arguments": {
"projectId": "…",
"commands": [{ "op": "addTextClip", "text": "Hello", "start": 0, "duration": 3 }],
"previewAt": [0.5, 1.5, 2.5]
}
}preview_frame tool taking projectId and time, for when you want to look without editing.lint[] array — overlapping text, off-canvas elements, font sizes too small to read at output resolution — and it tells you which timestamps to preview. Fix the warnings, verify at the suggested times, then render. The REST equivalent is POST /v1/projects/{id}/validate, which returns the same lint without persisting anything.Which one to use
| You are… | Use |
|---|---|
| A server-side pipeline checking its own output | GET /v1/projects/{id}/preview |
| Building a signed-in web UI on top of Vidmoat | The embedded composition iframe |
| An agent making edits and needing to see them | MCP previewAt on the edit call |
| Shipping a finished video to a user | A real render — previews are not deliverables |
v1 — new fields, new endpoints, new enum values. Ignore unknown fields; a client that rejects them will break on a routine release. Breaking changes get a /v2, and v1 is supported for at least 12 months after one exists. Send Vidmoat-Version: 2026-08-01 to pin date-based behaviour within v1.