Skip to main content

Webhooks

Webhooks let your application receive notifications when records change in Clio Manage, instead of polling the API for changes. You register a callback URL, subscribe it to a model and a set of events, and Clio sends a signed HTTP request to that URL whenever one of those events occurs.

This guide explains how to create and manage webhooks, how to secure your endpoint, and how to troubleshoot the problems that come up most often. For the full parameter reference, see the Webhooks API reference.

If you only need notifications for Clio Payments events, the Using Webhooks section of the Clio Payments guide covers that case specifically.

Supported Models

Each webhook subscribes to a single model. The models that support webhooks, and the OAuth scope each one requires, are:

ModelIdentifierRequired OAuth scope
MattermatterMatters
ActivityactivityActivities
BillbillBilling
Calendar Entrycalendar_entryCalendars
CommunicationcommunicationCommunications
ContactcontactContacts
TasktaskTasks
DocumentdocumentDocuments
FolderfolderDocuments
Clio Payments paymentclio_payments_paymentClio Payments

Note that folders use the Documents scope. To create a webhook for a given model, your application needs both the model's scope and the webhooks scope granted at authorization time.

Supported Events

Almost every model supports the three lifecycle events:

EventFired when
createdA record is created.
updatedA record is updated.
deletedA record is deleted.

Matters support three additional events tied to matter status changes:

EventFired when
matter_openedA matter's status changes to Open.
matter_pendedA matter's status changes to Pending.
matter_closedA matter's status changes to Closed.

Clio Payments payments are the one exception to the lifecycle events: they support only created and updated. Payments cannot be deleted, so there is no deleted event for them.

Creating a Webhook

Access Permissions

Creating webhooks requires the webhooks access permission, plus the access permission for the model you're subscribing to (see Supported Models). If you're not sure whether your application has these scopes, check your application settings in the Developer Portal.

Create a webhook by making a POST request to the /api/v4/webhooks endpoint. A sample request body is shown below:

{
"data": {
"url": "https://your-app.example.com/webhooks/clio",
"model": "matter",
"events": ["created", "updated", "deleted"],
"fields": "id,etag,display_number,status"
}
}

The request body accepts the following parameters:

ParameterTypeDescription
urlStringThe URL Clio will send events to. Must use the https scheme; http URLs are rejected.
modelStringThe model to subscribe to. Use the identifier from the Supported Models table (for example, matter).
eventsArrayOne or more event names to subscribe to.
fieldsStringOptional. A comma-separated list of fields to include in each event payload. See "Selecting Fields".
expires_atStringOptional. An ISO-8601 datetime for when the webhook should expire. See "Webhook Expiry and Renewal".

A newly created webhook is not active right away. Clio first confirms your endpoint through a handshake, described in "Confirming Your Endpoint" below.

Confirming Your Endpoint

Before Clio delivers any events, it confirms that the URL you registered is actually expecting them. This handshake runs immediately after you create a webhook, and again any time you change its URL. Until it succeeds, the webhook stays in the pending status and no events are sent.

To start the handshake, Clio sends a POST request to your URL with a secret in the X-Hook-Secret header. The request body includes the new webhook's ID as data.webhook_id. You can confirm the handshake in one of two ways. Store the secret when you receive it; it's the same value you'll use to verify signatures on every subsequent delivery. The secret should be treated like a credential: keep it out of source control and make sure it is filtered from your application logs.

The simplest is to respond synchronously: return a 200 response and echo the secret back in an X-Hook-Secret response header.

If you can't echo the secret in the same response (for example, because the request is handled by a queue), you can confirm it later by making a PUT request to the activate endpoint with the secret in the X-Hook-Secret header:

PUT /api/v4/webhooks/{id}/activate
Authorization: Bearer <access_token>
X-Hook-Secret: <the-secret-from-the-handshake-request>

Once the handshake succeeds, the webhook's status changes from pending to enabled and events begin to flow.

Verifying Payload Signatures

