Rendley docs

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

  1. POST /v1/agent with your prompt. It returns a job_id, a project_id and a thread_id right away.
  2. Poll GET /v1/agent/jobs/{jobID} until the status is terminal.
  3. 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

POSThttps://api.rendley.com/v1/agent

Send 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

Body

What you want done, in plain language. Up to 500,000 characters. Also accepted as message.

filesoptionalobject[]

Media 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

GEThttps://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.

StatusTerminalMeaning
pendingnoAccepted, waiting to start.
runningnoThe agent is working on the edit.
waiting_inputnoPaused for a decision. Answer it with the respond endpoint.
completedyesThe edit finished and was saved.
failedyesThe edit did not finish. Read error.
canceledyesStopped before it finished. Spelled with one l.
job_idstring

The job id you polled.

project_idstring

The project the agent edited.

thread_idstring

The conversation thread. Pass it back to continue editing.

statusstring

One of pending, running, waiting_input, completed, failed, canceled.

reasonstring

Why the job ended, e.g. waiting_input, timeout, needs_upgrade, canceled, agent_error.

last_messagestring

The agent's closing summary of what it did.

errorstring

A short error message. Absent unless the job failed.

retryableboolean

Whether the same request can be retried.

commands_appliednumber

Timeline operations that ran without error.

commands_failednumber

Timeline operations that failed. Non-zero with a completed status means a partial edit.

command_errorstring

The first command failure, when there was one.

save_statusstring

One 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 | null

The pending question, present only while status is waiting_input.

created_atnumber

Epoch milliseconds.

updated_atnumber

Epoch 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
  }
}