Skip to main content

For developers

Lexidesk Developer Docs

How to embed the Lexidesk widget on your site, send its events to your own analytics, and receive every qualified lead in your systems via the post-conversation webhook and CRM integrations.

1. Embed the widget

Add the embed code to every page where the widget should appear (typically site-wide via your header/footer template or tag manager). Copy the exact snippet with your agent token from your dashboard under Agent → How to Setup; the samples below show the shape. Replace YOUR_AGENT_JWT with your agent token.

Floating bubble

<!-- Floating bubble: paste before </body> (copy the exact tag from your dashboard) -->
<script
  src="https://lexidesk.ai/load-widget.js"
  data-widget-url="https://lexidesk.ai/widget?agentJwt=YOUR_AGENT_JWT"
></script>

Hero (full-width inline)

<!-- Hero (full-width inline) -->
<script
  src="https://lexidesk.ai/load-hero-inline-widget.js"
  data-agent-jwt="YOUR_AGENT_JWT"
  data-backend-url="https://api.lexidesk.ai"
></script>
<iframe
  id="lexidesk-hero-iframe"
  src="https://lexidesk.ai/inline-widget/hero-section?agentJwt=YOUR_AGENT_JWT"
  title="Lexidesk Hero Widget"
  allow="microphone; autoplay; clipboard-read; clipboard-write"
  style="width:100%;height:100vh;border:0;display:block;overflow:hidden;"
></iframe>

Block (inline chat box)

<!-- Block (inline chat box). The <script> is optional, but required for
     marketing attribution AND for multi-widget coordination on the same page. -->
<script src="https://lexidesk.ai/load-block-inline-widget.js"></script>
<iframe
  id="lexidesk-block-iframe"
  src="https://lexidesk.ai/inline-widget/block?agentJwt=YOUR_AGENT_JWT"
  title="Lexidesk Block Widget"
  allow="microphone; autoplay; clipboard-read; clipboard-write"
  style="width:100%;height:600px;border:0;display:block;overflow:hidden;"
></iframe>

WordPress: insert via a header/footer-scripts plugin or a Custom HTML / Code block (not a plain text block). If a caching/JS-optimization plugin breaks the widget, exclude the Lexidesk loader from JS combine/defer/minify.

2. Multiple widgets on one page

You can run more than one surface on a page (for example a hero embed plus the floating bubble). The loaders auto-load a small coordinator on your page that makes them behave as one visitor with one conversation, and prevents duplicate analytics. To enable it:

  • Include the loader script for each surface on the page (the block loader becomes required to participate).
  • Point all widgets at the same Lexidesk environment.
  • Don’t embed two of the same surface type (e.g. two bubbles).

A single widget behaves exactly as before — the coordinator is a no-op when only one is present.

3. Send events to your analytics

The widget pushes Google Tag Manager dataLayer custom events. It does not call gtag() directly — you wire the events into your tool. Each push looks like:

window.dataLayer.push({
  event: 'lexidesk_user_message',
  widgetType: 'hero',            // 'bubble' | 'hero' | 'block' — which surface
  widgetInstanceId: '…uuid…',    // which specific widget instance fired it
});

TypeScript shape of each event:

// Every Lexidesk push onto window.dataLayer has this shape:
type LexideskDataLayerEvent = {
  event:
    | 'lexidesk_widget_loaded'
    | 'lexidesk_chat_opened'
    | 'lexidesk_chat_closed'
    | 'lexidesk_user_message'
    | 'lexidesk_bot_message'
    | 'lexidesk_onboarding_passed'
    | 'lexidesk_contacts_submitted';
  widgetType: 'bubble' | 'hero' | 'block' | null;  // which surface
  widgetInstanceId: string | null;                 // which specific widget instance
};

widgetType and widgetInstanceId are additive; triggers that match only on the event name keep working. Events (payload carries no PII):

  • lexidesk_widget_loaded — widget present (once per page)
  • lexidesk_chat_opened — visitor opens the chat (bubble surface)
  • lexidesk_chat_closed — visitor closes/minimises (bubble surface)
  • lexidesk_user_message — visitor sends a message
  • lexidesk_bot_message — assistant replies
  • lexidesk_onboarding_passed — intake questions completed (once)
  • lexidesk_contacts_submitted — contact details submitted