Clio signs every event delivery so that you can confirm the request came from Clio and was not modified in transit. Each delivery includes an X-Hook-Signature header containing an HMAC-SHA256 signature of the raw request body, keyed with the secret from the handshake. You should verify this signature on every request.

To verify it, compute the same HMAC over the raw request body using your stored secret, and compare it to the value in the header:

def valid_webhook?(request, shared_secret)
expected = OpenSSL::HMAC.hexdigest("SHA256", shared_secret, request.body.read)
Rack::Utils.secure_compare(request.env["HTTP_X_HOOK_SIGNATURE"], expected)
end
import hmac, hashlib

def valid_webhook(body: bytes, shared_secret: str, signature_header: str) -> bool:
expected = hmac.new(shared_secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header)

Always use a constant-time comparison, such as hmac.compare_digest or Rack::Utils.secure_compare, rather than ==. A plain string comparison can leak timing information that helps an attacker forge a signature.

Event Payload Format

Each event is delivered as a POST request to your URL with a JSON body in the following shape:

{
"data": {
"id": 152,
"etag": "\"9a103be2201ae758992733a91f02903f\""
},
"meta": {
"event": "created",
"webhook_id": 1234
}
}

The data object holds the record fields, controlled by the fields parameter you set when creating the webhook. The meta object contains the event name and the webhook_id of the subscription that triggered the delivery.

Selecting Fields

The fields parameter controls which fields of the record are included in each event payload, using the same syntax as the fields parameter on regular API requests. If you don't set it, payloads default to id and etag. The value is a comma-separated string and cannot be longer than 1000 characters.

It's worth choosing fields deliberately, because on updated events the fields parameter doubles as a change filter: Clio only delivers an updated event when at least one of the fields you selected has actually changed. Narrowing fields to the data your integration cares about therefore cuts down both the size of each payload and the number of deliveries you receive.

There is one exception to the change filter. The first time your webhook would deliver an updated event for a particular record, that event is always sent, even if none of the selected fields changed. After that, the filter applies normally.

Responding to Events

Clio waits only a short time for your endpoint to respond before it treats the delivery as failed, so your endpoint should return a 2xx response as quickly as it can. If handling an event involves slower work, such as database writes or calls to other APIs, enqueue that work and respond 200 right away rather than doing it inline.

If your endpoint returns anything other than a 2xx or 3xx status, or doesn't respond in time, Clio retries the delivery with exponential backoff. A webhook that keeps failing is eventually suspended.

The one exception is a 410 Gone response. Clio treats that as a deliberate signal that the endpoint is gone for good, and disables the subscription immediately without retrying.

Webhook Expiry and Renewal

Every webhook has an expires_at timestamp. If you don't provide one when creating the webhook, it defaults to 3 days after creation. The maximum is 31 days.

Once a webhook expires it stops receiving events, so a long-lived integration needs to renew its webhooks before they lapse. To extend one, send a PATCH request to /api/v4/webhooks/{id} with a new expires_at:

{
"data": {
"expires_at": "2026-09-30T23:59:59Z"
}
}

A common approach is to check your webhooks on a schedule, for example once a day, and extend any that are due to expire within the next couple of days.

Webhook Status

A webhook is always in one of three states:

StatusMeaning
pendingThe handshake hasn't been completed yet. Events are not delivered.
enabledThe webhook is active and events are being delivered.
suspendedThe webhook has been disabled, usually after repeated delivery failures.

You can check a webhook's current status by fetching it with GET /api/v4/webhooks/{id}.

Managing Webhooks

The webhooks endpoint supports the standard set of operations. They operate within your application and the authorizing user's grant, you manage the webhooks created under that token, and event deliveries are limited to records that user is permitted to see.

OperationRequest
List your webhooksGET /api/v4/webhooks
Fetch a single webhookGET /api/v4/webhooks/{id}
Update a webhookPATCH /api/v4/webhooks/{id}
Delete a webhookDELETE /api/v4/webhooks/{id}

You can update a webhook's URL, events, fields, or expiry. Changing the URL starts a new handshake, so the webhook returns to pending until the new endpoint is confirmed.