OverviewAuthenticationScopesEndpointsVideo previewErrors & limitsPluginsBuild a platform
Documentation

Video preview

See a frame before you spend a render on it. Three ways, from cheapest to most interactive.
Do not render without having looked at a frame. Positions are numeric offsets from the canvas centre, and text is centre-anchored — two elements with plausible-looking coordinates overlap far more often than you would expect. A preview costs nothing and takes a second or two; a render costs an export from your quota and minutes of wall clock.

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.jpg
Needs render.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.
QueryMeaning
formatjson (default), image or html — they answer different questions, see below.
atTimeline timestamp in seconds. Required for format=image; on format=json it adds one rendered frame to the response.
resolutionfull or half. Half is noticeably faster and enough to check layout.
Where to sample. Preview the first frame of every clip you just added, plus any moment where two elements share screen time. Those are where overlap and off-canvas errors live; the middle of a static shot tells you nothing you did not already know.

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.

Live — this is format=html, not a video
Built from the four commands below and served with no API key — open the raw document and read the source if you want to see exactly what you get back.
The commands that produced it. Note where 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
    }
  ]
}
Drive it from the parent frame. Both handles are on the composition’s own window:
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 run
Use setCompositionTime 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.
Owner-scoped and same-origin.The composition embeds the project’s own media URLs, so 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.
It is a real browser preview, not a proxy for the render.The playhead is driven by the page’s own clock, so what you see depends on your machine keeping up. Frame-accuracy guarantees belong to the render worker, which drives the composition deterministically and never drops a frame.

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.

The demo composition at 0.8 seconds
at=0.8Title only. Check it clears the top safe area.
The demo composition at 2 seconds
at=2Title and subtitle together — the overlap moment.
The demo composition at 4.3 seconds
at=4.3Captions running, titles gone.
The demo composition at 5.8 seconds
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]
  }
}
Up to three timestamps. There is also a standalone preview_frame tool taking projectId and time, for when you want to look without editing.
Read the lint first. Every edit returns a 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 outputGET /v1/projects/{id}/preview
Building a signed-in web UI on top of VidmoatThe embedded composition iframe
An agent making edits and needing to see themMCP previewAt on the edit call
Shipping a finished video to a userA real render — previews are not deliverables
Versioning. Additive changes ship into 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.