> 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/3b.-originator-full-polling.md).

# Originator: Full Integration with Polling

Send cases and poll for updates on the full invitation lifecycle and status.

***

## 🎯 Overview

> **📌 TL;DR**
>
> Send cases to Lexamica and poll for updates across the full invitation lifecycle — sent, evaluated, accepted, declined, expired, and more — plus status and settlement updates. Set up your mappings with one call (recommended) or full manual control, then track mapped items so retried sends never create duplicate cases. Same visibility as webhooks, but you pull the data on your schedule.

**This guide is for you if:**

* You send referrals to other firms via Lexamica
* You can't receive webhooks (firewall, no public endpoint)
* You prefer batch processing or scheduled syncs

**What you'll build:**

* Mappings for Case, CaseInvitation, and CaseUpdate data — via a single Quick Setup call (recommended) or full manual control over field names
* A `sendCase` flow that checks your own mapped-item store before creating a case, so retries don't create duplicates
* Stored event subscriptions covering the full canonical invitation lifecycle and update/settlement events
* Multi-event polling loop
* Optional: relay-stage visibility (see the [Advanced (Optional): Relay Visibility](#-advanced-optional-relay-visibility) appendix)

For real-time webhooks instead, see [Originator: Full Integration with Webhooks](/example-integrations/3a.-originator-full-webhook.md)

***

## 📖 Key Terms

| Term               | Definition                                                                                                                                                                                                                                                 |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Originator**     | The firm that creates and sends out a case for referral                                                                                                                                                                                                    |
| **Relay Engine**   | Lexamica's automatic matching system that finds partner firms                                                                                                                                                                                              |
| **CaseInvitation** | The object tracking one firm's invitation to handle a case, from evaluation through a final outcome                                                                                                                                                        |
| **Stored Event**   | An event record stored for later retrieval via polling                                                                                                                                                                                                     |
| **Quick Setup**    | A single endpoint call that creates all your mappings with Lexamica's standard field names — the same setup used for every integration Lexamica builds internally                                                                                          |
| **Mapped Item**    | The link between a case's `LexamicaId` and your CRM's foreign ID, kept in your own persisted store. It's what lets `sendCase()` avoid creating a duplicate case on a retry, and can also be used to attribute a polled event back to a specific CRM record |

***

## ⚙️ Architecture

```
Full Originator Integration (Polling)
─────────────────────────────────────

          OUTBOUND                              INBOUND
          (You send cases)                      (You poll for updates)

┌─────────────────┐                     ┌─────────────────┐
│   Your System   │                     │   Your System   │
│                 │                     │                 │
│ 1. Send case    │                     │ Every N min:    │
│    via API      │                     │ GET /stored-    │
│                 │                     │ events          │
│ 2. Store        │                     │       │         │
│    mapped item  │                     │       ▼         │
└────────┬────────┘                     │ Process:        │
         │                              │ • Invitations   │
         │ POST                         │ • Updates       │
         ▼                              │ • Relay (opt.)  │
┌─────────────────────────────────────────────────────────┐
│                   LEXAMICA                              │
│                                                         │
│  Case Created ──▶ Relay Engine ──▶ Events Stored       │
│                                                         │
│  ┌─────────────────────────────────────────────────┐   │
│  │              Stored Events Database              │   │
│  │                                                  │   │
│  │  • Case Relay Matched    • Case Invitation...   │◄──┼── Your poll
│  │  • Case Relay Rejected   • Case Update Made     │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘
```

***

## 📋 Prerequisites

**Credentials:**

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

**Infrastructure:**

* [ ] Ability to run scheduled jobs (cron, Task Scheduler, etc.)

**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) — sending data to Lexamica
* [ ] [Event Storage and Polling](/5.-event-storage-polling.md) — polling for events from Lexamica

***

## 💡 Step-by-Step Implementation

> **ℹ️ Heads up:** from Step 2 onward, every field name shown (`AssociatedCaseId`, `UpdateTitle`, etc.) is what **Quick Setup** produces — the same field names used across every integration Lexamica runs internally. If you chose **Full Custom Setup** in Step 1 instead, the endpoints and patterns are identical; just substitute whatever field names you configured for the ones shown.

### Step 1: Create Mappings

You have two options. Pick one — don't mix them for the same model.

#### Quick Setup (recommended)

One call creates every mapping you need, using Lexamica's standard field names — the same setup used for every integration built internally.

```bash
curl -X POST \
  "https://integration.lexamica.com/organization/{orgId}/setup/default-mappings" \
  -H "Authorization: Bearer your_private_key"
```

No request body. It's idempotent — safe to call more than once. Any model that already has a mapping is skipped, not duplicated.

This creates `Default Case Mapping`, `Default Case Invitation Mapping`, and two `CaseUpdate` mappings — `Default Case Update Mapping` (outbound, includes `Stage`/`WasUpdateOverdue`) and `Default Inbound Case Update Mapping` (a smaller field set, unused in this guide since polling doesn't cover pushing updates in). It also creates `Default Attachment Mapping`, `Default Law Firm Mapping`, and `Default Law Firm User Mapping` — not used by this guide, safe to ignore.

**Reference — the fields this guide actually uses:**

| Model                 | Field you'll see                                                                                   | What it is                                                             |
| --------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| Case                  | `LexamicaId`                                                                                       | The case's own ID                                                      |
| Case                  | `FirstName` / `LastName` / `Email` / `Phone`                                                       | Client contact info                                                    |
| Case                  | `IncidentDate` / `Summary` / `IncidentAddressState`                                                | Incident details (required for sending a case)                         |
| Case                  | `PracticeArea`                                                                                     | Case type                                                              |
| Case                  | `CaseStatus` / `CaseStatusCycle`                                                                   | Current stage label / cycle                                            |
| Case                  | `TotalRecovery` / `NetRecovery` / `CaseReferralFee` / `RecoveryNotes`                              | Settlement figures                                                     |
| CaseInvitation        | `AssociatedCaseId`                                                                                 | **The case this invitation is for** — note the name                    |
| CaseInvitation        | `AcceptedDate` / `DeclinedDate` / `EvaluatedDate` / `ExpiredDate` / `CancelledDate` / `ClosedDate` | Per-outcome timestamps                                                 |
| CaseInvitation        | `DeclineReason`                                                                                    | Why it was declined                                                    |
| CaseInvitation        | `InvitedFirm.Name` / `InvitedFirm.LexamicaId`                                                      | The handler firm, nested (not a flat ID)                               |
| CaseUpdate (outbound) | `LexamicaCaseId`                                                                                   | **The case this update is for** — different name than CaseInvitation's |
| CaseUpdate (outbound) | `UpdateTitle` / `UpdateContent` / `UpdateType` / `WasUpdateOverdue`                                | Update content and metadata                                            |

> **⚠️ `AssociatedCaseId` vs `LexamicaCaseId`:** the field that tells you which case an event belongs to is named differently depending on the model — `AssociatedCaseId` on `CaseInvitation`, `LexamicaCaseId` on `CaseUpdate`. That's Lexamica's own default naming, not a typo.

You'll notice **`customFields` isn't part of any default mapping.** If you want a correlation field for attribution, add it yourself after Quick Setup:

```bash
curl -X POST \
  "https://integration.lexamica.com/organization/{orgId}/mapping/{caseMappingId}/update" \
  -H "Authorization: Bearer your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "fieldMappings": [
      { "lexamicaField": "customFields.crm_id", "foreignField": "crm_id", "lexamicaFieldType": "String", "foreignFieldType": "String" }
    ]
  }'
```

#### Full Custom Setup (alternative)

If you'd rather choose every field name yourself, create each mapping manually.

**Case Mapping:**

```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": "lexamica_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": "caseType", "foreignField": "practice_area", "lexamicaFieldType": "String", "foreignFieldType": "String" },
      { "lexamicaField": "customFields.crm_id", "foreignField": "crm_id", "lexamicaFieldType": "String", "foreignFieldType": "String" }
    ]
  }'
```

**CaseInvitation Mapping:**

```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 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": "invitee._id", "foreignField": "handler_firm_id", "lexamicaFieldType": "String", "foreignFieldType": "String" },
      { "lexamicaField": "accepted", "foreignField": "accepted_at", "lexamicaFieldType": "Date", "foreignFieldType": "String" },
      { "lexamicaField": "declined", "foreignField": "declined_at", "lexamicaFieldType": "Date", "foreignFieldType": "String" },
      { "lexamicaField": "declineReason", "foreignField": "decline_reason", "lexamicaFieldType": "String", "foreignFieldType": "String" }
    ]
  }'
```

**CaseUpdate Mapping:**

`CaseUpdate` is its own model, separate from `Case` — it's what backs the `Case Update Made` event below. It has its own `_id` and reaches its parent case via `referral._id`, the same way `CaseInvitation` does.

```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": "update_title", "lexamicaFieldType": "String", "foreignFieldType": "String" },
      { "lexamicaField": "content", "foreignField": "update_content", "lexamicaFieldType": "String", "foreignFieldType": "String" },
      { "lexamicaField": "overdue", "foreignField": "is_overdue", "lexamicaFieldType": "Boolean", "foreignFieldType": "Boolean" },
      { "lexamicaField": "type", "foreignField": "update_type", "lexamicaFieldType": "String", "foreignFieldType": "String" }
    ]
  }'
```

### Step 2: Create Stored Event Subscriptions

Create subscriptions for the events you need to track. This guide's **canonical set is the invitation lifecycle plus update/settlement events** — that's what most integrations need. Relay events exist but are optional; see the [Advanced appendix](#-advanced-optional-relay-visibility) if you want them.

**Invitation Events:**

The full canonical set has nine events. Each uses the same call — only `event` changes:

```bash
# Invitation sent
curl -X POST \
  "https://integration.lexamica.com/organization/{orgId}/stored-event-subscriptions/create" \
  -H "Authorization: Bearer your_private_key" \
  -H "Content-Type: application/json" \
  -d '{"event": "Case Invitation Sent", "mapping": "invitation_mapping_id", "description": "Invitation sent to a firm", "active": true}'

# Invitation accepted
curl -X POST \
  "https://integration.lexamica.com/organization/{orgId}/stored-event-subscriptions/create" \
  -H "Authorization: Bearer your_private_key" \
  -H "Content-Type: application/json" \
  -d '{"event": "Case Invitation Accepted", "mapping": "invitation_mapping_id", "description": "Partner accepted case", "active": true}'
```

`invitation_mapping_id` is the `_id` of `Default Case Invitation Mapping` (Quick Setup) or your own CaseInvitation mapping (Custom Setup). Repeat the same call for the rest of the set, changing only `"event"`:

| Event                                         | What it means                               | Typically triggers                        |
| --------------------------------------------- | ------------------------------------------- | ----------------------------------------- |
| `Case Invitation Sent`                        | Invitation went out to a firm               | Informational — no action required        |
| `Case Invitation Evaluated`                   | Firm marked it "under evaluation"           | Optional: surface "under review" to staff |
| `Case Invitation Evaluated Contact Attempted` | Firm attempted to contact the client        | Optional: log the attempt                 |
| `Case Invitation Evaluated Consult Complete`  | Firm completed a consultation               | Optional: log the consult                 |
| `Case Invitation Accepted`                    | Firm accepted (terminal)                    | Update status, store handler info         |
| `Case Invitation Declined`                    | Firm declined (terminal)                    | Log reason, wait for other partners       |
| `Case Invitation Expired`                     | Invitation passed its expiration (terminal) | Alert staff, may need re-routing          |
| `Case Invitation Cancelled`                   | You cancelled it (terminal)                 | Sync cancellation, stop waiting           |
| `Case Invitation Closed`                      | Invitation closed out                       | Finalize local record                     |

> **💡 Why this matters:** if you only poll for `Accepted`/`Declined`, you have no visibility into invitations stuck "evaluating" for days, or invitations that quietly expired — two of the most common support-ticket-generating blind spots in a referral integration.

> **ℹ️ Under Quick Setup, `Contact Attempted` and `Consult Complete` carry no dedicated timestamp field** — the default `CaseInvitation` mapping doesn't map one. You'll still get the event and the common fields (`LexamicaId`, `DateCreated`, `DateUpdated`, `AssociatedCaseId`) — just log it as "this stage happened," not "at this specific time."

**Update and Settlement Events:**

```bash
# Status updates
curl -X POST \
  "https://integration.lexamica.com/organization/{orgId}/stored-event-subscriptions/create" \
  -H "Authorization: Bearer your_private_key" \
  -H "Content-Type: application/json" \
  -d '{"event": "Case Update Made", "mapping": "case_update_mapping_id", "description": "Status update posted", "active": true}'

# Settlement
curl -X POST \
  "https://integration.lexamica.com/organization/{orgId}/stored-event-subscriptions/create" \
  -H "Authorization: Bearer your_private_key" \
  -H "Content-Type: application/json" \
  -d '{"event": "Case Settlement", "mapping": "case_mapping_id", "description": "Case settlement started", "active": true}'
```

`case_update_mapping_id` is the **outbound** `Default Case Update Mapping` — the one with `Stage`/`WasUpdateOverdue`. `Case Update Made` fires whenever anyone posts a note or status update on the case.

> **💡 What to expect from `Case Settlement`:** under Quick Setup, the payload includes real settlement figures automatically — `TotalRecovery`, `NetRecovery`, `CaseReferralFee`, `CaseStatus`, `CaseStatusCycle` (values like `closed.final.withFee` / `closed.final.withNoFee`), `RecoveryNotes`, plus all the standard client/incident fields since this uses the Case mapping. The common pattern — shown in the `Case Settlement` handler in the Complete Code Example — is to render the settlement-relevant fields into a single note rather than modeling each one as a structured status field.

### Step 3: Send Cases

Send cases using your Public Key, same as the webhook version — but check your own mapped-item store first.

> **💡 Why this matters:** whatever triggers `sendCase()` on your end — a status change, a retried job, a user action — firing twice for the same CRM record calls Lexamica's create-case endpoint twice. Lexamica has no way to know these two calls represent the same case; it will create two separate ones. The fix is a lookup before you create: if a mapped item already exists for this CRM record, it's already in Lexamica — return it. If not, this case is new and not yet in Lexamica — create it.

```javascript
async function sendCase(foreignId, caseData) {
  const existing = await findMappedItemByForeignId(foreignId);
  if (existing) {
    // Already in Lexamica — return the existing mapping instead of
    // creating a duplicate case.
    return existing;
  }

  // No mapped item found: this case is new, not yet in Lexamica.
  const response = await fetch(
    `${BASE_URL}/organization/${ORG_ID}/inbound-webhooks/case/${CASE_MAPPING_ID}/send?Key=${PUBLIC_KEY}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(caseData)
    }
  );

  const result = await response.json();

  // Record the mapped item so future sends can find this case by your
  // foreign ID, and polled events can be attributed back to it if needed.
  await saveMappedItem(foreignId, result.LexamicaId);

  return result;
}

