Widget JavaScript API
Control the chat widget from your page: start it, open it, close it, send what you know about the visitor.
This page describes what is implemented. A call that is not listed here is silently ignored.
Embed code
In the panel, copy the snippet under Websites → site → Install and paste it before the closing </body> tag.
<script>
window.lunachat = window.lunachat || function () {
(lunachat.q = lunachat.q || []).push(arguments);
};
lunachat('init', 'pk_live_7f3a9c2e8b1d4a6f');
</script>
<script async src="https://cdn.lunachat.co/w.js"></script>The first block sets up a queue. The script itself loads async and never blocks rendering; once it arrives it replays the queued calls in order. That is why calling a command before the script has loaded is safe — nothing is lost. A malformed call in the queue does not take the others down with it.
Commands
| Command | Signature | What it does |
|---|---|---|
| init | (publicKey, options?) | Starts the widget |
| set | (attributes) | Sends custom attributes |
| identify | (token) | Introduces the user as verified |
| track | (event, payload) | Reports a conversion (an order) |
| open | () | Opens the chat |
| close | () | Closes the chat |
| shutdown | () | Clears the session |
| on | (event, callback) | Listens for events |
init(publicKey, options?)
lunachat('init', 'pk_live_7f3a9c2e8b1d4a6f');
// Pin the language
lunachat('init', 'pk_live_7f3a9c2e8b1d4a6f', { lang: 'en' });publicKey identifies the site and is not a secret. Which domains the widget may run on is set in the panel; another site that copies your snippet cannot run it.
lang — 'tr' or 'en'. Without it the visitor's browser language is used. If you serve a separate directory such as /en, pin the language — the browser cannot know about it.
{ lang: 'en' } does not produce English until English is enabled — the widget should not appear to speak a language your team cannot answer in.Calling init more than once: the second call is ignored.
set(attributes)
Carries what your site knows about the visitor into the panel, where the agent sees it on the contact card.
lunachat('set', {
plan: 'premium',
cartTotal: 1250,
isMember: true,
});Values may be text (up to 500 characters), a number, a boolean or null; 30 fields at most. It can be called before init and is sent once the widget starts.
identify(token)
Introduces a signed-in user as verified. The difference from set: your server signs this token, so the name and email land on the contact record itself, and when the same user writes from another device the records merge.
// ON YOUR SERVER — the secret never reaches the browser
const token = jwt.sign(
{
sub: user.id, // required
name: user.name,
email: user.email,
attrs: { plan: 'pro' }, // verified custom fields
exp: Math.floor(Date.now() / 1000) + 3600,
},
process.env.LUNACHAT_SECRET,
{ algorithm: 'HS256' },
);
// On the page
lunachat('identify', token);Generate the secret under Settings → Websites → Install. It is shown once at creation; generating a new one invalidates the old.
| Claim | Description |
|---|---|
| sub | Required. Your own user id; merging is keyed on it. |
| exp | Required. At most 24 hours ahead. |
| name / email / phone | Optional. Fills whichever contact fields are empty. |
| attrs | Optional. Verified custom fields, 30 at most. |
track(event, payload)
Reports an order to Lunachat. The revenue figure and the "conversion rate of visitors who chatted" comparison on the Reports screen are built from this call. There is one event today: 'purchase'.
lunachat('track', 'purchase', {
value: 1250, // lira, cart total including VAT
orderId: 'SP-88421', // keeps the same order from being counted twice
});| Field | Type | What it is |
|---|---|---|
| value | number | Amount in LIRA. Do not convert to kuruş. |
| orderId | string | Your order number. Without it duplicates cannot be caught. |
| currency | string | Only 'TRY' today. Defaults when omitted. |
The call opens no chat and shows the visitor nothing. If the widget has not loaded yet the call is queued and sent once the connection is up, so it cannot get lost on the page you most want measured.
The order is linked to a chat the visitor had in the last 7 days. If they never wrote, the record is still kept: the other half of the comparison report comes from those rows.
open() / close()
For opening the chat from your own button.
document.querySelector('#support').addEventListener('click', () => {
lunachat('open');
});If the widget has not loaded yet, the call passes silently.
shutdown()
// When the user logs out of your site
lunachat('shutdown');Clears the visitor token, closes the connection and removes the widget.
on(event, callback)
lunachat('on', 'ready', () => {
console.log('widget ready');
});
lunachat('on', 'message', (message) => {
console.log('message from agent', message);
});| Event | When | Payload |
|---|---|---|
| ready | Widget loaded and rendered | none |
| open | Chat panel opened | none |
| close | Chat panel closed | none |
| message | Message arrived from an agent | message object |
| automation | Automation bubble shown | { runId, body } |
An error thrown by your listener does not take the widget down. The message payload is passed through as the server sends it; its fields may change between versions, so do not write code that depends on the shape.
Common patterns
Introduce a signed-in user
lunachat('init', 'pk_live_…');
// Verified: signed on your server, name and email land on the contact record
lunachat('identify', token);
// Unverified: context for the agent, nothing more
lunachat('set', { cartTotal: 1250 });identify says who the user is; set is for context that does not need to be trusted. Keep them apart: a wrong cart total costs the agent nothing, a wrong identity puts them in the wrong conversation.
Follow the page language
lunachat('init', 'pk_live_…', {
lang: document.documentElement.lang.startsWith('en') ? 'en' : 'tr',
});Measure an order
<!-- order confirmation page -->
<script>
lunachat('track', 'purchase', {
value: {{ order.total }},
orderId: '{{ order.number }}',
});
</script>The template variables depend on your e-commerce platform. The only rule: the amount is in lira and the order number is the real one.
Things worth knowing
Page transitions. The widget follows URL changes on its own; in a single-page app do not call initagain on every route. The "current page" the agent sees may lag by a few seconds.
Automation. Rules you build in the panel (visitor waits on a page, is about to leave …) show a message beside the bubble. The message text and the rule itself stay on the server; only conditions the browser already knows are sent down. Custom attributes you send with set can be used as rule conditions.
A triggered message does not open a conversation. If the visitor replies, the conversation opens at that moment and the automated message is prepended to what the agent reads. No visitor gets more than one automated message per 30 minutes, and a dismissed message does not come back during that visit.
Style collisions. The widget lives in a Shadow DOM: your CSS does not leak in and the widget does not break your site. You cannot restyle it from the page — colour, position and copy are set in the panel.
Size. The script is under 40 KB gzipped and loads async; it has no measurable effect on page speed.
Failure. If startup fails (wrong key, unauthorised domain) nothing is left on the page — a broken bubble is worse than no bubble. You will not see a console error; check the domain list in the panel.