Polling & Webhooks
Two ways to get notified when your FFmpeg commands complete -- polling and webhooks.
Polling & Webhooks
After submitting a command, you need to know when it finishes. RenderIO supports two approaches: polling and webhooks.
Polling
Polling is the simplest approach. Send a GET request to check the command status at a regular interval.
Endpoint
GET /api/v1/commands/:commandIdHow it works
- Submit your command and receive a
command_id - Send GET requests to
/api/v1/commands/:commandIdat a regular interval - Check the
statusfield in the response - When the status is
SUCCESSorFAILED, the command is complete
Statuses
| Status | Description |
|---|---|
QUEUED | Command received, waiting to be processed |
PROCESSING | FFmpeg is currently running |
SUCCESS | Completed -- output files are available via storage_url |
FAILED | Failed -- see error_status and error_message for details |
Recommended polling interval
Poll every 1-2 seconds. Most commands complete within a few seconds for short media files. Avoid polling more frequently than once per second.
Example
COMMAND_ID="your-command-id"
while true; do
RESULT=$(curl -s https://renderio.dev/api/v1/commands/$COMMAND_ID \
-H "X-API-KEY: $RENDERIO_API_KEY")
STATUS=$(echo $RESULT | jq -r '.status')
echo "Status: $STATUS"
if [ "$STATUS" = "SUCCESS" ] || [ "$STATUS" = "FAILED" ]; then break; fi
sleep 2
done
echo $RESULT | jq .import time
import requests
def wait_for_command(command_id, api_key):
base_url = "https://api.renderio.dev"
while True:
response = requests.get(
f"{base_url}/api/v1/commands/{command_id}",
headers={"X-API-KEY": api_key},
)
result = response.json()
if result["status"] == "SUCCESS":
return result
if result["status"] == "FAILED":
raise Exception(f"Command failed: {result.get('error_message')}")
time.sleep(2)interface CommandResult {
command_id: string;
status: "QUEUED" | "PROCESSING" | "SUCCESS" | "FAILED";
error_message?: string;
output_files: Record<string, { file_id: string; storage_url: string }>;
}
async function waitForCommand(commandId: string, apiKey: string): Promise<CommandResult> {
const BASE_URL = "https://api.renderio.dev";
while (true) {
const response = await fetch(
`${BASE_URL}/api/v1/commands/${commandId}`,
{ headers: { "X-API-KEY": apiKey } },
);
const result = (await response.json()) as CommandResult;
if (result.status === "SUCCESS") {
return result;
}
if (result.status === "FAILED") {
throw new Error(`Command failed: ${result.error_message}`);
}
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}async function waitForCommand(commandId, apiKey) {
const BASE_URL = "https://api.renderio.dev";
while (true) {
const response = await fetch(
`${BASE_URL}/api/v1/commands/${commandId}`,
{ headers: { "X-API-KEY": apiKey } },
);
const result = await response.json();
if (result.status === "SUCCESS") {
return result;
}
if (result.status === "FAILED") {
throw new Error(`Command failed: ${result.error_message}`);
}
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}<?php
function waitForCommand(string $commandId, string $apiKey): array {
$baseUrl = "https://api.renderio.dev";
while (true) {
$ch = curl_init("$baseUrl/api/v1/commands/$commandId");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-KEY: $apiKey"]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($result["status"] === "SUCCESS") {
return $result;
}
if ($result["status"] === "FAILED") {
throw new Exception("Command failed: " . $result["error_message"]);
}
sleep(2);
}
}Webhooks
Webhooks push results to your server as soon as a command completes. This eliminates the need for polling and is ideal for production workflows.
Configure a global webhook
Set up a webhook endpoint that applies to all your commands using PUT /api/v1/webhook-config:
curl -X PUT https://renderio.dev/api/v1/webhook-config \
-H "Content-Type: application/json" \
-H "X-API-KEY: ffsk_your_api_key_here" \
-d '{
"url": "https://your-server.com/webhooks/renderio",
"secret": "your_webhook_secret_here"
}'The secret field is optional but recommended. When set, RenderIO signs every webhook payload with HMAC-SHA256 so you can verify it came from RenderIO.
Per-command webhook
You can also set a webhook_url on individual commands. This overrides the global webhook configuration for that specific command:
curl -X POST https://renderio.dev/api/v1/run-ffmpeg-command \
-H "Content-Type: application/json" \
-H "X-API-KEY: ffsk_your_api_key_here" \
-d '{
"input_files": { "in_video": "https://example.com/sample.mp4" },
"output_files": { "out_video": "result.webm" },
"ffmpeg_command": "ffmpeg -i {{in_video}} -c:v libvpx-vp9 {{out_video}}",
"webhook_url": "https://your-server.com/webhooks/renderio"
}'Webhook payload
When a command completes (either SUCCESS or FAILED), RenderIO sends a POST request to your webhook URL with the following payload:
{
"data": {
"command_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "SUCCESS",
"command_type": "FFMPEG_COMMAND",
"total_processing_seconds": 3.42,
"ffmpeg_command_run_seconds": 2.18,
"original_request": {
"input_files": { "in_video": "https://example.com/sample.mp4" },
"output_files": { "out_video": "result.webm" },
"ffmpeg_command": "ffmpeg -i {{in_video}} -c:v libvpx-vp9 {{out_video}}"
},
"output_files": {
"out_video": {
"file_id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
"storage_url": "https://storage.renderio.dev/files/f1a2b3c4...",
"status": "STORED",
"filename": "result.webm",
"size_mbytes": 1.24
}
}
},
"timestamp": 1700000000000
}The data field contains the same response you would get from polling GET /api/v1/commands/:commandId.
Webhook headers
RenderIO includes these headers with every webhook delivery:
| Header | Description |
|---|---|
Content-Type | application/json |
User-Agent | RenderIO-Webhook/1.0 |
X-Webhook-Signature | HMAC-SHA256 hex digest of the request body, signed with your webhook secret. Only present if you configured a secret. |
Verifying the signature
If you configured a webhook secret, verify the X-Webhook-Signature header to confirm the request came from RenderIO.
# Verify a webhook signature manually
BODY='{"data":{"command_id":"..."},"timestamp":1700000000000}'
SECRET="your_webhook_secret_here"
EXPECTED=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
echo "Expected signature: $EXPECTED"import hmac
import hashlib
from flask import Flask, request
app = Flask(__name__)
def verify_signature(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route("/webhooks/renderio", methods=["POST"])
def webhook_handler():
signature = request.headers.get("X-Webhook-Signature", "")
raw_body = request.get_data()
if not verify_signature(raw_body, signature, os.environ["WEBHOOK_SECRET"]):
return "Invalid signature", 401
payload = request.get_json()
print(f"Command completed: {payload['data']['command_id']}")
print(f"Status: {payload['data']['status']}")
return "OK", 200import { createHmac } from "node:crypto";
import express, { type Request, type Response } from "express";
const app = express();
function verifyWebhookSignature(body: string, signature: string, secret: string): boolean {
const expected = createHmac("sha256", secret)
.update(body)
.digest("hex");
return expected === signature;
}
app.post("/webhooks/renderio", (req: Request, res: Response) => {
const signature = req.headers["x-webhook-signature"] as string;
const rawBody = req.body as string;
if (!verifyWebhookSignature(rawBody, signature, process.env.WEBHOOK_SECRET!)) {
return res.status(401).send("Invalid signature");
}
const payload = JSON.parse(rawBody);
console.log("Command completed:", payload.data.command_id);
console.log("Status:", payload.data.status);
res.status(200).send("OK");
});import { createHmac } from "node:crypto";
function verifyWebhookSignature(body, signature, secret) {
const expected = createHmac("sha256", secret)
.update(body)
.digest("hex");
return expected === signature;
}
// In your webhook handler (e.g. Express)
app.post("/webhooks/renderio", (req, res) => {
const signature = req.headers["x-webhook-signature"];
const rawBody = req.body; // raw string, not parsed JSON
if (!verifyWebhookSignature(rawBody, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
const payload = JSON.parse(rawBody);
console.log("Command completed:", payload.data.command_id);
console.log("Status:", payload.data.status);
res.status(200).send("OK");
});<?php
function verifyWebhookSignature(string $body, string $signature, string $secret): bool {
$expected = hash_hmac("sha256", $body, $secret);
return hash_equals($expected, $signature);
}
// In your webhook handler
$rawBody = file_get_contents("php://input");
$signature = $_SERVER["HTTP_X_WEBHOOK_SIGNATURE"] ?? "";
$secret = getenv("WEBHOOK_SECRET");
if (!verifyWebhookSignature($rawBody, $signature, $secret)) {
http_response_code(401);
echo "Invalid signature";
exit;
}
$payload = json_decode($rawBody, true);
echo "Command completed: " . $payload["data"]["command_id"] . "\n";
echo "Status: " . $payload["data"]["status"] . "\n";
http_response_code(200);
echo "OK";Retry schedule
If your server does not respond with a 2xx status code within 15 seconds, RenderIO retries the delivery with exponential backoff:
| Attempt | Delay after failure |
|---|---|
| 1 | Immediate |
| 2 | 5 seconds |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 5 hours |
| 7 | 10 hours |
| 8 | 10 hours |
After 8 consecutive failures, the webhook is automatically disabled. You can re-enable it from the dashboard or by sending a new PUT request to /api/v1/webhook-config.
Managing your webhook
Check your current webhook configuration:
curl https://renderio.dev/api/v1/webhook-config \
-H "X-API-KEY: ffsk_your_api_key_here"Disable your webhook:
curl -X DELETE https://renderio.dev/api/v1/webhook-config \
-H "X-API-KEY: ffsk_your_api_key_here"When to use which
| Approach | Best for |
|---|---|
| Polling | Simple integrations, scripts, one-off jobs, prototyping. No server infrastructure required. |
| Webhooks | Production pipelines, event-driven architectures, high-volume workloads. Eliminates wasted requests and delivers results faster. |
You can use both at the same time. For example, configure a global webhook for your production pipeline and use polling in development scripts.
Automate with integrations
Webhooks work well with workflow automation platforms. RenderIO has integration guides for n8n and Zapier, and also works with Pipedream and Make.
Next steps
- Webhook Payload reference -- full payload format, signatures, and retries
- Error Handling -- handle webhook delivery failures and command errors