GTM → GA4: create a Custom Event trigger per lexidesk_* event and a GA4 Event tag for each; optionally map widgetType via a Data Layer Variable.

GA4 without GTM: forward the events with a small listener:

<script>
  window.dataLayer = window.dataLayer || [];
  var LEXIDESK_EVENTS = [
    'lexidesk_widget_loaded','lexidesk_chat_opened','lexidesk_chat_closed',
    'lexidesk_user_message','lexidesk_bot_message','lexidesk_onboarding_passed','lexidesk_contacts_submitted'
  ];
  var _push = window.dataLayer.push.bind(window.dataLayer);
  window.dataLayer.push = function (entry) {
    if (entry && LEXIDESK_EVENTS.indexOf(entry.event) !== -1 && typeof gtag === 'function') {
      gtag('event', entry.event, {
        widget_type: entry.widgetType,
        widget_instance_id: entry.widgetInstanceId,
      });
    }
    return _push(entry);
  };
</script>

Segment / Tealium / Adobe: same pattern — read window.dataLayer pushes and map them to your SDK (e.g. analytics.track(entry.event, …)).

4. Link your own session id

Tie a Lexidesk conversation to your own analytics/CRM. These are safe to call before or after the widget loads, and the values are forwarded to your integrations (see below).

<script>
  // Tag this visitor with your own analytics/session id (e.g. GA client_id)
  window.lexidesk.setParentPageBrowserSessionId('GA1.2.123456.789');

  // Attach an arbitrary JSON object of host context (replaces on each call;
  // capped at 8 KB / 50 top-level keys). Forwarded to your CRM/webhook integrations.
  window.lexidesk.setParentPageBrowserSessionPayload({
    gaClientId: 'GA1.2.123456.789',
    crmContactId: 'abc-123',
    landingCampaign: 'spring-pi',
  });
</script>

API signatures:

// Host-page API available on window once a Lexidesk loader is present.
interface Window {
  lexidesk: {
    fireEvent(
      name:
        | 'HIDE_LEXIDESK_WIDGET'
        | 'SHOW_LEXIDESK_WIDGET'
        | 'START_OR_CONTINUE_LEXIDESK_WIDGET_CONVERSATION',
    ): void;
    setParentPageBrowserSessionId(id: string): void;
    setParentPageBrowserSessionPayload(payload: Record<string, unknown>): void;
  };
}

5. Post-conversation webhook (Custom Webhook)

After a conversation is completed and analyzed, Lexidesk can POST a JSON payload to a URL you control (use this for Make.com, Zapier, n8n, or your own endpoint). Your analytics/attribution is included in this payload (UTM, referrer, landing pages, navigation journey, and the session id/JSON you set via the APIs above).

Setup: Dashboard → Agent → Integrations Custom Webhook. Set the conversation filter (new leads / existing clients / both), the webhook URL, and any custom headers, then save.

PropertyBehavior
MethodPOST
Content-Typeapplication/json
AuthNone built-in. Add your own via custom headers (e.g. Authorization or X-Webhook-Secret). No HMAC signing.
Timeout10 seconds
RetriesNone — single attempt; each run is recorded in the conversation’s integration execution log.
FilterConversations whose reason is neither a prospect nor existing-client reason are not sent.

Example payload:

