API

Webhooks

Signed POST requests when a try-on finishes, so you can stop polling.

Configure an endpoint under Settings → Webhooks (owner or admin only) and we send it a signed POST when a generation started through the Try-On API finishes. More than one endpoint per workspace is allowed — a common shape is one for staging and one for production.

The endpoint must be https, and must not resolve to a private or loopback address — that check runs against the address the connection actually uses, not just the literal host in the URL, so it cannot be defeated by DNS.

Setting one up

  1. Endpoint URL — where we POST.
  2. Events — generation.completed, generation.failed, or both. An endpoint subscribed to neither is refused rather than saved.
  3. Signing secret — generated on save and shown once in the form. Unlike an API key, it can be revealed again later from the same screen.

Send test fires a real delivery through the same enqueue, signer and dispatcher as a live event — a body identical to a real one but for "test": true, so a passing test means the path that matters actually works.

The request

POST https://your-app.com/webhooks/try-on
Content-Type: application/json
User-Agent: GenFashion-Webhooks/1
X-Webhook-Signature: sha256=<hex>
X-Webhook-Event:     generation.completed
X-Webhook-Delivery:  whd_… (the delivery code)
X-Webhook-Attempt:   1
json
{
  "id": "whd_8f1c…",
  "type": "generation.completed",
  "timestamp": "2026-09-13T09:41:02.113Z",
  "accountId": "acc_636028ff…",
  "data": {
    "generationId": "gen_eb56ae63…",
    "status": "COMPLETED",
    "productExternalId": "PN-0041",
    "shopperRef": null,
    "durationMs": 45113,
    "creditsCharged": 1,
    "provider": "openai",
    "model": "gpt-image-2.5-sunburst"
  }
}

id is the delivery id and equals X-Webhook-Delivery. Dedupe on it — a retry resends the identical body, so acting twice on one id is acting twice on one event.

A failed generation carries status: "FAILED" and an errorCode, never an error message — same rule as the API itself: the code is stable and written for you, the message is not.

Verifying the signature

X-Webhook-Signature is sha256= followed by the hex HMAC-SHA-256 of the raw request body with your endpoint's signing secret.

Hash the bytes you received, not a re-encoding of them. A framework that parses the JSON first and hands you an object has already lost the exact bytes — key order, spacing and unicode escaping all change under a re-encode, and the signature will never match.

js
// Express — note express.raw, not express.json
app.post("/webhooks/try-on", express.raw({ type: "application/json" }), (req, res) => {
  const expected = "sha256=" + crypto
    .createHmac("sha256", process.env.GENFASHION_WEBHOOK_SECRET)
    .update(req.body)                       // the Buffer, not a re-encoded object
    .digest("hex");

  const a = Buffer.from(expected), b = Buffer.from(req.get("X-Webhook-Signature") ?? "");
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString("utf8"));
  // dedupe on event.id, then acknowledge fast
  res.sendStatus(200);
});

The timestamp is inside the signed body rather than in a header, deliberately — a header can be edited in flight without breaking the signature, which is exactly what replay protection must not allow.

Retries

The first attempt happens immediately, at the end of the try-on. Everything but a 2xx is retried, including a `4xx` — your endpoint answering 404 mid-deploy looks identical to a permanent one, and cannot be told apart without guessing.

AttemptWhen
1immediately
2+30s
3+2m
4+10m
5+1h
6+6h

After the sixth the delivery is given up and shows as failed in the delivery log, alongside every earlier attempt's response code, duration and next-retry time.

Return `2xx` as soon as you have stored the event, and do your own processing afterwards. A receiver that finishes its work before acknowledging will be sent the event again.

Limits

There is no absolute cap on delivery volume. A workspace generating 200 try-ons an hour receives 200 webhooks an hour — spread out one at a time per endpoint rather than arriving as a burst, but not deferred or throttled below that.

Rotating the secret

Rotate mints a new secret and shows it once. There is no overlap window — the old secret stops verifying the moment the button returns, so deploy the new one on your side first, or accept a short gap where verification fails.