# Guide to Demodesk Webhooks: Features and Best Practices

Demodesk webhooks tell your systems what happens to your sales meetings. When a meeting is scheduled, moved, handed over, started or ended, or when its recording and transcript are ready, Demodesk can send the details to a URL you choose, so your CRM, data warehouse or coaching workflow stays current without polling the API.

This guide covers the events and payload, how to set up an endpoint and authenticate it, the limits you will run into, and how to receive Demodesk webhooks reliably.

## What are Demodesk webhooks?

Demodesk webhooks are HTTP `POST` requests with a JSON body, sent when a meeting (a "demo" in Demodesk's API) or a recording changes state. Company admins manage them in the **Demodesk API** section of the integration settings. Each endpoint has a URL, one event and optional custom headers.

Demodesk does not sign webhooks. The way to authenticate them is a custom header you add to the endpoint, for example `Authorization: Bearer <token>`, which Demodesk sends with every request to that endpoint. Your receiver checks that the header is there and that its value matches.

## Demodesk webhook features

| Feature | Details |
| --- | --- |
| Configuration | Settings > Integrations > Connections > Demodesk API > Webhooks |
| Who can manage | Company admins, on eligible paid plans |
| Events | 10 meeting and recording events |
| Events per endpoint | One |
| Method and format | `POST`, `application/json` |
| Authentication | Optional custom headers you define |
| Signature and timestamp | None |
| Event name | `meta.event.action` |
| Retries | Non-2xx responses are retried; no schedule published |
| Failure handling | Endpoints that keep failing are disabled |
| Endpoint status | Healthy, Failing or Disabled |

## Demodesk webhook events

| Event | Sent when |
| --- | --- |
| `demo.scheduled` | A demo is scheduled |
| `demo.rescheduled` | A demo is rescheduled |
| `demo.event_response_updated` | Attendees' event responses are updated |
| `demo.handovered` | A demo is handed over to another host |
| `demo.canceled` | A demo is cancelled |
| `demo.started` | A demo starts |
| `demo.call_note_updated` | Call notes are updated for a demo |
| `demo.ended` | A demo ends |
| `recording.uploaded` | A recording upload has finished |
| `recording.transcription_postprocessed` | Transcription and post-processing of a recording are complete |

The help article lists all ten events. The older event list in Demodesk's API reference article shows eight, without `demo.event_response_updated` and `demo.call_note_updated`, so go by the help article and the V2 OpenAPI reference.

## The payload

Every request has the same envelope. `meta.event` carries the event name in `action`, an `apiVersion` (currently `v1`) and a `createdAt` timestamp. `data` follows a JSON:API layout, with an `id`, a `type` (`demos` or `recordings`) and an `attributes` object.

A trimmed `demo.ended` request has this shape (values are illustrative):

```json
{
  "meta": {
    "event": {
      "action": "demo.ended",
      "apiVersion": "v1",
      "createdAt": "2026-10-20T14:32:10Z"
    }
  },
  "data": {
    "id": "48213",
    "type": "demos",
    "attributes": {
      "token": "abc123",
      "name": "Product demo with Acme",
      "hostId": 1021,
      "hostEmail": "host@example.com",
      "startDate": "2026-10-20T14:00:00Z",
      "endDate": "2026-10-20T14:05:00Z",
      "duration": 300,
      "status": "no-show",
      "hubspotDealId": null,
      "salesforceOpportunityId": null
    }
  }
}

```

Demo attributes include CRM references for Salesforce, HubSpot and Pipedrive, and calendar event IDs for Google and Office 365, many of which can be `null`. Recording events carry `demoId`, `url` and a `temporaryVideoUrl`, as well as the same host and CRM fields.

Demodesk's FAQ notes one case where you get more than one `demo.ended` for a meeting: if a meeting is ended manually and then restarted, a second `demo.ended` arrives with a different status (the FAQ's example of a first status is `no-show`). Treat the latest one as the truth.

