Rendley docs

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 job
const 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"