{
  "conversationId": "00000000-0000-0000-0000-000000000001",
  "conversationSource": "WEB_TEXT",
  "conversationReason": "NEW_CLIENT_ENQUIRY",
  "conversationOutcome": "QUALIFIED",
  "caseSummary": "Caller is seeking representation for a divorce…",
  "caseData": {
    "case_name": "Smith divorce enquiry",
    "short_case_description": "Contested divorce with custody questions",
    "reason_for_priority": "Time-sensitive custody hearing",
    "lead_priority": 7,
    "language": "en",
    "county": null,
    "opposing_parties": null
  },
  "lawTypes": { "Family": ["Divorce", "Child custody"] },
  "clientType": "PERSONAL",
  "isQualifiedLead": true,
  "leadQualificationStatus": "YES",
  "isQualifiedLeadReason": "Has a clear matter and is in-jurisdiction",
  "isDuringWorkingHours": true,
  "isBillable": true,
  "referredBy": null,
  "client": {
    "name": "Jane Smith",
    "email": "jane@example.com",
    "phone": "+441234567890",
    "contactedWithPhoneNumber": "+441234567890",
    "askedToContactWithPhoneNumber": null,
    "createdAt": "2026-06-16T12:05:00.000Z"
  },
  "agent": {
    "name": "Intake Agent",
    "email": "intake@kinglaw.example",
    "firmName": "King Law Offices",
    "timezone": "Europe/London"
  },
  "appointment": { "isBooked": true, "startsAt": "2026-06-20T15:00:00.000Z" },
  "appointmentOffer": { "name": "Free consult", "reason": "Qualified lead", "proposedUrl": "https://…" },
  "transfer": { "destinationName": "Senior PI Partner", "destinationPhoneNumber": "+441234500000", "reason": "High-value matter" },
  "attribution": {
    "browserSessionAttributionId": "11111111-1111-4111-8111-111111111111",
    "persistentAttributionId": "22222222-2222-4222-8222-222222222222",
    "parentPageBrowserSessionId": "GA1.2.123456.789",
    "parentPageBrowserSessionPayload": { "crmContactId": "abc-123" },
    "utmSource": "google", "utmMedium": "cpc", "utmCampaign": "summer",
    "utmTerm": "lawyer", "utmContent": "hero",
    "referrer": "https://google.com/search",
    "firstLoadedAtUrl": "https://kinglaw.example/",
    "firstLoadedAtUrlQuery": "?utm_source=google",
    "firstLoadedAtUrlFragment": "#top",
    "firstLoadedAtUrlFull": "https://kinglaw.example/?utm_source=google#top",
    "firstUserMessageAtUrl": "https://kinglaw.example/family-law",
    "lastUserMessageAtUrl": "https://kinglaw.example/pricing",
    "firstMessageAfterLoadSeconds": 42.5,
    "firstMessageAfterWidgetOpenSeconds": 12,
    "locationCity": "Charlotte", "locationRegion": "North Carolina", "locationCountry": "United States",
    "navigationJourney": [
      { "url": "https://kinglaw.example/", "durationSeconds": 30,
        "events": [{ "type": "lexidesk_chat_opened", "atSeconds": 10 }] }
    ]
  },
  "durationSeconds": 180,
  "timestamp": "2026-06-16T12:31:00.000Z",
  "eventType": "new_lead"
}

6. Webhook field reference

Full TypeScript types

The complete contract — copy into your project as typings. Field-by-field explanations follow below.

// ── Lexidesk post-conversation webhook payload (JSON body of the POST) ──────────
// All dates are serialized as ISO 8601 strings.

type AfterCallIntegrationPayload = {
  conversationId: string;
  conversationSource?: ConversationSource;
  conversationReason?: ConversationReason;
  conversationOutcome?: ConversationOutcome;
  transfer?: ConversationTransfer;
  caseSummary?: string;
  audioRecordingUrl?: string;
  createdAt: string;                       // ISO datetime
  updatedAt: string;                       // ISO datetime
  caseData?: CaseData;
  lawTypes?: Record<string, string[]>;     // practice area -> matter subtypes
  clientType?: ClientType;
  isQualifiedLead?: boolean;               // YES/MAYBE => true, NO => false
  leadQualificationStatus?: LeadQualificationStatus;
  isQualifiedLeadReason?: string;
  isDuringWorkingHours?: boolean;
  isBillable?: boolean;
  referredBy?: string;
  attribution?: ConversationAttribution;   // omitted for phone-only conversations
  client: {
    name?: string;
    email?: string;
    phone?: string;
    contactedWithPhoneNumber?: string;
    askedToContactWithPhoneNumber?: string;
    createdAt?: string;                    // ISO datetime
  };
  agent: {
    name?: string;
    email?: string;
    firmName?: string;
    timezone?: string;
  };
  appointment: {
    isBooked: boolean;
    startsAt?: string;                     // ISO datetime, present when booked
  };
  appointmentOffer: {
    name?: string | null;
    reason?: string | null;
    proposedUrl?: string | null;
  };
  durationSeconds?: number;
  timestamp: string;                       // ISO datetime the webhook was sent
  eventType: string;                       // always "new_lead" today
};

