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 messagelexidesk_bot_message— assistant replieslexidesk_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.
| Property | Behavior |
|---|---|
| Method | POST |
| Content-Type | application/json |
| Auth | None built-in. Add your own via custom headers (e.g. Authorization or X-Webhook-Secret). No HMAC signing. |
| Timeout | 10 seconds |
| Retries | None — single attempt; each run is recorded in the conversation’s integration execution log. |
| Filter | Conversations 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:
| Field | Type | Description | Filled |
|---|---|---|---|
conversationId | string (UUID) | Unique id of the conversation. | Always |
conversationSource | enum | Channel the conversation came through: WEB_TEXT (website text chat), WEB_CALL (website voice), PHONE, SMS, EMAIL, WHATSAPP. | Usually |
conversationReason | enum | Why the person made contact, classified by the AI (e.g. new client enquiry, existing client). | Conditional |
conversationOutcome | enum | How the conversation was classified at the end (e.g. qualified, transferred, booked). | Conditional |
caseSummary | string | AI-written summary of the enquiry. | Conditional (after analysis) |
audioRecordingUrl | string (URL) | Link to the call recording. | Recorded voice calls only |
createdAt | ISO datetime | When the conversation started. | Always |
updatedAt | ISO datetime | When the conversation was last updated. | Always |
caseData | object | Structured case details — see the caseData table below. | Conditional |
lawTypes | object (map) | Practice areas → matter subtypes detected, e.g. { "Family": ["Divorce"] }. | Conditional |
clientType | PERSONAL | BUSINESS | Whether the enquiry is personal or business. | Conditional |
isQualifiedLead | boolean | Whether the lead is qualified. Derived: YES/MAYBE → true, NO → false. | Conditional |
leadQualificationStatus | YES | NO | MAYBE | The raw qualification verdict from the AI. | Conditional |
isQualifiedLeadReason | string | The AI’s rationale for the qualification verdict. | Conditional |
isDuringWorkingHours | boolean | Whether the conversation happened during the firm’s working hours. | Conditional |
isBillable | boolean | Whether the conversation is billable. | Conditional |
referredBy | string | Referral code/source captured for this conversation. | Conditional |
transfer | object | If transferred to a human: destinationName, destinationPhoneNumber, reason. | Only when a transfer happened |
client | object | The captured contact — see the client table below. | Object always present |
agent | object | The Lexidesk agent/firm — see the agent table below. | Object always present |
appointment | object | isBooked (boolean, always) and startsAt (ISO, only when booked). | Object always present |
appointmentOffer | object | The appointment offered to the client: name, reason, proposedUrl. | Conditional |
attribution | object | Marketing attribution — see section 7 for every field. | Website-widget conversations; omitted for phone-only |
durationSeconds | number | Conversation/call length in seconds. | Conditional |
timestamp | ISO datetime | When the webhook was sent. | Always |
eventType | string | Event name. Always new_lead today (not yet configurable). | Always |
caseData object
| Field | Type | Description |
|---|---|---|
case_name | string | Short label for the matter. |
short_case_description | string | One/two-line description of the matter. |
reason_for_priority | string | Why the AI set this priority. |
lead_priority | number | Priority score for the lead. |
language | string | Detected language (e.g. en). |
county | string | null | County/region if detected. |
opposing_parties | string | null | Opposing parties if mentioned (for conflict checks). |
client object
| Field | Type | Description |
|---|---|---|
name | string | Contact’s name. |
email | string | Contact’s email. |
phone | string | Contact’s phone number. |
contactedWithPhoneNumber | string | The number the contact actually reached you on. |
askedToContactWithPhoneNumber | string | A number the contact asked to be called back on. |
createdAt | ISO datetime | When the contact record was created. |
agent object
| Field | Type | Description |
|---|---|---|
name | string | Agent (assistant) name. |
email | string | Agent contact email. |
firmName | string | The firm’s name. |
timezone | string | The 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’ssessionStorage, 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 inlocalStorage, so it survives across visits and tabs until the visitor clears their browser storage. ManybrowserSessionAttributionIds roll up to onepersistentAttributionId. 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
| Field | Type | Description |
|---|---|---|
browserSessionAttributionId | string (UUID) | Per-visit session id (sessionStorage). See above. |
persistentAttributionId | string (UUID) | Returning-visitor id across visits (localStorage). See above. |
parentPageBrowserSessionId | string | Your own session id, if you set it via setParentPageBrowserSessionId. |
parentPageBrowserSessionPayload | object | Arbitrary host JSON you set via setParentPageBrowserSessionPayload. |
utmSource | string | First-touch utm_source. |
utmMedium | string | First-touch utm_medium. |
utmCampaign | string | First-touch utm_campaign. |
utmTerm | string | First-touch utm_term. |
utmContent | string | First-touch utm_content. |
referrer | string | The external referrer URL for the visit. |
firstLoadedAtUrl | string | Landing page URL (path only). |
firstLoadedAtUrlQuery | string | Landing page query string. |
firstLoadedAtUrlFragment | string | Landing page hash/fragment. |
firstLoadedAtUrlFull | string | Full landing page URL (path + query + fragment). |
firstUserMessageAtUrl | string | Page URL where the visitor sent their first message. |
lastUserMessageAtUrl | string | Page URL of the visitor’s most recent message. |
firstMessageAfterLoadSeconds | number | Seconds from page load to first message. |
firstMessageAfterWidgetOpenSeconds | number | Seconds from opening the widget to first message. |
locationCity | string | Approx. city (from IP; no raw IP stored). |
locationRegion | string | Approx. region/state. |
locationCountry | string | Approx. country. |
navigationJourney | array | Ordered 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).
| Integration | What it does on a completed conversation |
|---|---|
| Custom Webhook | POSTs 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 Manage | Find/create contact, attach a conversation note. |
| HubSpot | Find/create contact, attach a note. |
| GoHighLevel | Find/create contact in your location, attach a note. |
| Lawmatics | Find/create contact, attach an HTML note. |
| Actionstep | Create/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.5sThe 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.
eventTypeis alwaysnew_leadfor 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.