Guides
Webhooks
Setting up an endpoint
- Open Webhooks in the console and press Add endpoint.
- Enter a public https URL on your server and pick the events it should receive.
- Copy the signing secret (
whsec_...). You can reveal it again later from the same page. - Press Send test to receive a
pingevent 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
| Event | Sent when | data |
|---|---|---|
generation.succeeded | A generation finished and its outputs are ready. | The generation object |
generation.failed | A generation failed. The payload carries the error. | The generation object |
upload.ready | An upload passed moderation and can be used as a reference. | The upload object |
upload.rejected | An upload was rejected by moderation. | The upload object |
ping | You 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
{
"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"
}
}
}| Header | Value |
|---|---|
Rogue-Event | The event type, the same as type in the body. |
Rogue-Delivery | A delivery id (dlv_...), the same on every retry of this delivery. |
Rogue-Signature | t=<unix seconds>,v1=<hex signature> |
User-Agent | Rogue-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:
- Read the raw request body before any JSON parsing; re-serialised JSON will not match.
- Split
Rogue-Signatureintotandv1. - Compute the HMAC of
t, a dot and the raw body, and compare it withv1in constant time. - 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:
| Attempt | After the previous one |
|---|---|
| 1 | Immediately |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 6 hours |
| 7 | 12 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
metadatawhen you create a generation: they come back indata.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.