Webhooks

Receive HTTP notifications when render jobs complete or fail — no polling required. Includes HMAC signature verification.

Webhooks let Renderly call your server when something happens. The two events you can subscribe to are render.completed and render.failed. Every delivery is signed with an HMAC so you can verify it came from us.

When to use webhooks

  • Your renders take more than a few seconds and you don't want to hold a poll loop open.
  • You need to update your own database or notify a user the moment a render is ready.
  • You're orchestrating render jobs from a workflow engine (Temporal, Inngest, n8n) that's event-driven.

If a render takes under 5 seconds and you're already in an HTTP request when you kick it off, polling is fine. Webhooks shine for fire-and-forget flows.

Registering a webhook

curl -X POST https://renderly.video/api/v1/webhooks \
  -H "Authorization: Bearer rnd_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/renderly",
    "events": ["render.completed", "render.failed"]
  }'

events defaults to both events if omitted.

The response includes a signing secret that's shown only at creation time:

{
  "success": true,
  "data": {
    "id": "whk_abc123",
    "url": "https://your-app.com/webhooks/renderly",
    "secret": "whsec_8f2c1b7e...",
    "events": ["render.completed", "render.failed"],
    "isActive": true,
    "createdAt": "2026-05-16T12:00:00Z"
  }
}

Save the secret immediately — there's no way to retrieve it later. If you lose it, delete the webhook and create a new one.

You can register up to 5 active webhooks per account.

Per-render webhooks (one-off)

If you don't want a long-lived registration — e.g. you want to be notified about one specific render — pass webhookUrl directly in the render request:

{
  "inputProps": { /* ... */ },
  "webhookUrl": "https://your-app.com/callback/render-42"
}

Renderly POSTs to that URL when the render completes or fails. One-off webhooks are not HMAC-signed since there's no registered secret — use a per-request URL with a unguessable token in the path instead.

For all production integrations we strongly recommend registered webhooks with HMAC verification.

The event payload

When a render finishes or fails, Renderly POSTs JSON to your URL with this shape:

{
  "event": "render.completed",
  "deliveryId": "wev_xyz789",
  "createdAt": "2026-05-16T10:01:30Z",
  "data": {
    "jobId": "clx456def",
    "projectId": "proj_abc123",
    "status": "COMPLETED",
    "outputUrl": "https://renderly-output.s3.amazonaws.com/.../final.mp4",
    "outputSize": 12458960,
    "durationInFrames": 1800,
    "width": 1080,
    "height": 1920,
    "fps": 30,
    "creditsUsed": 1,
    "duration": 45000,
    "completedAt": "2026-05-16T10:01:30Z"
  }
}

For render.failed, status is "FAILED", outputUrl is null, and the payload includes an errorMessage field describing what went wrong.

Verifying the HMAC signature

Every delivery includes two headers:

HeaderPurpose
X-Renderly-SignatureHex-encoded HMAC-SHA256 of the raw request body, signed with your webhook's secret.
X-Renderly-TimestampUnix epoch (seconds) when Renderly sent the request.

To verify, compute the same HMAC over the raw request body and compare in constant time.

Node.js

import { createHmac, timingSafeEqual } from "node:crypto";
 
function verifyRenderlyWebhook(
  rawBody: string,            // the raw request body, NOT JSON.parse'd
  signatureHeader: string,    // request.headers["x-renderly-signature"]
  secret: string,             // whsec_... from the create response
): boolean {
  const expected = createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
 
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(signatureHeader, "hex");
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

Verify against the raw body bytes, before any JSON parsing. Frameworks like Express may rewrite or reformat the body — use the raw-body buffer for HMAC, then JSON.parse separately for application logic.

Python

import hmac, hashlib
 
def verify_renderly_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

Replay protection

The X-Renderly-Timestamp header lets you reject deliveries that are too old. We recommend dropping any request whose timestamp is more than 5 minutes off your server clock — this makes replay attacks impractical even if a signed body leaks.

const skewSeconds = Math.abs(Date.now() / 1000 - Number(timestampHeader));
if (skewSeconds > 300) return new Response("stale", { status: 400 });

Delivery, retries, and idempotency

  • Renderly retries failed deliveries (5xx, timeouts, network errors) with exponential backoff for up to 24 hours.
  • Each delivery includes a deliveryId (wev_…) that's stable across retries. Idempotency-key your handler on this field — never process the same deliveryId twice.
  • Respond with any 2xx within 10 seconds to acknowledge. Anything else is treated as a failure and triggers a retry.
  • If a webhook URL fails consistently for an extended period, we deactivate it. You'll see isActive: false in GET /webhooks. Reactivate from the dashboard once your endpoint is healthy.

Managing webhooks

List:

curl https://renderly.video/api/v1/webhooks \
  -H "Authorization: Bearer rnd_..."

Delete:

curl -X DELETE "https://renderly.video/api/v1/webhooks?id=whk_abc123" \
  -H "Authorization: Bearer rnd_..."

To rotate a secret, delete the webhook and create a new one — the secret is the only way to verify deliveries.

Recipe — end-to-end

Putting it all together:

// 1. Register the webhook once at deploy time
const { data } = await fetch("https://renderly.video/api/v1/webhooks", {
  method: "POST",
  headers: { Authorization: `Bearer ${RENDERLY_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ url: `${PUBLIC_URL}/webhooks/renderly` }),
}).then(r => r.json());
 
// Store data.secret in your secrets manager.
 
// 2. Kick off renders normally — no webhookUrl needed
await fetch("https://renderly.video/api/v1/renders", { /* ... */ });
 
// 3. Handle deliveries
app.post("/webhooks/renderly", express.raw({ type: "application/json" }), (req, res) => {
  const ok = verifyRenderlyWebhook(
    req.body.toString("utf8"),
    req.header("x-renderly-signature"),
    process.env.RENDERLY_WEBHOOK_SECRET,
  );
  if (!ok) return res.status(401).end();
 
  const event = JSON.parse(req.body.toString("utf8"));
  // dedupe on event.deliveryId, then update your DB
  res.status(204).end();
});

Where to next