Skip to Content
🎉 Welcome to Delivery Chat Documentation
V1SdkHeadless Mode

Headless Mode

Headless mode initializes the full SDK infrastructure — WebSocket, visitor session, conversation management — without rendering any UI. Use it to build a completely custom chat experience.

Getting Started

import { init, getSdkApi } from "@deliverychat/sdk"; init({ appId: "your-app-uuid", headless: true }); const chat = getSdkApi();

In headless mode:

  • No Shadow DOM, launcher, or chat window is created
  • WebSocket connects immediately (no user interaction needed)
  • All messaging and identity methods work identically
  • open(), close(), toggle(), hideWidget(), showWidget() are silent no-ops
  • open and close events are suppressed; all other events fire normally

Reference Implementation

Here is a minimal custom chat UI using headless mode:

<div id="my-chat"> <div id="messages"></div> <form id="chat-form"> <input type="text" id="chat-input" placeholder="Type a message..." /> <button type="submit">Send</button> </form> </div> <script type="module"> import { init, getSdkApi } from "@deliverychat/sdk"; init({ appId: "your-app-uuid", headless: true }); const chat = getSdkApi(); const messagesEl = document.getElementById("messages"); const form = document.getElementById("chat-form"); const input = document.getElementById("chat-input"); // Render a message function appendMessage(msg) { const div = document.createElement("div"); div.className = `msg msg-${msg.senderRole}`; div.textContent = msg.content; div.dataset.id = msg.id; messagesEl.appendChild(div); messagesEl.scrollTop = messagesEl.scrollHeight; } // Listen for incoming messages chat.on("message:received", (msg) => { appendMessage(msg); }); // Track sent messages chat.on("message:sent", (msg) => { const pending = messagesEl.querySelector(`[data-id="${msg.id}"]`); if (pending) pending.classList.remove("pending"); }); // Handle unread count chat.on("unread:changed", ({ count }) => { document.title = count > 0 ? `(${count}) Support` : "Support"; }); // Conversation resolved chat.on("conversation:resolved", () => { appendMessage({ id: "system", content: "This conversation has been resolved.", type: "system", senderRole: "admin", senderId: "", status: "sent", createdAt: new Date().toISOString(), }); }); // Send message on form submit form.addEventListener("submit", async (e) => { e.preventDefault(); const text = input.value.trim(); if (!text) return; input.value = ""; try { const msg = await chat.sendMessage(text); appendMessage(msg); } catch (err) { console.error("Failed to send:", err.message); } }); </script>

React Example

Uses useSyncExternalStore to subscribe to the SDK as an external store — no useEffect needed for state sync.

import { useRef, useState, useSyncExternalStore, useCallback } from "react"; import { init, destroy, getSdkApi } from "@deliverychat/sdk"; import type { ChatMessage } from "@deliverychat/sdk"; function createChatStore(appId: string) { init({ appId, headless: true }); const chat = getSdkApi(); let messages: ChatMessage[] = []; const listeners = new Set<() => void>(); const notify = () => listeners.forEach((l) => l()); chat.on("message:received", (msg) => { messages = [...messages, msg]; notify(); }); chat.on("message:sent", (msg) => { messages = messages.map((m) => (m.id === msg.id ? msg : m)); notify(); }); return { subscribe: (listener: () => void) => { listeners.add(listener); return () => listeners.delete(listener); }, getSnapshot: () => messages, sendMessage: (text: string) => chat.sendMessage(text), destroy: () => destroy(), }; } export function Chat({ appId }: { appId: string }) { const [input, setInput] = useState(""); const storeRef = useRef<ReturnType<typeof createChatStore>>(); if (!storeRef.current) { storeRef.current = createChatStore(appId); } const store = storeRef.current; const messages = useSyncExternalStore(store.subscribe, store.getSnapshot); const handleSend = useCallback( async (e: React.FormEvent) => { e.preventDefault(); if (!input.trim()) return; const text = input; setInput(""); await store.sendMessage(text); }, [input, store], ); return ( <div> <div> {messages.map((msg) => ( <div key={msg.id} className={`msg-${msg.senderRole}`}> {msg.content} </div> ))} </div> <form onSubmit={handleSend}> <input value={input} onChange={(e) => setInput(e.target.value)} placeholder="Type a message..." /> <button type="submit">Send</button> </form> </div> ); }

What Works in Headless Mode

FeatureAvailableNotes
sendMessage()YesCreates conversation automatically on first call
getConversation()YesReturns current conversation state
identify()YesWorks identically to widget mode
on() / off()YesAll events except open/close
Auto-reconnectYesWebSocket reconnects on disconnect
open() / close() / toggle()No-opSilent — no error thrown
hideWidget() / showWidget()No-opSilent — no error thrown

When to Use Headless Mode

  • Custom chat UI — You want full control over the look and feel
  • Mobile apps — Use the SDK in a WebView with native-rendered UI
  • Chatbots — Integrate with your own bot logic before routing to human operators
  • Notifications — Listen to events without showing a chat window
  • Testing — Automated tests that need to send/receive messages without DOM rendering
Last updated on