> ## Documentation Index
> Fetch the complete documentation index at: https://docs.molin.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# JavaScript lifecycle events

> React to widget lifecycle events with settings hooks or standard JavaScript listeners, to trigger integrations or adjust your page layout.

The Molin AI widget notifies your website at key moments in its lifecycle. You can use these hooks to trigger custom logic such as initializing integrations, adjusting page layout, or tracking analytics.

## Settings hooks (recommended)

The simplest and most reliable approach is to define callback functions directly in `window.molinSettings`. Because this object is defined before the widget script loads, the `onReady` hook is **guaranteed to fire**.

```html theme={null}
<script>
  window.molinSettings = {
    onReady: (e) => {
      console.log('Widget ready:', e.widgetId);
      window.Molin.openChat();
    },
    onChatOpen: (e) => {
      console.log('Chat opened:', e.widgetId);
    },
    onChatClose: (e) => {
      console.log('Chat closed:', e.widgetId);
    },
    onMessageSent: (e) => {
      console.log('Message sent:', e.widgetId);
    },
    onLinkClick: (e) => {
      console.log('Link clicked:', e.widgetId, e.url);
    },
    onAddToCart: (e) => {
      console.log('Added to cart:', e.widgetId, e.url);
    },
    onLeadCollected: (e) => {
      console.log('Lead collected:', e.widgetId);
    },
  };
</script>
```

### Available hooks

