RenderIO
Getting started

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/:commandId

How it works

  1. Submit your command and receive a command_id
  2. Send GET requests to /api/v1/commands/:commandId at a regular interval
  3. Check the status field in the response
  4. When the status is SUCCESS or FAILED, the command is complete

Statuses

StatusDescription
QUEUEDCommand received, waiting to be processed
PROCESSINGFFmpeg is currently running
SUCCESSCompleted -- output files are available via storage_url
FAILEDFailed -- see error_status and error_message for details

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:

HeaderDescription
Content-Typeapplication/json
User-AgentRenderIO-Webhook/1.0
X-Webhook-SignatureHMAC-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", 200
import { 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:

AttemptDelay after failure
1Immediate
25 seconds
35 minutes
430 minutes
52 hours
65 hours
710 hours
810 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

ApproachBest for
PollingSimple integrations, scripts, one-off jobs, prototyping. No server infrastructure required.
WebhooksProduction 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

On this page