Fill these in to personalise every example on this page. Nothing is sent anywhere.
Render API reference
Endpoints, request bodies and event payloads of the Render API. For concepts and workflows see the Render guide.
| Base URL | https://render-host/api/v1/render |
| Format | JSON request and response bodies (Content-Type: application/json). |
| Auth | Execution URLs and status streams: none. POST endpoints: Authorization: Bearer <render key>. |
| Times | ISO-8601 UTC strings. Durations (ttl, timeout) are in milliseconds, sizes in bytes. |
Endpoints
Signed execution URLno auth
{executionUrl}Start (or re-attach to) the job described by a signed execution URL. Use the URL exactly as received; it already contains the job and its limits.
Response: StartResponse — 202 new job, 200 existing job.
const res = await fetch(urls.executionUrl);
const start = await res.json(); // 202 = new job, 200 = already exists
console.log(start.jobId, start.status);import requests
start = requests.get(urls["executionUrl"], timeout=60).json()
print(start["jobId"], start["status"])<?php
$start = json_decode(file_get_contents($urls["executionUrl"]), true);
echo $start["jobId"], " ", $start["status"], PHP_EOL;curl "$EXECUTION_URL"Start a jobBearer auth
/api/v1/render/executeValidate and immediately start a job.
Body: JobRequest. Response: StartResponse — 202 new, 200 already exists, 400 invalid body, 401 bad token.
const res = await fetch("https://api-host/api/v1/render/execute", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_RENDER_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
id: "clip-8f14e45f",
inputs: [{ url: "https://cdn.example.com/clip.mp4", ext: "mp4" }],
output: { ext: "m4a" },
operation: { type: "extract_audio" },
}),
});
const job = await res.json();
console.log(job);import uuid, requests
res = requests.post(
"https://api-host/api/v1/render/execute",
headers={"Authorization": "Bearer YOUR_RENDER_KEY"},
json={
"id": "clip-8f14e45f",
"inputs": [{"url": "https://cdn.example.com/clip.mp4", "ext": "mp4"}],
"output": {"ext": "m4a"},
"operation": {"type": "extract_audio"},
},
timeout=60,
)
job = res.json()
print(job)<?php
$body = [
"id" => "clip-8f14e45f",
"inputs" => [["url" => "https://cdn.example.com/clip.mp4", "ext" => "mp4"]],
"output" => ["ext" => "m4a"],
"operation" => ["type" => "extract_audio"],
];
$ch = curl_init("https://api-host/api/v1/render/execute");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer YOUR_RENDER_KEY",
"Content-Type: application/json",
],
]);
$job = json_decode(curl_exec($ch), true);
print_r($job);curl -X POST "https://api-host/api/v1/render/execute" \
-H "Authorization: Bearer YOUR_RENDER_KEY" \
-H "Content-Type: application/json" \
-d '{"id":"clip-8f14e45f","inputs":[{"url":"https://cdn.example.com/clip.mp4","ext":"mp4"}],"output":{"ext":"m4a"},"operation":{"type":"extract_audio"}}'Cache a jobBearer auth
/api/v1/render/cacheStore a job description without running it and get a public execution URL. Re-caching the same id replaces the stored description.
Body: JobRequest. Response: CacheResponse (200).
const res = await fetch("https://api-host/api/v1/render/cache", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_RENDER_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
id: "clip-8f14e45f",
inputs: [{ url: "https://cdn.example.com/clip.mp4", ext: "mp4" }],
output: { ext: "m4a" },
operation: { type: "extract_audio" },
}),
});
const cached = await res.json(); // { cacheId, executionUrl, statusUrl, expiresAt }
console.log(cached);import uuid, requests
res = requests.post(
"https://api-host/api/v1/render/cache",
headers={"Authorization": "Bearer YOUR_RENDER_KEY"},
json={
"id": "clip-8f14e45f",
"inputs": [{"url": "https://cdn.example.com/clip.mp4", "ext": "mp4"}],
"output": {"ext": "m4a"},
"operation": {"type": "extract_audio"},
},
timeout=60,
)
cached = res.json()
print(cached)<?php
$body = [
"id" => "clip-8f14e45f",
"inputs" => [["url" => "https://cdn.example.com/clip.mp4", "ext" => "mp4"]],
"output" => ["ext" => "m4a"],
"operation" => ["type" => "extract_audio"],
];
$ch = curl_init("https://api-host/api/v1/render/cache");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer YOUR_RENDER_KEY",
"Content-Type: application/json",
],
]);
$cached = json_decode(curl_exec($ch), true);
print_r($cached);curl -X POST "https://api-host/api/v1/render/cache" \
-H "Authorization: Bearer YOUR_RENDER_KEY" \
-H "Content-Type: application/json" \
-d '{"id":"clip-8f14e45f","inputs":[{"url":"https://cdn.example.com/clip.mp4","ext":"mp4"}],"output":{"ext":"m4a"},"operation":{"type":"extract_audio"}}'Run a cached jobno auth
/api/v1/render/execute/{cacheId}Start the job stored by Cache a job. Usually called via the returned executionUrl.
| Parameter | In | Description |
|---|---|---|
cacheId | path | The cacheId returned when caching. |
Response: StartResponse, or 404 {"error": "Cache with ID '…' not found."}.
const res = await fetch(cached.executionUrl); // or `https://api-host/api/v1/render/execute/${cacheId}`
if (res.status === 404) throw new Error("Cached job not found");
const start = await res.json();
console.log(start.jobId, start.status);import requests
res = requests.get(cached["executionUrl"], timeout=60)
if res.status_code == 404:
raise RuntimeError("Cached job not found")
start = res.json()
print(start["jobId"], start["status"])<?php
$ch = curl_init($cached["executionUrl"]);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 60]);
$start = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) === 404) {
throw new RuntimeException("Cached job not found");
}
echo $start["jobId"], " ", $start["status"], PHP_EOL;curl "https://api-host/api/v1/render/execute/CACHE_ID"Status stream (WebSocket)no auth
/api/v1/render/status/{jobId}Upgrade to a WebSocket and receive a StatusEvent text frame about every second. The server closes the socket after done, failed or not_found. Nothing needs to be sent by the client.
const ws = new WebSocket(job.statusUrl);
ws.onmessage = (event) => {
const job = JSON.parse(event.data);
console.log(job.status, job.progress);
if (job.status === "done") console.log("Download:", job.output.url);
if (job.status === "failed" || job.status === "not_found") console.error(job.error);
};# pip install websockets
import asyncio, json, websockets
async def watch(url):
async with websockets.connect(url) as ws:
async for message in ws:
job = json.loads(message)
print(job["status"], job.get("progress"))
if job["status"] in ("done", "failed", "not_found"):
return job
job = asyncio.run(watch(job["statusUrl"]))<?php
// composer require textalk/websocket:^1.5
require __DIR__ . "/vendor/autoload.php";
$client = new WebSocket\Client($job["statusUrl"], ["timeout" => 900]);
while (true) {
$job = json_decode($client->receive(), true);
echo $job["status"], " ", $job["progress"] ?? "", PHP_EOL;
if (in_array($job["status"], ["done", "failed", "not_found"], true)) {
break;
}
}
$client->close();Status stream (Server-Sent Events)no auth
/api/v1/render/status/sse/{jobId}GET with Accept: text/event-stream. Each event's data field is a JSON StatusEvent. The stream ends after a terminal status.
function watchRender(sseUrl) {
return new Promise((resolve, reject) => {
const events = new EventSource(sseUrl);
events.onmessage = (event) => {
const job = JSON.parse(event.data);
console.log(job.status, job.progress);
if (job.status === "done") { events.close(); resolve(job.output); }
if (job.status === "failed" || job.status === "not_found") { events.close(); reject(new Error(job.error)); }
};
});
}
const output = await watchRender(job.sseStatusUrl);
console.log("Download:", output.url);import json, requests
def watch_render(sse_url):
with requests.get(sse_url, stream=True, timeout=900) as stream:
for line in stream.iter_lines(decode_unicode=True):
if not line or not line.startswith("data:"):
continue
job = json.loads(line[5:])
print(job["status"], job.get("progress"))
if job["status"] == "done":
return job["output"]
if job["status"] in ("failed", "not_found"):
raise RuntimeError(job.get("error"))
output = watch_render(job["sseStatusUrl"])
print("Download:", output["url"])<?php
function watchRender(string $sseUrl): array
{
$buffer = "";
$final = null;
$ch = curl_init($sseUrl);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Accept: text/event-stream"],
CURLOPT_TIMEOUT => 900,
CURLOPT_WRITEFUNCTION => function ($ch, string $chunk) use (&$buffer, &$final) {
$buffer .= $chunk;
while (($pos = strpos($buffer, "\n\n")) !== false) {
$event = substr($buffer, 0, $pos);
$buffer = substr($buffer, $pos + 2);
foreach (explode("\n", $event) as $line) {
if (!str_starts_with($line, "data:")) {
continue;
}
$job = json_decode(trim(substr($line, 5)), true);
echo $job["status"], " ", $job["progress"] ?? "", PHP_EOL;
if (in_array($job["status"], ["done", "failed", "not_found"], true)) {
$final = $job;
return 0; // stop reading, the job has finished
}
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
if ($final === null) {
throw new RuntimeException("Status stream ended unexpectedly");
}
if ($final["status"] !== "done") {
throw new RuntimeException($final["error"] ?? "Render failed");
}
return $final["output"];
}
$output = watchRender($job["sseStatusUrl"]);
echo "Download: ", $output["url"], PHP_EOL;curl -N "$SSE_STATUS_URL"
# data: {"job_id":"…","status":"downloading_inputs","progress":22,"output":null,"error":null,…}
# data: {"job_id":"…","status":"done","progress":100,"output":{"url":"https://…","size":98234112,"sizeText":"93.68 MB",…},…}Healthno auth
/Returns plain text OK when the service is running.
const ok = (await (await fetch("https://api-host/")).text()) === "OK";
console.log(ok);import requests
print(requests.get("https://api-host/", timeout=10).text == "OK")<?php
var_dump(file_get_contents("https://api-host/") === "OK");curl "https://api-host/"Request objects
JobRequest
| Field | Type | Rules | Description |
|---|---|---|---|
id required | string | min 5 chars | Your unique job id. Same id = same job (see idempotency). Use a UUID or a hash of the inputs. |
inputs required | Input[] | 1–10 items | Source files, in the order the operation expects. |
output required | OutputOptions | What to produce. | |
operation required | Operation | What to do with the inputs. | |
ttl | integer (ms) | 180 000 – 86 400 000 | How long the job and file are kept. Default 3 600 000 (1 hour). |
progressBrackets | ProgressBrackets | How overall progress is split between stages. | |
limits | Limits | each limit ≤ 5 GB | Size guards for inputs. Defaults to 5 GB per input and 5 GB in total. |
{
"id": "yt-dQw4w9WgXcQ-1080p",
"inputs": [
{
"url": "https://cdn.example.com/video-1080p.mp4",
"ext": "mp4",
"timeout": 240000,
"chunkDownload": { "type": "header", "size": 33554432, "concurrency": 4 }
},
{ "url": "https://cdn.example.com/audio.m4a", "ext": "m4a" }
],
"output": { "ext": "mp4", "downloadName": "Never Gonna Give You Up (1080p).mp4" },
"operation": { "type": "join_streams", "copyStreams": true },
"ttl": 7200000,
"progressBrackets": { "downloading": [0, 50], "processing": [50, 60], "uploading": [60, 100] },
"limits": { "inputSize": { "maxSizePerInput": 1572864000, "maxTotalSize": 1677721600 } }
}Input
| Field | Type | Rules / default | Description |
|---|---|---|---|
url required | string | absolute URL | Where to download the file from. |
ext required | string | Input extension, e.g. mp4, m4a, webm, m3u8. See extensions. | |
requestOptions.method | string | GET | HTTP method. |
requestOptions.headers | object<string,string> | Extra request headers (referer, cookie, user-agent…). | |
requestOptions.body | string | Request body. | |
chunkDownload.type | header | query | header | How byte ranges are requested: Range header or a range query parameter. |
chunkDownload.size | integer (bytes) | ≥ 1, default 10 485 760 (10 MB) | Chunk size. |
chunkDownload.concurrency | integer | 1–10, default 2 | Parallel chunks. |
HLSPlaylistDownload.concurrency | integer | 1–10, default 2 | Parallel segment downloads for m3u8 inputs. |
timeout | integer (ms) | 1 000 – 600 000, default 120 000 | Maximum time to download this input. |
OutputOptions
| Field | Type | Rules / default | Description |
|---|---|---|---|
ext required | string | Output extension, e.g. mp4, m4a, mp3, webm. | |
downloadName | string | trimmed, newlines removed | File name suggested to browsers when downloading output.url. |
chunkUpload.size | integer (bytes) | ≥ 1, default 104 857 600 (100 MB) | Upload part size. |
chunkUpload.concurrency | integer | 1–10, default 4 | Parallel uploads. |
Operation
| Field | Type | Default | Description |
|---|---|---|---|
type required | OperationType | The operation to run. | |
copyStreams | boolean | true | Copy streams without re-encoding (fast). false re-encodes. |
ProgressBrackets
Three [start, end] ranges that must be continuous and cover 0–100: downloading[0] = 0, downloading[1] = processing[0], processing[1] = uploading[0], uploading[1] = 100.
{ "downloading": [0, 70], "processing": [70, 85], "uploading": [85, 100] }Limits
| Field | Type | Description |
|---|---|---|
inputSize.maxSizePerInput | integer (bytes) | Fail if any single input is larger. Maximum and default: 5 368 709 120 (5 GB). |
inputSize.maxTotalSize | integer (bytes) | Fail if all inputs together are larger. Maximum and default: 5 368 709 120 (5 GB). |
Response objects
StartResponse
| Field | Type | Description |
|---|---|---|
jobId | string | The job id (your id). |
status | queued | already_exists | queued for a new job, already_exists when attaching to an existing one. |
message | string | Human-readable summary. |
statusUrl | string (wss) | WebSocket status stream. |
sseStatusUrl | string (https) | Server-Sent Events status stream. |
CacheResponse
| Field | Type | Description |
|---|---|---|
cacheId | string | Equals the job id. |
executionUrl | string (https) | Public URL that starts the job with GET. |
statusUrl | string (wss) | WebSocket status stream for the job. |
expiresAt | string (date-time) | When the job would expire if started now (now + ttl). |
StatusEvent
| Field | Type | Description |
|---|---|---|
job_id | string | Job id. |
status | JobStatus | Current stage. |
progress | integer | Overall progress 0–100. |
output | JobOutput | null | Set when status is done. |
error | string | null | Set when status is failed. |
created_at | string (date-time) | When the job was first created. |
expires_at | string (date-time) | When the job and its file will be deleted. |
When no job exists for the id, a single event with a different shape is sent and the stream closes:
{ "id": "JOB_ID", "status": "not_found", "error": "Job with ID 'JOB_ID' not found." }JobOutput
| Field | Type | Description |
|---|---|---|
url | string (https) | Download link, valid until expires_at. |
size | integer | File size in bytes. |
sizeText | string | Readable size, e.g. 93.68 MB. |
key | string | Storage key of the file. Informational; don't rely on its format. |
Error bodies
| When | Body |
|---|---|
Body validation failed (400) | { "success": false, "error": { … } } describing each invalid field. |
Cached job missing (404) | { "error": "Cache with ID '…' not found." } |
Bad Bearer token (401) | Plain text Unauthorized. |
Other errors (4xx/5xx) | { "message": "…", "code": "…", "statusCode": 500 } |
Enums
JobStatus
| Value | Meaning |
|---|---|
pending | Accepted and about to start. |
queued | Waiting for a free worker because the service is busy. It will start automatically; keep listening. |
downloading_inputs | Fetching the source files. progress moves as bytes arrive. |
processing | Merging / converting. |
uploading_output | Storing the result for download. |
done | Finished. output is set. Terminal state. |
failed | Stopped with an error; see error. Terminal state. |
not_found | No job with this id exists (never started, or already expired). Only sent by the status streams. Terminal. |
OperationType
| Value | Inputs | Result |
|---|---|---|
join_streams | 1+ (e.g. video, audio) | Put all input streams into one file without re-encoding. The usual way to merge a video-only and an audio-only stream. |
convert_format | 1 | Change the container to output.ext (e.g. HLS/TS → MP4). Re-encodes only when copyStreams is false. |
replace_audio_in_video | 2 — video, then audio | Keep the video of input 1 and use the audio of input 2. |
extract_audio | 1 | Drop the video, keep the audio (e.g. to m4a/mp3). |
extract_video | 1 | Drop the audio, keep the video. |
mix_audio_and_video | 2 — video with audio, then audio | Keep the video of input 1 and mix both audio tracks together (e.g. voice-over on top of the original sound). |
no_process | 1 | No processing: store the first input as-is and return a download link. |
File extensions
Commonly used values for inputs[].ext and output.ext:
| Kind | Extensions |
|---|---|
| Video | mp4 mkv webm mov avi wmv flv mpg mpeg m4v 3gp ogv |
| Streaming (input only) | m3u8 |
| Audio | mp3 m4a aac wav ogg flac wma aiff |