# Clio Webhooks

## When to Use This Skill

* How do I receive Clio webhooks?
* How do I verify Clio webhook signatures (`X-Hook-Signature`)?
* How do I complete the Clio `X-Hook-Secret` handshake / activation?
* How do I handle `created`, `updated`, `deleted`, or matter lifecycle events?
* Why is my Clio webhook signature verification failing?
* How do I keep a Clio webhook from expiring?

## How Clio Webhooks Work

Clio Manage delivers webhooks in two distinct kinds of POST request to your URL:

1. Handshake — Immediately after a webhook is created (or its URL changes),
  Clio sends a POST containing an `X-Hook-Secret` header with a freshly
  generated shared secret. Your endpoint must confirm it (echo the same
  header back with `200 OK`). Clio's docs say the webhook is not enabled until the handshake
  succeeds — though in one observed EU test the webhook auto-enabled and began
  delivering without any handshake request arriving (see
  [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/clio-webhooks/references/setup.md)). Implement the echo regardless: it is
  how you obtain the secret, and it is the key for verifying every later event.
2. Events — Every subsequent delivery is signed. Clio computes
  `HMAC-SHA256(shared_secret, raw_request_body)` and puts the digest in the
  `X-Hook-Signature` header. Verify it against the raw body.

> Clio does not ask you to supply the secret when creating the webhook — Clio
> generates it and hands it to you during the handshake. Save it (e.g. keyed by
> `webhook_id`) as `CLIO_WEBHOOK_SECRET`.

## Verification (core)

`X-Hook-Signature` is the HMAC-SHA256 digest of the raw body, keyed with the
shared secret. Pass the raw body (never re-serialized JSON) and compare
timing-safe.

Clio's docs state only that it "will compute an HMAC-SHA256 signature based on
the shared secret and the request body" — they never say whether the digest is
hex or base64 encoded.

Verified against a live delivery: it is lowercase hex (64 characters). This
was confirmed by recomputing HMAC-SHA256 over the raw body with the webhook's
`shared_secret` and matching the header exactly. The handlers below still compute
the digest once and accept either encoding, so they keep working if Clio ever
differs by region or changes it — but hex is what you should expect.

Node:

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

function verifyClioWebhook(rawBody, signatureHeader, secret) {
  if (!signatureHeader) return false;
  const digest = crypto.createHmac('sha256', secret).update(rawBody).digest();
  // Encoding is unspecified in Clio's docs — accept hex or base64.
  return [digest.toString('hex'), digest.toString('base64')].some((expected) => {
    try {
      return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
    } catch {
      return false; // length mismatch → not a match
    }
  });
}

```

Python:

```python
import hmac, hashlib, base64

def verify_clio_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
    if not signature_header:
        return False
    digest = hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()
    # Encoding is unspecified in Clio's docs — accept hex or base64.
    return (
        hmac.compare_digest(signature_header, digest.hex())
        or hmac.compare_digest(signature_header, base64.b64encode(digest).decode())
    )

```

Handle the handshake before signature verification — a request carrying an
`X-Hook-Secret` header is the handshake and must be echoed back, not verified:

```javascript
// if (req.headers['x-hook-secret']) { res.set('X-Hook-Secret', secret); return res.status(200).end(); }

```

> For complete handlers with the handshake, event dispatch, and tests, see:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/clio-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/clio-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/clio-webhooks/examples/fastapi/)

## Common Event Types

The event name arrives in the payload at `meta.event` (with `meta.webhook_id`).
All models support `created`, `updated`, `deleted` (Clio Payments payment
supports only `created`/`updated`). Matters add lifecycle events.

| Event | Fired When |
| --- | --- |
| `created` | A record of the subscribed model is created |
| `updated` | A watched field on the subscribed model changes |
| `deleted` | A record of the subscribed model is deleted |
| `matter_opened` | A matter's status changes to "Open" (matter model) |
| `matter_pended` | A matter's status changes to "Pending" (matter model) |
| `matter_closed` | A matter's status changes to "Close" (matter model) |

Models you can subscribe to: `activity`, `bill`, `calendar_entry`,
`clio_payments_payment`, `communication`, `contact`, `document`, `folder`,
`matter`, `task`.

Example event payload:

```json
{ "data": { "id": 152, "etag": "\"9a103be2...\"" },
  "meta": { "event": "created", "webhook_id": 1234 } }

```

> For the full model/event reference, see [Clio Webhooks docs](https://docs.developers.clio.com/api-reference/#tag/Webhooks).

## Important Headers

| Header | Description |
| --- | --- |
| `X-Hook-Signature` | HMAC-SHA256 digest of the raw body (verify this). Observed as lowercase hex; the examples accept base64 too as a safety net |
| `X-Hook-Secret` | Shared secret sent during the handshake; echo it back to activate |

## Environment Variables

```bash
# The shared secret Clio delivered in the X-Hook-Secret handshake header.
CLIO_WEBHOOK_SECRET=your_shared_secret_here

```

## Webhook Expiration (important)

Clio webhooks expire — 3 days after creation by default, up to a maximum of
31 days via `expires_at`. Clio does not track usage, so renew before expiry
by updating `expires_at` (PATCH the webhook) to keep delivery active.

Create a webhook (needs the OAuth `webhook` scope plus the model's scope):

```bash
curl -X POST https://app.clio.com/api/v4/webhooks.json \
  -H "Authorization: Bearer $CLIO_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"data":{"url":"https://your.app/webhooks/clio","model":"matter","fields":"id,etag","events":["created","updated","deleted"]}}'

```

> Regional base URLs differ: US `app.clio.com`, EU `eu.app.clio.com`,
> AU `au.app.clio.com`, CA `ca.app.clio.com`. Only `https` URLs are accepted.

## Local Development

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

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/clio-webhooks/references/overview.md) - Clio webhook concepts, models, events
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/clio-webhooks/references/setup.md) - Creating webhooks, handshake, expiration
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/clio-webhooks/references/verification.md) - Signature verification details and gotchas