enum ConversationSource { PHONE, WEB_CALL, WEB_TEXT, SMS, EMAIL, WHATSAPP, WEB }
enum ClientType { PERSONAL, BUSINESS }
enum LeadQualificationStatus { YES, NO, MAYBE }

enum ConversationReason {
  // Prospective / new clients
  PROSPECT_NEW_CASE_ENQUIRY, PROSPECT_CONSULTATION_OR_BOOKING, PROSPECT_FEES_OR_PRICING,
  PROSPECT_SERVICE_INFO_OR_ELIGIBILITY, PROSPECT_URGENT_NEW_MATTER, PROSPECT_INTAKE_FOLLOW_UP,
  PROSPECT_REFERRED,
  // Existing clients
  CLIENT_ASKING_CASE_UPDATE, CLIENT_PROVIDING_CASE_UPDATE, CLIENT_GENERAL_QUESTION,
  CLIENT_STRATEGY_DISCUSSION, CLIENT_URGENT_ACTIVE_CASE, CLIENT_BILLING_OR_PAYMENT,
  CLIENT_APPOINTMENT_OR_RESCHEDULE,
  // Third parties
  THIRD_PARTY_OPPOSING_COUNSEL, THIRD_PARTY_COURT_OR_AGENCY, THIRD_PARTY_INSURANCE_COMPANY,
  THIRD_PARTY_SALES_OR_VENDOR_OUTREACH,
  // Internal / administrative
  ADMIN_RECRUITING_OR_JOB, ADMIN_MEDIA_ENQUIRY, ADMIN_REGULATORY_BODY,
  // General
  GENERAL_WRONG_NUMBER, GENERAL_OTHER,
}

enum ConversationOutcome {
  ABANDONED, PARTIALLY_ABANDONED, APPOINTMENT_PROPOSAL_REJECTED, APPOINTMENT_PROPOSAL_ACCEPTED,
  APPOINTMENT_BOOKED, CALLBACK_REQUESTED, REFERRED_AWAY, LEAD_TURNED_DOWN, MESSAGE_TAKEN,
  ESCALATED_TO_HUMAN, RESOLVED, GENERAL_OTHER,
}

type ConversationTransfer = {
  destinationPhoneNumber?: string;
  destinationName?: string;
  reason?: string;
};

type CaseData = {
  case_name: string;
  short_case_description: string;
  reason_for_priority: string;
  lead_priority: number;
  language: string;          // 2-letter ISO code, e.g. "en"
  county: string | null;
  opposing_parties: string | null;
};

type NavigationJourneyEvent = { type: string; atSeconds: number };
type NavigationJourneyEntry = {
  url: string;
  durationSeconds: number;
  events: NavigationJourneyEvent[];
};

// Note: 'fingerprint' exists on the internal attribution type but is NOT sent in the webhook.
type ConversationAttribution = {
  browserSessionAttributionId?: string;          // per-visit id (sessionStorage)
  persistentAttributionId?: string;              // returning-visitor id (localStorage)
  parentPageBrowserSessionId?: string;           // your own id, if set
  parentPageBrowserSessionPayload?: Record<string, unknown>;  // your own JSON, if set
  utmSource?: string;
  utmMedium?: string;
  utmCampaign?: string;
  utmTerm?: string;
  utmContent?: string;
  referrer?: string;
  firstLoadedAtUrl?: string;
  firstLoadedAtUrlQuery?: string;
  firstLoadedAtUrlFragment?: string;
  firstLoadedAtUrlFull?: string;
  firstUserMessageAtUrl?: string;
  lastUserMessageAtUrl?: string;
  firstMessageAfterLoadSeconds?: number;
  firstMessageAfterWidgetOpenSeconds?: number;
  locationCity?: string;
  locationRegion?: string;
  locationCountry?: string;
  navigationJourney?: NavigationJourneyEntry[];
};

Top-level fields

Every top-level field in the payload:

