Skip to main content

How to read this page

A firm subscription receives 13 business events plus one synthetic webhook.test event. There is no per-subscription event filter: every active subscription receives every event your firm is entitled to, so branch on the event field (or the X-CaseXchange-Event header) and route from there. Every business event arrives inside the same envelope. The envelope fields (eventId, occurredAt, perspective, piiMasked, referralId, baseCaseId, actor, and so on) are documented on Anatomy of a delivery. This page covers what changes from event to event: the data object and who receives it. The data schema names below (CaseSnapshot, StatusChangeData, NoteData, …) are the component names the API reference uses for the same shapes, and the case snapshot uses the same field vocabulary as GET /cases, so a push and a poll line up field for field there.
Ignore event names you do not recognise. An unknown event value is not an error and must not fail your handler.

Catalog

Notes on the table:
  • Can be masked means the event can arrive with piiMasked: true. Only the status family can. Before a receiving firm acknowledges a referral it receives the status family (referral.received, referral.accepted, referral.rejected, case.status_changed) and, in addition, note.created and note.updated for notes its own firm owns: a note is that firm’s own content, and it can write notes while the referral is still in sent. What is withheld from an unacknowledged receiving firm is case.updated, document.uploaded, document.deleted, and the counterparty’s notes. The full rules are on Client data and masking.
  • changedFields is present only on case.updated and note.updated. On case.updated it is already intersected with what your firm may see; a recipient whose intersection is empty is not delivered the event at all. The sentinel value customFields means your custom field values changed and you should re-GET the case; a masked recipient never receives it.
  • referral.accepted is normally the first unmasked delivery a receiving firm gets. If an administrator or an integration made the transition without the receiving firm acknowledging, it stays masked. An administrator override such as sent to signed arrives as case.status_changed, not referral.accepted, and does not unmask.
  • document.uploaded and document.deleted are stricter than case content: document access ends at closed, where case.updated and note.* keep flowing. Fee-agreement documents produce no event.
  • case.rated is the only way to observe a tier. The case snapshot carries no tier field, and a receiving firm never receives this event.

Status vocabulary

Every status and previousStatus value, in the envelope’s data and inside a case snapshot, uses the same normalized lower-case vocabulary as GET /cases: draft, sent, received, under_evaluation, investigating, signed, in_litigation, closing, won, lost, closed, rejected, withdrawn, pending_spin_off Two normalizations apply:
  • The legacy received status is published as under_evaluation.
  • Private terminated and exhausted categorizations are published as rejected.
See Case Lifecycle for what each status means and which transitions are allowed.

StatusChangeData

Delivered as data on referral.sent, referral.received, referral.accepted, referral.rejected and case.status_changed. One shape for all five, so you can point them at a single handler.
One transaction can commit two status rows (for example closing then closed) with a byte-identical occurredAt. There is deliberately no tiebreaker: chain them by previousStatus to status.

Example: referral.accepted, sending firm’s copy

The receiving firm acknowledged the referral. This delivery went to the sending firm (perspective: "sent"), so the snapshot is complete.

MaskedStatusChangeData

The status-family data delivered to a receiving firm that has not acknowledged the referral. Same keys as StatusChangeData, with two differences:
  • case is a MaskedCaseSnapshot (14 keys; everything else absent, not null).
  • message is forced to null.
The envelope carries piiMasked: true. Check that flag before reading any client field.

Example: masked referral.received

A referral that is withdrawn or rejected before the receiving firm ever acknowledges it never unmasks. Do not build a flow that waits for client fields to appear on such a referral.

CaseSnapshot

The referral as it stands after the change committed, using the same vocabulary as GET /cases. It appears in two places:
  • Directly as data on case.created and case.updated.
  • Nested as data.case on the status family (StatusChangeData).
It never contains tier, source, or custom field values. Custom field changes surface only as the customFields sentinel in changedFields; re-GET the case to read them.

Identity

Status and outcome

Descriptors

Client

These fields are absent from the masked snapshot.

Warm transfer

Money

Dates

Example: case.updated

data is the full snapshot; changedFields sits on the envelope. Here the sending firm updated the projected settlement and a custom field, and the delivery went to the sending firm itself. To ignore your own writes, compare the delivered state with what you last wrote; actor.firmId tells you which firm acted, not which channel.
PATCH /sent-cases/{id} reports the columns it wrote in changedFields, not a value diff. Re-sending an identical value can still produce a case.updated naming that column.

MaskedCaseSnapshot

The pre-acknowledgment snapshot: ids, the shared status, and non-personal descriptors only. It contains exactly these 14 keys and nothing else. Omitted fields are absent from the object, not present with a null value, so code that reads data.case.clientLastName will find undefined, not null. The masked referral.received example earlier on this page shows a full masked delivery; it is the canonical one, and other pages point here for it.

NoteData

Delivered as data on note.created and note.updated. Notes carry no case snapshot: ids and the note body only. Notes are per-firm; each side of a referral keeps its own.

Example: note.updated

changedFields is always ["body"]: the body is the only editable field.
In this example the receiving firm owns the note, so authorId is present. The same fact delivered to the sending firm carries authorId: null.
There is no note.deleted event. Notes written automatically alongside a status transition, a case creation or a CSV import are not published as note.created; a note supplied to PATCH /sent-cases/{id} is a real note and is. See Known gaps.

DocumentData

Delivered as data on document.uploaded. It goes to the case’s sending firm (the case owner), which is not necessarily whoever uploaded the file, and to each counterparty whose hop still grants document access. Metadata only: there is no download URL in the event, because the signed download URL expires. To fetch the bytes, call GET /cases/{referralId}/documents/{id}/download using the envelope’s referralId — always present, unlike data.referralId — together with data.id, then follow the pre-signed URL it returns. See Documents examples.

Example: document.uploaded

Here the receiving firm uploaded the file and this copy went to the sending firm, so uploadedById is null.

DocumentTombstone

Delivered as data on document.deleted. Ids only.

CaseTombstone

Delivered as data on case.deleted, which announces a deleted draft. A draft has no receiving firm, so this event goes to the sending firm alone and referentFirm on the envelope is null. The ids were captured before the row was removed; the envelope fields describe the state before the delete.

CaseRatingData

Delivered as data on case.rated. A tier is the sending firm’s private appraisal of its own case, so this event goes to the owning firm only, whether or not the receiving firm has acknowledged. The case snapshot carries no tier, which makes this event the only way to observe one. ratingNotes is never published.

webhook.test

Queued by POST /webhooks/{id}/test. It is not an envelope: none of the fields above are present. This is the canonical webhook.test body; other pages link here rather than repeating it. The body is exactly:
Its eventId travels only in the X-CaseXchange-Event-Id header, and it is signed like any other delivery, so it is a good first check of your verifier. See Testing webhooks for the full test flow and echo mode.
For an end-to-end test with real business events, create a test case and drive it through a status change. Every delivery for it carries isTestCase: true in the envelope, so your handler can ignore or route it.

Anatomy of a delivery

Envelope fields, headers, ordering and deduplication.

Client data and masking

The full rules for who receives what, and when a receiving firm unmasks.

Verifying signatures

Check X-CaseXchange-Signature before you trust any body on this page.

Known gaps

Writes that produce no event, and how to reconcile around them.