Agent skill

Dentally Webhooks Skill

Receive and verify Dentally webhooks. Use when setting up Dentally webhook handlers, debugging X-Dentally-Signature verification, or handling dental practice events like appointment.created, patient.updated, or payment.created.

Install this skill

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


When to Use This Skill

  • How do I receive Dentally webhooks?
  • How do I verify the X-Dentally-Signature header?
  • Why is my Dentally webhook signature verification failing?
  • How do I handle appointment.created, patient.updated or payment.created events?
  • Why did Dentally disable my webhook endpoint?

Verification (core)

Dentally signs the raw POST body with HMAC-SHA256, keyed on the secret string you set on the webhook endpoint (used as UTF-8). The signature is a bare lowercase hex digest in X-Dentally-Signature: no prefix, no timestamp. Validate the header is exactly 64 hex characters before decoding it (Buffer.from(x, 'hex') silently truncates invalid hex), then compare timing-safe.

Node:

const crypto = require('crypto');

function verify(rawBody, signatureHeader, secret) {
  if (!secret || typeof signatureHeader !== 'string') return false;
  if (!/^[0-9a-f]{64}$/i.test(signatureHeader)) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
  return crypto.timingSafeEqual(expected, Buffer.from(signatureHeader, 'hex'));
}

Python:

import hmac, hashlib

def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
    if not signature_header or not secret:
        return False
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest().encode()
    # Compare bytes so a non-ASCII header is a mismatch (401), not a TypeError (500)
    return hmac.compare_digest(expected, signature_header.lower().encode())

In Express read the body with express.raw({ type: '*/*' }) so verification works whatever Content-Type is sent.

For complete handlers with route wiring, event dispatch, and tests, see:

Common Event Types

EventDescription
appointment.createdAn appointment was booked
appointment.updatedAn appointment changed (state, time, practitioner)
appointment.deletedAn appointment was deleted
patient.createdA patient record was created
patient.updatedA patient record changed
patient.deletedA patient record was deleted
payment.createdA payment was recorded
payment.updatedA payment changed
payment.deletedA payment was deleted

For the full reference, see Dentally API: Webhooks

Delivery Behaviour

  • Payload: { "data": {...}, "event": "appointment.created", "object": "appointment", "user": {...} }. user is the Dentally user who triggered the event, if any.
  • Respond with any 2xx (200 or 204). Anything else triggers a retry (up to 10 times, increasing intervals).
  • More than 50 failed deliveries disables the webhook automatically. Re-enable it in Developer Settings.
  • Webhooks may arrive out of order; new event types may be added at any time. Ignore unknown events and return 204 No Content.
  • Webhook URLs must be HTTPS (plain HTTP is allowed only in the sandbox).

Important Headers

HeaderDescription
X-Dentally-SignatureHex HMAC-SHA256 of the raw body
User-AgentDentally-Webhook
Content-Typeapplication/json

Environment Variables

DENTALLY_WEBHOOK_SECRET=your_webhook_secret   # The "secret" you set when creating the webhook endpoint

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 dentally --path /webhooks/dentally

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 6, 2026

View on GitHub →