# Guide to Clio Webhooks: Features and Best Practices

Clio webhooks notify your systems about legal-practice activity: a matter is created, a contact is updated, an activity or bill changes. If you're building on Clio Manage, webhooks are how you react to case and client events without polling.

This guide covers how Clio webhooks work, the split model/events subscription design, how to verify the `X-Hook-Signature`, the mandatory renewal (webhooks expire), and the best practices for production.

## What are Clio webhooks?

Clio Manage webhooks are HTTP POSTs delivered to a URL you register, each signed with an HMAC-SHA256 in the `X-Hook-Signature` header, computed over the raw body with the `shared_secret` you supply when creating the webhook.

Clio's subscription design is split: you pick a `model` (like `matter` or `contact`) and an array of `events` (`created`, `updated`, `deleted`). Payloads reference these as `model.action` (for example `matter.created`).

## Clio webhook features

| Feature | Details |
| --- | --- |
| Configuration | `POST /api/v4/webhooks` on your region's host (US `app.clio.com`, EU `eu.app.clio.com`, CA `ca.app.clio.com`, AU `au.app.clio.com`) |
| Signature header | `X-Hook-Signature` |
| Signature scheme | lowercase-hex HMAC-SHA256 over the raw body, keyed with the `shared_secret` you supply |
| Model | one of `activity`, `bill`, `calendar_entry`, `communication`, `contact`, `matter`, `task` |
| Events | array of `created` / `updated` / `deleted` |
| Enablement | May be created `pending` and auto-enable within ~90s (no handshake echo required) |
| Expiry | Default 3 days; extend via `expires_at` (up to ~31 days); renewal mandatory |
| SDK | None (plain REST) |

## Common events

Payloads reference `model.action` compositions:

| Event | Fires when |
| --- | --- |
| `matter.created` | A matter is created |
| `matter.updated` | A matter is updated |
| `contact.created` | A contact is created |
| `contact.updated` | A contact is updated |
| `task.updated` | A task is updated |

You compose these by choosing a `model` and its `events` at subscription time.

## Setting up Clio webhooks

Create a webhook with `POST /api/v4/webhooks` on your region's Clio host, providing an HTTPS `url`, a `model`, the `events` array, the `fields` you want, a `shared_secret` you generate, and an optional `expires_at`.

Use the host that matches your app's region. Clio is regionalized (US `app.clio.com`, EU `eu.app.clio.com`, CA `ca.app.clio.com`, AU `au.app.clio.com`), and an app only exists on its own region's host. Call the wrong one and you get `invalid_client`, which reads like a bad secret and sends you debugging in the wrong place, when the real fix is the host.

Because Clio doesn't track usage, you must renew (or re-create) the webhook before it expires, the default lifetime is just 3 days, extendable via `expires_at` to about 31 days.

The webhook may auto-enable. A new subscription can start in a `pending` state and then flip to `enabled` on its own within about 90 seconds, and start delivering, without any handshake request ever reaching your endpoint. So Clio's "not enabled until the handshake succeeds" isn't absolute: don't block your rollout on implementing an echo you may never receive.

## Securing Clio webhooks

Each delivery carries an `X-Hook-Signature` header: a lowercase-hex HMAC-SHA256 of the raw request body, keyed with the `shared_secret` you supplied. Recompute it over the raw bytes and compare in constant time, this matches Clio's header exactly (verified against real deliveries, not inferred).

```javascript
const crypto = require("crypto");

const SECRET = process.env.CLIO_SHARED_SECRET; // you supplied this at creation

function verify(rawBody, signature) {
  const expected = crypto.createHmac("sha256", SECRET).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body, req.headers["x-hook-signature"])) {
    return res.sendStatus(401);
  }

  res.sendStatus(200); // acknowledge fast
  processQueue.add(JSON.parse(req.body)); // process asynchronously
});

```

The same check in Python:

```python
import hashlib
import hmac
import os

SECRET = os.environ["CLIO_SHARED_SECRET"].encode()

def verify(raw_body: bytes, signature: str) -> bool:
    expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")

```

Verify against the raw body, before any JSON parsing, or the recomputed HMAC won't match.

## Clio webhook limitations and pain points

### Webhooks expire and must be renewed

The Problem: A Clio webhook expires (default 3 days, up to ~31 via `expires_at`), and Clio doesn't track usage, so if you don't renew or re-create it before expiry, delivery silently stops.

Why It Happens: Clio time-boxes webhooks and requires explicit renewal.

Workarounds:

* Set a long `expires_at` and still renew/re-create from a scheduled job well before expiry.

How Hookdeck Can Help: Hookdeck's observability surfaces a drop in delivery volume, so a lapsed Clio webhook shows up as an alert rather than silent data loss.

### The split model/events design

The Problem: You subscribe by `model` plus an `events` array, and payloads use `model.action`. Expecting flat event names (or the wrong composition) mis-routes events.

Why It Happens: Clio separates the resource model from the action events.

Workarounds:

* Compose `model` + `events` at subscription time, and branch on `model.action` in the payload.

How Hookdeck Can Help: Hookdeck's filters route on the `model.action` you receive, so composition is handled in one place.

### Regional hosts are mandatory

The Problem: Clio is regionalized, and an app only exists on its own region's host. Create a webhook (or authenticate) against the wrong host and Clio returns `invalid_client`, which looks exactly like a bad secret and sends you debugging credentials when the real problem is the URL.

Why It Happens: Clio partitions accounts by region (US, EU, CA, AU), each on its own host, with no cross-region routing.

Workarounds:

* Use the host matching your app's region (US `app.clio.com`, EU `eu.app.clio.com`, CA `ca.app.clio.com`, AU `au.app.clio.com`) for both authentication and webhook creation.

How Hookdeck Can Help: Hookdeck gives you one stable ingestion URL regardless of Clio's region, so your endpoint configuration doesn't shift per region.

### Duplicates

The Problem: Retries can deliver the same event more than once.

Why It Happens: At-least-once delivery favors eventual delivery.

Workarounds:

* Dedupe on an event id and make side effects idempotent.

How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Best practices

### Use your region's host

Create webhooks and authenticate on the Clio host for your app's region (US/EU/CA/AU). A wrong-region call returns `invalid_client`, which masquerades as a bad secret.

### Verify the lowercase-hex signature over the raw body

Compute a lowercase-hex HMAC-SHA256 over the raw body with your `shared_secret` and compare in constant time.

### Renew before expiry

Set `expires_at` and renew/re-create before the (default 3-day) expiry; don't rely on usage keeping it alive.

### Acknowledge fast, process asynchronously

Return 200 quickly and defer work to a queue. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Compose model + events and dedupe

Subscribe with the right `model`/`events`, branch on `model.action`, and dedupe so retries don't double-process.

## Conclusion

Clio webhooks verify with a lowercase-hex `X-Hook-Signature` HMAC over the raw body, keyed with the `shared_secret` you supply, using a split `model` + `events` subscription design. Two sharp edges to plan around: use your region's Clio host (a wrong-region call returns a misleading `invalid_client`), and renew before the default 3-day expiry. Verify against the raw body, renew ahead of expiry, and dedupe.

[Hookdeck](https://hookdeck.com) verifies the signature, surfaces lapsed subscriptions, deduplicates, and durably queues every event at the edge, so your app processes verified, unique events without silently losing delivery.

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