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

# Web chat widget

> Embed the MINDO chat on your website: snippet, signed identity for logged-in users (JWT) and customization.

## What it is

The web chat widget is **just another channel** in MINDO (like WhatsApp, Instagram
or ManyChat): a chat bubble you embed on your site with a `<script>` that connects
your visitors to your MINDO inbox. Whatever they write lands in the same Inbox where
you already handle WhatsApp, and your team (or your AI agent) replies from there —
the visitor gets the answer **in real time**, without reloading the page.

It supports two kinds of visitor:

* **Anonymous** — requires nothing from your backend. The widget generates its own
  visitor identifier (stored in the browser's `localStorage`) so it recognizes the
  person between messages.
* **Identified** — if the visitor is already logged in to your site, your backend can
  sign their identity so MINDO recognizes them. This merges, under a single contact,
  every chat that user opens from different devices, and keeps the conversation
  available when they come back.

## Before you start

Make sure you have:

* Access to **Settings → Channels** in MINDO with an administrator role.
* Access to your site's code (to paste a `<script>`).
* If you're going to identify logged-in users: access to your site's backend, to sign
  the identity. The signature is **always computed on the server**, never in the
  browser.

## Step by step

<Steps>
  <Step title="Create the Web channel">
    In MINDO, go to **Settings** → **Channels** and find the **Web chat widget**
    section. Click **+ Add Web channel**.

    <Frame caption="Web chat widget section in Settings → Channels">
      <img src="https://mintcdn.com/mindo/9-0t-xPyhXm8rvsY/images/widget-web-canales.png?fit=max&auto=format&n=9-0t-xPyhXm8rvsY&q=85&s=4946fae76e7e1e249e3244108c534d8a" alt="Web channels section in MINDO" width="1440" height="900" data-path="images/widget-web-canales.png" />
    </Frame>

    Fill in:

    * **Session name** — identifies the channel inside MINDO and is also the **title
      the visitor sees** in the chat header (for example, "Store chat").
    * **Primary color** — the color of the panel header, the visitor's bubbles and the
      Send button.
    * **Welcome message** — the first message the visitor sees while there's no
      conversation yet. It isn't stored as a message and doesn't trigger the agent.
    * **Agent** (optional) — if you want an AI agent to reply automatically on this
      channel.

    <Frame caption="Web channel creation form">
      <img src="https://mintcdn.com/mindo/4ooKWGhGXU37KYb5/images/widget-web-crear-canal.png?fit=max&auto=format&n=4ooKWGhGXU37KYb5&q=85&s=6cd3b1a7f9805edce02d2153ce603534" alt="Form to create a Web channel" width="1440" height="900" data-path="images/widget-web-crear-canal.png" />
    </Frame>

    <Note>
      The agent **won't reply on the widget just because it exists** in your company:
      it has to be assigned here. With **No agent**, messages wait for a human reply in
      the Inbox, like any other channel.
    </Note>

    <Note>
      A company can have **several Web channels** at once —one per brand, per site or
      per country—. Each has its own token, color and agent, and conversations from one
      never mix with the other's.
    </Note>
  </Step>

  <Step title="Copy the snippet">
    When you create the channel, MINDO shows you the snippet ready to copy:

    ```html theme={null}
    <script
      src="https://app.mindosoftware.com/widget.js"
      data-mindo-token="YOUR_CHANNEL_TOKEN"
      async
    ></script>
    ```

    You'll also see an **HMAC Secret** — you only need it if you're going to identify
    logged-in visitors (Step 4). Keep it somewhere safe; it must never be exposed in
    your site's HTML.

    <Frame caption="Snippet and HMAC Secret of the newly created channel">
      <img src="https://mintcdn.com/mindo/4ooKWGhGXU37KYb5/images/widget-web-snippet.png?fit=max&auto=format&n=4ooKWGhGXU37KYb5&q=85&s=dc04ba1af9f184db63bcd31046c364e2" alt="Embed snippet and HMAC Secret of the channel" width="1440" height="900" data-path="images/widget-web-snippet.png" />
    </Frame>

    The snippet's **token** is public: it travels in your site's HTML and it's expected
    to be visible. The **Secret** isn't: it's shown masked and only users with the
    **ADMIN** role of the company can see it (for a MEMBER or VIEWER the API returns the
    field empty).
  </Step>

  <Step title="Paste the snippet on your site">
    Paste the `<script>` right before the closing `</body>` on the pages where you want
    the chat to appear. No build step or dependency is needed — it's a self-contained
    vanilla script.

    When the page loads you'll see a chat bubble at the bottom right. Clicking it opens
    a panel with the chat (it's an `<iframe>` to MINDO, so your site doesn't need to
    implement any chat UI).

    <Frame caption="Widget panel open over a sample site">
      <img src="https://mintcdn.com/mindo/4ooKWGhGXU37KYb5/images/widget-web-panel-abierto.png?fit=max&auto=format&n=4ooKWGhGXU37KYb5&q=85&s=52c8f093933c4b65361946cfcd9b1e80" alt="Widget panel open" width="1280" height="720" data-path="images/widget-web-panel-abierto.png" />
    </Frame>

    <Note>
      If the bubble doesn't show up, check the browser console: the most common error is
      an empty or wrong `data-mindo-token`.
    </Note>

    <Note>
      **The floating bubble is always blue.** The *primary color* you configured applies
      inside the panel (header, visitor bubbles, Send button), not to the bubble. That's
      the expected behavior, not a configuration error.
    </Note>

    From here on, every visitor message creates a chat in your Inbox with channel
    **Web**, and your team handles it with the same composer as always. The widget chat
    adds one piece of data no other channel has: the **"On the page"** indicator, which
    tells you whether the visitor still has your site open right now.

    <Frame caption="Widget chat in the Inbox, with the presence indicator">
      <img src="https://mintcdn.com/mindo/4ooKWGhGXU37KYb5/images/widget-web-inbox-presencia.png?fit=max&auto=format&n=4ooKWGhGXU37KYb5&q=85&s=45e66289d464c192e4c518a3a263fa57" alt="Web chat in the MINDO Inbox with presence indicator" width="1440" height="900" data-path="images/widget-web-inbox-presencia.png" />
    </Frame>
  </Step>

  <Step title="(Optional) Identify logged-in visitors">
    If your site has logged-in users and you want MINDO to recognize them (instead of
    treating them as anonymous), your **backend** signs a **JWT** and passes it in the
    `data-mindo-identity` attribute.

    The JWT is signed with the **HS256** algorithm using the channel's **HMAC Secret**,
    and its payload accepts these fields:

    | Field   | Required | What it is                                                                                  |
    | ------- | -------- | ------------------------------------------------------------------------------------------- |
    | `sub`   | Yes      | Your user's unique identifier — the one you use internally.                                 |
    | `exp`   | Yes      | Expiration (Unix timestamp, in seconds).                                                    |
    | `name`  | No       | The user's name, so the contact isn't left as "Visitor".                                    |
    | `email` | No       | The user's email.                                                                           |
    | `phone` | No       | Phone in international format. It's what allows merging this contact with the WhatsApp one. |

    <Warning>
      **`exp` can't be more than 7 days away.** A JWT with a further expiration is
      considered invalid and the visitor stays anonymous. It's a deliberate cap: if
      someone steals a token, seven days is the most that theft can be worth. A TTL of
      24 to 72 hours is the usual choice.
    </Warning>

    <CodeGroup>
      ```javascript Node.js theme={null}
      const jwt = require("jsonwebtoken");

      function signIdentity(user, channelSecret, ttlSeconds = 60 * 60 * 24) {
        return jwt.sign(
          {
            sub: String(user.id),
            name: user.name,
            email: user.email,
            phone: user.phone,
            exp: Math.floor(Date.now() / 1000) + ttlSeconds,
          },
          channelSecret,
          { algorithm: "HS256" }
        );
      }
      ```

      ```python Python theme={null}
      import time
      import jwt  # pip install pyjwt

      def sign_identity(user, channel_secret, ttl_seconds=60 * 60 * 24):
          payload = {
              "sub": str(user.id),
              "name": user.name,
              "email": user.email,
              "phone": user.phone,
              "exp": int(time.time()) + ttl_seconds,
          }
          return jwt.encode(payload, channel_secret, algorithm="HS256")
      ```
    </CodeGroup>

    Render the token into the `<script>` from a server-side template:

    ```html theme={null}
    <script
      src="https://app.mindosoftware.com/widget.js"
      data-mindo-token="YOUR_CHANNEL_TOKEN"
      data-mindo-identity="{{ identityToken }}"
      async
    ></script>
    ```

    <Warning>
      The **HMAC Secret** is a server-side secret. Never include it in HTML, client-side
      JavaScript, or a public repository — it should only exist in your backend, the
      same way you'd store any other API key.
    </Warning>

    With the identity signed, the chat stops showing "Visitor ####" and becomes the real
    person, with their phone and email already filled in on the contact record:

    <Frame caption="The same chat, now identified as the real contact">
      <img src="https://mintcdn.com/mindo/9-0t-xPyhXm8rvsY/images/widget-web-contacto-identificado.png?fit=max&auto=format&n=9-0t-xPyhXm8rvsY&q=85&s=310b5dde9c7b6436926d3802022dd4b7" alt="Widget chat with the identified contact in the Inbox" width="1440" height="900" data-path="images/widget-web-contacto-identificado.png" />
    </Frame>

    <Note>
      The signature is **optional by design**: if it's missing, expired or invalid, the
      visitor simply stays anonymous — the chat is never blocked by a signature error.
      That means you can test without risking a broken widget.

      The flip side is that a malformed JWT **fails silently**. If the contact still
      shows up as "Visitor ####", check in this order: that the secret belongs to the
      **same channel** as the token, that `exp` is in **seconds** (not milliseconds) and
      less than 7 days away, that the claim is named `sub`, and that the algorithm is
      **HS256**.
    </Note>

    <AccordionGroup>
      <Accordion title="Previous method: the three attributes with an HMAC signature">
        Before the JWT, identity was passed with three separate attributes. **It still
        works and you don't need to migrate**: if you set both schemes at once, the JWT
        wins, and if the JWT is broken it falls back to this one.

        | Attribute              | What it is                                                  |
        | ---------------------- | ----------------------------------------------------------- |
        | `data-mindo-user`      | Your user's unique identifier (`userId`).                   |
        | `data-mindo-expires`   | Unix timestamp (seconds) until when the signature is valid. |
        | `data-mindo-signature` | `HMAC_SHA256(secret, "{userId}:{expires}")` in hexadecimal. |

        <CodeGroup>
          ```javascript Node.js theme={null}
          const crypto = require("crypto");

          function signVisitor(userId, channelSecret, ttlSeconds = 60 * 60 * 24) {
            const expires = Math.floor(Date.now() / 1000) + ttlSeconds;
            const signature = crypto
              .createHmac("sha256", channelSecret)
              .update(`${userId}:${expires}`)
              .digest("hex");
            return { userId, expires, signature };
          }
          ```

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

          def sign_visitor(user_id, channel_secret, ttl_seconds=60 * 60 * 24):
              expires = int(time.time()) + ttl_seconds
              message = f"{user_id}:{expires}".encode("utf-8")
              signature = hmac.new(
                  channel_secret.encode("utf-8"), message, hashlib.sha256
              ).hexdigest()
              return {"userId": user_id, "expires": expires, "signature": signature}
          ```
        </CodeGroup>

        ```html theme={null}
        <script
          src="https://app.mindosoftware.com/widget.js"
          data-mindo-token="YOUR_CHANNEL_TOKEN"
          data-mindo-user="{{ userId }}"
          data-mindo-signature="{{ signature }}"
          data-mindo-expires="{{ expires }}"
          async
        ></script>
        ```

        **Why the JWT is the better choice if you're starting now**: this scheme signs
        the string `userId:expires` using `:` as a separator, so it can't carry a name
        or an email without becoming ambiguous. The JWT signs the exact data and lets
        you send `name`, `email` and `phone` — which is what gives the contact a real
        name and merges it with their WhatsApp chat.
      </Accordion>
    </AccordionGroup>
  </Step>

  <Step title="(Optional) Identify the visitor after they log in">
    If your site is an SPA, or the visitor opens the chat **before** logging in, there's
    no need to reload the page for MINDO to recognize them. The widget exposes a
    `window.Mindo` object:

    ```javascript theme={null}
    // After your user logs in, with the JWT your backend signed:
    window.Mindo.setUser(identityToken);

    // On logout:
    window.Mindo.clearUser();
    ```

    The script loads with `async`, so `window.Mindo` may not exist yet when your code
    runs. That's what the command queue is for: queue the calls and the widget processes
    them once it finishes loading.

    ```javascript theme={null}
    window.Mindo = window.Mindo || { q: [] };
    (window.Mindo.q = window.Mindo.q || []).push(["setUser", identityToken]);
    ```

    You can also do it **without writing any integration code**: if your page stores the
    JWT in `localStorage` under the key `mindo_identity`, the widget picks it up on its
    own. This covers logging in from a new tab or through an OAuth redirect, because the
    widget listens for changes to that key. The same value can be broadcast over a
    `BroadcastChannel` named `mindo-identity`.

    <Note>
      Besides `setUser` and `clearUser`, `window.Mindo` exposes `open()`, `close()` and
      `toggle()` to control the panel from your own UI (for example, a "Need help?"
      button), and `on(event, callback)` / `off(event, callback)` to listen for `open`,
      `close`, `unread`, `identity`, `identity:cleared`, `auth:login` and
      `auth:register`.
    </Note>
  </Step>

  <Step title="(Optional) Invite visitors to log in from the chat">
    The widget can show the **anonymous** visitor a block with **Log in** and **Sign up**
    buttons pointing to your site's pages. It disappears on its own once the visitor is
    identified.

    <Frame caption="Login and sign-up buttons inside the widget panel">
      <img src="https://mintcdn.com/mindo/9-0t-xPyhXm8rvsY/images/widget-web-botones-auth.png?fit=max&auto=format&n=9-0t-xPyhXm8rvsY&q=85&s=5cb3c8b8f602b6ce6863936fdde8af86" alt="Widget with log in and sign up buttons" width="1280" height="720" data-path="images/widget-web-botones-auth.png" />
    </Frame>

    You configure it in **Settings → Channels → Web chat widget**, editing the channel:
    turn on **Login buttons** and fill in the URLs, the button labels and the message
    above them.

    <Frame caption="Login buttons section when editing the channel">
      <img src="https://mintcdn.com/mindo/9-0t-xPyhXm8rvsY/images/widget-web-config-botones-auth.png?fit=max&auto=format&n=9-0t-xPyhXm8rvsY&q=85&s=39bdb87b81ef4263f4ba4e81c1383101" alt="Configuration of the widget's login and sign-up buttons" width="1440" height="900" data-path="images/widget-web-config-botones-auth.png" />
    </Frame>

    | Field                         | What it is                                          |
    | ----------------------------- | --------------------------------------------------- |
    | **Login URL**                 | Where to send a visitor who already has an account. |
    | **Sign-up URL**               | Where to send them if they don't have one yet.      |
    | **Button labels**             | Optional. Default to "Log in" and "Sign up".        |
    | **Message above the buttons** | The sentence explaining why logging in is worth it. |

    **One** of the two URLs is enough: the button without a URL isn't rendered.

    You can also do it through the API, with an ADMIN user of the company:

    ```bash theme={null}
    curl -X PATCH https://api.mindosoftware.com/web-widget/channels/<CHANNEL_ID>/ \
      -H "Authorization: Bearer <AN_ADMIN_TOKEN>" \
      -H "Content-Type: application/json" \
      -d '{
        "auth": {
          "enabled": true,
          "loginUrl": "https://yoursite.com/login",
          "registerUrl": "https://yoursite.com/signup",
          "loginLabel": "Log in",
          "registerLabel": "Sign up",
          "prompt": "Log in to see your orders and pick up the conversation from any device."
        }
      }'
    ```

    On click, the widget opens the URL in a new tab. If you'd rather handle navigation
    yourself (for example, opening your own login modal), intercept it: returning
    `false` from the handler cancels the opening.

    ```javascript theme={null}
    window.Mindo.on("auth:login", ({ url }) => {
      openMyLoginModal();
      return false;
    });
    ```

    <Warning>
      URLs must be **`https://`**. An `http://` one is rejected on save, and if it was
      loaded by hand from the admin the button simply isn't rendered.
    </Warning>

    <Warning>
      Send the block in the **`auth`** field, not inside `config`. An explicit `config`
      **replaces** the stored one (except for `prechat` and `auth`), so a
      `{"config": {"auth": {...}}}` wipes your color and welcome message. The flat `auth`
      field is saved on its own, without touching anything else.
    </Warning>
  </Step>