> **Inspect Demodesk webhooks as they arrive.** Add a [Hookdeck Inspect](https://inspect.hookdeck.com) URL as a webhook endpoint to see each Demodesk request and its headers, with no account or setup required.

## Setting up Demodesk webhooks

You need to be a company admin, and your company needs to be on a plan that includes the Demodesk API. Managers and regular users cannot manage webhook endpoints.

1. Generate a long random secret, for example with `openssl rand -hex 32`.
2. Go to **Settings > Integrations > Connections** and scroll to the **Demodesk API** section.
3. In the **Webhooks** area, add a webhook.
4. Enter the **Webhook URL**.
5. Select the **event** for this endpoint.
6. Add a custom header, for example name `Authorization` and value `Bearer <your secret>`.
7. Click **Add endpoint**.

Repeat for each event you need. You can use the same URL and header for every endpoint and route on `meta.event.action` in your receiver.

Custom headers each need a name and a value, and names must be unique. You cannot set `Content-Type`, `Content-Length` or `Host`, because Demodesk manages them. The V2 OpenAPI reference still says to email Demodesk support to enable webhooks, while the current help article describes admins adding endpoints themselves. If you do not see the **Webhooks** area, check your plan and role, then contact support.

To route through Hookdeck, create a Hookdeck source with the **Demodesk** type and use its URL as the Webhook URL. The source has two fields:

* **API Key Header**: the name of the custom header you added, such as `Authorization`.
* **API Key**: the exact header value, including any `Bearer ` prefix.

The source accepts `POST`, which is the method Demodesk uses.

## Securing Demodesk webhooks

Look up the header you chose and compare its whole value with your secret in constant time.

```javascript
const crypto = require('crypto');
const express = require('express');

const HEADER = (process.env.DEMODESK_WEBHOOK_HEADER || 'authorization').toLowerCase();
const EXPECTED = process.env.DEMODESK_WEBHOOK_SECRET; // the whole value, e.g. "Bearer 3f9a..."

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/demodesk', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.headers[HEADER];
  if (!EXPECTED || typeof received !== 'string' || !safeEqual(received, EXPECTED)) {
    return res.sendStatus(401);
  }

  let payload;
  try {
    payload = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  const action = payload?.meta?.event?.action;
  // Store the payload keyed by action and data.id, then process it asynchronously.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

HEADER = os.environ.get("DEMODESK_WEBHOOK_HEADER", "Authorization")
EXPECTED = os.environ.get("DEMODESK_WEBHOOK_SECRET", "")

def verify_demodesk_request(headers) -> bool:
    if not EXPECTED:
        return False  # never accept when unconfigured
    received = headers.get(HEADER)  # use a case-insensitive headers object
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), EXPECTED.encode("utf-8"))

```

A custom header authenticates the sender, not the body. It cannot detect a modified payload, and with no signature or timestamp a captured request can be replayed. Use an HTTPS URL and a long random value, and if the value might have leaked, edit each endpoint in Demodesk with a new one and update your receiver.

> **Make Demodesk webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks your secret header on every request, queues each event, and retries delivery to your service on your schedule, so a slow or failing endpoint does not get disabled in Demodesk.

## Demodesk webhook limitations and pain points

### Failing endpoints get disabled

**The Problem:** After a run of failures, Demodesk stops sending to your endpoint, and events during and after the outage never arrive.

**Why It Happens:** Demodesk retries non-2xx responses, and an endpoint that keeps failing is disabled automatically. The retry schedule and the threshold are not published.

**Workarounds:**

* Return `200` as soon as the request is stored, and do the work afterwards.
* Check endpoint status in the Webhooks list. **Failing** is your warning before **Disabled**.
* After an outage, fetch recent demos and recordings from the V2 API to fill the gap.

**How Hookdeck Can Help:** Hookdeck accepts each request and responds straight away, so Demodesk sees a healthy endpoint even when your service is down. Events are queued and delivered with [retries](/docs/retries) you control, and [Issues](/docs/issues) notify you when deliveries fail.

### One event per endpoint

**The Problem:** Receiving all ten events means managing ten endpoints, each with its own copy of the header.

**Why It Happens:** Each webhook endpoint subscribes to a single event.

**Workarounds:**

* Use the same URL and header value for every endpoint and route on `meta.event.action`.
* Keep a checklist of endpoints so none is missed when you change the secret.

**How Hookdeck Can Help:** Point every endpoint at one Hookdeck source, then use [filters](/docs/filters) on `meta.event.action` and separate [connections](/docs/connections) to send each event to the service that needs it.

### No delivery ID

**The Problem:** A retry and a genuine second event look alike.

**Why It Happens:** Demodesk sends no delivery or event ID, only the resource `id` and `meta.event.createdAt`.

**Workarounds:**

* Build an idempotency key from `meta.event.action`, `data.id` and `meta.event.createdAt`. Demodesk does not document whether `createdAt` stays the same on a retry, so also make the processing itself safe to repeat. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

**How Hookdeck Can Help:** [Deduplication](/docs/deduplication) can drop requests whose chosen fields match a recent one before they reach your service.

### Static header authentication

**The Problem:** A leaked header value lets anyone send requests your receiver will accept.

**Why It Happens:** Webhooks are not signed, so the custom header is the only check.

**Workarounds:**

* Use HTTPS, a long random value, and change it if it might have leaked.
* Treat payload contents as a prompt to act, and fetch anything sensitive from the API.

**How Hookdeck Can Help:** Hookdeck's Demodesk source checks the header on every request. Hookdeck answers Demodesk with a `200` either way, but a request with a missing or wrong value is recorded as failed verification and never reaches your service.

## Best practices

### Route on meta.event.action

Every request names its event in `meta.event.action`. Read it first and dispatch from there, rather than relying on which URL received the request.

### Expect repeated demo.ended events

A restarted meeting produces a second `demo.ended` with a different status. Update the record you already have rather than creating a new one.

### Use temporary video URLs promptly

Recording events include a `temporaryVideoUrl`. As the name suggests, do not store it for later; download or process the video soon after the event, or fetch a fresh link from the API.

### Acknowledge fast and process asynchronously

Check the header, store the request, return `200`, and update your CRM afterwards. Fast responses keep the endpoint **Healthy**. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Demodesk webhooks cover the life of a sales meeting, from scheduling to the finished transcript, in a consistent envelope with the event name in `meta.event.action`. They are not signed, so authenticate them with a custom header you compare in constant time over HTTPS. Plan for endpoints being disabled after repeated failures, one endpoint per event, and no delivery ID.

[Hookdeck Event Gateway](https://hookdeck.com) checks the header, keeps your endpoint healthy while your service recovers, and routes each event where it needs to go.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Demodesk webhooks reliably in minutes.