Agent skill

Demodesk Webhooks Skill

Receive and authenticate Demodesk webhooks. Use when setting up a Demodesk webhook endpoint, checking the custom header you add to it, debugging 401s, or handling meeting and recording events like demo.scheduled, demo.ended and recording.uploaded.

Install this skill

npx skills add hookdeck/webhook-skills --skill demodesk-webhooks


Demodesk sends a JSON webhook when a sales meeting or its recording changes: a demo is scheduled, rescheduled, canceled, started or ended, or a recording is uploaded. Webhooks are not signed. Instead, you add a custom header (for example Authorization: Bearer abc123) to the webhook endpoint in Demodesk, and your receiver compares that header with the value you stored.

When to Use This Skill

  • How do I receive Demodesk webhooks?
  • Does Demodesk sign webhook payloads? (No: use a custom header as a shared secret)
  • How do I authenticate Demodesk webhook requests?
  • How do I handle demo.scheduled, demo.ended or recording.uploaded events?
  • Why does my Demodesk webhook endpoint return 401?

Verification (core)

const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyDemodeskRequest(headers, name = process.env.DEMODESK_WEBHOOK_HEADER || 'Authorization',
                               secret = process.env.DEMODESK_WEBHOOK_SECRET) {
  if (!secret) return false;                    // never accept when unconfigured
  const received = headers[name.toLowerCase()]; // Node lowercases header names
  return Boolean(received) && safeEqual(received, secret);
}
  • The header name is yours to choose (default Authorization), set in DEMODESK_WEBHOOK_HEADER. The expected value is DEMODESK_WEBHOOK_SECRET: the exact, whole header value, including any Bearer prefix you typed. Look the header up case-insensitively, and compare the whole value; don't strip or parse a Bearer prefix.
  • Python: hmac.compare_digest(received.encode(), secret.encode()) on bytes (a str argument raises on non-ASCII input).
  • Return 401 for a missing header or a mismatch, 400 for a body that isn't a JSON object, 200 once handled.
  • Read the body raw (express.raw({ type: '*/*' })) and authenticate before parsing it.

A static shared credential authenticates the sender, not the body. There is no signature and no timestamp, so it gives no replay protection and cannot detect a tampered payload. Serve the endpoint over HTTPS only, and rotate the header value (edit the webhook in Demodesk and update DEMODESK_WEBHOOK_SECRET) if it leaks.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Types

The event name is in meta.event.action. Each webhook endpoint subscribes to one event.

EventSent When
demo.scheduledA meeting is scheduled
demo.rescheduledA meeting is moved
demo.event_response_updatedAn invitee's response to the meeting changes
demo.handoveredA meeting is handed over to another host
demo.canceledA meeting is canceled
demo.startedA meeting starts
demo.call_note_updatedA call note is updated
demo.endedA meeting ends (can fire again if it is restarted)
recording.uploadedA recording is uploaded
recording.transcription_postprocessedA transcript finishes post-processing

Environment Variables

DEMODESK_WEBHOOK_HEADER=Authorization                # custom header name you added in Demodesk
DEMODESK_WEBHOOK_SECRET=Bearer your-long-random-secret   # exact whole header value

Local Development

Demodesk can't reach localhost. Tunnel with the Hookdeck CLI (no account required):

npx hookdeck-cli listen 3000 demodesk --path /webhooks/demodesk

Use the printed URL as the Webhook URL in Demodesk.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 9, 2026

View on GitHub →