Documentation

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"
  }'

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);
}