Lunachat
All integrations

Add live chat to your WordPress site

There's no plugin to install — Lunachat is a plain script tag. Add it to your site's head once and it runs on every page.

Setup

  1. Step 1: Copy your embed code

    In the Lunachat panel, open Websites, pick your site and copy the embed snippet. The public key inside it belongs to that one site.

    HTML
    <script>
      window.lunachat = window.lunachat || function () {
        (lunachat.q = lunachat.q || []).push(arguments);
      };
      lunachat('init', 'pk_live_YOUR_KEY');
    </script>
    <script async src="https://cdn.lunachat.co/w.js"></script>
  2. Step 2: Check whether your theme is classic or block

    If Appearance shows an Editor entry you are on a block theme (Twenty Twenty-Four and later), and those themes have NO header.php file. If Appearance shows Customize, it is a classic theme. The first method below works on both.

  3. Step 3: Add it with a code snippet plugin

    This is the durable route. Install a snippet plugin such as WPCode, create a snippet, choose HTML as the type, set the placement to the site-wide head and paste the code. It survives both theme updates and theme changes.

  4. Step 4: Without a plugin: wp_head in a child theme

    Add the hook below to your child theme's functions.php. It works on block themes too, because wp_head is called by every theme. Never edit the parent theme directly, an update wipes it.

    HTML
    add_action('wp_head', function () { ?>
      <script>
        window.lunachat = window.lunachat || function () {
          (lunachat.q = lunachat.q || []).push(arguments);
        };
        lunachat('init', 'pk_live_YOUR_KEY');
      </script>
      <script async src="https://cdn.lunachat.co/w.js"></script>
    <?php });
  5. Step 5: Save and open your site

    Reload the page and the chat bubble appears in the bottom right. If it doesn't, clear your caching plugin's cache first, then hard-refresh the browser.

Worth knowing

  • Themes and builders like Elementor, Astra and GeneratePress have their own custom-code fields; those work too.
  • The JavaScript combine and defer options in caching plugins can break the widget. Exclude w.js from combining in WP Rocket, LiteSpeed Cache and Autoptimize; it already loads async, so deferring buys you nothing.
  • If you run a cookie consent plugin, check which category the script lands in. Put it under marketing rather than strictly necessary and the chat box never opens until a visitor consents.
  • On WooCommerce stores it appears on product, cart and checkout pages automatically, with nothing extra to configure.
  • The widget is under 40 KB gzipped and loads async, so it won't hurt your Core Web Vitals.

If you run WooCommerce

Once the widget is in, an agent already sees which page the visitor is on. WooCommerce can add two more things: what is in the cart, and who the person writing actually is.

Show the cart total to your agents

Fields you pass through set appear next to the visitor on the conversation screen. Someone with 240 dollars sitting in their cart does not deserve the same answer as someone browsing a 9 dollar item. These fields arrive from the page, so they count as UNVERIFIED: useful context for an agent, never written to the contact's identity.

PHP
add_action('wp_footer', function () {
    if (!function_exists('WC') || !WC()->cart) {
        return;
    }
    $data = array(
        'cartTotal' => (float) WC()->cart->get_cart_contents_total(),
        'cartItems' => WC()->cart->get_cart_contents_count(),
    );
    ?>
    <script>
      lunachat('set', <?php echo wp_json_encode($data); ?>);
    </script>
    <?php
});

Verify the logged-in customer

Unlike a cart total, a name and email can be verified. Your server signs a token with your secret, the widget sends it through identify, and the panel shows the person as verified. When the same customer writes from their phone the records merge. You generate the secret under the Setup tab of the site's settings, and it is shown only once.

PHP
function lunachat_token(array $claims, string $secret): string {
    $b64 = fn($v) => rtrim(strtr(base64_encode($v), '+/', '-_'), '=');
    $body = $b64(wp_json_encode(array('alg' => 'HS256', 'typ' => 'JWT')))
        . '.' . $b64(wp_json_encode($claims));

    return $body . '.' . $b64(hash_hmac('sha256', $body, $secret, true));
}

add_action('wp_footer', function () {
    if (!is_user_logged_in()) {
        return;
    }
    $user = wp_get_current_user();
    $token = lunachat_token(array(
        'sub'   => (string) $user->ID,
        'name'  => $user->display_name,
        'email' => $user->user_email,
        'iat'   => time(),
        'exp'   => time() + 3600,
    ), LUNACHAT_SECRET);
    ?>
    <script>
      lunachat('identify', <?php echo wp_json_encode($token); ?>);
    </script>
    <?php
});

Frequently asked