Guides
Render a video
Start from a template and export it. Four steps: pick a workspace, create the project, render, download.
For a custom edit instead of a template, generate it from a prompt or assemble the timeline with the SDK. Export the same way.
#1. Pick a workspace
https://api.rendley.com/v1/workspacesProjects live in a workspace. List yours and keep a workspace_id.
Don't have one? Create an API key
const res = await fetch("https://api.rendley.com/v1/workspaces", {
headers: { Authorization: "Bearer YOUR_API_KEY" },
});
const { data } = await res.json();curl "https://api.rendley.com/v1/workspaces" \
-H "Authorization: Bearer YOUR_API_KEY"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 200
{
"data": [
{
"id": "a52c9d47-8e13-4b06-95fa-3d71e4c8b209",
"name": "My workspace"
}
]
}#2. Find a template
https://api.rendley.com/v1/templatesThe template endpoints are public. Browse them with or without a key, and keep a template’s id.
GET /v1/templates/categories lists the category ids, and GET /v1/templates/{id} returns one template.
const res = await fetch("https://api.rendley.com/v1/templates", {
});
const { data } = await res.json();curl "https://api.rendley.com/v1/templates"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 200
{
"data": [
{
"id": "tpl_xyz...",
"name": "Product demo",
"thumbnail_url": "https://...",
"preview_video_url": "https://...",
"fit_duration": 15,
"width": 1080,
"height": 1920,
"is_premium": false
}
]
}#3. Create a project from the template
https://api.rendley.com/v1/projectsThe response is the new project, including its id. Drop template_id to start from an empty project.
Don't have one? Create an API key
Project name.
The workspace to create the project in. Optional when the account has exactly one workspace.
Template to start from. Omit for an empty project.
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": "a52c9d47-8e13-4b06-95fa-3d71e4c8b209",
"template_id": "tpl_xyz..."
}),
});
const { data } = await res.json();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": "a52c9d47-8e13-4b06-95fa-3d71e4c8b209",
"template_id": "tpl_xyz..."
}'import requests
res = requests.post(
"https://api.rendley.com/v1/projects",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"name": "Launch promo",
"workspace_id": "a52c9d47-8e13-4b06-95fa-3d71e4c8b209",
"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" => "a52c9d47-8e13-4b06-95fa-3d71e4c8b209", "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": "a52c9d47-8e13-4b06-95fa-3d71e4c8b209",
"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: "a52c9d47-8e13-4b06-95fa-3d71e4c8b209", 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"]Response 200
{
"data": {
"id": "3f0d5a4e-3d2a-4c1f-9b3e-2a1c4d5e6f70"
}
}#4. Export
https://api.rendley.com/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 first, post the same body to /v1/export/cost. It answers with { "data": { "credits": 12 } } and charges nothing.
Don't have one? Create an API key
The project to render.
settingsoptionalExport settings.
One of 360p, 480p, 720p, 1080p, 2K, 4K. Defaults to 1080p.
h264 or vp8.
high, medium or low. Higher quality produces larger files.
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": "3f0d5a4e-3d2a-4c1f-9b3e-2a1c4d5e6f70",
"settings": {
"target_resolution": "1080p",
"codec": "h264"
}
}),
});
const { data } = await res.json();curl -X POST "https://api.rendley.com/v1/export" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"project_id": "3f0d5a4e-3d2a-4c1f-9b3e-2a1c4d5e6f70",
"settings": {
"target_resolution": "1080p",
"codec": "h264"
}
}'import requests
res = requests.post(
"https://api.rendley.com/v1/export",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"project_id": "3f0d5a4e-3d2a-4c1f-9b3e-2a1c4d5e6f70",
"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" => "3f0d5a4e-3d2a-4c1f-9b3e-2a1c4d5e6f70", "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": "3f0d5a4e-3d2a-4c1f-9b3e-2a1c4d5e6f70",
"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: "3f0d5a4e-3d2a-4c1f-9b3e-2a1c4d5e6f70", 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"]Response 200
{
"data": {
"job_id": "c41d8f2b-7a63-4e09-bd15-8f3a29c7e604"
}
}Export a project inline
If the project is not stored on Rendley, for example one you serialized from the SDK in the browser, send the JSON as project instead of project_id. Never both.
The render runs on Rendley’s servers, so blob URLs and local files are invisible to it. Every image, video and audio entry in library.media needs a permanentUrl on a public http:// or https:// address. Miss one and the request is rejected with 400 MEDIA_MISSING_PERMANENT_URL. Nothing is queued and no credits are spent.
The SDK sets permanentUrl 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, the project uploads API), then record the result
mediaData.setPermanentUrl("https://cdn.example.com/clips/intro.mp4");
const project = await Engine.getInstance().serialize();
The Storage route sets permanent URLs for you on upload.
#Export a project inline
https://api.rendley.com/v1/exportA serialized project is accepted as-is. Every item in library.media needs a permanentUrl.
URLs fail in two ways, at two different moments:
- Missing or non-HTTP
permanentUrlis caught up front. The request returns400 MEDIA_MISSING_PERMANENT_URLand nothing is queued. - A well-formed URL the worker cannot reach passes the up-front check. The export is accepted and fails later, during polling. Signed URLs that expire between the request and the render land here. See Jobs and polling.
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.
Don't have one? Create an API key
The serialized project JSON. Must include library.media with permanentUrl on each item.
settingsoptionalExport settings.
One of 360p, 480p, 720p, 1080p, 2K, 4K. Defaults to 1080p.
h264 or vp8.
high, medium or low. Higher quality produces larger files.
const 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;curl -X POST https://api.rendley.com/v1/export \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d @project-export.json{
"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" }
}#Poll the export
https://api.rendley.com/v1/jobs/{job_id}Exports are ordinary jobs. Poll until completed and read output.url. See Jobs and polling.
The URL is time limited. Download the file soon after the job completes. If the link has expired, poll again for a fresh one.
Don't have one? Create an 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();curl "https://api.rendley.com/v1/jobs/{job_id}" \
-H "Authorization: Bearer YOUR_API_KEY"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 200
{
"data": {
"status": "completed",
"output": {
"url": "https://cdn.rendley.com/exports/render.mp4?signature=...",
"media_id": "c448b6e3-2b78-4bda-b369-d1afc6aec07f",
"mime_type": "video/mp4",
"size": 4823104
}
}
}