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

# Install the chat widget

> One script tag on your site, and the colours, position and launcher you can override from the page.

The widget is a chat bubble your visitors talk to; the conversations land in the
Linkiasoft inbox alongside WhatsApp, Messenger and the rest. Setting it up in the app is
covered in [Website chat](/help/channels/website-chat) — this page is the part that goes
in your codebase.

## The snippet

```html theme={null}
<script
  src="https://social.linkiasoft.com/widget/v1/loader.js"
  data-website-id="wc_pub_7c8d9e0f1a2b"
  async
></script>
```

Put it before the closing `</body>` tag of every page that should have the bubble. Your
`data-website-id` is on the channel's **Install** tab.

That is the whole installation. Everything below is optional.

<Note>
  **The website id is public and meant to be.** It sits in a page anyone can read the source
  of. What makes it safe is the domain allowlist on the channel: the widget refuses to boot
  on any origin the channel's owner hasn't listed, so a copied id is useless elsewhere.

  Never put an API key in a page. A Linkiasoft API key carries workspace-wide access — see
  [Authentication](/authentication).
</Note>

## What it costs your page

* The script is `async` and boots when the browser is idle, so it is never on the critical
  path to your first paint.
* The chat UI itself is an iframe on our origin, and it is **not fetched until somebody
  opens the panel** — or until a reply arrives for a visitor who has one waiting.
* The live connection is a single `EventSource`. No polling, no WebSocket upgrade to be
  mangled by a corporate proxy, no client library.

The iframe is also what keeps your CSS out of the widget and the widget's CSS out of your
page. Nothing we ship can restyle your site, and nothing your site does can break the
composer.

## Calling it before it loads

If you need to talk to the widget from your own code — identify a signed-in user, open it
from your own button, listen for events — add the queue stub **above** the script tag:

```html theme={null}
<script>
  window.Linkia =
    window.Linkia ||
    function () {
      (Linkia.q = Linkia.q || []).push(arguments);
    };
</script>
<script
  src="https://social.linkiasoft.com/widget/v1/loader.js"
  data-website-id="wc_pub_7c8d9e0f1a2b"
  async
></script>
```

Calls made before the loader arrives are queued and replayed once it does, so nothing is
lost and nothing has to wait for an `onload`. The full command list is in
[the JavaScript API](/widget-api).

<Warning>
  If your site sets a `script-src` policy with a nonce, the inline stub above needs that
  nonce. The plain one-line snippet does not — it has no inline script, which is exactly why
  it is the default.
</Warning>

## Overriding settings on one page

The dashboard holds the defaults. A single page can override the look, the launcher and
the greeting before boot:

```html theme={null}
<script>
  window.linkiaSettings = {
    appearance: { primaryColor: '#1d4ed8', position: 'left' },
    greeting: 'Questions about pricing? Ask away.',
    hideLauncher: false,
  };
</script>
```

Set it before the loader runs, or pass the same object through `Linkia('init', …)`.

| Field          | Effect                                                                                                     |
| -------------- | ---------------------------------------------------------------------------------------------------------- |
| `appearance`   | Any appearance field from the dashboard — colours, position, offsets, radius, `zIndex`, `launcherIconUrl`. |
| `greeting`     | Replaces the greeting for this page only. Not translated — set it per locale yourself.                     |
| `hideLauncher` | Renders no bubble. Pair with your own button; see below.                                                   |
| `autoOpen`     | `'never'` or `'on_new_message'`.                                                                           |
| `websiteId`    | An alternative to the `data-website-id` attribute.                                                         |

Copy other than the greeting stays server-side deliberately: support can fix a typo in the
offline message without a customer redeploying their site.

## Your own launcher

Any element carrying `data-linkia-open` opens the widget, and any element carrying
`data-linkia-unread` receives the unread count as its text:

```html theme={null}
<button data-linkia-open>
  Chat with us <span data-linkia-unread></span>
</button>
```

Both are bound for elements added later too, so this works in a framework that renders the
button after boot. Set `hideLauncher: true` if you don't also want our bubble.

