Lunachat
Developers

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.

index.html
<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

CommandSignatureWhat 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.

If the language you pin is not enabled in the panel, the primary language is shown instead. So { 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.

This data is unverified.It comes from your page's JavaScript, so anyone who opens the browser console can change it. It is never used for identity merging and never written to the contact's name, email or phone — that is what keeps it apart from what the visitor typed into the form themselves. Do not base billing or access decisions on it.

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.

ClaimDescription
subRequired. Your own user id; merging is keyed on it.
expRequired. At most 24 hours ahead.
name / email / phoneOptional. Fills whichever contact fields are empty.
attrsOptional. Verified custom fields, 30 at most.
Sign the token on your server. A secret that reaches the page defeats the whole feature: anyone who reads it can mint a token for any user. If verification fails the chat still opens — the user simply stays anonymous.

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
});
FieldTypeWhat it is
valuenumberAmount in LIRA. Do not convert to kuruş.
orderIdstringYour order number. Without it duplicates cannot be caught.
currencystringOnly '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.

Do send the order number. Confirmation pages get refreshed, people hit the back button, and some platforms run the redirect twice. With a number the order is counted once; without one the revenue report quietly inflates.

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 shared devices this call is mandatory. Without it, the next person signing in on that browser sees the previous person's chat history. Add it to your logout flow.

on(event, callback)

lunachat('on', 'ready', () => {
  console.log('widget ready');
});

lunachat('on', 'message', (message) => {
  console.log('message from agent', message);
});
EventWhenPayload
readyWidget loaded and renderednone
openChat panel openednone
closeChat panel closednone
messageMessage arrived from an agentmessage object
automationAutomation 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.