// Usage — required fields per 3. inbound-webhooks.md: FirstName, LastName,
// Phone, PracticeArea, IncidentDate, IncidentAddressState, Summary.
await sendCase('crm-12345', {
  FirstName: 'Jane',
  LastName: 'Smith',
  Phone: '555-123-4567',
  PracticeArea: 'Personal Injury',
  IncidentDate: '2026-01-10',
  IncidentAddressState: 'CA',
  Summary: 'Rear-end collision on Highway 101.'
});

// Minimal mapped-item store — same pattern used in
// 3a. originator-full-webhook.md. Swap this for your own persistence
// layer; what matters is the lookup-before-create check above. Quick
// Setup doesn't echo your foreignId back (no customFields by default —
// see Step 1), so this local store is what makes attribution possible.
async function findMappedItemByForeignId(foreignId) {
  return db.mappedItems.findOne({ foreignId });
}

async function findMappedItemByCaseId(lexamicaCaseId) {
  return db.mappedItems.findOne({ lexamicaCaseId });
}

async function saveMappedItem(foreignId, lexamicaCaseId) {
  await db.mappedItems.updateOne(
    { foreignId },
    { $set: { foreignId, lexamicaCaseId, updatedAt: new Date() } },
    { upsert: true }
  );
}
```

> **⚠️ Which payload field holds the case ID depends on the event.** Under Quick Setup: `AssociatedCaseId` on `CaseInvitation` events, `LexamicaCaseId` on `CaseUpdate` events, `LexamicaId` on `Case`-level events (`Case Settlement`). Resolve whichever applies before calling `findMappedItemByCaseId` — see the `handleEvent` switch below.

### Step 4: Poll for Events

Build a polling loop that fetches and processes all event types.

***

## 🔧 Complete Code Example

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

const config = {
  orgId: process.env.LEXAMICA_ORG_ID,
  privateKey: process.env.LEXAMICA_PRIVATE_KEY,
  baseUrl: 'https://integration.lexamica.com'
};

// Events to poll for — the canonical invitation lifecycle plus
// update/settlement events. Relay events (Case Relay Matched/Rejected/
// Missed/Stopped) are optional and not included here — see the Advanced
// appendix for how to add them.
const EVENT_TYPES = [
  'Case Invitation Sent',
  'Case Invitation Evaluated',
  'Case Invitation Evaluated Contact Attempted',
  'Case Invitation Evaluated Consult Complete',
  'Case Invitation Accepted',
  'Case Invitation Declined',
  'Case Invitation Expired',
  'Case Invitation Cancelled',
  'Case Invitation Closed',
  'Case Update Made',
  'Case Settlement'
];

class OriginatorPoller {
  constructor() {
    this.lastPollTime = null;
  }

  async poll() {
    const now = new Date();
    const from = this.lastPollTime || new Date(now.getTime() - 24 * 60 * 60 * 1000);
    
    console.log(`Polling from ${from.toISOString()} to ${now.toISOString()}`);
    
    try {
      // Poll each event type
      for (const eventType of EVENT_TYPES) {
        const events = await this.fetchEvents(eventType, from, now);
        if (events.length > 0) {
          console.log(`Found ${events.length} ${eventType} event(s)`);
          await this.processEvents(eventType, events);
        }
      }
      
      this.lastPollTime = now;
      console.log('Poll complete');
      
    } catch (error) {
      console.error('Polling failed:', error.message);
    }
  }

  async fetchEvents(eventType, from, to) {
    const params = new URLSearchParams({
      event: eventType,
      from: from.toISOString(),
      to: to.toISOString(),
      limit: '100'
    });

    const response = await axios.get(
      `${config.baseUrl}/organization/${config.orgId}/stored-events?${params}`,
      { headers: { Authorization: `Bearer ${config.privateKey}` } }
    );

    return response.data.events || [];
  }

  async processEvents(eventType, events) {
    for (const event of events) {
      await this.handleEvent(eventType, event.payload);
    }
  }

  async handleEvent(eventType, payload) {
    // The case-reference field differs by model — see Step 3's warning.
    const caseId = payload.AssociatedCaseId || payload.LexamicaCaseId || payload.LexamicaId;
    const mappedItem = await findMappedItemByCaseId(caseId);
    if (!mappedItem) {
      console.log(`Can't attribute case ${caseId} to a CRM record — ignoring ${eventType}`);
      return;
    }

    switch (eventType) {
      case 'Case Invitation Sent':
        await this.handleInvitationSent(payload);
        break;
      case 'Case Invitation Evaluated':
        await this.handleInvitationEvaluated(payload);
        break;
      case 'Case Invitation Evaluated Contact Attempted':
        await this.handleInvitationContactAttempted(payload);
        break;
      case 'Case Invitation Evaluated Consult Complete':
        await this.handleInvitationConsultComplete(payload);
        break;
      case 'Case Invitation Accepted':
        await this.handleInvitationAccepted(payload);
        break;
      case 'Case Invitation Declined':
        await this.handleInvitationDeclined(payload);
        break;
      case 'Case Invitation Expired':
        await this.handleInvitationExpired(payload);
        break;
      case 'Case Invitation Cancelled':
        await this.handleInvitationCancelled(payload);
        break;
      case 'Case Invitation Closed':
        await this.handleInvitationClosed(payload);
        break;
      case 'Case Update Made':
        await this.handleUpdateMade(payload);
        break;
      case 'Case Settlement':
        await this.handleSettlement(payload);
        break;
      // If you've subscribed to relay events (see the Advanced appendix),
      // add cases here for 'Case Relay Matched' / 'Rejected' / 'Missed' /
      // 'Stopped' — they're not part of the canonical set polled above.
    }
  }

  // Event handlers — invitation lifecycle. Field names are Quick Setup's
  // defaults (Step 1) — substitute your own if you used Custom Setup.
  async handleInvitationSent(payload) {
    console.log(`Sent: ${payload.AssociatedCaseId}`);
    await this.updateCase(payload.AssociatedCaseId, { status: 'invited' });
  }

  async handleInvitationEvaluated(payload) {
    await this.addActivity(payload.AssociatedCaseId, { type: 'invitation_evaluating' });
  }

  async handleInvitationContactAttempted(payload) {
    // No dedicated timestamp field under Quick Setup — see Step 2.
    await this.addActivity(payload.AssociatedCaseId, { type: 'invitation_contact_attempted' });
  }

  async handleInvitationConsultComplete(payload) {
    await this.addActivity(payload.AssociatedCaseId, { type: 'invitation_consult_complete' });
  }

  async handleInvitationAccepted(payload) {
    console.log(`Accepted: ${payload.AssociatedCaseId} by ${payload.InvitedFirm?.Name}`);
    await this.updateCase(payload.AssociatedCaseId, {
      status: 'accepted',
      handlerFirmId: payload.InvitedFirm?.LexamicaId,
      handlerFirmName: payload.InvitedFirm?.Name,
      acceptedAt: payload.AcceptedDate
    });
  }

  async handleInvitationDeclined(payload) {
    console.log(`Declined: ${payload.AssociatedCaseId} - ${payload.DeclineReason}`);
    await this.updateCase(payload.AssociatedCaseId, { status: 'declined' });
    await this.addActivity(payload.AssociatedCaseId, {
      type: 'declined',
      reason: payload.DeclineReason
    });
  }

  async handleInvitationExpired(payload) {
    await this.updateCase(payload.AssociatedCaseId, { status: 'expired' });
    await this.alertStaff(`Invitation expired for case ${payload.AssociatedCaseId} — may need re-routing`);
  }

  async handleInvitationCancelled(payload) {
    await this.updateCase(payload.AssociatedCaseId, { status: 'cancelled' });
  }

  async handleInvitationClosed(payload) {
    await this.updateCase(payload.AssociatedCaseId, { status: 'closed' });
  }

  // Event handlers — update & settlement
  async handleUpdateMade(payload) {
    console.log(`Update: ${payload.LexamicaCaseId}`);
    await this.addActivity(payload.LexamicaCaseId, {
      type: 'status_update',
      title: payload.UpdateTitle,
      content: payload.UpdateContent,
      overdue: payload.WasUpdateOverdue
    });
  }

  async handleSettlement(payload) {
    console.log(`Settlement: ${payload.LexamicaId}`);

    // These are Quick Setup's real default fields — no guessing required.
    const noteBody = [
      `Case Referral Fee: ${payload.CaseReferralFee}`,
      `Net Recovery: ${payload.NetRecovery}`,
      `Total Recovery: ${payload.TotalRecovery}`,
      `Closed Status: ${payload.CaseStatus}`,
      `Closing Notes: ${payload.RecoveryNotes || ''}`
    ].join('\n');

    await this.addActivity(payload.LexamicaId, {
      type: 'settlement',
      title: 'Settlement Details',
      content: noteBody
    });

    await this.updateCase(payload.LexamicaId, { status: 'settling' });
  }

  // Placeholder methods - implement for your system
  async updateCase(caseId, data) {
    console.log('Update:', caseId, data);
  }

  async addActivity(caseId, activity) {
    console.log('Activity:', caseId, activity);
  }

  async alertStaff(message) {
    console.log('Alert:', message);
  }
}

