What a delivery is
A delivery is one HTTPSPOST from CaseXchange carrying one event, sent to your subscription’s url — or, while echo mode is on, to the CaseXchange capture endpoint instead (see Testing webhooks). Every active subscription of your firm receives every event, so if you have three subscriptions, one event produces three deliveries. This page covers the request, the headers, the JSON envelope, and the processing rules your handler must follow. Event-specific data shapes live in the Event catalog; signature verification lives in Verifying signatures.
The request
Answer
2xx as soon as the body is durably stored, and do the real work asynchronously. A failed attempt is retried on a fixed schedule; see Reliability for the retry ladder and the circuit breaker.
Your subscription’s url is re-checked at every delivery against the same rules that apply on create: https:// on port 443, no credentials in the URL, and a hostname that resolves only to public addresses. See Managing subscriptions for the full list.
Headers
Every delivery carries these four headers.Event id versus delivery id
The two UUIDs answer different questions.X-CaseXchange-Event-Ididentifies what happened. When a referral is sent, the sending firm receivesreferral.sentand the receiving firm receivesreferral.receivedfor the same fact, with the same event id. If your firm has two active subscriptions, both receive a delivery with the same event id. Use it to correlate, never to deduplicate: two deliveries with the same event id are not necessarily duplicates.X-CaseXchange-Deliveryidentifies this attempt to tell this subscription. Retries resend the same body with the same delivery id. Two requests with the same delivery id are always duplicates.
The envelope
Every firm event carries the same top-level shape. All fields are present on every event;changedFields is the one exception and appears only on case.updated and note.updated.
webhook.test deliveries do not use this envelope, so branch on event before you read envelope fields — the exact body is in the Event catalog.Perspective
perspective tells you which side of the referral your firm is on: sent if your firm referred the case out, received if your firm received it. The same fact can produce a sent delivery for one firm and a received delivery for the other. piiMasked can only be true on the received side, and only until your firm acknowledges the referral. See Client data and masking for what is withheld while masked.
Actor
actor says who caused the event, at firm granularity.
actor.firmIdis the firm that acted, ornullfor platform administrators and system jobs.actor.sourceis the channel: one ofmanual,case_status,salesforce,smartadvocate,system,dearlegal,api,filemaker,clio,admin,ai.apicovers both the Public API and the CaseXchange web application. The two are not distinguished.actornever contains a user id. Where a person is relevant,datacarries a display name (createdByName,authorName,uploadedByName). The only user identifiers anywhere in a delivery areauthorIdonnote.createdandnote.updated, anduploadedByIdondocument.uploaded(document.deletedcarries record ids only, never a user id), and those are sent only to the firm that owns the note or uploaded the document; the counterparty receivesnull.
changedFields and the customFields sentinel
changedFields is present only on case.updated and note.updated. On note.updated it is always ["body"]. On case.updated it lists the case fields that changed, already intersected with what your firm is allowed to see; if the intersection would be empty, your firm does not receive the event at all.
One entry is special. The value customFields means “your firm’s custom field values changed”. Custom field values are never included in data, so on customFields re-fetch the case with the API to read them. A masked recipient never receives customFields.
changedFields reports the columns that were written, not a value diff. A PATCH /sent-cases/{id} that re-sends an identical value can still produce a case.updated naming that column. Treat changedFields as a hint about where to look, and compare against your stored copy before acting.recipientFirmId
recipientFirmId is the firm this delivery was built for. The masking and field intersection in data were computed for that firm. Compare it with your own firm id (from GET /firms/me) and drop anything that does not match; a mismatch means the delivery was not meant for you.
referralId versus baseCaseId
One case (baseCaseId) can carry several referrals over its life, each with its own referralId, and each firm sees only the hops it is a party to. Key your records on referralId for hop-level state (status, fees, lead attorney) and on baseCaseId when you need to group hops of the same case. See Client data and masking for how multi-hop cases are scoped.
Processing rules
Deliveries are at-least-once, concurrent, and unordered. Build your handler around the rules below rather than around what you observe on a quiet day.Deduplicate on the delivery id
Retries resend the same body with the sameX-CaseXchange-Delivery. Store that UUID before you acknowledge, and treat any later request with the same value as already handled. Do not deduplicate on eventId: one fact fans out to every subscription of your firm, and to the counterparty, with the same eventId, so a second delivery with the same event id can be a legitimate delivery to a different subscription.
Do not assume ordering
Dispatch is concurrent, and deliveries to a single subscription are not serialised, so expect parallelPOSTs. A delivery that failed and is being retried can arrive up to about 43 minutes after a later event for the same referral. Never apply state in arrival order. Compare occurredAt with the last value you applied for that referralId and keep the greatest.
Identical occurredAt: chain the statuses
One transaction can commit two status changes at once, for exampleclosing followed by closed, and both deliveries carry a byte-identical occurredAt. There is deliberately no tiebreaker. For status events, chain them instead: data.previousStatus of one delivery matches data.status of the other, so order them by walking that link rather than by timestamp.
Your own writes come back
Most of what your firm changes, through the API or in the web application, comes back to you as an event — but not everything: see Known gaps for the writes that produce none.actor.firmId tells you which firm acted, not which channel, so you cannot recognise your own integration’s writes from the actor alone. To ignore your own echo, compare the delivered state with what you last wrote and skip when nothing differs.
No backfill: reconcile against the API
A subscription receives only events that happen after it is created. There is no replay or redeliver endpoint, and the delivery log atGET /webhooks/{id}/deliveries reports status, attempt count, and last error but never the payload. Store the bodies you receive yourself, and treat webhooks as a signal to reconcile against GET /status-updates, GET /referrals, GET /sent-cases, and GET /received-referrals, which remain the authority.
A minimal handler
The handler below shows the order that matters: verify on the raw bytes, deduplicate, acknowledge, then do the work off the request path and apply byoccurredAt. verify is the reference function from Verifying signatures.
Next steps
Event catalog
Every event name, who receives it, and the exact
data shape.Verifying signatures
HMAC-SHA256 over the raw body, the 300-second window, and secret rotation.
Reliability
Retry schedule, delivery statuses, the circuit breaker, and retention.
Client data and masking
What a receiving firm sees before and after acknowledging.

