Guides
Webhooks
Get a signed request the moment a preview finishes.
Receive events
Pass a webhook_url when you create a preview. We send a POST request to that address when the preview finishes, and again when a 4K version of it finishes.
Webhook body
{
"type": "preview.succeeded",
"created_at": "2026-10-07T22:29:14.908Z",
"data": {
"id": "jd7e8r1qxqnm3gz7e3awx1ypn18fv15j",
"object": "preview",
"status": "succeeded",
...
}
}preview.succeededevent- The preview is ready and data has its image link.
preview.failedevent- The preview could not be made. No credit was used.
upscale.succeededevent- The 4K version is ready under data.upscale.
upscale.failedevent- The 4K version could not be made.
Retries
Reply with a 2xx status within ten seconds. Otherwise we try again up to four more times over about an hour. The same event can arrive more than once, so use the preview id to ignore repeats.
Verify the signature
Every request carries a PaintVisualizer-Signature header, signed with the webhook signing secret shown when you created the key. Check it before you trust the body.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
// header looks like "t=1791412154,v1=f068f92f..."
function isFromPaintVisualizer(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(
header.split(",").map((part) => part.split("="))
);
const expected = createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
return (
v1.length === expected.length &&
timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
);
}Sign the raw request body exactly as it arrived, before any JSON parsing.