FieldTypeDescriptionFilled
conversationIdstring (UUID)Unique id of the conversation.Always
conversationSourceenumChannel the conversation came through: WEB_TEXT (website text chat), WEB_CALL (website voice), PHONE, SMS, EMAIL, WHATSAPP.Usually
conversationReasonenumWhy the person made contact, classified by the AI (e.g. new client enquiry, existing client).Conditional
conversationOutcomeenumHow the conversation was classified at the end (e.g. qualified, transferred, booked).Conditional
caseSummarystringAI-written summary of the enquiry.Conditional (after analysis)
audioRecordingUrlstring (URL)Link to the call recording.Recorded voice calls only
createdAtISO datetimeWhen the conversation started.Always
updatedAtISO datetimeWhen the conversation was last updated.Always
caseDataobjectStructured case details — see the caseData table below.Conditional
lawTypesobject (map)Practice areas → matter subtypes detected, e.g. { "Family": ["Divorce"] }.Conditional
clientTypePERSONAL | BUSINESSWhether the enquiry is personal or business.Conditional
isQualifiedLeadbooleanWhether the lead is qualified. Derived: YES/MAYBE true, NOfalse.Conditional
leadQualificationStatusYES | NO | MAYBEThe raw qualification verdict from the AI.Conditional
isQualifiedLeadReasonstringThe AI’s rationale for the qualification verdict.Conditional
isDuringWorkingHoursbooleanWhether the conversation happened during the firm’s working hours.Conditional
isBillablebooleanWhether the conversation is billable.Conditional
referredBystringReferral code/source captured for this conversation.Conditional
transferobjectIf transferred to a human: destinationName, destinationPhoneNumber, reason.Only when a transfer happened
clientobjectThe captured contact — see the client table below.Object always present
agentobjectThe Lexidesk agent/firm — see the agent table below.Object always present
appointmentobjectisBooked (boolean, always) and startsAt (ISO, only when booked).Object always present
appointmentOfferobjectThe appointment offered to the client: name, reason, proposedUrl.Conditional
attributionobjectMarketing attribution — see section 7 for every field.Website-widget conversations; omitted for phone-only
durationSecondsnumberConversation/call length in seconds.Conditional
timestampISO datetimeWhen the webhook was sent.Always
eventTypestringEvent name. Always new_lead today (not yet configurable).Always

caseData object

FieldTypeDescription
case_namestringShort label for the matter.
short_case_descriptionstringOne/two-line description of the matter.
reason_for_prioritystringWhy the AI set this priority.
lead_prioritynumberPriority score for the lead.
languagestringDetected language (e.g. en).
countystring | nullCounty/region if detected.
opposing_partiesstring | nullOpposing parties if mentioned (for conflict checks).

client object

FieldTypeDescription
namestringContact’s name.
emailstringContact’s email.
phonestringContact’s phone number.
contactedWithPhoneNumberstringThe number the contact actually reached you on.
askedToContactWithPhoneNumberstringA number the contact asked to be called back on.
createdAtISO datetimeWhen the contact record was created.

agent object

FieldTypeDescription
namestringAgent (assistant) name.
emailstringAgent contact email.
firmNamestringThe firm’s name.
timezonestringThe agent’s timezone (e.g. Europe/London).

Not included today: the message transcript, a device fingerprint, and CRM note text fields (those are used only by the CRM integrations below, not the webhook).

7. Attribution & visitor ids

The attribution object describes how the visit/visitor arrived. It is present for conversations that came through the website widget and is omitted for phone-only conversations.

Browser-session vs. persistent attribution

The object carries two different visitor identifiers that answer two different questions:

  • browserSessionAttributionId — “this visit”. Identifies a single browsing session (roughly one visit, one tab). Stored in the browser’s sessionStorage, so it is created fresh when the visitor opens your site and discarded when that tab/session ends. The first-touch fields (UTM, referrer, landing page) are captured against this session and never overwritten for the rest of the visit. Use it to attribute a conversation to how this particular visit arrived.
  • persistentAttributionId — “this person, across visits”. Identifies the returning visitor across sessions. Stored in localStorage, so it survives across visits and tabs until the visitor clears their browser storage. Many browserSessionAttributionIds roll up to one persistentAttributionId. Use it to recognise a returning visitor and tie multiple sessions/conversations back to the same person.

