Rendley docs

Get started

Quickstart

Build a video from a set of images and download the final MP4. This example uses the REST API directly, with no Rendley SDK to install.

Before you start, you need an active Rendley subscription and an API key. The image URLs in this example are public. If you replace them, use direct file URLs that Rendley can fetch without cookies, login pages, or custom request headers.

1. Create an API key

Create one under Settings → API Keys and copy it. Store the key on your server, not in browser or mobile code.

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"])

Three ids come back:

  • job_id identifies this edit run. Use it with the agent job endpoint.
  • project_id identifies the editable video project. Use it when you export or start another edit.
  • thread_id identifies the agent conversation. Keep it if you want to send follow-up instructions later.

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 agent finished and the timeline was saved. last_message summarizes what it did. If the job ends as failed or canceled, do not continue to export.

4. Get the MP4

The agent builds an editable project, not a video file. Export that project, then poll the export job until it finishes. Agent jobs and export jobs use different polling endpoints.

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 an export job. Poll exportJob.job_id at /v1/jobs/{id}. Do not send it to the agent job endpoint used in step 3.

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"