Guides

Webhooks

Instead of polling, let us tell you. When a generation finishes or an upload is checked, we POST a signed JSON event to your server.

Setting up an endpoint

  1. Open Webhooks in the console and press Add endpoint.
  2. Enter a public https URL on your server and pick the events it should receive.
  3. Copy the signing secret (whsec_...). You can reveal it again later from the same page.
  4. Press Send test to receive a ping event and watch it arrive under recent deliveries.

Webhooks belong to your console account and cover generations from all of your keys. You can have up to 10 endpoints and pause one at any time without deleting it.

Events

EventSent whendata
generation.succeededA generation finished and its outputs are ready.The generation object
generation.failedA generation failed. The payload carries the error.The generation object
upload.readyAn upload passed moderation and can be used as a reference.The upload object
upload.rejectedAn upload was rejected by moderation.The upload object
pingYou press Send test in the console. Always delivered, whatever the endpoint subscribes to.{ "message": "..." }

A generation that finishes with some outputs missing is still generation.succeeded, with error.code set to partial_failure. Canceling a queued generation sends no event.

Payload and headers

POST to your endpoint
{
  "id": "evt_01k6g2t4v6x8z0b2d4f6h8k0m2",
  "object": "event",
  "type": "generation.succeeded",
  "created_at": "2026-09-28T12:00:31.912Z",
  "data": {
    "id": "gen_01k6g2m9x4c8vq3n7r5t2y8w0z",
    "object": "generation",
    "kind": "image",
    "status": "succeeded",
    "model": "xxxv2alt-sd-50prostandard",
    "cost": {
      "credits": 40
    },
    "outputs": [
      {
        "id": "48213376",
        "type": "image",
        "url": "https://cdn.example.com/rogue/48213376.png",
        "nsfw": true
      }
    ],
    "error": null,
    "metadata": {
      "order_id": "1042"
    }
  }
}
HeaderValue
Rogue-EventThe event type, the same as type in the body.
Rogue-DeliveryA delivery id (dlv_...), the same on every retry of this delivery.
Rogue-Signaturet=<unix seconds>,v1=<hex signature>
User-AgentRogue-API-Webhooks/1.0

Verifying signatures

v1 is the hex HMAC-SHA256 of the string <t>.<raw body>, keyed with your endpoint's signing secret (the whole string, including whsec_). To verify a delivery:

  1. Read the raw request body before any JSON parsing; re-serialised JSON will not match.
  2. Split Rogue-Signature into t and v1.
  3. Compute the HMAC of t, a dot and the raw body, and compare it with v1 in constant time.
  4. Reject timestamps more than five minutes from your clock, so a captured request cannot be replayed later.
import crypto from "node:crypto";
import express from "express";

const SECRET = process.env.ROGUE_WEBHOOK_SECRET; // whsec_...
const TOLERANCE_SECONDS = 300;

function verifyRogueSignature(rawBody, header) {
  const parts = {};
  for (const piece of String(header ?? "").split(",")) {
    const at = piece.indexOf("=");
    if (at > 0) parts[piece.slice(0, at).trim()] = piece.slice(at + 1).trim();
  }
  const { t, v1 } = parts;
  if (!t || !v1 || !/^\d+$/.test(t)) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > TOLERANCE_SECONDS) return false;

  const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${rawBody}`).digest();
  const received = Buffer.from(v1, "hex");
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

// Keep the raw bytes: the signature covers the body exactly as sent.
app.post("/webhooks/rogue", express.raw({ type: "application/json" }), (req, res) => {
  const rawBody = req.body.toString("utf8");
  if (!verifyRogueSignature(rawBody, req.get("Rogue-Signature"))) {
    return res.status(400).send("Invalid signature");
  }

  const event = JSON.parse(rawBody);
  if (event.type === "generation.succeeded") {
    for (const output of event.data.outputs) console.log(output.url);
  }
  res.sendStatus(204); // Answer fast; do slow work in the background.
});

app.listen(3000);

Delivery and retries

We POST with a 10 second timeout. Any 2xx answer counts as delivered. Anything else (including redirects, which are not followed) and timeouts count as failures and are retried:

AttemptAfter the previous one
1Immediately
21 minute
35 minutes
430 minutes
52 hours
66 hours
712 hours

After the seventh attempt, about 21 hours after the event, the delivery is marked failed. The console lists every delivery with its last response.

Order and duplicates

Events can arrive out of order and, rarely, more than once (for example when your server processed a request but the answer did not reach us). Use the event id or Rogue-Delivery to skip duplicates, and created_at or a fresh GET /v1/generations/{id} when order matters.

Best practices

  • Verify every signature, and keep the secret in your server's environment.
  • Answer within a few seconds and do slow work (downloads, processing) in a background job.
  • Put your own ids in metadata when you create a generation: they come back in data.metadata, so you can match events without a lookup.

Output URLs point at Rogue's storage. Download what you want to keep: treat the URLs as links to the files, not as your own copy.