</Steps>

## What happens next?

* Every message a visitor sends creates (or continues) a chat in your MINDO Inbox, with
  `Channel: Web`.
* If you identified the visitor and that same person writes from another device with the
  same `sub`, MINDO merges the history under a single contact.
* If the JWT includes `phone` and that number already exists as a WhatsApp contact, MINDO
  merges them: you get **one contact with both conversations** (the widget one and the
  WhatsApp one) instead of two different people.
* If you have an AI agent assigned to the channel, it can reply automatically; if not,
  the chat waits for a human reply like any other channel.

## Frequently asked questions

<AccordionGroup>
  <Accordion title="Do I need to install anything on my site besides the script?">
    No. The `<script>` is self-contained: it creates its own bubble and its own chat
    panel (an iframe). It requires no CSS, dependencies or build step.
  </Accordion>

  <Accordion title="Can I have the widget on several pages of my site?">
    Yes, paste the same snippet on every page where you want it to appear. The visitor is
    identified by an ID stored in their browser, so navigating between your pages keeps
    the same conversation.
  </Accordion>

  <Accordion title="What happens if the visitor reloads the page mid-conversation?">
    Nothing is lost: the widget restores the conversation automatically on reload and
    shows it as soon as they open the panel, without them having to type again.
  </Accordion>

  <Accordion title="Does the visitor notice if we reply while the chat is closed?">
    As long as they still have your page open, yes: the bubble shows a counter with the
    messages that arrived while the panel was closed, and it clears when they open it.

    <Frame caption="Unread counter over the bubble">
      <img src="https://mintcdn.com/mindo/9-0t-xPyhXm8rvsY/images/widget-web-badge.png?fit=max&auto=format&n=9-0t-xPyhXm8rvsY&q=85&s=4d96e3accaecc5cce4e2698e2694d748" alt="Widget bubble with the unread counter" width="1280" height="720" data-path="images/widget-web-badge.png" />
    </Frame>

    If they closed the tab, there's no notice today: next time they come back to your
    site they'll find the reply when they open the chat, but no notification reaches them
    meanwhile. That's why it's worth asking for a phone or email through the JWT
    (`phone` / `email`): with that data you can pick the conversation back up on another
    channel.
  </Accordion>

  <Accordion title="Can visitors send photos or audio?">
    No. The widget composer is **text only**, with a cap of 4,000 characters per message.
    On the operator's side files can be sent, and the visitor receives them in the panel.
  </Accordion>

  <Accordion title="Are there usage limits on the public endpoints?">
    Yes, anti-abuse caps per IP, well above what a real person does: **30 per minute** to
    send messages, **20 per minute** for the unread count and **10 per minute** to open a
    session with a signed identity.
  </Accordion>

  <Accordion title="What if I don't set up identification?">
    Nothing breaks: every visitor stays anonymous, each with their own chat. You can add
    identification later without changing anything you already installed.
  </Accordion>

  <Accordion title="Do I have to migrate from the three-attribute method to the JWT?">
    No. The previous scheme is still supported and has no sunset date. Migrate only if you
    want to send the user's name, email or phone — things the old format doesn't support.
  </Accordion>

  <Accordion title="Can I restrict which sites can use my token?">
    Yes. The channel has an **allowed origins** list (`allowed_origins`): with an empty
    list —the default— there's no restriction; with origins configured, the page's
    `Origin` header has to match one **exactly** or the API responds `403`.

    It isn't exposed in the Settings form yet: you set it through the API
    (`PATCH /web-widget/channels/<id>/` with `allowed_origins`) or from the admin. If you
    turn it on, include **every** domain and subdomain your site is served from: a missing
    one leaves the chat broken on those pages.
  </Accordion>

  <Accordion title="Can I ask the visitor for a phone or email before they write?">
    **Don't turn it on today.** The configuration exists in the backend (`config.prechat`,
    with `off` / `optional` / `required` levels for `phone` and `email`) but **the widget
    doesn't render that form yet**, so it never sends that data.

    <Warning>
      With `phone` or `email` set to `required`, a new visitor's first message gets
      `422 {"error": "Faltan campos obligatorios"}` and **the visitor can't start the
      conversation**. Leave it at `off` until the UI exists.
    </Warning>
  </Accordion>

  <Accordion title="Does it have an extra cost?">
    The Web channel has no messaging cost like WhatsApp (it doesn't depend on Meta). Check
    with your MINDO plan whether any usage limit applies.
  </Accordion>
</AccordionGroup>
