Outbound Webhooks allow Lawmatics to automatically notify an external server whenever something happens in your account. When a supported event occurs, Lawmatics sends a signed HTTP POST request to a URL you configure — your server receives the notification and can take action immediately.
This feature requires a developer or technical resource. Lawmatics handles the sending; your team handles receiving and processing. Available on Premium plans.
Overview
For the firm administrator setting up webhooks in Lawmatics.
Getting Started
Go to Settings > Webhooks and click Add New Webhook.
You'll need to provide:
Webhook Name — a label to identify this endpoint (e.g. "Matter Converted — Production")
Endpoint URL — the HTTPS URL your server is listening on. Must be HTTPS — HTTP is not accepted.
Event Type — select the event(s) you want the webhook subscription to fire on:
matter.created – new matter created
matter.status_changed – status changes (e.g. Prospect → Hired)
form.submitted – intake form submitted
invoice.paid – invoice paid in full
document.signed – signature completed
contact.updated – contact details changed
matter.converted - matter converted
invoice.created – new invoice created
contact.merged – two contacts merged
matter.note_added – note added to a matter
matter.task_completed – matter task marked complete
contact.deleted – contact deleted
matter.updated – matter fields updated
Click Save. Your signing secret will be displayed — copy it immediately and share it with your developer. It will not be shown again.
The Signing Secret
Every delivery is signed so your server can verify it actually came from Lawmatics. The signing secret is what your developer uses to verify each incoming request.
Shown in full only once — at creation, or when regenerated
Your developer stores it on their server and uses it to verify each delivery
Lawmatics sends two signature headers with every request:
X-Lawmatics-SignatureandX-Lawmatics-Timestamp
If the secret is lost: Open the webhook, click Regenerate next to the signing secret field. A new secret is shown once. The old secret stops working immediately — your developer must update their server right away or deliveries will fail with a 401 error.
Enabling and Disabling a Webhook
New webhooks are enabled by default. To pause deliveries without deleting the webhook, toggle the Active switch off in the Edit Webhook panel. Toggle it back on to resume.
Viewing Delivery History
To check whether a webhook fired and what happened:
Go to Settings > Webhooks
Click the events log icon on the webhook row
Each event shows: status, event type, event ID, number of attempts, timestamp, and response code
Click any event row to see the full delivery detail — including the response body your server returned and a per-attempt breakdown.
Status labels:
Status | Meaning |
Queued | Waiting to be sent |
Delivered | Successfully received by your server (2xx response) |
Retrying | Failed at least once, still trying (up to 7 attempts) |
Failed | All 7 attempts exhausted — will not retry |
Retry Behavior
If a delivery fails, Lawmatics retries automatically:
Attempt | Delay |
1 | Immediate |
2 | ~15 seconds |
3 | ~1 minute |
4 | ~5 minutes |
5 | ~30 minutes |
6 | ~1 hour |
7 | ~2 hours |
A 4xx response (other than 429) is treated as a permanent failure — Lawmatics will not retry. After all 7 attempts fail, the event is marked Failed and will not be retried. There is no manual replay in the current version.
Limits
Up to 2 webhook subscriptions per event type per firm
Troubleshooting
Symptom | Likely cause | Fix |
Deliveries showing 401 | Signing secret mismatch | Have your developer confirm the correct |
Deliveries showing 404 | Endpoint URL is wrong or missing a path (e.g. | Have your developer confirm the exact URL their server is listening on, then update it in Settings > Webhooks |
Deliveries showing 502 | Server is down or unreachable | Your developer needs to verify the server is running and publicly accessible |
Webhook not receiving events | Webhook may be disabled | Check that the Active toggle is on in Settings > Webhooks |
"Already used in 2 webhooks" error | 2-subscription limit reached | Delete or disable an existing subscription before creating a new one |
Secret not copied in time | Secret is only shown once | Use Regenerate — developer must update their server immediately |
Developer Guide
How It Works
A firm administrator registers your endpoint URL in Lawmatics under Settings > Webhooks.
When a subscribed event occurs, Lawmatics sends a signed HTTP POST to that URL.
Your server verifies the signature and processes the event.
You return a
2xxresponse to acknowledge receipt.
Upon creation, a signing secret is displayed once. The firm administrator must securely share it with you. Store it in your environment:
LAWMATICS_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Payload Envelope
Every event uses the same envelope structure:
{ "event_id": "evt_550e8400-e29b-41d4-a716-446655440000", "firm_id": 123, "event_type": "matter.converted", "version": "v1", "timestamp": "2026-06-02T18:30:00.000Z", "data": { "matter_id": 456 }}
Field | Type | Description |
| string | Unique ID for this event — use as an idempotency key |
| integer | The Lawmatics firm that triggered the event |
| string | Event name (e.g. |
| string | Payload schema version ( |
| ISO 8601 | When the event occurred |
| object | Event-specific payload (see Events Reference below) |
Request Headers
Header | Description |
|
|
| HMAC-SHA256 signature — use to verify the request |
| Unix timestamp (seconds) of when the request was sent |
| Unique event identifier — use for idempotency |
Verifying Signatures
Always verify the signature before processing a webhook. This confirms the request came from Lawmatics and the body has not been tampered with.
How the signature is constructed:
signed_payload = "<X-Lawmatics-Timestamp>.<raw_request_body>"signature = "sha256=" + HMAC-SHA256(secret, signed_payload)
Use the full
whsec_...string as the secretX-Lawmatics-Timestampis the Unix epoch integer from the request headerSign the raw JSON string — do not parse or re-serialize before signing
The resulting signature is hex-encoded and prefixed with
sha256=
Node.js
const crypto = require('crypto');function verifySignature(secret, timestamp, rawBody, signature) { if (!signature || !signature.startsWith('sha256=')) return false; const payload = `${timestamp}.${rawBody}`; const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(payload).digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));}
Ruby
require 'openssl'def verify_signature(secret, timestamp, raw_body, signature) return false unless signature&.start_with?('sha256=') payload = "#{timestamp}.#{raw_body}" expected = 'sha256=' + OpenSSL::HMAC.hexdigest('sha256', secret, payload) ActiveSupport::SecurityUtils.secure_compare(expected, signature)end
Python
import hmac, hashlibdef verify_signature(secret, timestamp, raw_body, signature): if not signature or not signature.startswith('sha256='): return False payload = f"{timestamp}.{raw_body}" expected = 'sha256=' + hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature)
Use a constant-time comparison (e.g. crypto.timingSafeEqual, hmac.compare_digest) to prevent timing attacks.
Replay Attack Prevention (Recommended)
Reject requests where the timestamp is older than 5 minutes:
const MAX_AGE_SECONDS = 300;const now = Math.floor(Date.now() / 1000);if (Math.abs(now - parseInt(timestamp)) > MAX_AGE_SECONDS) { return res.status(400).send('Request too old');}
Responding to Webhooks
Return a
2xxstatus code to acknowledge receipt.Respond within 10 seconds — Lawmatics enforces a 10-second total timeout. If processing takes longer, acknowledge immediately and handle the event asynchronously.
The response body is ignored.
A
4xxresponse (other than429) causes the delivery to be immediately abandoned with no retries — return2xxeven if you plan to process asynchronously.
Idempotency
Use event_id as an idempotency key. In rare cases (network issues, retries), your endpoint may receive the same event more than once. Deduplicate on event_id to handle this safely.
Events Reference
matter.converted — Fired when a matter is converted. The conversion process moves it from any status to Hired.
Field | Type | Description |
| integer | ID of the newly converted matter |
Use the matter_id to fetch full matter details: GET /v1/matters/{matter_id} — see Lawmatics API Docs.
matter.created — Fired when a new matter is created. Does not fire on bulk CSV import.
Field | Type | Description |
| integer | ID of the newly created matter |
| integer | ID of the associated contact |
| ISO 8601 | Timestamp the matter was created |
{
"event_id": "evt_a1b2c3d4-0000-0000-0000-000000000001",
"firm_id": 123,
"event_type": "matter.created",
"version": "v1",
"timestamp": "2026-06-18T14:32:07.000Z",
"data":
{
"matter_id": 42891,
"contact_id": 73201,
"created_at": "2026-06-18T14:32:07Z"
}
}
form.submitted — Fired when a contact submits an intake form.
Field | Type | Description |
| integer | ID of the submitted form |
| integer or null | Associated matter, if applicable |
| integer or null | Associated contact, if applicable |
{
"event_id": "evt_a1b2c3d4-0000-0000-0000-000000000002",
"firm_id": 123,
"event_type": "form.submitted",
"version": "v1",
"timestamp": "2026-06-18T14:32:07.000Z",
"data":
{
"form_id": 3291,
"matter_id": 42891,
"contact_id": 73201
}
}
invoice.paid — Fired when an invoice is paid in full. Covers all payment paths: manual, LawPay, write-off, stored payment.
Field | Type | Description |
| integer | ID of the associated matter |
| integer | ID of the paid invoice |
{
"event_id": "evt_a1b2c3d4-0000-0000-0000-000000000003",
"firm_id": 123,
"event_type": "invoice.paid",
"version": "v1",
"timestamp": "2026-06-18T14:32:07.000Z",
"data":
{
"matter_id": 42891,
"invoice_id": 88142
}
}
document.signed — Fired when a document signature is completed.
Field | Type | Description |
| integer | ID of the associated matter |
| integer | ID of the signed document |
{
"event_id": "evt_a1b2c3d4-0000-0000-0000-000000000004",
"firm_id": 123,
"event_type": "document.signed",
"version": "v1",
"timestamp": "2026-06-18T14:32:07.000Z",
"data":
{
"matter_id": 42891,
"document_id": 98234
}
}
contact.updated — Fired when a contact's details are updated. Covers changes to email, phone, address, tags, notes, and custom fields. Does not fire on bulk CSV import updates.
Field | Type | Description |
| integer | ID of the updated contact |
{
"event_id": "evt_a1b2c3d4-0000-0000-0000-000000000005",
"firm_id": 123,
"event_type": "contact.updated",
"version": "v1",
"timestamp": "2026-06-18T14:32:07.000Z",
"data":
{
"contact_id": 73201
}
}
matter.status_changed — Fired when a matter's status changes (e.g. Prospect to Hired). Does not fire on a sub-status-only change: only a top-level status transition triggers this event.
Field | Type | Description |
| integer | ID of the matter |
| string | Previous status (internal code: |
| string | New status (internal code) |
| string | Previous sub-status (firm-defined display name, not a code) |
| string | New sub-status (firm-defined display name) |
| integer or null | User who made the change. Can be null even for user-initiated changes: do not treat null as a reliable system-vs-user signal |
{
"event_id": "evt_a1b2c3d4-0000-0000-0000-000000000006",
"firm_id": 123,
"event_type": "matter.status_changed",
"version": "v1",
"timestamp": "2026-06-18T14:32:07.000Z",
"data":
{
"matter_id": 42891,
"status": { "old": "pnc", "new": "hired" },
"sub_status":
{
"old": "Initial Consultation",
"new": "Engagement Letter Sent"
},
"changed_by_user_id": 5501
}
}
invoice.created — Fired when a new invoice is created for a matter, whether created individually or in bulk (one event fires per invoice created).
Field | Type | Description |
| integer | ID of the matter the invoice belongs to |
| integer | ID of the newly created invoice |
{
"event_id": "evt_550e8400-e29b-41d4-a716-446655440001",
"firm_id": 123,
"event_type": "invoice.created",
"version": "v1",
"timestamp": "2026-08-05T18:30:00.000Z",
"data":
{
"matter_id": 42891,
"invoice_id": 88142
}
}Does not fire for LawPay-processed invoices.
contact.merged — Fired when two contacts are merged into one. The payload contains the surviving contact's ID; the duplicate contact's ID is not included.
Field | Type | Description |
| integer | ID of the surviving contact after the merge |
{
"event_id": "evt_550e8400-e29b-41d4-a716-446655440002",
"firm_id": 123,
"event_type": "contact.merged",
"version": "v1",
"timestamp": "2026-08-05T18:30:00.000Z",
"data":
{
"contact_id": 73201
}
}A merge can also trigger contact.deleted (for the removed duplicate) and contact.updated (for the surviving contact, if any of its fields changed as part of the merge). Expect up to 3 separate event deliveries, across 2 different contact_ids, from a single merge action.
matter.note_added — Fired when a note is added to a matter.
Field | Type | Description |
| integer | ID of the matter the note was added to |
| integer | ID of the newly created note |
{
"event_id": "evt_550e8400-e29b-41d4-a716-446655440003",
"firm_id": 123,
"event_type": "matter.note_added",
"version": "v1",
"timestamp": "2026-08-05T18:30:00.000Z",
"data":
{
"matter_id": 42891,
"note_id": 91820
}
}Only fires for notes added directly to a matter. Notes added to a contact or company do not fire this event: a contact note instead fires contact.updated (even though nothing on the contact record itself changed), and a company note fires no webhook at all today. Notes created via CSV import do not fire this event either.
matter.task_completed — Fired when a task attached to a matter is marked complete, through any action that flips its done flag from false to true, not just the primary "mark done" action.
Field | Type | Description |
| integer | ID of the matter the task belongs to |
| integer | ID of the completed task |
{
"event_id": "evt_550e8400-e29b-41d4-a716-446655440004",
"firm_id": 123,
"event_type": "matter.task_completed",
"version": "v1",
"timestamp": "2026-08-05T18:30:00.000Z",
"data":
{
"matter_id": 42891,
"task_id": 61234
}
}Only covers top-level tasks attached to matters. Subtasks, and tasks attached to a contact, company, or user, do not fire this event. Un-marking a completed task does not fire the event; only the false-to-true transition does. There is no bulk "mark done" action in the product; marking several tasks done one after another fires one event per task.
contact.deleted — Fired when a contact is deleted, individually or in bulk (one event per contact deleted).
Field | Type | Description |
| integer | ID of the deleted contact |
{
"event_id": "evt_550e8400-e29b-41d4-a716-446655440005",
"firm_id": 123,
"event_type": "contact.deleted",
"version": "v1",
"timestamp": "2026-08-05T18:30:00.000Z",
"data":
{
"contact_id": 73201
}
}The contact_id will no longer resolve via the API after deletion; use it to remove the record on your end rather than to look up further details.
matter.updated — Fired when a matter's fields are updated, standard fields or custom fields. Does not fire on matter creation (see matter.created) or on deletion.
Field | Type | Description |
| integer | ID of the updated matter |
{
"event_id": "evt_550e8400-e29b-41d4-a716-446655440006",
"firm_id": 123,
"event_type": "matter.updated",
"version": "v1",
"timestamp": "2026-08-06T18:30:00.000Z",
"data":
{
"matter_id": 42891
}
}A single save that changes several fields at once (for example, a matter's status plus multiple custom fields) is collapsed into one delivery rather than one per field. This collapsing applies within a single matter's own save; a bulk operation touching custom fields across many different matters may still produce one event per matter affected, so high-volume bulk edits can still generate a meaningful number of deliveries. If you expect to process bulk updates, build for that possibility rather than assuming a fixed low volume.
More event types coming soon.
Testing Locally
Use ngrok to expose your local server during development:
ngrok http 3000# Gives you: https://abc123.ngrok-free.app
Register that URL (with your path appended, e.g. /webhook) in the Lawmatics dashboard, then trigger a conversion in your staging account to fire a matter.converted event.
For questions or feedback, contact [email protected].





