Webhooks

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.