Webhooks

Skip polling. Register a URL on the Webhooks page and we'll POST a signed job.completed or job.failed event to your endpoint the moment an async render finishes.

Headers on every delivery

X-Rendershot-Event:     job.completed
X-Rendershot-Delivery:  <uuid of this delivery attempt>
X-Rendershot-Timestamp: <unix seconds>
X-Rendershot-Signature: sha256=<hmac hex>

Payload

{
  "event":        "job.completed",
  "job_id":       "abc123...",
  "status":       "completed",
  "job_type":     "screenshot",        // or "pdf"
  "format":       "png",
  "result_url":   "https://api.rendershot.io/v1/jobs/abc123.../result",
  "created_at":   "2026-04-19T10:00:00+00:00",
  "completed_at": "2026-04-19T10:00:05+00:00",
  "expires_at":   "2026-04-20T10:00:05+00:00"
}

// job.failed omits result_url/expires_at and adds:
//   "error_message": "Render timed out after 30s"

Verify the signature (Python)

import hmac, hashlib, time

def verify(secret, body, sig_header, ts_header, max_age=300):
    if abs(time.time() - int(ts_header)) > max_age:
        return False
    expected = "sha256=" + hmac.new(
        secret.encode(),
        f"{ts_header}.".encode() + body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, sig_header)

Verify the signature (Node)

import crypto from "node:crypto";

export function verify(secret, body, sigHeader, tsHeader, maxAgeSec = 300) {
  if (Math.abs(Date.now() / 1000 - Number(tsHeader)) > maxAgeSec) return false;
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret)
      .update(`${tsHeader}.`)
      .update(body)
      .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sigHeader));
}

Respond with 2xx within 10 seconds to acknowledge. Non-2xx responses and timeouts are retried with exponential backoff (1m → 5m → 30m → 2h → 8h, up to 5 attempts). Use X-Rendershot-Delivery to deduplicate if your handler isn't already idempotent.