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-Signatureheader? - Why is my Dentally webhook signature verification failing?
- How do I handle
appointment.created,patient.updatedorpayment.createdevents? - 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
| Event | Description |
|---|---|
appointment.created | An appointment was booked |
appointment.updated | An appointment changed (state, time, practitioner) |
appointment.deleted | An appointment was deleted |
patient.created | A patient record was created |
patient.updated | A patient record changed |
patient.deleted | A patient record was deleted |
payment.created | A payment was recorded |
payment.updated | A payment changed |
payment.deleted | A payment was deleted |
For the full reference, see Dentally API: Webhooks
Delivery Behaviour
- Payload:
{ "data": {...}, "event": "appointment.created", "object": "appointment", "user": {...} }.useris the Dentally user who triggered the event, if any. - Respond with any 2xx (
200or204). 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
| Header | Description |
|---|---|
X-Dentally-Signature | Hex HMAC-SHA256 of the raw body |
User-Agent | Dentally-Webhook |
Content-Type | application/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
- references/overview.md - Dentally webhook concepts and events
- references/setup.md - Creating a webhook endpoint and secret
- references/verification.md - Signature verification details and gotchas