📡

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 TypeTrigger
matter.createdA new matter is created via API or dashboard
matter.updatedA matter's fields are updated
matter.archivedA matter is archived
client.createdA new client record is created
document.uploadedA client uploads a document via the intake portal
document.approvedAn attorney approves a document upload
document.rejectedAn attorney rejects a document upload
intake.completedA client submits all required documents
intake.link.sentAn intake link is sent to a client
webhook.testManual test event from the dashboard or API

Event Payload Schemas

matter.created

Triggered when a new matter is created.

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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:

FieldTypeDescription
idstringUnique event ID (evt_ prefix)
typestringEvent type string
created_atstringISO 8601 timestamp when event occurred
firm_idstringID of the firm that owns the resource
dataobjectEvent-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

http
X-CaseHug-Signature: sha256=a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0

Verification Code

Node.js / TypeScript

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

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}, 200

Retry 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.

AttemptDelayTotal Time Elapsed
1 (initial)Immediate0s
230 seconds~30s
35 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.

Best Practices

Respond immediately, process asynchronously
Return 200 immediately after verifying the signature. Process the event in a background job to avoid timeouts.
🔒
Always verify signatures
Never process a webhook without verifying the HMAC signature. This prevents spoofed events.
♻️
Handle duplicate events
Under rare conditions, the same event may be delivered more than once. Use the event id for idempotency.
📋
Log raw payloads
Store the raw webhook payload before processing. Useful for debugging and replaying events.
🔔
Subscribe to only what you need
Avoid subscribing to "*" (all events) unless required. Targeted subscriptions reduce noise.