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).
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:
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_atand 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+eventsat subscription time, and branch onmodel.actionin 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, EUeu.app.clio.com, CAca.app.clio.com, AUau.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.
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.
Compose model + events and dedupe
Subscribe with the right model/events, branch on model.action, and dedupe so retries don't double-process.
Make Clio webhooks production-ready
Hookdeck verifies X-Hook-Signature, surfaces lapsed subscriptions, deduplicates, and durably queues every event
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 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 for free and handle Clio webhooks reliably in minutes.