Jobs and polling
Every AI endpoint and every export returns a job id straight away and works in the background. Poll the job until it finishes.
Image generation, transcription and exports share the same lifecycle. One polling loop covers all of them.
The loop
- Call an endpoint. It returns
{ "data": { "job_id": "..." } }. GET /v1/jobs/{id}every few seconds.- When
statusiscompleted, readoutput.url.
The completed job carries the download URL. There is no second call to resolve the file.
Status values
| Status | Terminal | Meaning |
|---|---|---|
queued | no | Accepted, waiting for a worker. |
processing | no | A worker is running it. |
completed | yes | Finished. Read output. |
failed | yes | Did not finish. Read error. |
canceled | yes | Canceled with DELETE /v1/jobs/{id}. |
Poll until the status is one of the three terminal values. Use an interval of three to five seconds. Most image jobs finish in under a minute, video in a few.
The completed job
{
"data": {
"id": "c41d8f2b-7a63-4e09-bd15-8f3a29c7e604",
"type": "generate_video",
"status": "completed",
"output": {
"url": "https://cdn.rendley.com/generated/video.mp4?signature=...",
"url_expires_at": "2026-08-31T15:00:00Z",
"media_id": "c448b6e3-2b78-4bda-b369-d1afc6aec07f",
"file_hash": "c6c8ec4f9a6fdd9d",
"mime_type": "video/mp4",
"size": 4823104,
"duration": 5
}
}
}
idstringThe job id you polled.
typestringWhat the job did, e.g. generate_video, transcription, export_video.
statusstringOne of queued, processing, completed, failed, canceled.
outputobject | nullThe result. Present once the job completes.
urlstringSigned download URL for the generated file.
url_expires_atstringWhen that URL stops working. Poll the job again for a fresh one.
media_idstringThe stored file, as a UUID. Use it to reference the output in a later call.
file_hashstringThe engine's XXH64 content hash, 16 hex characters.
mime_typestringContent type of the generated file.
sizenumberFile size in bytes.
durationnumberDuration in seconds, for audio and video.
errorstring | nullWhy the job failed. Null unless the status is failed.
result_datastring | nullThe raw worker result, as a JSON string. output is the parsed, url-resolved version of it. Read output.
The job also carries input_data, acknowledged, source_type and source_id. They are bookkeeping for the Rendley app, not something an API integration needs.
output.url is a time-limited signed link. Download the file soon after the job completes, or re-poll the job for a fresh URL. Do not store the URL itself.
A polling loop
const API_KEY = "YOUR_API_KEY";
const API = "https://api.rendley.com/v1";
const headers = { "Authorization": "Bearer " + API_KEY };
// A job is finished when it reaches one of these.
const TERMINAL = ["completed", "failed", "canceled"];
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function waitForJob(jobId, interval = 3000, timeout = 600000) {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
const response = await fetch(`${API}/jobs/${jobId}`, { headers });
if (!response.ok) {
throw new Error("Job lookup failed: " + response.status);
}
const body = await response.json();
const job = body.data;
if (TERMINAL.includes(job.status)) {
if (job.status !== "completed") {
throw new Error("Job " + job.status + ": " + (job.error || ""));
}
return job;
}
// Wait before asking again, so a long render does not turn into
// thousands of requests.
await sleep(interval);
}
// Give up rather than poll forever if something upstream is stuck.
throw new Error("Timed out waiting for the job");
}import time
import requests
API_KEY = "YOUR_API_KEY"
API = "https://api.rendley.com/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
# A job is finished when it reaches one of these.
TERMINAL = {"completed", "failed", "canceled"}
def wait_for_job(job_id, interval=3, timeout=600):
deadline = time.time() + timeout
while time.time() < deadline:
response = requests.get(f"{API}/jobs/{job_id}", headers=HEADERS)
response.raise_for_status()
job = response.json()["data"]
if job["status"] in TERMINAL:
if job["status"] != "completed":
raise RuntimeError(f"Job {job['status']}: {job.get('error', '')}")
return job
# Wait before asking again, so a long render does not turn into
# thousands of requests.
time.sleep(interval)
# Give up rather than poll forever if something upstream is stuck.
raise TimeoutError("Timed out waiting for the job")Listing and canceling
GET /v1/jobs returns your recent jobs, newest first, capped at 200. It omits the output object. Fetch a single job when you need the download URL.
DELETE /v1/jobs/{id} cancels a job that is still queued or processing. It returns 200 either way, so read the job back to confirm the status changed. A job that already finished keeps its status. Canceled jobs are not deleted. They move to canceled and stay readable.
When a job fails
A failed job carries a human-readable error. Common causes:
| Cause | What to do |
|---|---|
Invalid params for the model | Check the model’s schema on its capability page. Parameters differ per model. |
| A referenced file is missing or unreadable | Confirm the media_id or URL you passed is reachable. |
| The provider rejected the request | Content policy or an unsupported input. The message says which. |
Status codes
| Code | Meaning |
|---|---|
200 | The request was accepted. For generation, the body carries the job_id. |
400 | The body is invalid, or the account is out of credits (NOT_ENOUGH_CREDITS). Most billable endpoints have a /cost twin that prices a call first. |
401 | Missing or invalid API key. |
402 | A monthly plan quota is exhausted. Upgrade the plan. |
404 | No such job, or it is not yours. |