Skip to main content

What a delivery is

A delivery is one HTTPS POST 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.
Deliveries never follow redirects. If your endpoint sits behind a redirecting host (for example a www or trailing-slash redirect), every attempt fails with Redirect not allowed. Subscribe to the final URL.
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-Id identifies what happened. When a referral is sent, the sending firm receives referral.sent and the receiving firm receives referral.received for 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-Delivery identifies 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.firmId is the firm that acted, or null for platform administrators and system jobs.
  • actor.source is the channel: one of manual, case_status, salesforce, smartadvocate, system, dearlegal, api, filemaker, clio, admin, ai.
  • api covers both the Public API and the CaseXchange web application. The two are not distinguished.
  • actor never contains a user id. Where a person is relevant, data carries a display name (createdByName, authorName, uploadedByName). The only user identifiers anywhere in a delivery are authorId on note.created and note.updated, and uploadedById on document.uploaded (document.deleted carries 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 receives null.

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 same X-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 parallel POSTs. 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 example closing 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 at GET /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.
Some changes never produce an event at all (creating a case by CSV import, some connected-system writes, archiving). See Known gaps before relying on webhooks for completeness.

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 by occurredAt. verify is the reference function from Verifying signatures.
Keep the raw body (req.body above) exactly as received. Re-serialising the JSON changes the bytes and breaks signature verification, and the delivery log will never give you the payload again.

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.