Rendley docs

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

  1. Call an endpoint. It returns { "data": { "job_id": "..." } }.
  2. GET /v1/jobs/{id} every few seconds.
  3. When status is completed, read output.url.

The completed job carries the download URL. There is no second call to resolve the file.

Status values

StatusTerminalMeaning
queuednoAccepted, waiting for a worker.
processingnoA worker is running it.
completedyesFinished. Read output.
failedyesDid not finish. Read error.
canceledyesCanceled 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
    }
  }
}
idstring

The job id you polled.

typestring

What the job did, e.g. generate_video, transcription, export_video.

statusstring

One of queued, processing, completed, failed, canceled.

outputobject | null

The result. Present once the job completes.

urlstring

Signed download URL for the generated file.

url_expires_atstring

When that URL stops working. Poll the job again for a fresh one.

media_idstring

The stored file, as a UUID. Use it to reference the output in a later call.

file_hashstring

The engine's XXH64 content hash, 16 hex characters.

mime_typestring

Content type of the generated file.

sizenumber

File size in bytes.

durationnumber

Duration in seconds, for audio and video.

errorstring | null

Why the job failed. Null unless the status is failed.

result_datastring | null

The 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:

CauseWhat to do
Invalid params for the modelCheck the model’s schema on its capability page. Parameters differ per model.
A referenced file is missing or unreadableConfirm the media_id or URL you passed is reachable.
The provider rejected the requestContent policy or an unsupported input. The message says which.

Status codes

CodeMeaning
200The request was accepted. For generation, the body carries the job_id.
400The 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.
401Missing or invalid API key.
402A monthly plan quota is exhausted. Upgrade the plan.
404No such job, or it is not yours.