Rendley docs

Auto-edit from a prompt

Describe the edit you want, attach the footage, and poll until the agent returns a finished project.

Use it when the output is an edited video rather than a single generated asset.

How it differs from the AI endpoints

AI endpointsThe agent
You give itA model and its paramsA sentence and some footage
It returnsOne generated fileAn edited project, ready to export
You controlEvery parameterThe brief

The script

Three steps: describe the edit, poll until it finishes, export. The agent has its own polling endpoint and status values, separate from AI jobs.

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 job
// 1. Describe the edit and attach the footage. Returns immediately.
async function startEdit() {
  const response = await fetch(`${API}/agent`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      prompt:
        "Cut interview.mp4 into a 60-second highlight reel. Add captions, " +
        "keep the strongest quotes, and put calm background music under it.",
      files: [{ url: "https://cdn.example.com/interview.mp4" }],
    }),
  });

  if (!response.ok) {
    throw new Error("Could not start the edit: " + response.status);
  }

  const body = await response.json();
  return body.data;
}


// 2. Render the finished project to a file.
async function exportProject(projectId) {
  const response = await fetch(`${API}/export`, {
    method: "POST",
    headers,
    body: JSON.stringify({ project_id: projectId }),
  });

  const body = await response.json();
  return body.data.job_id;
}


const started = await startEdit();
console.log("Job:", started.job_id, "Project:", started.project_id);

const edit = await waitForAgentJob(started.job_id);

console.log("Applied", edit.commands_applied, "operations");

const exportJobId = await exportProject(edit.project_id);
const finished = await waitForJob(exportJobId);

// The signed URL is the result. Fetch it, pipe it to your storage,
// or hand it to the browser.
console.log(finished.output.url);
import requests


def start_edit():
    """Describe the edit and attach the footage. Returns immediately."""
    response = requests.post(
        f"{API}/agent",
        headers=HEADERS,
        json={
            "prompt": (
                "Cut interview.mp4 into a 60-second highlight reel. "
                "Add captions, keep the strongest quotes, and put calm "
                "background music under it."
            ),
            "files": [{"url": "https://cdn.example.com/interview.mp4"}],
        },
    )
    response.raise_for_status()

    return response.json()["data"]


def export_project(project_id):
    """Render the finished project to a file."""
    response = requests.post(
        f"{API}/export",
        headers=HEADERS,
        json={"project_id": project_id},
    )
    response.raise_for_status()

    return response.json()["data"]["job_id"]


def download(url, filename):
    """Stream the finished file to disk."""
    with requests.get(url, stream=True) as response:
        response.raise_for_status()

        with open(filename, "wb") as out:
            for chunk in response.iter_content(1 << 16):
                out.write(chunk)


started = start_edit()
print("Job:", started["job_id"], "Project:", started["project_id"])

edit = wait_for_agent_job(started['job_id'])

print("Applied", edit["commands_applied"], "operations")

export_job_id = export_project(edit["project_id"])
finished = wait_for_job(export_job_id)

download(finished["output"]["url"], "edit.mp4")
print("Saved edit.mp4")

Writing a good prompt

The agent runs unattended by default and never stops to ask. Your prompt is the only steering. Be specific.

VagueSpecific
“Make it shorter”“Cut to 60 seconds, keeping the three strongest quotes”
“Add music”“Add calm instrumental music at low volume under the dialogue”
“Make it vertical”“Reframe to 9:16 for Reels, keeping the speaker centered”

Name your files as they appear in files. Uploads land in the project’s media library, not on the timeline, so tell the agent which file to use where.

Send "interactive": true to approve the plan before credits are spent. The job pauses at waiting_input, and you answer it with POST /v1/agent/jobs/{id}/respond. See Automated editing.

waiting_input is not a terminal status, so the script above would poll a paused job forever. Pass the helper’s third argument to handle the pause: waitForAgentJob(jobId, 5000, onPause), where onPause posts the answer. The script on this page leaves interactive off, so it never pauses.

Continuing the edit

Pass the same project_id and thread_id back for follow-up changes. The agent keeps the context of what it already did:

{
  "prompt": "Make the intro shorter and add a call-to-action at the end.",
  "project_id": "3f0d5a4e-3d2a-4c1f-9b3e-2a1c4d5e6f70",
  "thread_id": "7e5b3c19-4d82-4a76-9f01-6c8d2b5a3e47"
}