> ## Documentation Index
> Fetch the complete documentation index at: https://developers.demosmith.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Get a signed request for each step of a demo, including when the agent needs an answer.

Instead of polling, give Demosmith an HTTPS URL and it sends a `POST` each time a demo moves on.

There are two ways to receive events:

* **Endpoints for the whole organization.** Add them on the [Developers page](https://app.demosmith.ai/developers) or with [`POST /webhooks`](https://developers.demosmith.ai/api-reference/webhooks/add-a-webhook-endpoint). Each endpoint chooses which events it gets.
* **A callback for one demo.** Pass `callback_url` when you [create a demo](https://developers.demosmith.ai/quickstart.md). It gets every event for that demo except `demo.progress`.

## Events

| Event | Sent when |
| - | - |
| `demo.queued` | The demo is waiting for a recording slot |
| `demo.started` | Recording started |
| `demo.progress` | The demo moved to another stage (recording, analyzing, narrating, editing, rendering). Opt-in. |
| `demo.input_required` | The agent is waiting for an answer, see `data.demo.input_request` |
| `demo.completed` | The video is ready, `data.demo.video` has the links |
| `demo.failed` | The demo couldn't be completed, `data.demo.failure.message` says why |
| `demo.canceled` | The demo was canceled |

A new endpoint gets every event except `demo.progress`. Choose events on the Developers page, or subscribe to `"*"` for all of them.

## The request

```http theme={null}
POST /webhooks/demosmith HTTP/1.1
Content-Type: application/json
webhook-id: evt_6f1c2b9d4e8a4f0b9c3d2e1f0a9b8c7d
webhook-timestamp: 1794063120
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
```

```json theme={null}
{
  "id": "evt_6f1c2b9d4e8a4f0b9c3d2e1f0a9b8c7d",
  "type": "demo.completed",
  "created_at": "2026-11-07T12:12:00Z",
  "data": {
    "demo": {
      "id": "5b9e3c7a-2f41-4c8e-9a51-7d0b6e2f1c34",
      "object": "demo",
      "status": "completed",
      "video": { "url": "https://...", "download_url": "https://...", "expires_at": "2026-11-07T14:12:00Z" },
      "metadata": { "crm_account": "acct_123" }
    }
  }
}
```

`data.demo` is the full [demo object](https://developers.demosmith.ai/api-reference/introduction.md#the-demo-object) as it is when the event is sent. When a demo moves quickly, it can already be ahead of the event `type`, so act on `type` and read the current state from `data.demo`.

`demo.completed` events carry fresh video links on every delivery attempt, valid for 2 hours.

## Verify the signature

Every request is signed in the [Standard Webhooks](https://www.standardwebhooks.com/) format with your organization's signing secret (`whsec_...`, on the Developers page). Verify the signature on the **raw** request body before you trust it.

```python Python theme={null}
# pip install standardwebhooks
from standardwebhooks import Webhook

wh = Webhook(os.environ["DEMOSMITH_WEBHOOK_SECRET"])

def handle(request):
    event = wh.verify(request.body, request.headers)  # raises if the signature is wrong
    if event["type"] == "demo.completed":
        save_video(event["data"]["demo"])
    return 204
```

```javascript Node.js theme={null}
// npm install standardwebhooks
import { Webhook } from "standardwebhooks";

const wh = new Webhook(process.env.DEMOSMITH_WEBHOOK_SECRET);

app.post("/webhooks/demosmith", express.raw({ type: "application/json" }), (req, res) => {
  const event = wh.verify(req.body, req.headers); // throws if the signature is wrong
  if (event.type === "demo.completed") saveVideo(event.data.demo);
  res.sendStatus(204);
});
```

```python Without a library theme={null}
import base64, hashlib, hmac, time

def verify(secret, headers, raw_body):
    msg_id = headers["webhook-id"]
    timestamp = headers["webhook-timestamp"]
    if abs(time.time() - int(timestamp)) > 300:
        raise ValueError("timestamp too old")
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = "v1," + base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    if not any(hmac.compare_digest(expected, s) for s in headers["webhook-signature"].split()):
        raise ValueError("bad signature")
```

## Retries

Answer with any `2xx` status within 10 seconds. Do slow work after you answer.

If your endpoint fails or times out, the event is sent again after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 3 hours, 6 hours and 12 hours, about a day in all. Every retry has the same `webhook-id`, so use it to ignore an event you've already handled.

An endpoint that keeps failing for 3 days or more is turned off, and the organization's owners and admins get an email. Turn it back on from the Developers page once it's fixed.

## Test an endpoint

Select **Test** next to the endpoint on the Developers page, or call `POST /webhooks/{endpoint_id}/test`. Demosmith sends a signed `ping` event straight away and shows you the response. The last 100 deliveries of each endpoint, with their status codes, are on the same page.

## Rotate the signing secret

Select **Rotate** on the Developers page. For the next 24 hours, each request carries two signatures, one with the old secret and one with the new one, so you can deploy the new secret without missing events. Standard Webhooks libraries accept a request when any signature matches.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.