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
/v1/workspacesProjects live in a workspace. List yours and keep a workspace_id.
curl "https://api.rendley.com/v1/workspaces" \
-H "Authorization: Bearer YOUR_API_KEY"const res = await fetch("https://api.rendley.com/v1/workspaces", {
headers: { Authorization: "Bearer YOUR_API_KEY" },
});
const { data } = await res.json();import requests
res = requests.get(
"https://api.rendley.com/v1/workspaces",
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
data = res.json()["data"]$ch = curl_init("https://api.rendley.com/v1/workspaces");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer YOUR_API_KEY"]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true)["data"];package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.rendley.com/v1/workspaces", nil)
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Println(string(b))
}require "net/http"
require "json"
uri = URI("https://api.rendley.com/v1/workspaces")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer YOUR_API_KEY"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)["data"]Response
{
"data": [
{
"id": "wsp_abc...",
"name": "My workspace"
}
]
}#2. Find a template
/v1/templatesThe template endpoints are public, so you can browse them with or without a key. Keep a template’s id.
curl "https://api.rendley.com/v1/templates"const res = await fetch("https://api.rendley.com/v1/templates", {
});
const { data } = await res.json();import requests
res = requests.get(
"https://api.rendley.com/v1/templates",
)
data = res.json()["data"]$ch = curl_init("https://api.rendley.com/v1/templates");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true)["data"];package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.rendley.com/v1/templates", nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Println(string(b))
}require "net/http"
require "json"
uri = URI("https://api.rendley.com/v1/templates")
req = Net::HTTP::Get.new(uri)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)["data"]Response
{
"data": [
{
"id": "tpl_xyz...",
"name": "Product demo",
"thumbnail_url": "https://..."
}
]
}#3. Create a project from the template
/v1/projectsThe response is the new project, including its id. Drop template_id to start from an empty project instead.
curl -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": "wsp_abc...",
"template_id": "tpl_xyz..."
}'const res = await fetch("https://api.rendley.com/v1/projects", {
method: "POST",
headers: { Authorization: "Bearer YOUR_API_KEY", "Content-Type": "application/json" },
body: JSON.stringify({
"name": "Launch promo",
"workspace_id": "wsp_abc...",
"template_id": "tpl_xyz..."
}),
});
const { data } = await res.json();import requests
res = requests.post(
"https://api.rendley.com/v1/projects",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"name": "Launch promo",
"workspace_id": "wsp_abc...",
"template_id": "tpl_xyz..."
},
)
data = res.json()["data"]$ch = curl_init("https://api.rendley.com/v1/projects");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer YOUR_API_KEY", "Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["name" => "Launch promo", "workspace_id" => "wsp_abc...", "template_id" => "tpl_xyz..."]));
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true)["data"];package main
import (
"fmt"
"io"
"net/http"
"bytes"
)
func main() {
body := bytes.NewBufferString(`{
"name": "Launch promo",
"workspace_id": "wsp_abc...",
"template_id": "tpl_xyz..."
}`)
req, _ := http.NewRequest("POST", "https://api.rendley.com/v1/projects", body)
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Println(string(b))
}require "net/http"
require "json"
uri = URI("https://api.rendley.com/v1/projects")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer YOUR_API_KEY"
req["Content-Type"] = "application/json"
req.body = { name: "Launch promo", workspace_id: "wsp_abc...", template_id: "tpl_xyz..." }.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)["data"]Project name.
The workspace to create the project in.
Template to start from. Omit for an empty project.
Response
{
"data": {
"id": "prj_123..."
}
}#4. Export
/v1/exportStart a render for the project. It returns a job_id right away; the render runs on Rendley’s workers.
settings is optional. To check the credit cost before committing, post the same body to /v1/export/cost first.
curl -X POST "https://api.rendley.com/v1/export" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"project_id": "prj_123...",
"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": "prj_123...",
"settings": {
"target_resolution": "1080p",
"codec": "h264"
}
}),
});
const { data } = await res.json();import requests
res = requests.post(
"https://api.rendley.com/v1/export",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"project_id": "prj_123...",
"settings": {
"target_resolution": "1080p",
"codec": "h264"
}
},
)
data = res.json()["data"]$ch = curl_init("https://api.rendley.com/v1/export");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer YOUR_API_KEY", "Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["project_id" => "prj_123...", "settings" => ["target_resolution" => "1080p", "codec" => "h264"]]));
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true)["data"];package main
import (
"fmt"
"io"
"net/http"
"bytes"
)
func main() {
body := bytes.NewBufferString(`{
"project_id": "prj_123...",
"settings": {
"target_resolution": "1080p",
"codec": "h264"
}
}`)
req, _ := http.NewRequest("POST", "https://api.rendley.com/v1/export", body)
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Println(string(b))
}require "net/http"
require "json"
uri = URI("https://api.rendley.com/v1/export")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer YOUR_API_KEY"
req["Content-Type"] = "application/json"
req.body = { project_id: "prj_123...", settings: { target_resolution: "1080p", codec: "h264" } }.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)["data"]The project to render.
settingsExport settings.
e.g. 1080p, 720p, 4k.
e.g. h264, h265.
Response
{
"data": {
"job_id": "job_8f2c..."
}
}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.
#Export a project inline
/v1/exportA serialized project is accepted as-is. The important part is library.media — every item needs a permanentUrl.
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.
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.
curl -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;{
"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" }
}The serialized project JSON. Must include library.media with permanentUrl on each item.
settingsExport settings.
e.g. 1080p, 720p, 4k.
e.g. h264, h265.
#5. Poll, then download
/v1/jobs/{job_id}Poll the job until it is completed, then read the file URL from result_data. See Jobs and polling for the full loop.
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.
curl "https://api.rendley.com/v1/jobs/{job_id}" \
-H "Authorization: Bearer YOUR_API_KEY"const res = await fetch("https://api.rendley.com/v1/jobs/{job_id}", {
headers: { Authorization: "Bearer YOUR_API_KEY" },
});
const { data } = await res.json();import requests
res = requests.get(
"https://api.rendley.com/v1/jobs/{job_id}",
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
data = res.json()["data"]$ch = curl_init("https://api.rendley.com/v1/jobs/{job_id}");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer YOUR_API_KEY"]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true)["data"];package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.rendley.com/v1/jobs/{job_id}", nil)
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
fmt.Println(string(b))
}require "net/http"
require "json"
uri = URI("https://api.rendley.com/v1/jobs/{job_id}")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer YOUR_API_KEY"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)["data"]Response
{
"data": {
"status": "completed",
"result_data": "{\"storage_url\":\"https://storage.rendley.com/exports/...mp4\",\"media_id\":\"med_...\"}"
}
}