Webhooks
Get notified when a run finishes instead of polling for it.
Pass webhook_url on create and RunDiffusion POSTs to it when the run reaches a
terminal state. The URL must be public HTTPS. An unreachable or malformed URL is
rejected at create time with INVALID_WEBHOOK_URL rather than failing silently
later.
The payload matches the status response minus the polling-only fields, so a single handler can serve both paths. Each delivery is signed, and delivery retries with exponential backoff for up to about 24 hours.
Verifying the signature
Every webhook carries three headers:
| Header | Value |
|---|---|
X-RD-Signature | hex(hmac_sha256(secret, timestamp + "." + raw_body)) |
X-RD-Timestamp | Unix epoch seconds, as a string |
X-RD-Event | The event type |
Recompute the signature over the raw request body and reject any request whose signature does not match, or whose timestamp is too old for your tolerance.
import express from 'express';
import crypto from 'crypto';
const app = express();
// Capture the raw body: the signature is computed over exact bytes.
app.post(
'/rundiffusion/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const timestamp = req.header('X-RD-Timestamp');
const signature = req.header('X-RD-Signature');
const expected = crypto
.createHmac('sha256', process.env.RD_WEBHOOK_SECRET)
.update(`${timestamp}.${req.body}`)
.digest('hex');
if (
signature?.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
) {
return res.sendStatus(401);
}
// Acknowledge immediately, then process out of band.
res.sendStatus(200);
const event = JSON.parse(req.body.toString());
void handleEvent(event);
},
);import hashlib
import hmac
import os
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ["RD_WEBHOOK_SECRET"].encode()
@app.post("/rundiffusion/webhook")
def rundiffusion_webhook():
timestamp = request.headers.get("X-RD-Timestamp", "")
signature = request.headers.get("X-RD-Signature", "")
raw = request.get_data()
expected = hmac.new(
SECRET, f"{timestamp}.".encode() + raw, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
return "", 401
# Acknowledge immediately, then process out of band.
enqueue(request.get_json())
return "", 200