## Single-page apps

The widget survives client-side navigation on its own — it is not tied to a route, and the
loader ignores a second injection. So:

* **Inject the script once**, in your root layout or `index.html`, not in a page component.
* **Don't remount it per route.** If you must remove it entirely, call `Linkia('destroy')`.
* **Re-identify only when the user changes**, not on every navigation.

<CodeGroup>
  ```jsx React / Next.js theme={null}
  // app/layout.tsx — rendered once, not per route
  export default function RootLayout({ children }) {
    return (
      <html>
        <body>
          {children}
          <script
            src="https://social.linkiasoft.com/widget/v1/loader.js"
            data-website-id="wc_pub_7c8d9e0f1a2b"
            async
          />
        </body>
      </html>
    );
  }
  ```

  ```js Vue / Nuxt theme={null}
  // plugins/linkia.client.js
  export default defineNuxtPlugin(() => {
    const script = document.createElement('script');
    script.src = 'https://social.linkiasoft.com/widget/v1/loader.js';
    script.async = true;
    script.dataset.websiteId = 'wc_pub_7c8d9e0f1a2b';
    document.body.appendChild(script);
  });
  ```
</CodeGroup>

## Content Security Policy

If your site sends a CSP, allow our origin in four directives:

```
script-src  https://social.linkiasoft.com;
frame-src   https://social.linkiasoft.com;
connect-src https://social.linkiasoft.com;
img-src     https://social.linkiasoft.com data:;
```

`connect-src` covers both the widget's requests and its `EventSource` stream. `img-src`
needs whatever host serves your logo and any images your visitors send, which is our media
CDN unless you set your own.

<Note>
  A blocked directive shows up in the browser console as a CSP violation naming the
  directive that refused. That message is the fastest route to the fix — read it before
  changing anything else.
</Note>

## What the widget stores

| Where                                                   | What                                                                          | Why                                                        |
| ------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------- |
| Your page's `localStorage`, key `linkia.wc.<websiteId>` | A signed visitor token, valid 30 days                                         | So a returning visitor sees their own conversation history |
| Our server                                              | The visitor's locale, timezone, user agent, current page, and a **hashed** IP | Context for the agent, and rate limiting                   |

The token lives in your page's storage rather than the iframe's on purpose: Safari and
Firefox partition storage inside a cross-origin iframe, so a token kept there would be
discarded on every reload and every returning visitor would look new.

No cookies are set, and nothing is shared with any other site.

<Warning>
  Call `Linkia('logout')` wherever your own sign-out runs. On a shared computer, the next
  person to open the widget would otherwise be reading the previous person's conversation.
</Warning>

## Limits

|                               |                                                    |
| ----------------------------- | -------------------------------------------------- |
| Message length                | 4,000 characters                                   |
| Attachment size               | 25 MB, when attachments are enabled on the channel |
| Messages per visitor          | 30 a minute                                        |
| New conversations per visitor | 10 in 10 minutes                                   |

Exceeding one gets a `429`; the widget shows the message as unsent with a retry rather
than losing it.

## When it doesn't appear

Work down in order — the first four cover almost everything:

1. **Console.** The widget logs one line beginning `[Linkia chat]` when it refuses to
   start, and it names the reason.
2. **The domain allowlist.** `403` from `/boot` means this origin isn't listed. `acme.com`
   covers `www.acme.com`; a subdomain needs `*.acme.com`.
3. **The website id.** `401` means unknown or paused — check the channel is connected and
   the id matches the Install tab.
4. **CSP.** A violation in the console names the directive to add.
5. **Ad blockers.** Several block chat widgets by hostname. Test in a clean profile.

## Next

<CardGroup cols={2}>
  <Card title="JavaScript API" icon="code" href="/widget-api">
    Open and close it, identify visitors, listen for events.
  </Card>

  <Card title="Identity verification" icon="shield-halved" href="/widget-api#identity-verification">
    Sign the user id on your server so a browser cannot claim someone else's.
  </Card>
</CardGroup>
