> For the complete documentation index, see [llms.txt](https://integrate.lexamica.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://integrate.lexamica.com/example-integrations/4a.-handler-firm-webhook.md).

# Handler Firm: Full Integration with Webhooks

Receive case invitations via webhook, respond via API, and sync status updates bidirectionally.

***

## 🎯 Overview

> **📌 TL;DR**
>
> You're a firm that receives referrals from other firms. Get notified instantly when invitations arrive, accept or decline via API, and send/receive status updates—all through webhooks for real-time sync.

**This guide is for you if:**

* You receive case referrals from other firms via Lexamica
* You want to accept/decline invitations programmatically
* You need to send status updates from your system
* You want real-time webhook notifications

**What you'll build:**

* Mappings for invitations and case updates
* Webhook subscriptions for invitations and incoming updates
* API calls to respond to invitations
* API calls to send your status updates

**This is the most complex integration pattern** because data flows in both directions.

***

## 📖 Key Terms

| Term            | Definition                                                  |
| --------------- | ----------------------------------------------------------- |
| **Handler**     | Your firm—you receive and handle referred cases             |
| **Originator**  | The firm that sent the case to you                          |
| **Invitation**  | A request for you to handle a case                          |
| **Case Update** | A status note posted to a case (you send and receive these) |

***

## ⚙️ Architecture

```
Handler Firm: Full Integration (Webhooks)
─────────────────────────────────────────

┌─────────────────────────────────────────────────────────────────┐
│                         YOUR SYSTEM                             │
├─────────────────────────────────┬───────────────────────────────┤
│       RECEIVE (Webhooks)        │         SEND (API Calls)      │
│                                 │                               │
│ ┌─────────────────────────────┐ │ ┌─────────────────────────────┐
│ │   Webhook Handler           │ │ │   API Client                │
│ │                             │ │ │                             │
│ │ Events:                     │ │ │ Actions:                    │
│ │ • Case Invitation Sent      │ │ │ • Accept invitation         │
│ │ • Case Update Made          │ │ │ • Decline invitation        │
│ │                             │ │ │ • Evaluate invitation       │
│ │ Process:                    │ │ │ • Send status updates       │
│ │ • Store new invitations     │ │ │                             │
│ │ • Sync incoming updates     │ │ │                             │
│ └──────────────▲──────────────┘ │ └──────────────┬──────────────┘
│                │                │                │               │
└────────────────┼────────────────┴────────────────┼───────────────┘
                 │ Webhooks                        │ POST
                 │                                 ▼
┌─────────────────────────────────────────────────────────────────┐
│                         LEXAMICA                                │
│                                                                 │
│  Invitation Sent ◄──── Originator    Handler ────► Accept/Decline
│        │                                                │       │
│        ▼                                                ▼       │
│  Stored in DB ─────────────────────────────────► Process Action │
│                                                                 │
│  Case Update Made ◄──────────────────────────── POST /update   │
└─────────────────────────────────────────────────────────────────┘
```

***

## 📋 Prerequisites

**Credentials:**

* [ ] Organization ID, Public Key, and Private Key from Lexamica

**Infrastructure:**

* [ ] A publicly accessible HTTPS endpoint for webhooks

**Foundational Docs:**

* [ ] [Organizations and Authentication](/1.-organizations-and-authentication.md)— understand your API keys
* [ ] [Mapping Engine](/2.-mapping-engine.md) — how field transformations work
* [ ] [Inbound Webhooks](/3.-inbound-webhooks.md) — responding to invitations and sending updates
* [ ] [Webhook Subscriptions](/4.-webhook-subscriptions.md) — receiving events from Lexamica

***

## 💡 Step-by-Step Implementation

### Step 1: Create Mappings

**CaseInvitation Mapping (for receiving invitations):**

```bash
curl -X POST \
  "https://integration.lexamica.com/organization/{orgId}/mapping/create" \
  -H "Authorization: Bearer your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Invitation Mapping",
    "modelName": "CaseInvitation",
    "fieldMappings": [
      { "lexamicaField": "_id", "foreignField": "invitation_id", "lexamicaFieldType": "String", "foreignFieldType": "String" },
      { "lexamicaField": "referral._id", "foreignField": "case_id", "lexamicaFieldType": "String", "foreignFieldType": "String" },
      { "lexamicaField": "creator", "foreignField": "originator_firm_id", "lexamicaFieldType": "String", "foreignFieldType": "String" },
      { "lexamicaField": "expires", "foreignField": "expires_at", "lexamicaFieldType": "Date", "foreignFieldType": "String" }
    ]
  }'
```

**Case Mapping (for receiving case details with invitation):**

```bash
curl -X POST \
  "https://integration.lexamica.com/organization/{orgId}/mapping/create" \
  -H "Authorization: Bearer your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Case Mapping",
    "modelName": "Case",
    "fieldMappings": [
      { "lexamicaField": "_id", "foreignField": "case_id", "lexamicaFieldType": "String", "foreignFieldType": "String" },
      { "lexamicaField": "client.firstName", "foreignField": "client_first_name", "lexamicaFieldType": "String", "foreignFieldType": "String" },
      { "lexamicaField": "client.lastName", "foreignField": "client_last_name", "lexamicaFieldType": "String", "foreignFieldType": "String" },
      { "lexamicaField": "client.email", "foreignField": "client_email", "lexamicaFieldType": "String", "foreignFieldType": "String" },
      { "lexamicaField": "client.phoneNumber", "foreignField": "client_phone", "lexamicaFieldType": "String", "foreignFieldType": "String" },
      { "lexamicaField": "caseType", "foreignField": "practice_area", "lexamicaFieldType": "String", "foreignFieldType": "String" },
      { "lexamicaField": "incident.synopsis", "foreignField": "description", "lexamicaFieldType": "String", "foreignFieldType": "String" }
    ]
  }'
```

**CaseUpdate Mapping (for sending/receiving updates):**

```bash
curl -X POST \
  "https://integration.lexamica.com/organization/{orgId}/mapping/create" \
  -H "Authorization: Bearer your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Case Update Mapping",
    "modelName": "CaseUpdate",
    "fieldMappings": [
      { "lexamicaField": "_id", "foreignField": "update_id", "lexamicaFieldType": "String", "foreignFieldType": "String" },
      { "lexamicaField": "referral._id", "foreignField": "case_id", "lexamicaFieldType": "String", "foreignFieldType": "String" },
      { "lexamicaField": "title", "foreignField": "title", "lexamicaFieldType": "String", "foreignFieldType": "String" },
      { "lexamicaField": "content", "foreignField": "content", "lexamicaFieldType": "String", "foreignFieldType": "String" },
      { "lexamicaField": "type", "foreignField": "update_type", "lexamicaFieldType": "String", "foreignFieldType": "String" }
    ]
  }'
```

### Step 2: Create Webhook Subscriptions

```bash
# New invitations
curl -X POST \
  "https://integration.lexamica.com/organization/{orgId}/webhook-subscriptions/create" \
  -H "Authorization: Bearer your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "Case Invitation Sent",
    "url": "https://your-system.com/webhooks/lexamica",
    "secret": "your_webhook_secret",
    "mapping": "your_invitation_mapping_id",
    "description": "New case invitations",
    "active": true
  }'

# Incoming status updates from originator
curl -X POST \
  "https://integration.lexamica.com/organization/{orgId}/webhook-subscriptions/create" \
  -H "Authorization: Bearer your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "Case Update Made",
    "url": "https://your-system.com/webhooks/lexamica",
    "secret": "your_webhook_secret",
    "mapping": "your_update_mapping_id",
    "description": "Status updates on cases",
    "active": true
  }'
```

### Step 3: Handle Incoming Webhooks

```javascript
const webhookHandlers = {
  'Case Invitation Sent': async (payload) => {
    // New invitation received!
    await createPendingCase({
      invitationId: payload.invitation_id,
      caseId: payload.case_id,
      originatorFirmId: payload.originator_firm_id,
      expiresAt: payload.expires_at,
      // Case details are included in the payload
      clientName: `${payload.client_first_name} ${payload.client_last_name}`,
      practiceArea: payload.practice_area,
      description: payload.description
    });
    
    // Notify intake team
    await notifyIntakeTeam(payload);
  },
  
  'Case Update Made': async (payload) => {
    // Status update from originator
    await recordIncomingUpdate({
      caseId: payload.case_id,
      title: payload.title,
      content: payload.content,
      type: payload.update_type
    });
  }
};
```

### Step 4: Respond to Invitations via API

**Accept an invitation:**

```bash
curl -X POST \
  "https://integration.lexamica.com/organization/{orgId}/inbound-webhooks/case-invitation/{invitationMappingId}/accept?Key=your_public_key" \
  -H "Content-Type: application/json" \
  -d '{
    "invitation_id": "64f1a2b3c4d5e6f7a8b9c0d4"
  }'
```

**Decline an invitation:**

```bash
curl -X POST \
  "https://integration.lexamica.com/organization/{orgId}/inbound-webhooks/case-invitation/{invitationMappingId}/decline?Key=your_public_key" \
  -H "Content-Type: application/json" \
  -d '{
    "invitation_id": "64f1a2b3c4d5e6f7a8b9c0d4",
    "decline_reason": "Conflict of interest"
  }'
```

**Mark as evaluating:**

```bash
curl -X POST \
  "https://integration.lexamica.com/organization/{orgId}/inbound-webhooks/case-invitation/{invitationMappingId}/evaluate?Key=your_public_key" \
  -H "Content-Type: application/json" \
  -d '{
    "invitation_id": "64f1a2b3c4d5e6f7a8b9c0d4"
  }'
```

### Step 5: Send Status Updates

**Post a status update to a case you're handling:**

```bash
curl -X POST \
  "https://integration.lexamica.com/organization/{orgId}/inbound-webhooks/case/{updateMappingId}/update?Key=your_public_key" \
  -H "Content-Type: application/json" \
  -d '{
    "case_id": "64f1a2b3c4d5e6f7a8b9c0d3",
    "title": "Discovery Phase Complete",
    "content": "All depositions completed. Moving to settlement negotiations.",
    "update_type": "status"
  }'
```

***

## 🔧 Complete Code Example

```javascript
const express = require('express');
const crypto = require('crypto');
const axios = require('axios');

const app = express();
app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));

const config = {
  orgId: process.env.LEXAMICA_ORG_ID,
  publicKey: process.env.LEXAMICA_PUBLIC_KEY,
  webhookSecret: process.env.LEXAMICA_WEBHOOK_SECRET,
  invitationMappingId: process.env.LEXAMICA_INVITATION_MAPPING_ID,
  updateMappingId: process.env.LEXAMICA_UPDATE_MAPPING_ID,
  baseUrl: 'https://integration.lexamica.com'
};

// ============================================
// WEBHOOK HANDLER (Receive)
// ============================================

function verifySignature(rawBody, signature) {
  if (!signature) return false;
  const expected = 'sha256=' + crypto
    .createHmac('sha256', config.webhookSecret)
    .update(rawBody)
    .digest('hex');
  try {
    return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  } catch { return false; }
}

const handlers = {
  'Case Invitation Sent': async (payload) => {
    console.log(`New invitation: ${payload.invitation_id}`);
    
    // Store the pending case
    await db.pendingCases.create({
      invitationId: payload.invitation_id,
      caseId: payload.case_id,
      clientName: `${payload.client_first_name} ${payload.client_last_name}`,
      clientPhone: payload.client_phone,
      practiceArea: payload.practice_area,
      description: payload.description,
      expiresAt: payload.expires_at,
      status: 'pending'
    });
    
    // Alert intake team
    await sendSlackNotification(`New case invitation: ${payload.practice_area}`);
  },
  
  'Case Update Made': async (payload) => {
    console.log(`Update on case ${payload.case_id}: ${payload.title}`);
    
    // Record the update from originator
    await db.caseUpdates.create({
      caseId: payload.case_id,
      title: payload.title,
      content: payload.content,
      source: 'originator',
      receivedAt: new Date()
    });
  }
};

app.post('/webhooks/lexamica', async (req, res) => {
  const signature = req.headers['x-lexamica-signature'];
  const eventType = req.headers['x-lexamica-event'];
  
  if (!verifySignature(req.rawBody, signature)) {
    return res.status(401).send('Invalid signature');
  }
  
  res.status(200).send('OK');
  
  const handler = handlers[eventType];
  if (handler) {
    try {
      await handler(req.body);
    } catch (error) {
      console.error(`Error: ${eventType}`, error);
    }
  }
});

// ============================================
// API CLIENT (Send)
// ============================================

class LexamicaClient {
  async acceptInvitation(invitationId) {
    return axios.post(
      `${config.baseUrl}/organization/${config.orgId}/inbound-webhooks/case-invitation/${config.invitationMappingId}/accept`,
      { invitation_id: invitationId },
      { params: { Key: config.publicKey } }
    );
  }
  
  async declineInvitation(invitationId, reason) {
    return axios.post(
      `${config.baseUrl}/organization/${config.orgId}/inbound-webhooks/case-invitation/${config.invitationMappingId}/decline`,
      { invitation_id: invitationId, decline_reason: reason },
      { params: { Key: config.publicKey } }
    );
  }
  
  async evaluateInvitation(invitationId) {
    return axios.post(
      `${config.baseUrl}/organization/${config.orgId}/inbound-webhooks/case-invitation/${config.invitationMappingId}/evaluate`,
      { invitation_id: invitationId },
      { params: { Key: config.publicKey } }
    );
  }
  
  async sendStatusUpdate(caseId, title, content) {
    return axios.post(
      `${config.baseUrl}/organization/${config.orgId}/inbound-webhooks/case/${config.updateMappingId}/update`,
      { case_id: caseId, title, content, update_type: 'status' },
      { params: { Key: config.publicKey } }
    );
  }
}

const lexamica = new LexamicaClient();

// ============================================
// YOUR APPLICATION ENDPOINTS
// ============================================

// Accept invitation from your UI
app.post('/api/invitations/:id/accept', async (req, res) => {
  try {
    await lexamica.acceptInvitation(req.params.id);
    await db.pendingCases.updateStatus(req.params.id, 'accepted');
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Decline invitation from your UI
app.post('/api/invitations/:id/decline', async (req, res) => {
  try {
    await lexamica.declineInvitation(req.params.id, req.body.reason);
    await db.pendingCases.updateStatus(req.params.id, 'declined');
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Send status update from your UI
app.post('/api/cases/:caseId/updates', async (req, res) => {
  try {
    await lexamica.sendStatusUpdate(req.params.caseId, req.body.title, req.body.content);
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000, () => console.log('Handler firm server running'));
```

***

## 📋 Invitation Lifecycle

| Action                | Endpoint             | When to Use                              |
| --------------------- | -------------------- | ---------------------------------------- |
| **Accept**            | `/accept`            | You want to handle the case              |
| **Decline**           | `/decline`           | You can't take the case (provide reason) |
| **Evaluate**          | `/evaluate`          | Need more time to decide                 |
| **Contact Attempted** | `/contact-attempted` | Tried to reach the client                |
| **Consult Complete**  | `/consult-complete`  | Had initial consultation                 |

***

## ❓ FAQ

### ❓ "How do I know when an invitation expires?"

The `expires_at` field in the invitation payload tells you the deadline. Set up alerts in your system for approaching expirations.

***

### ❓ "Can I accept after marking as 'evaluating'?"

Yes. The evaluation state just signals to the originator that you're considering it. You can still accept or decline later.

***

## ➡️ Next Steps

* **Can't receive webhooks?** See [Handler Firm: Full Integration with Polling](/example-integrations/4b.-handler-firm-polling.md)
* **Need to upload files?** See [File Operations](/example-integrations/apx-1.-file-operations.md)

***

*Last updated: January 2026*
