Quickstart
Build a video from a set of images and download the final MP4.
1. Create an API key
Create one under Settings -> API Keys and copy it.
2. Start the edit
Send your prompt and public URLs for your images. The response returns immediately, before the edit runs.
const API_KEY = "YOUR_API_KEY";
const API = "https://api.rendley.com/v1";
const headers = {
"Authorization": "Bearer " + API_KEY,
"Content-Type": "application/json",
};
const response = await fetch(`${API}/agent`, {
method: "POST",
headers,
body: JSON.stringify({
prompt: "Turn these images into a 9:16 carousel video, one image per slide, with a smooth transition between each.",
files: [
{ url: "https://images.pexels.com/photos/15943144/pexels-photo-15943144.jpeg" },
{ url: "https://images.pexels.com/photos/18835565/pexels-photo-18835565.jpeg" },
{ url: "https://images.pexels.com/photos/25713111/pexels-photo-25713111.jpeg" },
],
}),
});
if (!response.ok) {
throw new Error("Could not start the edit: " + response.status);
}
const body = await response.json();
const job = body.data;
console.log("Job:", job.job_id, "Project:", job.project_id);curl -X POST https://api.rendley.com/v1/agent \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Turn these images into a 9:16 carousel video, one image per slide, with a smooth transition between each.",
"files": [
{ "url": "https://images.pexels.com/photos/15943144/pexels-photo-15943144.jpeg" },
{ "url": "https://images.pexels.com/photos/18835565/pexels-photo-18835565.jpeg" },
{ "url": "https://images.pexels.com/photos/25713111/pexels-photo-25713111.jpeg" }
]
}'import requests
API_KEY = "YOUR_API_KEY"
API = "https://api.rendley.com/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
response = requests.post(
f"{API}/agent",
headers=HEADERS,
json={
"prompt": "Turn these images into a 9:16 carousel video, one image per slide, with a smooth transition between each.",
"files": [
{"url": "https://images.pexels.com/photos/15943144/pexels-photo-15943144.jpeg"},
{"url": "https://images.pexels.com/photos/18835565/pexels-photo-18835565.jpeg"},
{"url": "https://images.pexels.com/photos/25713111/pexels-photo-25713111.jpeg"},
],
},
)
response.raise_for_status()
job = response.json()["data"]
print("Job:", job["job_id"], "Project:", job["project_id"])Two ids come back. Poll with job_id. Export with project_id. Keep both.
3. Wait for it to finish
Poll the job until status reads completed, failed or canceled. This endpoint long-polls, holding the connection open while it waits. Sleep a few seconds between calls anyway, so a fast response cannot put you in a tight request loop.
The polling helpers used below
const API_KEY = "YOUR_API_KEY";
const API = "https://api.rendley.com/v1";
const headers = {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
};
// Both endpoints share these three terminal statuses. They differ only in
// their in-progress names: queued/processing vs pending/running.
const TERMINAL = ["completed", "failed", "canceled"];
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Generation and export jobs. The finished job carries output.url.
async function waitForJob(jobId, interval = 5000) {
let job = null;
while (job === null || !TERMINAL.includes(job.status)) {
await sleep(interval);
const response = await fetch(`${API}/jobs/${jobId}`, { headers });
if (!response.ok) {
throw new Error("Job lookup failed: " + response.status);
}
const body = await response.json();
job = body.data;
console.log("Job status:", job.status);
}
if (job.status !== "completed") {
throw new Error("Job " + job.status + ": " + (job.error || ""));
}
return job;
}
// Agent jobs. The finished job carries project_id, thread_id and
// last_message, but no output: export the project to get a file.
// onPause runs when an interactive run stops to ask something.
async function waitForAgentJob(jobId, interval = 5000, onPause) {
let job = null;
while (job === null || !TERMINAL.includes(job.status)) {
// This endpoint long-polls. Sleep anyway, so a fast response
// cannot turn this into a tight request loop.
await sleep(interval);
const response = await fetch(`${API}/agent/jobs/${jobId}`, { headers });
if (!response.ok) {
throw new Error("Job lookup failed: " + response.status);
}
const body = await response.json();
job = body.data;
console.log("Edit status:", job.status);
// Only interactive runs reach this; unattended runs never pause.
if (job.status === "waiting_input" && onPause) {
await onPause(job);
}
}
if (job.status !== "completed") {
throw new Error("The edit did not finish: " + (job.error || job.reason));
}
return job;
}import time
import requests
API_KEY = "YOUR_API_KEY"
API = "https://api.rendley.com/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
# Both endpoints share these three terminal statuses. They differ only in
# their in-progress names: queued/processing vs pending/running.
TERMINAL = {"completed", "failed", "canceled"}
def wait_for_job(job_id, interval=5):
"""Generation and export jobs. The finished job carries output.url."""
job = None
while job is None or job["status"] not in TERMINAL:
time.sleep(interval)
response = requests.get(f"{API}/jobs/{job_id}", headers=HEADERS)
response.raise_for_status()
job = response.json()["data"]
print("Job status:", job["status"])
if job["status"] != "completed":
raise RuntimeError(f"Job {job['status']}: {job.get('error', '')}")
return job
def wait_for_agent_job(job_id, interval=5, on_pause=None):
"""Agent jobs. The finished job carries project_id, thread_id and
last_message, but no output: export the project to get a file.
on_pause runs when an interactive run stops to ask something.
"""
job = None
while job is None or job["status"] not in TERMINAL:
# This endpoint long-polls. Sleep anyway, so a fast response
# cannot turn this into a tight request loop.
time.sleep(interval)
response = requests.get(f"{API}/agent/jobs/{job_id}", headers=HEADERS)
response.raise_for_status()
job = response.json()["data"]
print("Edit status:", job["status"])
# Only interactive runs reach this; unattended runs never pause.
if job["status"] == "waiting_input" and on_pause:
on_pause(job)
if job["status"] != "completed":
raise RuntimeError("The edit did not finish: " + (job.get("error") or job.get("reason", "")))
return jobconst edit = await waitForAgentJob(job.job_id);
console.log("Agent summary:", edit.last_message);edit = wait_for_agent_job(job['job_id'])
print("Agent summary:", edit["last_message"])# Long-polls. Call it in a loop, waiting a few seconds between calls,
# until "status" comes back as completed, failed or canceled.
curl https://api.rendley.com/v1/agent/jobs/YOUR_JOB_ID \
-H "Authorization: Bearer YOUR_API_KEY"completed means the timeline is saved. last_message holds the agent’s summary of what it did.
4. Get the MP4
The agent builds a project, not a video file. Export it, then poll the export job until it finishes.
Start the export
const exportResponse = await fetch(
`${API}/projects/${job.project_id}/export`,
{
method: "POST",
headers,
body: JSON.stringify({
settings: { target_resolution: "1080p", codec: "h264" },
}),
},
);
if (!exportResponse.ok) {
throw new Error("Could not start the export: " + exportResponse.status);
}
const exportBody = await exportResponse.json();
const exportJob = exportBody.data;curl -X POST https://api.rendley.com/v1/projects/YOUR_PROJECT_ID/export \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "settings": { "target_resolution": "1080p", "codec": "h264" } }'export_response = requests.post(
f"{API}/projects/{job['project_id']}/export",
headers=HEADERS,
json={"settings": {"target_resolution": "1080p", "codec": "h264"}},
)
export_response.raise_for_status()
export_job = export_response.json()["data"]This returns a job, the same as step 2. Poll it with exportJob.job_id.
Poll until it’s done
const exported = await waitForJob(exportJob.job_id);
// The signed URL is the result. Fetch it, pipe it to your storage,
// or hand it to the browser.
console.log(exported.output.url);exported = wait_for_job(export_job['job_id'])
# The signed URL is the result. Fetch it, pipe it to your storage,
# or hand it to the browser.
print(exported["output"]["url"])# Poll the export job every few seconds. The finished job carries the download URL.
curl https://api.rendley.com/v1/jobs/YOUR_EXPORT_JOB_ID \
-H "Authorization: Bearer YOUR_API_KEY"