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.