Automated video editing
Automated editing
Send a prompt and your source files. The agent builds a timeline from them, cutting clips, adding captions, and choosing music where you asked for it. The result is saved as a Rendley project.
What comes back is a project, not a rendered file. Export it, open it in the editor, or send another prompt to refine it.
The flow
POST /v1/agentwith your prompt. It returns ajob_id, aproject_idand athread_idright away.- Poll
GET /v1/agent/jobs/{jobID}until the status is terminal. - Optionally export the project to an MP4.
Runs are unattended by default. Your prompt is the only steering the agent gets, so be specific about length, tone and which moments matter. Set "interactive": true to approve the plan first.
#Step 1: Start the agent
https://api.rendley.com/v1/agentSend a prompt describing what you want, and attach source files as public direct URLs or existing Rendley media ids.
Save both job_id (to poll) and thread_id (to continue the conversation later). If you omitted project_id, the response tells you which project was created.
Name your files in the prompt. Files land in the media library, not on the timeline, so tell the agent what goes where. A bare url takes its name from the last path segment, pexels-photo-15943144.jpeg above.
prompt/files and message/attachments are the same fields under two names. Send either. If you send both, message and attachments win.
Don't have one? Create an API key
What you want done, in plain language. Up to 500,000 characters. Also accepted as message.
filesoptionalMedia to bring in. Up to 20 per request. Each entry needs at least one of url, storage_url, or media_id. Also accepted as attachments.
Public direct URL for the file. It must be reachable without cookies, login, or custom request headers. Rendley fetches it and adds it to the project library.
A Rendley storage URL, if you already uploaded the file. Takes precedence over url.
An existing media id, to reuse a file already in the project library.
The name the agent refers to the file by. Derived from the URL when omitted.
Edit an existing project. When omitted, a new project is created for you. Must be a valid uuid.
Continue a previous conversation. Requires project_id.
Default false (unattended): the agent approves its own plan and never stops to ask. Set true to have the run pause at waiting_input so you can approve plans and answer questions.
const res = await fetch("https://api.rendley.com/v1/agent", {
method: "POST",
headers: { Authorization: "Bearer YOUR_API_KEY", "Content-Type": "application/json" },
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"
}
]
}),
});
const { data } = await res.json();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
res = requests.post(
"https://api.rendley.com/v1/agent",
headers={"Authorization": "Bearer YOUR_API_KEY"},
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"
}
]
},
)
data = res.json()["data"]$ch = curl_init("https://api.rendley.com/v1/agent");
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(["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 = 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(`{
"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"
}
]
}`)
req, _ := http.NewRequest("POST", "https://api.rendley.com/v1/agent", 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/agent")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer YOUR_API_KEY"
req["Content-Type"] = "application/json"
req.body = { 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" }] }.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",
"project_id": "3f0d5a4e-3d2a-4c1f-9b3e-2a1c4d5e6f70",
"thread_id": "7e5b3c19-4d82-4a76-9f01-6c8d2b5a3e47",
"status": "pending",
"commands_applied": 0,
"commands_failed": 0,
"created_at": 1735689600000,
"updated_at": 1735689600000
}
}#Step 2: Poll until the agent finishes
https://api.rendley.com/v1/agent/jobs/{jobID}This endpoint long-polls, holding the request open while it waits for a change. Loop on it until the status is terminal. Sleep a few seconds between calls, otherwise a fast response puts you in a tight loop firing dozens of requests a second.
Agent jobs are a separate state machine from export and AI jobs, with their own statuses and shape. There is no result_data. The fields sit on the job itself.
| Status | Terminal | Meaning |
|---|---|---|
pending | no | Accepted, waiting to start. |
running | no | The agent is working on the edit. |
waiting_input | no | Paused for a decision. Answer it with the respond endpoint. |
completed | yes | The edit finished and was saved. |
failed | yes | The edit did not finish. Read error. |
canceled | yes | Stopped before it finished. Spelled with one l. |
job_idstringThe job id you polled.
project_idstringThe project the agent edited.
thread_idstringThe conversation thread. Pass it back to continue editing.
statusstringOne of pending, running, waiting_input, completed, failed, canceled.
reasonstringWhy the job ended, e.g. waiting_input, timeout, needs_upgrade, canceled, agent_error.
last_messagestringThe agent's closing summary of what it did.
errorstringA short error message. Absent unless the job failed.
retryablebooleanWhether the same request can be retried.
commands_appliednumberTimeline operations that ran without error.
commands_failednumberTimeline operations that failed. Non-zero with a completed status means a partial edit.
command_errorstringThe first command failure, when there was one.
save_statusstringOne of synced, unchanged, failed. synced means the timeline was written; unchanged means there was nothing to write. Only failed means the edit did not persist.
interruptobject | nullThe pending question, present only while status is waiting_input.
created_atnumberEpoch milliseconds.
updated_atnumberEpoch milliseconds.
Don't have one? Create an API key
const API_KEY = "YOUR_API_KEY";
const API = "https://api.rendley.com/v1";
const headers = {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json",
};
// An agent job is finished when it reaches one of these. Anything else
// means the agent is still working, or waiting on you.
const TERMINAL = ["completed", "failed", "canceled"];
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Start the run. This returns as soon as the job is accepted.
async function startAgent(prompt, files) {
const response = await fetch(`${API}/agent`, {
method: "POST",
headers,
body: JSON.stringify({ prompt, files }),
});
if (!response.ok) {
throw new Error("Could not start the agent: " + response.status);
}
const body = await response.json();
return body.data;
}
// Approve whatever the agent paused on. Only interactive runs pause.
async function respond(jobId) {
const response = await fetch(`${API}/agent/jobs/${jobId}/respond`, {
method: "POST",
headers,
body: JSON.stringify({ response: "approve" }),
});
if (!response.ok) {
throw new Error("Could not answer the agent: " + response.status);
}
}
// Poll the agent job until it reaches a terminal status.
async function waitForAgentJob(jobId) {
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(5000);
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;
// Only reached on interactive runs; unattended runs never pause.
if (job.status === "waiting_input") {
await respond(jobId);
}
}
return job;
}
async function runAgent(prompt, files) {
const started = await startAgent(prompt, files);
const job = await waitForAgentJob(started.job_id);
if (job.status !== "completed") {
throw new Error("The edit did not finish: " + (job.error || job.reason));
}
// Keep thread_id to continue editing this project later.
return job;
}import time
import requests
API_KEY = "YOUR_API_KEY"
API = "https://api.rendley.com/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
# An agent job is finished when it reaches one of these. Anything else
# means the agent is still working, or waiting on you.
TERMINAL = {"completed", "failed", "canceled"}
def start_agent(prompt, files=None):
"""Start the run. Returns as soon as the job is accepted."""
response = requests.post(
f"{API}/agent",
headers=HEADERS,
json={"prompt": prompt, "files": files or []},
)
response.raise_for_status()
return response.json()["data"]
def respond(job_id):
"""Approve whatever the agent paused on. Only interactive runs pause."""
response = requests.post(
f"{API}/agent/jobs/{job_id}/respond",
headers=HEADERS,
json={"response": "approve"},
)
response.raise_for_status()
def wait_for_agent_job(job_id):
"""Poll the agent job until it reaches a terminal status."""
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(5)
response = requests.get(f"{API}/agent/jobs/{job_id}", headers=HEADERS)
response.raise_for_status()
job = response.json()["data"]
# Only reached on interactive runs; unattended runs never pause.
if job["status"] == "waiting_input":
respond(job_id)
return job
def run_agent(prompt, files=None):
"""Start an agent run, poll it to a terminal status, return the job."""
started = start_agent(prompt, files)
job = wait_for_agent_job(started["job_id"])
if job["status"] != "completed":
raise RuntimeError("The edit did not finish: " + (job.get("error") or job.get("reason")))
# Keep thread_id to continue editing this project later.
return job# Start the agent
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" }
]
}'
# Poll the job (long-polls; call in a loop until terminal)
curl https://api.rendley.com/v1/agent/jobs/JOB_ID \
-H "Authorization: Bearer YOUR_API_KEY"Response 200
{
"data": {
"job_id": "c41d8f2b-7a63-4e09-bd15-8f3a29c7e604",
"project_id": "3f0d5a4e-3d2a-4c1f-9b3e-2a1c4d5e6f70",
"thread_id": "7e5b3c19-4d82-4a76-9f01-6c8d2b5a3e47",
"status": "completed",
"last_message": "Built a 9:16 carousel from the 3 images, with a transition between each slide.",
"commands_applied": 34,
"commands_failed": 0,
"save_status": "synced",
"created_at": 1735689600000,
"updated_at": 1735689742000
}
}