Results and export
Export the result
A completed job means the timeline is ready, not that a file exists. Open the project in the editor, or export it to an MP4.
Export uses the standard job format, where the result sits under output. Agent jobs differ: their fields sit on the job object itself.
Errors
| Status | Code | What to do |
|---|---|---|
400 | BAD_REQUEST | A field is missing or contradicts another. Sending thread_id without project_id lands here, as does a files entry with no url, storage_url or media_id, and more than 20 files in one request. |
400 | VALIDATION_ERROR | A field failed its format check. The fields array names each one. |
400 | AGENT_JOB_REJECTED | The run could not be started. The message says why. |
401 | UNAUTHORIZED | Check the API key. |
403 | SUBSCRIPTION_REQUIRED | The agent requires an active subscription. |
403 | PLAN_LIMIT_REACHED | You omitted project_id and the plan does not allow another project. Delete one, or pass an existing project_id. |
404 | JOB_NOT_FOUND | No such job, or it is not yours. |
409 | AGENT_PROJECT_BUSY | Another edit is active on this project. Poll, answer, or cancel it. The fields array carries active_job_id and active_job_status. |
429 | AGENT_CONCURRENCY_LIMIT_REACHED | Too many edits running for this account. Wait for one to finish. |
429 | AGENT_CHAR_LIMIT_REACHED | The plan’s prompt character allowance is used up. It resets with the billing window, or upgrade. |
Downloading the video
Start the export with the project_id from the agent job, poll it, then fetch the signed URL the finished job carries.
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 jobconst PROJECT_ID = "3f0d5a4e-3d2a-4c1f-9b3e-2a1c4d5e6f70";
const exportResponse = await fetch(`${API}/projects/${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();
// Poll the export job until it finishes.
const job = await waitForJob(exportBody.data.job_id);
// Download the rendered video.
const videoResponse = await fetch(job.output.url);
const blob = await videoResponse.blob();
console.log("Downloaded " + blob.size + " bytes, type: " + blob.type);PROJECT_ID = "3f0d5a4e-3d2a-4c1f-9b3e-2a1c4d5e6f70"
export_response = requests.post(
f"{API}/projects/{PROJECT_ID}/export",
headers=HEADERS,
json={"settings": {"target_resolution": "1080p", "codec": "h264"}},
)
export_response.raise_for_status()
# Poll the export job until it finishes.
job = wait_for_job(export_response.json()["data"]["job_id"])
# Download the rendered video.
video = requests.get(job["output"]["url"])
video.raise_for_status()
with open("output.mp4", "wb") as f:
f.write(video.content)
print(f"Saved {len(video.content)} bytes")The download URL expires. output.url_expires_at says when. Poll the job again for a fresh one rather than storing it.
Handing off to the editor
Skip the export and open the project in the browser instead. Pass project_id to the SDK, or link the user straight to the Rendley editor:
https://app.rendley.com/editor/{project_id}
A human can then review the edit, tweak the timeline, and export manually.
Full automated editing flow
A typical integration chains three steps:
- Start an edit: send a prompt and media.
- Continue the edit: refine with follow-up instructions.
- Export the result (this page): render to MP4 and download.
For human-in-the-loop workflows, add approval gates between steps 1 and 3.