Webhooks
Instead of polling, pass a webhook_url on any job. When the job reaches a terminal state we POST the job resource to that URL, signed with your account’s webhook secret. Any metadata you attached is echoed back on the job so you can correlate it with your own records.
{
"model": "fast-image",
"prompt": "…",
"webhook_url": "https://example.com/hooks/fastgen",
"metadata": { "order_id": "A-1042" }
}Events
| Event | When |
|---|---|
job.succeeded | Output passed moderation and is available at the signed URL(s). |
job.failed | Provider error, timeout, or content_blocked. The reserved amount has been refunded. |
job.canceled | You called POST /v1/jobs/{id}/cancel while the job was in flight. |
Delivery
POST https://example.com/hooks/fastgen
Content-Type: application/json
User-Agent: FastGenCloud-Webhooks/1.0
X-FastGen-Event: job.succeeded
X-FastGen-Delivery: 2b7f… # unique per delivery (retries reuse it)
X-FastGen-Signature: t=1755424800,v1=5c1a… # see "Verifying"
{
"id": "evt_9a3c…",
"type": "job.succeeded",
"created": 1755424800,
"data": {
"object": { "id": "…", "object": "job", "status": "succeeded", "output": { "images": [ … ] }, … }
}
}- The URL must be public
https. Private hosts and redirects are not followed. - Respond with any
2xxwithin 10 seconds to acknowledge. Do the real work asynchronously. - Non-2xx responses and timeouts are retried up to 5 times with backoff: 1 min, 5 min, 30 min, 2 h, 6 h. Use
X-FastGen-Delivery(or the eventid) to de-duplicate. - Webhooks are a notification, not the source of truth — if in doubt,
GET /v1/jobs/{id}.
Verifying signatures
X-FastGen-Signature is t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256(secret, `${t}.${raw body}`). Compute it over the raw request body (before JSON parsing), compare in constant time, and reject timestamps older than 5 minutes to prevent replays. Your secret (whsec_…) is in Settings.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyFastGen(secret, header, rawBody, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.trim().split("=")));
const t = Number(parts.t);
const v1 = parts.v1 ?? "";
if (!t || Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSec) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(v1, "hex");
return expected.length === given.length && timingSafeEqual(expected, given);
}
// Express example — keep the raw body!
app.post("/hooks/fastgen", express.raw({ type: "application/json" }), (req, res) => {
const ok = verifyFastGen(process.env.FASTGEN_WEBHOOK_SECRET, req.get("X-FastGen-Signature") ?? "", req.body.toString("utf8"));
if (!ok) return res.status(400).send("bad signature");
const event = JSON.parse(req.body.toString("utf8"));
// handle event.type / event.data.object
res.sendStatus(200);
});Testing locally
Expose a local port with a tunnel (for example ngrok or Cloudflare Tunnel), use the https URL as webhook_url, and submit a cheap job on fast-image. The console’s job detail page shows the same payload we deliver.