# Guide to Dentally Webhooks: Features and Best Practices

Dentally webhooks tell your application when appointments, patients and payments change in a dental practice's Dentally account. If you build booking tools, patient communications or reporting on top of Dentally, webhooks let you react to a new appointment or a recorded payment as it happens instead of polling the API.

This guide covers how Dentally webhooks are created and signed, the nine events and their payload, how retries and auto-disabling work, and the practices that keep a Dentally receiver reliable.

## What are Dentally webhooks?

A Dentally webhook is an endpoint you register in Developer Settings or through the API, with a list of events and a secret you choose. When one of those events happens, Dentally sends a `POST` to every enabled webhook subscribed to it. Unlike many thin-payload systems, the body contains the full record, along with the Dentally user who caused the change.

Each request is signed. The `X-Dentally-Signature` header is the hex HMAC-SHA256 of the request body, keyed with the webhook's secret. There is no prefix, no timestamp and no event ID.

## Dentally webhook features

| Feature | Details |
| --- | --- |
| Configuration | Developer Settings, or `POST /v1/webhooks` |
| Subscription | An `events` list per webhook |
| Endpoint URL | HTTPS. Plain HTTP is accepted only in the sandbox |
| Signature header | `X-Dentally-Signature` |
| Scheme | HMAC-SHA256, lowercase hex, no prefix |
| Signed content | The raw request body only |
| Secret | The `secret` string you set on the webhook |
| Other headers | `User-Agent: Dentally-Webhook`, `Content-Type: application/json` |
| Success response | Any `2xx`, such as `200` or `204` |
| Retries | Up to 10 times at increasing intervals |
| Auto-disable | After more than 50 failed deliveries, until re-enabled in Developer Settings |
| Ordering | Not guaranteed |

Dentally publishes no response timeout, retry intervals or source IP list.

## Common events

Dentally documents nine events, three for each kind of record.

| Event | Sent when |
| --- | --- |
| `appointment.created` | An appointment is booked |
| `appointment.updated` | An appointment changes |
| `appointment.deleted` | An appointment is deleted |
| `patient.created` | A patient record is created |
| `patient.updated` | A patient record changes |
| `patient.deleted` | A patient record is deleted |
| `payment.created` | A payment is recorded |
| `payment.updated` | A payment changes |
| `payment.deleted` | A payment is deleted |

Dentally says new events can be added without that counting as a breaking change, and asks receivers to ignore events they don't recognise and return `204 No Content`.

The payload has four top-level fields. Here is Dentally's `appointment.created` example, trimmed:

```json
{
  "data": {
    "id": 2181,
    "patient_id": 1,
    "state": "Pending",
    "start_time": "2015-07-08 09:30:00 +0100",
    "reason": "Exam"
  },
  "event": "appointment.created",
  "object": "appointment",
  "user": {
    "id": 1,
    "first_name": "Gregory",
    "last_name": "Hays",
    "role": "Dentist"
  }
}

```

* `event` is the event name. Route on it.
* `object` is the record type: `appointment`, `patient` or `payment`.
* `data` is the record itself, so its shape depends on `object`.
* `user` is the Dentally user who triggered the event, when there is one.

## Setting up Dentally webhooks

Dentally doesn't generate a signing secret. You choose it when you create the webhook, so generate a long random value first, for example with `openssl rand -hex 32`, and store it server-side as `DENTALLY_WEBHOOK_SECRET`.

Then create the webhook in Developer Settings, or through the API. Parameters are wrapped in a `webhook` object:

```bash
curl https://api.dentally.co/v1/webhooks \
  -H "Authorization: Bearer $DENTALLY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "User-Agent: my-integration" \
  -d '{
    "webhook": {
      "active": true,
      "name": "Appointments sync",
      "url": "https://example.com/webhooks/dentally",
      "events": ["appointment.created", "appointment.updated", "appointment.deleted"],
      "secret": "'"$DENTALLY_WEBHOOK_SECRET"'"
    }
  }'

```

Larger practices can generate several thousand webhooks a day, so subscribe only to the events you use.

If you use Hookdeck, create a source with the Dentally source type, paste the same secret into it, and use the Hookdeck source URL as the webhook's `url`.

## Securing Dentally webhooks

Read `X-Dentally-Signature`, compute the HMAC-SHA256 of the raw request body with your secret as the key, hex-encode it, and compare the two in constant time.

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

function verifyDentallySignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;
  const expected = Buffer.from(
    crypto.createHmac('sha256', secret).update(rawBody).digest('hex'),
    'utf8'
  );
  const received = Buffer.from(signatureHeader.trim().toLowerCase(), 'utf8');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/dentally', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyDentallySignature(req.body, req.get('x-dentally-signature'), process.env.DENTALLY_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Route on event.event. Return 204 for events you don't handle.
  res.sendStatus(204);
});

```

The same check in Python:

```python
import hashlib
import hmac

def verify_dentally_signature(raw_body: bytes, signature_header, secret: str) -> bool:
    if not signature_header or not secret:
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(
        signature_header.strip().lower().encode("utf-8"), expected.encode("utf-8")
    )

# In FastAPI: raw_body = await request.body()
# verify_dentally_signature(raw_body, request.headers.get("x-dentally-signature"),
#                           os.environ["DENTALLY_WEBHOOK_SECRET"])

```

The signature covers only the body and there is no timestamp, so there is no replay window to check.

> **Make Dentally webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies the `X-Dentally-Signature` header on every delivery, answers Dentally straight away, and queues each appointment, patient and payment event until your service has processed it.

## Dentally webhook limitations and pain points

### 50 failures and the webhook is off

**The Problem:** Your integration goes quiet after a few bad days, and no new events arrive.

**Why It Happens:** Dentally retries a failed delivery up to 10 times. Once more than 50 deliveries have failed, it disables the webhook, and it stays disabled until someone re-enables it in Developer Settings. With thousands of events a day, a short outage is enough to reach 50.

**Workarounds:**

* Return a `2xx` as soon as the request is verified and stored, and do the work afterwards.
* Return `204` for events you don't handle, rather than an error.
* Check the webhook's status in Developer Settings after any incident, and reconcile through the API for the period it was off.

**How Hookdeck Can Help:** Hookdeck accepts each delivery as soon as it arrives, so failures on your side don't count against Dentally's limit. Events wait in Hookdeck and are delivered with [retries](/docs/retries) you configure.

### No event ID

**The Problem:** You can't tell a retry from a genuinely new event.

**Why It Happens:** The body has no event or delivery ID, and no header carries one. A retried `appointment.updated` looks the same as a second update.

**Workarounds:**

* Make processing idempotent: upsert the record in `data` by its `id` rather than appending changes.
* If you need to detect exact repeats, hash the raw body and remember recent hashes.

**How Hookdeck Can Help:** Hookdeck's [deduplication](/docs/deduplication) can drop exact repeats, or compare the body fields you choose, before they reach your service.

### Out-of-order deliveries

**The Problem:** An older version of an appointment overwrites a newer one.

**Why It Happens:** Dentally warns that webhooks may arrive out of order, and retries make this more likely.

**Workarounds:**

* Compare an update timestamp in `data` with the one you have stored, if the record carries one, and skip older versions.
* Fetch the record from the API when the order matters.

**How Hookdeck Can Help:** Hookdeck logs each request with the time it arrived, so you can see the sequence Dentally actually sent when debugging.

### No replay protection

**The Problem:** A captured request still verifies if someone sends it again.

**Why It Happens:** Only the body is signed, with no timestamp.

**Workarounds:**

* Keep the endpoint on HTTPS and the secret out of logs.
* Treat processing as idempotent so a repeat does no harm.

**How Hookdeck Can Help:** Hookdeck's deduplication drops identical bodies within a window you choose.

## Best practices

### Verify against the raw body

Use `express.raw({ type: '*/*' })` in Express, `await request.text()` in Next.js and `await request.body()` in FastAPI. Parse JSON only after the signature check passes.

### Ignore unknown events politely

Dentally can add events at any time. Log anything you don't recognise and return `204`, as Dentally asks.

### Subscribe narrowly

A busy practice sends thousands of webhooks a day. Subscribe only to the events your integration uses.

### Acknowledge fast and process asynchronously

Verify, store the event, return a `2xx`, and do the work afterwards. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks) and our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Dentally signs each webhook with `X-Dentally-Signature`, a bare hex HMAC-SHA256 of the raw body keyed with the secret you chose. Verify it against the raw bytes, upsert records idempotently because there is no event ID and ordering isn't guaranteed, return `204` for events you don't handle, and keep failures low, because more than 50 of them disable the webhook.

[Hookdeck Event Gateway](https://hookdeck.com) verifies every Dentally signature and stores each event until your service has handled it, so an outage on your side doesn't switch the webhook off.

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