Webhook Events
CaseHug delivers real-time event notifications to your webhook endpoint. Each event is a POST request with a JSON body and a signature header for verification.
Event Types
| Event Type | Trigger |
|---|---|
| matter.created | A new matter is created via API or dashboard |
| matter.updated | A matter's fields are updated |
| matter.archived | A matter is archived |
| client.created | A new client record is created |
| document.uploaded | A client uploads a document via the intake portal |
| document.approved | An attorney approves a document upload |
| document.rejected | An attorney rejects a document upload |
| intake.completed | A client submits all required documents |
| intake.link.sent | An intake link is sent to a client |
| webhook.test | Manual test event from the dashboard or API |
Event Payload Schemas
matter.created
Triggered when a new matter is created.
{
"id": "evt_01HXYZ",
"type": "matter.created",
"created_at": "2024-03-07T14:00:00Z",
"firm_id": "firm_01HFFF",
"data": {
"id": "mat_01HXYZ",
"title": "Johnson v. Smith — Personal Injury",
"status": "active",
"practice_area": "personal_injury",
"client_id": "cli_01HABC",
"created_at": "2024-03-07T14:00:00Z"
}
}document.uploaded
Triggered when a client uploads a file through the intake portal.
{
"id": "evt_01HAAA",
"type": "document.uploaded",
"created_at": "2024-03-07T15:30:00Z",
"firm_id": "firm_01HFFF",
"data": {
"upload": {
"id": "upl_01HBBB",
"filename": "drivers_license.pdf",
"content_type": "application/pdf",
"file_size_bytes": 245760,
"uploaded_at": "2024-03-07T15:30:00Z"
},
"document": {
"id": "doc_01HCCC",
"name": "Driver's License",
"matter_id": "mat_01HXYZ"
},
"matter": {
"id": "mat_01HXYZ",
"title": "Johnson v. Smith"
}
}
}document.approved
Triggered when a document is approved by a firm user.
{
"id": "evt_01HDDD",
"type": "document.approved",
"created_at": "2024-03-07T16:00:00Z",
"firm_id": "firm_01HFFF",
"data": {
"upload": {
"id": "upl_01HBBB",
"filename": "drivers_license.pdf",
"status": "approved",
"reviewer_notes": "All clear."
},
"document": {
"id": "doc_01HCCC",
"name": "Driver's License",
"matter_id": "mat_01HXYZ"
}
}
}intake.completed
Triggered when a client completes all required documents in the intake portal.
{
"id": "evt_01HEEE",
"type": "intake.completed",
"created_at": "2024-03-07T17:00:00Z",
"firm_id": "firm_01HFFF",
"data": {
"matter": {
"id": "mat_01HXYZ",
"title": "Johnson v. Smith",
"status": "active"
},
"client": {
"id": "cli_01HABC",
"first_name": "David",
"last_name": "Johnson",
"email": "david.johnson@example.com"
},
"intake_summary": {
"documents_requested": 5,
"documents_uploaded": 5,
"documents_pending_review": 3,
"completed_at": "2024-03-07T17:00:00Z"
}
}
}matter.archived
Triggered when a matter is archived.
{
"id": "evt_01HFFF",
"type": "matter.archived",
"created_at": "2024-03-07T18:00:00Z",
"firm_id": "firm_01HFFF",
"data": {
"id": "mat_01HXYZ",
"title": "Johnson v. Smith",
"archived_at": "2024-03-07T18:00:00Z",
"archived_by": "usr_01HAAA"
}
}Common Payload Fields
All event payloads share these top-level fields:
| Field | Type | Description |
|---|---|---|
| id | string | Unique event ID (evt_ prefix) |
| type | string | Event type string |
| created_at | string | ISO 8601 timestamp when event occurred |
| firm_id | string | ID of the firm that owns the resource |
| data | object | Event-specific payload (see schemas above) |
Signature Verification
Every webhook request includes an X-CaseHug-Signature header. This is an HMAC-SHA256 signature of the raw request body, computed using your webhook's signing secret. Always verify this before processing events.
Signature Format
X-CaseHug-Signature: sha256=a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0
Verification Code
Node.js / TypeScript
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSig = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
const expectedHeader = `sha256=${expectedSig}`;
// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedHeader)
);
}
// Express handler example
app.post('/webhooks/casehug', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-casehug-signature'] as string;
const rawBody = req.body.toString('utf8');
if (!verifyWebhookSignature(rawBody, signature, process.env.WEBHOOK_SECRET!)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(rawBody);
switch (event.type) {
case 'matter.created':
await handleMatterCreated(event.data);
break;
case 'document.uploaded':
await handleDocumentUploaded(event.data);
break;
case 'intake.completed':
await handleIntakeCompleted(event.data);
break;
}
res.status(200).json({ received: true });
});Python
import hmac
import hashlib
import json
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
expected_header = f"sha256={expected}"
return hmac.compare_digest(signature, expected_header)
# Flask handler example
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/webhooks/casehug', methods=['POST'])
def handle_casehug_webhook():
signature = request.headers.get('X-CaseHug-Signature', '')
raw_body = request.get_data()
if not verify_webhook_signature(raw_body, signature, WEBHOOK_SECRET):
abort(401, 'Invalid signature')
event = json.loads(raw_body)
event_type = event.get('type')
if event_type == 'matter.created':
handle_matter_created(event['data'])
elif event_type == 'document.uploaded':
handle_document_uploaded(event['data'])
elif event_type == 'intake.completed':
handle_intake_completed(event['data'])
return {'received': True}, 200Retry Policy
CaseHug considers a delivery successful when your endpoint responds with a 2xx status code within 5 seconds. If delivery fails, we retry with exponential backoff.
| Attempt | Delay | Total Time Elapsed |
|---|---|---|
| 1 (initial) | Immediate | 0s |
| 2 | 30 seconds | ~30s |
| 3 | 5 minutes | ~5.5 min |
| 4 (final) | 30 minutes | ~35.5 min |
After 4 failed attempts, the event is marked as failed and the webhook is automatically disabled if failure rate exceeds 50% over a 24-hour window. You'll receive an email alert.