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.