In short: browser-session = one visit (sessionStorage, resets each visit); persistent = one returning visitor (localStorage, spans visits). Both are anonymous, randomly-generated ids — not personal data on their own.

Every attribution field

FieldTypeDescription
browserSessionAttributionIdstring (UUID)Per-visit session id (sessionStorage). See above.
persistentAttributionIdstring (UUID)Returning-visitor id across visits (localStorage). See above.
parentPageBrowserSessionIdstringYour own session id, if you set it via setParentPageBrowserSessionId.
parentPageBrowserSessionPayloadobjectArbitrary host JSON you set via setParentPageBrowserSessionPayload.
utmSourcestringFirst-touch utm_source.
utmMediumstringFirst-touch utm_medium.
utmCampaignstringFirst-touch utm_campaign.
utmTermstringFirst-touch utm_term.
utmContentstringFirst-touch utm_content.
referrerstringThe external referrer URL for the visit.
firstLoadedAtUrlstringLanding page URL (path only).
firstLoadedAtUrlQuerystringLanding page query string.
firstLoadedAtUrlFragmentstringLanding page hash/fragment.
firstLoadedAtUrlFullstringFull landing page URL (path + query + fragment).
firstUserMessageAtUrlstringPage URL where the visitor sent their first message.
lastUserMessageAtUrlstringPage URL of the visitor’s most recent message.
firstMessageAfterLoadSecondsnumberSeconds from page load to first message.
firstMessageAfterWidgetOpenSecondsnumberSeconds from opening the widget to first message.
locationCitystringApprox. city (from IP; no raw IP stored).
locationRegionstringApprox. region/state.
locationCountrystringApprox. country.
navigationJourneyarrayOrdered pages visited, each with url, durationSeconds, and the widget events fired on it.

8. Post-call integrations

Besides the Custom Webhook, Lexidesk can push each completed conversation into a CRM/PMS. CRM integrations find-or-create a contact and attach a note summarizing the conversation. They require an email or phone for the contact (the webhook does not).

IntegrationWhat it does on a completed conversation
Custom WebhookPOSTs the full JSON payload (above) to your URL — for Make.com, Zapier, n8n, or your endpoint.
Clio Grow (Lead Token)Creates an inbox lead (name, email, phone, case summary message, source).
Clio ManageFind/create contact, attach a conversation note.
HubSpotFind/create contact, attach a note.
GoHighLevelFind/create contact in your location, attach a note.
LawmaticsFind/create contact, attach an HTML note.
ActionstepCreate/find participant → create action (matter) → file a note with summary + client details.

The CRM note looks like this (subject = the case name, or "Lexidesk Lead <conversationId>" when there is none):

Lexidesk Conversation:
https://app.lexidesk.ai/dashboard/conversations/CONVERSATION_ID

Is a Qualified Lead: Yes
Qualification Rationale:
Has a clear matter and is in-jurisdiction

Priority: 7
Priority Rationale:
Time-sensitive custody hearing

Summary:
Caller is seeking representation for a divorce…

Attribution:
Location: Charlotte, North Carolina, United States
First page: https://kinglaw.example/
Referrer: https://google.com/search
Contacts submitted on: https://kinglaw.example/pricing
Time on site before first message: 42.5s

The note’s attribution section is a human-readable subset; the full UTM/journey JSON is only in the Custom Webhook payload. (MyCase and OAuth Clio Grow exist in code but are not yet enabled in the dashboard.)

9. Customization — available vs. not yet

Configurable today (per agent, in the Integrations dialog):

  • Enable/disable each integration; display name.
  • Which conversations fire it: new leads / existing clients / both.
  • Webhook URL and custom HTTP headers (your auth).
  • Provider specifics: Clio region, Actionstep default action type / assignee / division, etc.

Not yet customizable (hardcoded today; may be added later):

  • The webhook JSON field names / shape and the CRM note template.
  • Per-field mapping to your CRM’s custom fields.
  • eventType is always new_lead for the webhook.
  • Including the transcript in the payload.
  • A built-in webhook signing secret / HMAC (use a custom header instead).
  • Automatic retries / backoff.
  • Multiple custom webhooks per agent (one per agent today).

Need a field, mapping, or integration that isn’t listed? Contact Lexidesk — several of the “not yet” items are on the roadmap.