One line to install.
Full API when you need it.
Drop in a script tag and you're live. Use the JavaScript API to share context with agents, hook events into your analytics, and control the widget programmatically.
Install
All plansPaste before the closing </body> tag on every page. Replace YOUR_SITE_ID with the ID from your dashboard.
<script src="https://api.ghostchat.dev/widget.js" data-site="YOUR_SITE_ID" data-no-optimize="1" defer></script>~15KB gzipped. No cookies, no tracking, no external dependencies. Read the source →
JavaScript API
All plansControl the widget programmatically. The GhostChat global is available after the script loads.
// Open / close / toggle the widget
GhostChat.open()
GhostChat.close()
GhostChat.toggle()
// Listen for events — wire into analytics, surveys, or custom UI
GhostChat.on("open", () => console.log("widget opened"))
GhostChat.on("close", () => console.log("widget closed"))
GhostChat.on("message", (msg) => console.log(msg.sender, msg.content))
// msg = { sender: "visitor" | "agent", content: string, createdAt: string }
// Shared device or kiosk? End the conversation and clear everything local —
// the next person starts a completely fresh chat
await GhostChat.reset()on(event, callback)
Events: open, close, message. Available immediately — register listeners before the widget finishes loading.
setContext(data)
Share visitor info (name, email, anything) with your agents. See the Visitor Context API below.
reset()
Programmatic "Clear chat from this device": archives the conversation for your dashboard, wipes the visitor's local history, identity, and context. Built for shared devices and kiosks. Returns a promise.
Track conversions in Google Analytics (GA4)
Use on() to send chat engagement to GA4 or Google Tag Manager. Then mark generate_lead as a key event in GA4 so it counts as a conversion — note GA4 requires value and currency for it to qualify.
<!-- After your GA4 / gtag base tag and the GhostChat embed -->
<script>
let counted = false;
GhostChat.on("open", () => gtag('event', 'chat_opened'));
GhostChat.on("message", () => {
if (!counted) { // fire the lead once per visit
counted = true;
gtag('event', 'generate_lead', { currency: 'USD', value: 1 });
}
});
</script>
<!-- Using Google Tag Manager instead? Push to the dataLayer: -->
<script>
GhostChat.on("message", () => dataLayer.push({ event: 'ghostchat_lead' }));
</script>Running several sites? See Multi-Site Live Chat for the full multi-site + GTM/GA4 setup.
Embed Mode
All plansRender the chat inline instead of as a floating bubble — useful for support pages or help centers. The widget auto-detects the container and hides the floating button.
<!-- Add this anywhere on the page — no extra script needed -->
<div id="ghostchat-embed" style="width: 400px; height: 600px;"></div>Works on WordPress, Shopify, static HTML, and any traditional multi-page site. Not supported in SPAs with client-side navigation (React, Next.js app router).
On WordPress: use the shortcode
Our WordPress plugin (1.4.0+) ships a shortcode that renders the same container for you — paste it into the page content and the plugin loads the widget on that page automatically.
[ghostchat_embed]
<!-- Set a size if you want one other than 400x600 -->
[ghostchat_embed width="400" height="600"]Pair it with Settings → GhostChat → Where it appears → “Built into a page”. The shortcode controls how the chat looks on that page; the setting is what stops the floating bubble appearing on every other page.
Visitor Context API
Pro / TeamPush any data to your agents in real-time — cart contents, user plan, page type, or anything else. Context appears as purple pills in the conversation header so your team knows what the visitor needs before they ask.
WooCommerce keys: cart_items, cart_total, product_name, cart_count, page_type
Any custom keys via setContext()
// Share cart contents, page info, or any custom data
GhostChat.setContext({
cart: ["Brake Pad Kit", "Oil Filter"],
vehicle: "2022 Can-Am Maverick X3",
page: "checkout"
})
// Merge more context later — previous keys are kept
GhostChat.setContext({ coupon: "SAVE10" })
// Clear a key by setting it to null
GhostChat.setContext({ coupon: null })Merge behavior
Each call merges with existing context. Nested objects are replaced (shallow merge), not deep-merged.
Limits
Max 20 keys per session. 4KB per setContext() call. Updates throttled to once per 5 seconds. Custom keys need Team — on Free and Pro the API rejects them (403 PLAN_UPGRADE_REQUIRED, listing the dropped keys) instead of silently discarding them. The widget sends context fire-and-forget, so the rejection shows in your Network tab, not the console.
Lifecycle
Context persists for the session. Cleared when the conversation is deleted or session expires.
WooCommerce — zero config
Our WordPress plugin auto-sends these keys on every page load and cart update:
page_typeproduct_namecart_itemscart_totalcart_countShopify — manual
No auto-integration. Call setContext() from your theme's JS with Liquid variables.
Kiosk & Shared Devices
One browser, many people — a support desk, a showroom tablet, a checkout kiosk. Each person needs their own conversation, and nothing from the last visitor can survive into the next one. Two calls cover the whole lifecycle.
// 1 — Your pre-chat form hands the details to the widget,
// then opens the chat. setContext() is synchronous.
function startSupportSession({ name, email, phone, reason }) {
GhostChat.setContext({ name, email, phone, reason })
GhostChat.open()
}
// 2 — Interaction over. Archives the conversation to your dashboard,
// then wipes every trace from the device: messages, name, email,
// session secret, custom context, and page journey.
async function endSupportSession() {
await GhostChat.reset()
}
// Safe to call when nobody chatted — with no conversation to archive
// it just clears the device.What reset() clears
Messages, name, email, session secret, custom context, identity hash, and buffered page journey. The visitor gets a brand-new session ID.
Idle backstop
Turn on privacy mode in site settings and the device also wipes itself after 15 minutes idle — for the visitor who walks away before anyone hits reset. It clears the device only: unlike reset() it doesn't archive, so the conversation stays open in your dashboard.
Resolving isn't resetting
Marking a conversation resolved in your dashboard does not clear the device. Only reset() or the widget's own clear-chat menu do that.
TeamThe custom keys above (name, email, phone, reason, and anything else of your own) require Team — on Free and Pro the API rejects the call with 403 PLAN_UPGRADE_REQUIRED and names the dropped keys. Check the Network tab while wiring this up: the widget doesn't surface the response, so nothing appears in the console. reset() itself works on every plan. See Visitor Context for limits — 20 keys per session, 4KB per call.
Webhooks
Pro / TeamTwo events, one URL: a lightweight POST per visitor message, and a full-transcript snapshot when a conversation is resolved. Connect to Slack, your CRM, Airtable, a ticket system, or any custom automation — filter on the event field.
// POST sent to your URL on each visitor message
{
"event": "message.new",
"siteId": "cl194a6c5368cc4cceb600c436",
"siteName": "My Store",
"sessionId": "sess_abc123",
"messageId": "msg_xyz789",
"content": "Hi, I need help with my order",
"imageUrl": "https://...", // only if the visitor attached an image
"visitorEmail": "jane@example.com",
"pageUrl": "https://mystore.com/checkout",
"referrer": "https://mystore.com/cart",
"country": "US",
"createdAt": "2026-03-16T12:00:00Z"
}// POST sent to the same URL when a conversation is resolved
// (manual Resolve, or the daily auto-archive). Full snapshot, keyed on
// the conversation — a re-resolve re-fires the SAME conversationId with
// the fuller transcript. Upsert by conversationId; latest wins.
{
"event": "conversation.resolved",
"conversationId": "cl...", // stable — your upsert key
"siteId": "cl194a6c5368cc4cceb600c436",
"siteName": "My Store",
"resolvedAt": "2026-03-16T12:00:00+00:00",
"visitor": { // hints — may be null
"id": "browser-scoped-uuid",
"name": null,
"email": "jane@example.com",
"country": "US"
},
"pageUrl": "https://mystore.com/checkout",
"referrer": "https://mystore.com/cart",
"pageJourney": [{ "url": "https://mystore.com/", "ts": "2026-03-16T11:58:00Z" }],
"messages": [
{
"id": "msg_xyz789",
"role": "visitor", // "visitor" | "agent"
"text": "Hi, I need help with my order",
"timestamp": "2026-03-16T11:59:00.000Z",
"attachment": { "url": "https://...", "name": "receipt.pdf" } // when present; name may be null
},
{
"id": "msg_abc012",
"role": "agent",
"agentName": "Support Bot", // present when a named agent or the AI replied
"text": "Happy to help — what's your order number?",
"timestamp": "2026-03-16T11:59:30.000Z"
}
]
}
// Conversations with zero messages never fire this event — a session created
// by a pageview that nobody typed into resolves silently, no delivery.Configure your webhook URL in Dashboard → Sites → [your site] → Webhook URL. Available on all paid plans. Every delivery is logged with its response status in Dashboard → Developer → Deliveries. View pricing →
Building an AI bot agent?
Use this webhook to receive visitor messages, call any LLM (Ollama, Claude, GPT-4...), and reply via the bot-reply API. No per-message AI costs, no vendor lock-in.
Learn about Bot Agent →Identity Verification
TeamPrevent visitors from impersonating other users. Your backend signs each visitor's email with a secret key (HMAC-SHA256), and GhostChat verifies the signature. Only works for logged-in users on your app — anonymous visitors can still chat normally.
How it works
Generate a secret key in Dashboard → Sites → Identity Verification
On your backend, compute HMAC-SHA256(secret, email) after the user logs in
Pass both the email and the hash to the widget via setContext
GhostChat verifies the hash server-side — if it matches, the visitor is marked as verified
Server-side: generate the hash
const crypto = require('crypto');
// Your identity secret (from Dashboard → Sites → Identity Verification)
const SECRET = process.env.GHOSTCHAT_IDENTITY_SECRET;
function generateUserHash(email) {
return crypto
.createHmac('sha256', SECRET)
.update(email.toLowerCase().trim())
.digest('hex');
}
// After your user logs in:
const userHash = generateUserHash(user.email);
// Pass to your frontend, then call:
// GhostChat.setContext({ email: user.email, userHash: userHash })import hmac, hashlib, os
SECRET = os.environ["GHOSTCHAT_IDENTITY_SECRET"]
def generate_user_hash(email: str) -> str:
return hmac.new(
SECRET.encode(),
email.lower().strip().encode(),
hashlib.sha256
).hexdigest()$secret = getenv('GHOSTCHAT_IDENTITY_SECRET');
$userHash = hash_hmac('sha256', strtolower(trim($email)), $secret);Client-side: pass it to the widget
// On your frontend, after getting the hash from your backend:
GhostChat.setContext({
email: "jane@example.com",
userHash: "a83f2c4e9b1d..." // from your server
})Important
Never expose your identity secret in client-side code. The hash must be generated on your backend and passed to the frontend. The secret stays on your server and in GhostChat — it never reaches the browser.
See Multi-Site Live Chat for how this fits into a shared team inbox.
FAQ
When is the GhostChat API available to call?
The GhostChat global appears on window once the widget script has loaded. GhostChat.on() is available immediately, so you can register event listeners as early as you like. For SPAs, call GhostChat.setContext() after your user data is available and the widget has loaded.
Does GhostChat work with React or Next.js?
Yes. Add the script tag once in your root layout (or use the useEffect pattern shown above). Embed mode works on traditional multi-page sites only — SPA client-side navigation is not supported for embed mode.
Can I trigger the widget from a custom button?
Yes. Call GhostChat.open() from any click handler. You can hide the default floating bubble by adding CSS: #ghostchat-bubble { display: none; } — then drive opens entirely from your own UI.
How do I test webhooks locally?
Use a tool like ngrok to expose your local server, then paste the ngrok URL as your webhook URL in Dashboard → Sites → [site] → Webhook URL. Webhooks are available on all paid plans (Pro and above).
Ready to integrate?
Free forever for 1 site. No credit card required. Get your Site ID in 30 seconds.