Webhooks
POST /api/hooks/render renders an image the same way POST /api/generate does, and accepts the
same request body, plus an optional webhookUrl for push-style delivery once the render
completes.
Render with a webhook
Render with a webhook
curl -X POST https://yourdomain.com/api/hooks/render \
-H 'Authorization: Bearer YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"templateId": "your-template-id",
"layers": {
"title": { "text": "Order shipped" }
},
"webhookUrl": "https://yourapp.com/webhooks/renderfast"
}'{
"success": true,
"imageUrl": "https://yourdomain.com/api/images/generated/...",
"cached": false,
"generationTimeMs": 842
}The response is synchronous: it’s returned as soon as the render finishes, whether or not a
webhook is configured. If webhookUrl is set, the exact same JSON body is also POSTed to that
URL after the response has already been sent.
webhookUrl constraints
webhookUrl must be a public http: or https: URL. Loopback,
private, and link-local hosts (and cloud metadata addresses) are rejected with a 400 before any render work happens. If you haven't generated a webhook secret yet, a request that
sets webhookUrl also gets a 400, since there'd be nothing to sign
the delivery with.Generating a webhook secret
Webhook deliveries are signed with a per-account secret. Generate one before using webhookUrl for the first time:
Webhook secret
curl -X POST https://yourdomain.com/api/settings/webhook-secret \
-H 'Authorization: Bearer YOUR_API_KEY'
# → 200 { "success": true, "webhookSecret": "whsec_..." }
# Store this now: it's returned once and there is no read-back endpoint.
# Calling this again rotates the secret and invalidates the old one immediately.You can also generate or rotate this secret from the app, under API Keys, without calling the endpoint directly.
Rotating replaces the old secret
There is only ever one active secret per account. Calling this endpoint again generates a new one and immediately invalidates the previous secret, there's no overlap window where both work. Update your webhook verification code before rotating in production.Webhook delivery and signature
A webhook delivery is a POST of the same JSON payload returned in the render response, with a
signature header:
X-Renderfast-Signature: t=<unix_seconds>,v1=<hex_hmac> where hex_hmac is HMAC-SHA256(secret, "${timestamp}.${rawBody}") computed over the exact
timestamp and raw request body being sent.
Delivery guarantees
Webhook delivery uses a 10 second timeout and is not retried in v1. A slow or failing webhook endpoint never affects the original render response, that response has already been sent by the time delivery is attempted.What your endpoint receives
Your webhookUrl gets exactly one delivery per render call: a POST request with a JSON body
and these headers, plus the signature header described above.
POST https://yourapp.com/webhooks/renderfast
Content-Type: application/json
X-Renderfast-Signature: t=1757200000,v1=5f8a...c091
{
"success": true,
"imageUrl": "https://yourdomain.com/api/images/generated/...",
"cached": false,
"generationTimeMs": 842
} The body is identical to the render response: same success, imageUrl, cached, and generationTimeMs fields. A warnings array is included only when the render produced at least
one warning. It’s omitted entirely otherwise.
There’s no retry. A 10 second delivery timeout applies, and a failed or slow endpoint on your side doesn’t get a second attempt in v1.
Verifying a webhook
Signature verification
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyRenderfastSignature(header, rawBody, secret) {
const parts = Object.fromEntries(
header.split(',').map((part) => part.split('='))
);
const timestamp = parts.t;
const expected = createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(parts.v1, 'utf8');
return a.length === b.length && timingSafeEqual(a, b);
}