Skip to content

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:

HeaderValue
X-RD-Signaturehex(hmac_sha256(secret, timestamp + "." + raw_body))
X-RD-TimestampUnix epoch seconds, as a string
X-RD-EventThe 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);
  },
);

View as Markdown