Overview
Clio Matter Bridge is an integration for Clio Manage, a practice-management product for law firms. It has an OAuth 2.0 flow with token refresh, an API client that paces itself, and a webhook receiver. It also has a boundary that decides what is allowed to reach a language model.
I built it and tested it against a live Clio Manage developer trial account. The account held only synthetic records I created myself, and the trial has since expired. It has no real client data and no users. I wrote it because I had never worked with Clio, and the quickest way to learn a platform is to hit the real API and write down where it disagrees with what I expected.
What it does
- OAuth 2.0 with refresh. A local callback server handles the authorization code flow, compares the CSRF state in constant time, and stores each token with an absolute expiry instant. Clio returns a relative
expires_in, which is wrong the moment it is read back from disk. - A paginating client that paces itself. It reads
X-RateLimit-Limitfrom every response, keeps a third of the budget as headroom, and pages by cursor only, because offset paging stops with a 422 past 10,000 records. - A webhook receiver. It echoes the handshake secret and verifies an HMAC-SHA256 of the raw request body, with record id plus etag as the idempotency key. I deployed it on Vercel and registered a real subscription against it.
- A confidentiality boundary. A field reaches a language model only if it is on an explicit allowlist. Identifiers get stable pseudonyms so a model can still reason about "the same party". The audit record stores a hash of what left and the field names, never the content.
- Zero runtime dependencies. Node built-ins only, so a clone runs with no install step.
What I measured
Clio's webhook documentation page returns 404, so I worked out the contract from the API's own validation errors, then confirmed it end to end. I created and edited a matter and captured the signed deliveries that came back. The captured bytes are committed as fixtures and a suite replays them.
The real payloads disproved three assumptions I had taken from a third-party guide. The event name is at meta.event, not the top level. There is no model field in a delivery at all. And there is no timestamp anywhere, so a replay guard that requires one rejects every genuine event. That guard is now opt-in, and a test pins the default so turning it on by accident fails locally.
Other measured behaviour: a webhook subscription expires about three days out and has to be renewed, the developer portal rejects localhost as a redirect URI, and Manage authorizes at app.clio.com, not the Platform host.
The test count is 265 assertions across six suites: OAuth 64, client 32, webhook 72, confidentiality 47, captured Clio deliveries 25, and a live suite of 25 that needs an authorized account. I re-ran the five offline suites and they pass. The live suite cannot run again until there is a new Clio account, so the animated terminal capture in the README is the record of it.
What broke
I got the rate limit wrong first. Clio documents 50 requests a minute at peak, with higher limits off-peak. In the morning five calls in a row reported a limit of 600, so I decided the docs were wrong and hardcoded the faster pace. Later the same day the header read 50. The docs were right and I had read one value of something that varies. The client now takes its pacing from the header on every response.
A test passed for the wrong reason. The redirect guard rejects localhost, and the test asserted that the error matched /localhost/. When I deleted the guard, the suite stayed green at 64 of 64, because the generic fallback error echoes the bad URI back and so also contains the word. The assertion now matches the phrase that only the dedicated branch produces, and the same deletion fails.
That is why the test runner breaks three guards on purpose and checks that the right suite goes red. I later broke ten guards one at a time. Nine failed a suite as they should. The tenth, the cap on the fields parameter, stayed green because nothing asserted it. While writing the missing suite I found a second bug: the rate-limit header lookup was case-sensitive, so a plain object carrying X-RateLimit-Limit returned nothing and pacing quietly fell back to its starting guess. Both are fixed.
What it does not do
This is API work. Configuring matters, calendaring, billing and intake inside Clio is a different skill and this project does not claim it. It does not use Clio's conflict-check scope, and it does not yet renew webhook subscriptions automatically.
Tech Stack
Runtime: Node.js, JavaScript (ES modules), built-ins only Integration: Clio Manage API v4, OAuth 2.0, webhooks signed with HMAC-SHA256 Hosting: Vercel function for the webhook receiver Testing: Plain Node assertion suites, captured real payloads as fixtures, shell-driven mutation controls