Rendley docs

Generate and download

A full script: start a generation, wait for it, write the file. Copy it, add your API key, run it.

The same shape works for every AI endpoint. Swap /ai/generate-video for /ai/generate-image or /ai/text-to-speech and adjust params to match the model. Each endpoint’s default model requires different fields, so check its schema first: text-to-speech needs a voice_id alongside the prompt.

/ai/transcribe is the one exception. It returns its transcript inline on the job rather than as a downloadable file.

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
// Start the generation. This returns as soon as the job is queued,
// long before the video exists.
async function generate(url, body) {
  const response = await fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
  });

  if (!response.ok) {
    const text = await response.text();
    throw new Error("Could not start the job: " + response.status + " " + text);
  }

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


const jobId = await generate(`${API}/ai/generate-video`, {
  params: {
    prompt: "A slow aerial push over a city at sunset",
    aspect_ratio: "16:9",
    duration: 5,
  },
});

console.log("Job", jobId);

const job = await waitForJob(jobId);

// The completed job carries a signed URL. Fetch it, pipe it to your own
// storage, or hand it to the browser. It expires, so use it soon.
console.log(job.output.url);
console.log(job.output.mime_type + ", " + job.output.size + " bytes");
import requests


def generate(url, body):
    """Start the generation. Returns as soon as the job is queued."""
    response = requests.post(url, headers=HEADERS, json=body)
    response.raise_for_status()

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


job_id = generate(f"{API}/ai/generate-video", {
    "params": {
        "prompt": "A slow aerial push over a city at sunset",
        "aspect_ratio": "16:9",
        "duration": 5,
    },
})

print("Job", job_id)

job = wait_for_job(job_id)

# The completed job carries a signed URL, download it before it expires.
with requests.get(job["output"]["url"], stream=True) as file:
    file.raise_for_status()

    with open("output.mp4", "wb") as out:
        for chunk in file.iter_content(1 << 16):
            out.write(chunk)

print(f"Saved output.mp4 ({job['output']['size']} bytes)")
#!/usr/bin/env bash
set -euo pipefail

API="https://api.rendley.com/v1"
AUTH="Authorization: Bearer $RENDLEY_API_KEY"

# 1. Start the generation.
JOB_ID=$(curl -sS -X POST "$API/ai/generate-video" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{
    "params": {
      "prompt": "A slow aerial push over a city at sunset",
      "aspect_ratio": "16:9",
      "duration": 5
    }
  }' | jq -r '.data.job_id')

echo "Job $JOB_ID"

# 2. Poll until it reaches a terminal status.
while true; do
  JOB=$(curl -sS "$API/jobs/$JOB_ID" -H "$AUTH")
  STATUS=$(echo "$JOB" | jq -r '.data.status')

  if [ "$STATUS" = "completed" ]; then
    break
  fi

  if [ "$STATUS" = "failed" ] || [ "$STATUS" = "canceled" ]; then
    echo "$JOB" | jq -r '.data.error // .data.status' >&2
    exit 1
  fi

  sleep 3
done

# 3. Download the file the job points at.
curl -sS -o output.mp4 "$(echo "$JOB" | jq -r '.data.output.url')"
echo "Saved output.mp4"

output.url is time limited. Download it as soon as the job completes. If the link has expired, poll the job again for a fresh one.

Checking the price first

Every AI endpoint has a /cost twin. It takes the same body and returns the credit price without generating anything:

curl -X POST https://api.rendley.com/v1/ai/generate-video/cost \
  -H "Authorization: Bearer $RENDLEY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "params": { "prompt": "A slow aerial push over a city at sunset" } }'

Choosing a different model

Omitting model_id uses the action’s default. To pick another, pass its id:

{
  "model_id": "veo-3.1",
  "params": { "prompt": "A slow aerial push over a city at sunset" }
}

Each model takes different params. Video generation has a dropdown showing what the selected model accepts.