Previewing
Check a render payload before you pay for it - see the video in the editor, confirm which replacements applied, and catch typo'd variables and clipped text.
POST /previews resolves a render payload without rendering it. It costs no credits, creates no render job, and takes exactly the same body as POST /renders - so you change one word in the URL to preview instead of render.
curl -X POST https://renderly.video/api/v1/previews \
-H "Authorization: Bearer $RENDERLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"projectId": "clx1234567890",
"replacements": { "guest_name": "Rebecca", "venue_name": "Casa Mira" }
}'{
"success": true,
"data": {
"previewUrl": "https://renderly.video/p/8fK2nQ7xM3vBpL0sYzWc1dRt",
"editUrl": "https://renderly.video/p/8fK2nQ7xM3vBpL0sYzWc1dRt/edit",
"expiresAt": "2026-08-09T12:00:00Z",
"mode": "project",
"quote": { "credits": 1.5, "durationMinutes": 1.2 },
"resolved": { "guest_name": "Rebecca", "venue_name": "Casa Mira" },
"warnings": []
}
}Why you want this
Renderly applies replacements by matching your keys against overlays marked dynamic. A key that matches nothing is ignored - the render succeeds, returns 200, charges credits, and quietly uses the value the template was authored with. The same is true for a font name we can't resolve (it becomes Roboto) and for text a little too wide for its box (the overflow is hidden, so $1,250,000 can render as $1,250,00).
None of that shows up in a render response. All of it shows up here.
Reading the response
resolved - what actually applied
The value that landed on each dynamic variable. This is the fastest way to catch a misspelled key: send venu_name, and resolved still shows the authored default for venue_name.
warnings - what's wrong
Most severe first. Triage on severity, not on the message text:
| Severity | Meaning |
|---|---|
error | The render will be wrong or will fail. Fix before rendering. |
warning | It will render, but not as authored. |
info | Advisory and estimated. Worth a look on the preview. |
| Code | Severity | What happened |
|---|---|---|
UNKNOWN_REPLACEMENT | error | Your key matches no dynamic variable, so it was ignored. Carries a hint with the closest real name. |
MEDIA_UNREACHABLE | error | A URL you supplied didn't respond. Expired presigned links are the usual cause. |
FONT_NOT_RESOLVED | warning | The font family isn't a resolvable Google font and will render as Roboto. CSS stacks like "Inter, sans-serif" are a common cause - use the bare family name. |
MEDIA_URL_REWRITTEN | warning | A media URL was normalized before rendering. |
TEXT_MAY_CLIP | info | Text is estimated to overflow its box. The renderer hides overflow rather than shrinking or adding an ellipsis. |
TEXT_MAY_CLIP is estimated from average character width, which varies by more than 2× across fonts. It's a pointer to an overlay worth looking at, not a verdict - the preview canvas is the ground truth.
previewUrl - watch it
A public page that plays the composition in your browser. No login, nothing rendered, no credits - frames are drawn client-side.
| URL | What you get |
|---|---|
previewUrl | the video with playback controls |
previewUrl?frame=120 | a single frame, full width |
previewUrl?mode=grid | a contact sheet of frames spanning the whole video, each labelled with its timestamp |
previewUrl?mode=grid&frames=0,45,120 | exactly those frames |
Anyone with the link can view it - treat it like a share link. It stops working after 24 hours. Only the account that created the preview can open it in the editor.
editUrl - fix it
Opens the same composition in the Renderly editor as a throwaway copy: editing it never touches the project your code renders. Requires signing in.
Once it looks right, use Copy corrected payload in the preview banner. That gives you back a request body reflecting your edits, ready to paste into your code:
{
"projectId": "clx1234567890",
"replacements": { "guest_name": "Rebecca", "venue_name": "Casa Mira" }
}Without that step, fixing the video in the editor does nothing for the code that produced it.
For AI agents
If an agent is writing your inputProps, it can check its own work before spending a credit. resolved and warnings catch what's machine-detectable; the grid catches what isn't - bad layout, colliding text, a wrong image, colours that don't work.
const { data } = await (await fetch("https://renderly.video/api/v1/previews", {
method: "POST",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: JSON.stringify({ projectId, replacements }),
})).json();
// 1. Machine-readable pass
const errors = data.warnings.filter((w) => w.severity === "error");
// 2. Visual pass - one screenshot shows the whole video
await page.goto(`${data.previewUrl}?mode=grid`);
await page.waitForSelector('[data-preview-ready="true"]');
await page.screenshot({ path: "preview.png", fullPage: true });
// ...then look at preview.png and decide whether to render.Wait for data-preview-ready="true" rather than a fixed delay - it's set once fonts have loaded and the first frame has painted, and fonts are what shift layout.
The page also carries data-preview-mode, data-preview-revision, data-preview-fps and data-preview-duration-frames.
Each rendered frame is wrapped in an element carrying data-preview-frame="<n>" and data-preview-overlays="<count>" — how many overlays are on screen at that frame. That second one resolves an ambiguity you will otherwise hit: a blank capture with data-preview-overlays="0" is a deliberately empty moment between scenes, while a blank capture with a non-zero count means something failed to paint and is worth investigating.
Grid tiles land on scene changes, so they are not evenly spaced in time. If you need a specific moment, ask for it with ?frame=N (N = seconds × fps) rather than picking the nearest grid tile — a neighbouring scene can show a different value for the same variable and give you a confident wrong answer.
Iterating on the same URL
When the frames show a problem, don't create a second preview - update the one you have. PUT /previews/{previewId} replaces the payload and keeps the URL, so you reload the page instead of re-navigating, and anyone already watching the link keeps watching the same one.
let { previewId, previewUrl } = data;
for (let attempt = 0; attempt < 3; attempt++) {
await page.goto(`${previewUrl}?mode=grid`);
await page.waitForSelector('[data-preview-ready="true"]');
await page.screenshot({ path: `attempt-${attempt}.png`, fullPage: true });
// ...look at the screenshot. If it's right, break and render.
const fixed = reviseReplacements(/* what you saw */);
const res = await fetch(`https://renderly.video/api/v1/previews/${previewId}`, {
method: "PUT",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: JSON.stringify({ projectId, replacements: fixed }),
});
const { data: next } = await res.json();
// Confirm the page you screenshot next is your write, not a cached one.
await page.goto(`${previewUrl}?mode=grid`);
await page.waitForSelector(`[data-preview-revision="${next.revision}"]`);
}The body is the same as POST /previews and replaces the payload rather than merging, so send the whole request each time. revision increments on every update — waiting on [data-preview-revision="N"] is how you know you're not looking at a cached earlier render.
An invalid payload is rejected before anything is written, so a bad iteration leaves the working link untouched.
If someone has already opened the preview in the editor, that copy is left alone — they may be mid-edit. The player always shows the latest payload; the editor copy is a snapshot from when it was opened.
Through the MCP server, renderly_preview_render returns the same URLs and costs nothing.
Using it in CI
Because previews are free and rate-limited on your normal request budget, you can gate deploys on them:
const res = await fetch("https://renderly.video/api/v1/previews", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RENDERLY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ projectId, replacements }),
});
const { data } = await res.json();
const errors = data.warnings.filter((w) => w.severity === "error");
if (errors.length) {
console.error("Payload would render incorrectly:", errors);
process.exit(1);
}Running this over a sample of rows before a batch of a few hundred renders catches a bad column mapping for the price of one API call.
From an AI assistant
If you use the Renderly MCP server, the renderly_preview_render tool takes the same arguments as renderly_create_render and costs nothing. Ask your assistant to preview first - it gets back a link you can open and the same warnings, so you can approve the values before any credits are spent.
Limits
- Previews cost no credits and are unlimited. They draw on your plan's normal per-minute request budget.
- A preview expires after 24 hours, after which the link 404s.
- Preview projects are hidden from
GET /projects, the dashboard, and MCP project listings. - Rendering from a preview in the editor charges credits normally.