Live Chat (developer)
Free Communication module (live-chat 1.6.0). Public widget traffic resolves the workspace by widget public_key (central/web routes), initializes tenancy, checks entitlement, then ends tenancy — same pattern as custom Lead webhooks.
Surfaces
| Surface | Auth |
|---|---|
GET/POST /api/public/live-chat/{publicKey}/… | Widget key + visitor Bearer session token (SHA-256 hashed at rest) |
/api/tenant/v1/live-chat/… | auth:tenant-api + module:live-chat + Spatie can: |
public/widgets/live-chat.js | Static embed script (data-key, data-api-base) — Tawk-style FAB/panel in a Shadow DOM host |
SPA /live-chat | Agent desk tabs: Overview / Inbox / Live visitors |
SPA /settings?tab=live-chat | Widget / departments / canned replies (live-chat.manage) |
Throttle: live-chat-widget (60/min by IP + key) and live-chat-widget-session (10/min for session create); disabled in testing. CSRF excepted for api/public/live-chat/*.
CORS: LiveChatPublicCors reflects Origin for credential-less public widget paths only (does not open the rest of the API).
Public DTOs: Visitor responses use LiveChatPublicConversationResource / LiveChatPublicMessageResource — uuid/status/body/direction/attachment meta/agent_name (agent rows; workspace user Name, no email/id)/timestamps only (no assignee, Lead, visitor IP, sender email, or direction=note).
Sessions: Expire after 7 days from creation. Regenerating the public key (or deactivating the widget / cancelling the module) revokes all visitor session hashes. Retention: live-chat:purge-visitors --days=90 (daily). Banned visitors (is_banned) cannot create sessions or send messages.
Embed resume (1.4.1): The widget persists session_token / conversation hints in localStorage keyed by public key. On load it validates via POST …/heartbeat (which returns open_conversation_uuid when an open thread exists), then hydrates both visitor and agent messages. Host-page CSS cannot restyle the pre-chat form because UI lives in Shadow DOM with explicit dark labels/input colors.
Branding, hours, offline, presence
Widget settings store primary_color, launcher_text, position, header_title, logo_url, offline_*, business_hours, email_notify_agents, idle_minutes, default_department_id, suggested_replies, show_powered_by, agent_display_name. Bootstrap returns branding plus within_hours / accepting_live and optional Reverb public hints. Outside hours (or inactive accepting), visitors leave offline messages when offline_enabled.
POST …/heartbeat (Bearer session) updates last_seen_at, page_url, referrer, UA-derived browser/os, optional country from CDN headers, and pages_viewed. Tenant GET …/visitors/live and GET …/stats power the Live visitors + Overview tabs. Presence broadcasts throttle on the inbox channel as LiveChatVisitorPresence.
Notes
Agent POST …/messages accepts as_note=true (LiveChatMessageDirectionEnum::Note). Notes are returned on tenant message lists and never on public visitor polls or broadcast visitor payloads.
Realtime
Agent channels (distinct from Team Chat):
private-tenant.{tenantId}.live-chat.inboxprivate-tenant.{tenantId}.live-chat.conversation.{uuid}
Visitor public channel: live-chat.visitor.{uuid} (UUID secrecy). Events: LiveChatMessageSent, LiveChatConversationUpdated, LiveChatVisitorPresence, LiveChatTyping (ShouldBroadcastNow, soft-failed on send/open/close paths). Embed keeps short-poll fallback; send lock + hydrate-before-retry on Retry. Typing also has HTTP endpoints: public + tenant POST …/typing. Settings embed snippet includes ?v= cache-bust (LiveChatWidgetService::WidgetScriptVersion).
Soft Leads / Help Desk
PATCH …/conversations/{id} with lead_id calls LiveChatConversationService::linkLead. Requires Leads entitled; null unlinks.
POST …/conversations/{id}/escalate creates a Help Desk ticket (source: live-chat) when Help Desk is entitled and sets help_desk_ticket_id. Escalation transcript omits notes.
Canned replies & departments
Tenant CRUD under /live-chat/canned-replies and /live-chat/departments (manage writes, view lists).
Automation
Wired triggers: live_chat.conversation_opened, live_chat.message_inbound, live_chat.conversation_closed — fan-out via IntegrationEventDispatcher.
Notifications
Do not fan out to every user with live-chat.view. Recipients are members of the conversation’s Live Chat department (widget default department on create) who also have live-chat.reply, capped at 20. Empty roster skips notify (live-chat.notify_skipped_empty_roster). Keep production workers out of Live Chat departments even if Staff still has Live Chat permissions.
| Event | Recipients | Channels |
|---|---|---|
| First visitor message (unassigned) | Department agents | database + broadcast + FCM; optional mail when email_notify_agents |
| Later visitor messages while unassigned | Department agents | database + broadcast |
| Visitor messages after assign | Assignee only | database + broadcast |
| First agent reply | Auto-assigns that agent; live-chat.assigned | FCM + in-app |
Idle (idle_minutes, default 5; 0 = off) | Department minus assignee | FCM + in-app once (live-chat.idle); live-chat:scan-idle every minute |
Inbox Reverb conversation list updates remain separate from notification wake-ups. Types: live-chat.inbound, live-chat.assigned, live-chat.idle.
Tests
Pest: tests/Feature/Tenant/LiveChat/. Playwright: npm run test:e2e:live-chat.
Production readiness: 1.0.1 baseline · 1.5.0 / 1.5.1 send resilience + typing.