// Run poller
const poller = new OriginatorPoller();
const POLL_INTERVAL = 5 * 60 * 1000; // 5 minutes

async function run() {
  await poller.poll();
}

run();
setInterval(run, POLL_INTERVAL);

console.log('Originator poller started');
```

***

## 📋 Event Reference

Field names below are Quick Setup's defaults.

| Event                                         | Case ID field      | What It Means                               | Typical Action                                                   |
| --------------------------------------------- | ------------------ | ------------------------------------------- | ---------------------------------------------------------------- |
| `Case Invitation Sent`                        | `AssociatedCaseId` | Invitation went out to a firm               | Informational — no action required                               |
| `Case Invitation Evaluated`                   | `AssociatedCaseId` | Firm marked it "under evaluation"           | Optional: surface "under review" to staff                        |
| `Case Invitation Evaluated Contact Attempted` | `AssociatedCaseId` | Firm attempted to contact the client        | Optional: log the attempt (no timestamp field)                   |
| `Case Invitation Evaluated Consult Complete`  | `AssociatedCaseId` | Firm completed a consultation               | Optional: log the consult (no timestamp field)                   |
| `Case Invitation Accepted`                    | `AssociatedCaseId` | Partner accepted (terminal)                 | Store `InvitedFirm.Name`/`LexamicaId`                            |
| `Case Invitation Declined`                    | `AssociatedCaseId` | Partner declined (terminal)                 | Log `DeclineReason`, wait for other partners                     |
| `Case Invitation Expired`                     | `AssociatedCaseId` | Invitation passed its expiration (terminal) | Alert staff, may need re-routing                                 |
| `Case Invitation Cancelled`                   | `AssociatedCaseId` | You cancelled it (terminal)                 | Sync cancellation, stop waiting                                  |
| `Case Invitation Closed`                      | `AssociatedCaseId` | Invitation closed out                       | Finalize local record                                            |
| `Case Update Made`                            | `LexamicaCaseId`   | Status update posted                        | Sync to CRM                                                      |
| `Case Settlement`                             | `LexamicaId`       | Settlement started                          | Render settlement fields into a note (see Complete Code Example) |

***

## 🔧 Advanced (Optional): Relay Visibility

**Why you might want this:** relay events give you visibility into the matching stage — *before* an invitation is even sent to a firm. Useful if you want to alert staff the moment the Relay Engine can't find anyone to invite, rather than waiting for an invitation-level signal.

**Why most integrations skip it:** relay activity is transient and internal to the matching process. The invitation lifecycle events above already tell you the outcome that actually matters (whether a firm was invited, and what happened next) — so most integrations get full operational visibility without polling for relay events at all.

| Event                 | Triggered When                          |
| --------------------- | --------------------------------------- |
| `Case Relay Matched`  | Relay Engine finds a matching partner   |
| `Case Relay Rejected` | Relay Engine can't find any matches     |
| `Case Relay Missed`   | Partners were invited but none accepted |
| `Case Relay Stopped`  | Relay matching process was stopped      |

**Subscribing:**

```bash
# Example: store relay-rejection events for polling
curl -X POST \
  "https://integration.lexamica.com/organization/{orgId}/stored-event-subscriptions/create" \
  -H "Authorization: Bearer your_private_key" \
  -H "Content-Type: application/json" \
  -d '{"event": "Case Relay Rejected", "mapping": "case_mapping_id", "description": "No partners available", "active": true}'
