Render a video
The fastest way to a finished file is to start from a template and export it. This walkthrough goes end to end: pick a workspace, create a project from a template, render it, and download the result.
To build a custom edit instead of using a template, generate it from a prompt or assemble the timeline with the SDK, then export the same way.
1. Pick a workspace
Projects live in a workspace. List yours and keep a workspace_id.
/v1/workspacescurl https://api.rendley.com/v1/workspaces \
-H "Authorization: Bearer YOUR_API_KEY"
2. Find a template
The template endpoints are public, so you can browse them with or without a key. Keep a template’s id.
/v1/templatescurl "https://api.rendley.com/v1/templates?limit=10"
3. Create a project from the template
/v1/projectscurl -X POST https://api.rendley.com/v1/projects \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Launch promo",
"workspace_id": "WORKSPACE_ID",
"template_id": "TEMPLATE_ID"
}'
The response is the new project, including its id. Drop template_id to start from an empty project instead.
4. Export
Start a render for the project. It returns a job_id right away; the render runs on Rendley’s workers.
/v1/exportcurl -X POST https://api.rendley.com/v1/export \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"project_id": "PROJECT_ID",
"settings": { "target_resolution": "1080p", "codec": "h264" }
}'const res = await fetch("https://api.rendley.com/v1/export", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
project_id: "PROJECT_ID",
settings: { target_resolution: "1080p", codec: "h264" },
}),
});
const { data } = await res.json();
const jobId = data.job_id;settings is optional. To check the credit cost before committing, post the same body to /v1/export/cost first.
Export a project inline
If your project does not live on Rendley — for example you assembled it in the browser with the SDK and serialized it — send the project JSON as project instead of project_id. Pass one or the other, never both.
The SDK sets permanentUrl automatically for media added from a URL. For media added from a File or Blob, upload it yourself and set the URL before serializing:
const mediaId = await Engine.getInstance().getLibrary().addMedia(myFile);
const mediaData = Engine.getInstance().getLibrary().getMediaById(mediaId);
// upload to your own storage (S3, CDN, /v1/uploads, ...), then record the result
mediaData.setPermanentUrl("https://cdn.example.com/clips/intro.mp4");
const project = await Engine.getInstance().serialize();
See Storage for the storage-provider route, which sets permanent URLs for you on upload.
A serialized project is accepted as-is. The important part is library.media — note that every item has a permanentUrl:
{
"project": {
"displayConfig": { "width": 1080, "height": 1920, "backgroundColor": "#000000" },
"timeline": {
"layers": [
{
"id": "be0e5c1d-f029-4b27-87c1-5ae4af00742b",
"clips": [
{
"id": "13fda042-c1ef-4821-a86c-136bc762e900",
"type": "video",
"mediaDataId": "cd446030-3af4-4ff5-b06d-83c76a45fc98",
"startTime": 0,
"duration": 6
}
]
}
]
},
"library": {
"media": [
{
"id": "cd446030-3af4-4ff5-b06d-83c76a45fc98",
"type": "video",
"filename": "intro.mp4",
"permanentUrl": "https://cdn.example.com/clips/intro.mp4"
},
{
"id": "72dbec19-192d-450f-8773-5d0d57d8d87d",
"type": "image",
"filename": "logo.png",
"permanentUrl": "https://cdn.example.com/brand/logo.png"
}
],
"subtitles": []
}
},
"settings": { "target_resolution": "1080p", "codec": "h264" }
}
Post that body to the same endpoint.
/v1/exportcurl -X POST https://api.rendley.com/v1/export \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d @project-export.jsonconst project = await Engine.getInstance().serialize();
// catch what the API would reject with MEDIA_MISSING_PERMANENT_URL
const missing = project.library.media.filter(
(m) => ["image", "video", "audio"].includes(m.type) && !/^https?:\/\//.test(m.permanentUrl ?? "")
);
if (missing.length) {
throw new Error(`Media without a permanent URL: ${missing.map((m) => m.filename).join(", ")}`);
}
const res = await fetch("https://api.rendley.com/v1/export", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
project,
settings: { target_resolution: "1080p", codec: "h264" },
}),
});
const { data } = await res.json();
const jobId = data.job_id;There are two distinct ways a URL can let you down, and they fail at different moments:
- Missing or non-HTTP
permanentUrl— caught up front. The request returns400 MEDIA_MISSING_PERMANENT_URLand nothing is queued. - A URL that is well-formed but unreachable when the worker downloads it — passes the up-front check, so the export is accepted and only fails later, during polling. Signed URLs that expire between the request and the render land here. See Jobs and polling for how that surfaces.
The up-front check covers image, video, and audio entries. Other library types are not validated, but anything the renderer has to download still needs to resolve at render time.
To validate and price a project without committing to it, post the same body to /v1/export/cost — it runs the same check and costs no credits.
5. Poll, then download
Poll the job until it is completed, then read the file URL from result_data. See Jobs and polling for the full loop.
curl https://api.rendley.com/v1/jobs/JOB_ID \
-H "Authorization: Bearer YOUR_API_KEY"
A completed job carries the download link:
{
"data": {
"status": "completed",
"result_data": "{\"storage_url\":\"https://storage.rendley.com/exports/...mp4\",\"media_id\":\"med_...\"}"
}
}
result_data is a JSON string, so parse it before reading storage_url. The link is time limited, so download the file soon after the job completes.