Skip to content

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.

request body
{
  "model": "fast-image",
  "prompt": "…",
  "webhook_url": "https://example.com/hooks/fastgen",
  "metadata": { "order_id": "A-1042" }
}

Events

EventWhen
job.succeededOutput passed moderation and is available at the signed URL(s).
job.failedProvider error, timeout, or content_blocked. The reserved amount has been refunded.
job.canceledYou called POST /v1/jobs/{id}/cancel while the job was in flight.

Delivery

webhook request
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 2xx within 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 event id) 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.