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

# Widget JavaScript API

> Commands, events, and how to prove a signed-in visitor is who your page says they are.

Everything the widget exposes goes through one function:

```js theme={null}
Linkia('open');
Linkia('identify', { userId: 'user_42', userHash: '…' });
Linkia('on', 'message', (message) => console.log(message.text));
```

It exists as soon as the queue stub runs, so you can call it before the widget has
loaded — see [Calling it before it loads](/widget#calling-it-before-it-loads). Calls are
replayed in order once it does, with `init` applied first whatever position it was queued
in.

## Commands

| Command                                  | What it does                                                       |
| ---------------------------------------- | ------------------------------------------------------------------ |
| `Linkia('open')`                         | Opens the panel. Loads the chat UI on first use.                   |
| `Linkia('close')`                        | Closes it.                                                         |
| `Linkia('toggle')`                       | Opens or closes.                                                   |
| `Linkia('hide')` / `Linkia('show')`      | Hides or restores the whole widget, launcher included.             |
| `Linkia('init', options)`                | Per-page overrides. Only has an effect before boot.                |
| `Linkia('boot')`                         | Boots immediately instead of waiting for the browser to go idle.   |
| `Linkia('identify', claim)`              | Tells us who the visitor is — see [below](#identity-verification). |
| `Linkia('logout')`                       | Ends the session and starts a fresh anonymous one.                 |
| `Linkia('trackEvent', name, properties)` | Fires an event automations can act on.                             |
| `Linkia('setLocale', 'ar')`              | Switches the widget's language, and its direction with it.         |
| `Linkia('on', event, handler)`           | Subscribes to an event.                                            |
| `Linkia('off', event, handler)`          | Unsubscribes the same function reference.                          |
| `Linkia('destroy')`                      | Removes the widget from the page entirely.                         |

Commands never return a value — a queued call has nothing to return yet. Read state
through events instead.

## Events

```js theme={null}
Linkia('on', 'unread', ({ count }) => {
  document.title = count ? `(${count}) Acme` : 'Acme';
});
```

| Event            | Payload                 | Fires when                                                              |
| ---------------- | ----------------------- | ----------------------------------------------------------------------- |
| `ready`          | `{ visitor }`           | The widget has booted and knows who this browser is.                    |
| `open` / `close` | —                       | The panel opened or closed, by any route.                               |
| `unread`         | `{ count }`             | The unread count changed, including back to zero.                       |
| `message`        | the message             | A reply arrived from an agent, an AI agent or an automation.            |
| `identified`     | `{ visitor, verified }` | An `identify()` call was accepted. `verified` is the part that matters. |
| `logout`         | —                       | The session was ended.                                                  |

`ready` fires once per page load. Subscribe before boot — from the queue stub — or you may
subscribe after it has already fired.

## Identity verification

If your visitors sign in, tell us who they are. Their conversations then attach to that
customer, and their history follows them from laptop to phone.

Do it with a signature. Without one, `identify()` is a browser asserting an identity, and
anyone who can open developer tools can assert somebody else's.

<Steps>
  <Step title="Get the identity secret">
    **Settings → Channels → Website chat → Security**. It is shown once, on creation, and once
    again each time you rotate it. It starts with `wc_sec_`.

    <Warning>
      This is a server-side secret. If it reaches your frontend bundle, verification proves
      nothing, because the visitor can compute the signature themselves. Keep it where your API
      keys live.
    </Warning>
  </Step>

  <Step title="Sign the user id on your server">
    `userHash` is an HMAC-SHA256 of the user id — the exact string you pass as `userId` —
    keyed with the secret, hex-encoded.

    <CodeGroup>
      ```javascript Node.js theme={null}
      import crypto from 'node:crypto';

      export function linkiaUserHash(userId) {
        return crypto
          .createHmac('sha256', process.env.LINKIA_WEBCHAT_SECRET)
          .update(String(userId))
          .digest('hex');
      }
      ```

      ```python Python theme={null}
      import hashlib
      import hmac
      import os

      def linkia_user_hash(user_id: str) -> str:
          return hmac.new(
              os.environ["LINKIA_WEBCHAT_SECRET"].encode(),
              str(user_id).encode(),
              hashlib.sha256,
          ).hexdigest()
      ```

      ```php PHP theme={null}
      function linkia_user_hash(string $userId): string {
          return hash_hmac('sha256', $userId, getenv('LINKIA_WEBCHAT_SECRET'));
      }
      ```
    </CodeGroup>
  </Step>

  <Step title="Pass it to the widget">
    Render the hash into the page for the signed-in user, and identify once after boot:

    ```html theme={null}
    <script>
      Linkia('identify', {
        userId: 'user_42',
        userHash: '5f3a9c…',          // computed on the server, above
        name: 'Nadia Farouk',
        email: 'nadia@acme.com',
        attributes: { plan: 'pro', seats: 12 },
      });
    </script>
    ```
  </Step>

  <Step title="Require it">
    Once your site is signing, turn on **Require a valid signature** under **Security**. Until
    you do, unsigned claims are accepted but stored as unverified — usable context for an
    agent, never trusted for identity.
  </Step>
</Steps>

### What "verified" changes

|                                                     | Unsigned claim | Verified claim |
| --------------------------------------------------- | -------------- | -------------- |
| Name and email shown to the agent                   | Yes            | Yes            |
| Attached to your `userId`                           | No             | Yes            |
| Reunited with this person's history on a new device | **No**         | Yes            |
| Accepted when verification is required              | No — `403`     | Yes            |

The third row is the point. Reuniting a browser with an existing identified visitor is
what would let "identify as somebody else" become an account takeover, so it happens only
on a signature we can check.

### Attributes

`attributes` is free-form context an agent sees beside the conversation: plan, cart value,
signup date. Scalars only — strings (truncated at 500 characters), numbers and booleans.
Up to 30 keys. Nested objects are dropped.

Don't put anything in there you wouldn't want an agent to read.

## Signing visitors out

```js theme={null}
Linkia('logout');
```

Call it wherever your own sign-out runs. It ends the session and mints a fresh anonymous
one, so the next person at that computer cannot read the previous person's conversation.

Nothing is deleted — the thread stays in the inbox with its history intact.

## Events for automations

```js theme={null}
Linkia('trackEvent', 'viewed_pricing', { plan: 'pro' });
```

Fires a `webchat.event` trigger for your automations. It is not stored: what makes an
event useful is an automation acting on it, and a table of every page view on every
customer's site would be a cost with no reader.

## Errors you might see

| Status | Meaning                                                                                                                                                             |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Unknown or paused website id, or a visitor session that has expired. The widget refreshes the session by itself; the call that failed stays on screen with a retry. |
| `403`  | This origin is not on the channel's domain allowlist — or a required signature was missing or wrong.                                                                |
| `429`  | A rate limit. The message stays on screen with a retry.                                                                                                             |

<Note>
  A conversation id that isn't this visitor's answers `404`, not `403`. That is deliberate:
  a `403` would confirm the conversation exists, which is exactly what someone guessing ids
  wants to learn.
</Note>
