# Webhooks

> Get notified when a run finishes instead of polling for it.

Canonical page: https://www.rundiffusion.com/docs/api/generate/webhooks
Authorization: OAuth device flow or Personal API Access Token or Company API Access Token

---

> **Agents recommended**
>
> **Generate endpoints are built for agents, not ideal for static integrations.**
>
> We strongly advise against building a fixed, hand-coded integration on top of these endpoints.
>
> The tool catalog is a living, evolving, and dynamic list. Tools gain and lose fields, and tools themselves arrive and disappear, at any time. That is why every generate call carries a `tool_fields_hash`: it is a fingerprint of the exact field schema you built your inputs from. When a tool changes, your hash stops matching and the call answers `409 TOOL_SCHEMA_STALE` instead of running something you did not describe.
>
> An agent handles that in a way a static client cannot. It would be capable of refetching the tool with [Get a tool](/docs/api/tools/get), reads the new `tool_fields_hash` and the new fields, rebuilds the request, and sends it again. If a tool is gone, or no longer does the job it used to, an agent can pick a different one from [List tools](/docs/api/tools/list). That self-recovery is the assumption these endpoints are designed around.
>
> **An MCP server is coming very soon** that uses this API underneath. If you are building an agent, it will almost certainly be a better interface to build on than calling Generate directly.

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.

JavaScript:

```javascript
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);
  },
);
```

Python:

```python
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
```

> **Webhooks retry**
>
> A handler that is slow, errors, or returns a non-2xx status is retried for up
> to about a day. Make yours idempotent by keying on `request_id`, and
> acknowledge before doing real work.