| Hook | Argument | Description |
| - | - | - |
| `onReady` | `{ widgetId: string }` | Called once when the widget has loaded and rendered |
| `onChatOpen` | `{ widgetId: string }` | Called whenever the chat window opens |
| `onChatClose` | `{ widgetId: string }` | Called whenever the chat window closes |
| `onMessageSent` | `{ widgetId: string }` | Called when the user sends a message |
| `onLinkClick` | `{ widgetId: string, url: string }` | Called when the user clicks a link in the chat |
| `onAddToCart` | `{ widgetId: string, url: string, name: string, price: string, sku?: string, gtin?: string, storefrontProductId?: string }` | Called when the user adds a product to the cart from the chat (see [`molin:add-to-cart`](#molinadd-to-cart)) |
| `onLeadCollected` | `{ widgetId: string }` | Called when the AI collects a lead (visitor name + contact details) |

<Note>Settings hooks can be combined with other `molinSettings` properties like `hidden` or `userData` in the same object.</Note>

### Custom cart integration

Molin drives the cart out of the box on [Shopify](/home/platforms/shopify#add-to-cart), [Shoper](/home/platforms/shoper#add-to-cart), [Shoprenter](/home/platforms/shoprenter#add-to-cart), and [UNAS](/home/platforms/unas#add-to-cart). On any other platform, register cart handlers in `window.molinSettings` and Molin uses yours instead: the add-to-cart button appears on product cards automatically, and the assistant can read and change the cart during a conversation.

`handleAddToCart` turns the integration on and takes precedence over Molin's built-in platform detection. Add the others to widen what the assistant is allowed to do, and it's offered only what you registered.

| Handler | Signature | What it enables |
| - | - | - |
| `handleAddToCart` | `(product) => Promise<void>` | Required. The add-to-cart button, and the assistant adding a product |
| `handleGetCart` | `() => Promise<Cart>` | The assistant reading the cart. Required by the two below |
| `handleRemoveFromCart` | `(lineId) => Promise<void>` | The assistant removing a line |
| `handleSetCartQuantity` | `(lineId, quantity) => Promise<void>` | The assistant setting a line to an exact quantity |

For custom integrations, keep `handleGetCart` quick and cheap: read your existing page state when possible. HTTP requests are supported, but this handler can also run before a chat starts to personalize an AI popup, including on later page loads. In the example below, replace `window.cartState` with your storefront's current cart state and keep it up to date after cart changes.

After your handler adds, removes, or changes an item, Molin immediately reads the cart again. Make sure `handleGetCart` returns the updated cart.

```html theme={null}
<script>
  window.molinSettings = {
    handleAddToCart: async (product) => {
      // product: { url: string, sku?: string, gtin?: string, storefrontProductId?: string }
      const response = await fetch('/api/my-cart/add', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ id: product.storefrontProductId, sku: product.sku, quantity: 1 }),
      });
      if (!response.ok) {
        throw new Error('could not add to cart');
      }
    },

    handleGetCart: async () => {
      const cart = window.cartState; // prefer current page state over an HTTP request to keep this quick and cheap
      return {
        lines: cart.items.map((item) => ({
          lineId: item.id, // whatever your cart uses to address one line, passed back to the handlers below
          name: item.title,
          quantity: item.quantity,
          price: item.price,
        })),
        totalPrice: cart.total,
        currency: cart.currency,
      };
    },

    handleRemoveFromCart: async (lineId) => {
      await fetch(`/api/my-cart/${lineId}`, { method: 'DELETE' });
    },

    handleSetCartQuantity: async (lineId, quantity) => {
      await fetch(`/api/my-cart/${lineId}`, { method: 'PATCH', body: JSON.stringify({ quantity }) });
    },
  };
</script>
```

Every handler is **awaited**, unlike the fire-and-forget `onAddToCart` notification. The product card button shows a spinner while your promise is pending, a checkmark when it resolves, and an error indicator when it rejects or doesn't settle within 30 seconds.

Address lines by the `lineId` you returned from `handleGetCart`, not by product ID, because one product can occupy several cart lines.

<Note>
  Your `handleAddToCart` may be called several times in a row for a single request, once per unit, so make it safe to repeat. Registering `handleSetCartQuantity` lets the assistant set a line already
  in the cart in one call instead.
</Note>

<Warning>
  The assistant changes the cart without asking the customer to confirm. Reject in your handler when a change shouldn't be allowed, and the customer is told why instead of the change going through.
</Warning>

<Note>
  When `handleAddToCart` resolves, Molin pushes the standard GA4 `add_to_cart` ecommerce event and fires `molin:add-to-cart`, so don't track your own `add_to_cart` inside the handler or it will be
  double-counted.
</Note>

## Window events (alternative)

The widget also dispatches standard `CustomEvent` events on the `window` object. Use this approach if you need multiple independent listeners for the same event, or if you prefer the DOM event API.

### Available events

#### `molin:ready`

Fired once when the widget has fully loaded its configuration and rendered for the first time.

```javascript theme={null}
window.addEventListener('molin:ready', (e) => {
  console.log('Molin widget is ready:', e.detail.widgetId);

  // safe to call widget methods now
  window.Molin.openChat({ message: 'Welcome!' });
});
```

| Property | Type | Description |
| - | - | - |
| `e.detail.widgetId` | `string` | The unique ID of the widget that fired |

<Note>
  If your script loads after the widget, the `molin:ready` event may have already fired. In that case, check if `window.Molin` already exists as a fallback:

  ```javascript theme={null}
  if (window.Molin) {
    // widget is already ready
    onMolinReady();
  } else {
    window.addEventListener('molin:ready', onMolinReady);
  }
  ```

  This race condition does not exist with the settings hooks approach above, which is why it is recommended.
</Note>

#### `molin:chat-open`

Fired whenever the chat window is opened, whether by the user clicking the bubble, a popup click, the floating input, a programmatic call to `openChat()`, or a restored session on mobile.

```javascript theme={null}
window.addEventListener('molin:chat-open', (e) => {
  console.log('Chat opened for widget:', e.detail.widgetId);
});
```

| Property | Type | Description |
| - | - | - |
| `e.detail.widgetId` | `string` | The unique ID of the widget that fired |

#### `molin:chat-close`

Fired whenever the chat window is closed, whether by the user clicking the close button, toggling the bubble, or a programmatic call to `closeChat()`.

```javascript theme={null}
window.addEventListener('molin:chat-close', (e) => {
  console.log('Chat closed for widget:', e.detail.widgetId);
});
```

| Property | Type | Description |
| - | - | - |
| `e.detail.widgetId` | `string` | The unique ID of the widget that fired |

#### `molin:message-sent`

Fired when the user sends a message in the chat.

```javascript theme={null}
window.addEventListener('molin:message-sent', (e) => {
  console.log('Message sent in widget:', e.detail.widgetId);
});
```

| Property | Type | Description |
| - | - | - |
| `e.detail.widgetId` | `string` | The unique ID of the widget that fired |

#### `molin:link-click`

Fired when the user clicks a link inside the chat conversation.

```javascript theme={null}
window.addEventListener('molin:link-click', (e) => {
  console.log('Link clicked:', e.detail.url);
});
```

| Property | Type | Description |
| - | - | - |
| `e.detail.widgetId` | `string` | The unique ID of the widget that fired |
| `e.detail.url` | `string` | The URL of the clicked link |

#### `molin:add-to-cart`

Fired after the product was successfully added to the cart, either by Molin's built-in integration on [Shopify](/home/platforms/shopify#add-to-cart), [Shoper](/home/platforms/shoper#add-to-cart), [Shoprenter](/home/platforms/shoprenter#add-to-cart), and [UNAS](/home/platforms/unas#add-to-cart) stores (where the button is enabled automatically), or by your own [`handleAddToCart`](#custom-cart-integration) handler on other platforms. Use the event to refresh your theme's cart drawer or cart count badge, since themes usually only update their cart UI in response to their own add-to-cart flows.

```javascript theme={null}
window.addEventListener('molin:add-to-cart', (e) => {
  console.log('Added to cart:', e.detail.url);

  // example: refresh your theme's cart drawer here
});
```

| Property | Type | Description |
| - | - | - |
| `e.detail.widgetId` | `string` | The unique ID of the widget that fired |
| `e.detail.url` | `string` | The URL of the product |
| `e.detail.name` | `string` | The product name shown on the card |
| `e.detail.price` | `string` | The product price shown on the card |
| `e.detail.sku` | `string` | The catalog SKU of the product, when known |
| `e.detail.gtin` | `string` | The catalog GTIN/EAN of the product, when known |
| `e.detail.storefrontProductId` | `string` | The platform-native identifier used by the storefront cart |

#### `molin:lead-collected`

Fired when the AI successfully collects a lead (the visitor's name and contact details) in the chat. Use this to fire your own conversion pixels (e.g. Meta Pixel, TikTok Pixel) on real Molin conversions.

```javascript theme={null}
window.addEventListener('molin:lead-collected', (e) => {
  console.log('Lead collected in widget:', e.detail.widgetId);
});
```

| Property | Type | Description |
| - | - | - |
| `e.detail.widgetId` | `string` | The unique ID of the widget that fired |

### Full example

```html theme={null}
<script>
  function onMolinReady(e) {
    console.log('Widget ready:', e.detail.widgetId);
  }

  if (window.Molin) {
    onMolinReady({ detail: { widgetId: 'already-loaded' } });
  } else {
    window.addEventListener('molin:ready', onMolinReady);
  }

  window.addEventListener('molin:chat-open', () => {
    document.body.classList.add('molin-chat-open');
  });

  window.addEventListener('molin:chat-close', () => {
    document.body.classList.remove('molin-chat-open');
  });
</script>
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.