```

Repeat for `Case Relay Matched`, `Case Relay Missed`, or `Case Relay Stopped` as needed, changing only `"event"`.

**Adding relay events to the poller** (add to `EVENT_TYPES` and the `handleEvent` switch in the Complete Code Example only if you've subscribed to these):

```javascript
// Add to EVENT_TYPES:
// 'Case Relay Matched', 'Case Relay Rejected', 'Case Relay Missed', 'Case Relay Stopped'

async handleRelayMatched(payload) {
  console.log(`Matched: ${payload.LexamicaId}`);
  await this.updateCase(payload.LexamicaId, { status: 'matched' });
}

async handleRelayRejected(payload) {
  console.log(`Rejected: ${payload.LexamicaId}`);
  await this.updateCase(payload.LexamicaId, { status: 'no_match' });
  await this.alertStaff(`No partners for case ${payload.LexamicaId}`);
}

async handleRelayMissed(payload) {
  console.log(`Missed: ${payload.LexamicaId}`);
  await this.updateCase(payload.LexamicaId, { status: 'all_declined' });
}

async handleRelayStopped(payload) {
  console.log(`Relay stopped: ${payload.LexamicaId}`);
  await this.updateCase(payload.LexamicaId, { status: 'relay_stopped' });
}
```

These are **not part of the canonical minimum-viable integration** — treat them as an add-on once the invitation + update flow is working.

***

## ❓ FAQ

### ❓ "Should I use Quick Setup or Custom Setup?"

**📝 Full Answer:** Quick Setup, unless you have a specific reason not to — it's the exact same mapping configuration used for every integration built internally, it's one API call, and it's idempotent (safe to re-run). Reach for Custom Setup only if you need field names that match your own CRM's existing property names exactly.

***

### ❓ "How often should I poll?"

| Urgency        | Interval      |
| -------------- | ------------- |
| Near real-time | 1-5 minutes   |
| Business hours | 15-30 minutes |
| Daily batch    | Once per day  |

### ❓ "Can I poll for specific cases only?"

Not directly. Events are filtered by type and date range. Filter by case ID in your processing logic.

***

## ➡️ Next Steps

* **Need real-time updates?** See [Originator: Full Integration with Webhooks](/example-integrations/3a.-originator-full-webhook.md)
* **Importing existing cases?** See [Import Existing Cases with Polling](/example-integrations/5b.-import-existing-polling.md)
* **Need file handling?** See [File Operations](/example-integrations/apx-1.-file-operations.md)

***

*Last updated: July 2026*
