# An AI chat for your web app or site

The web widget puts your agent's chat in your web app or site with one script tag, or the widget's npm package, and a publishable `vatpub_` key that only works from your app's origins. This guide is the task path: what to ask for, what you will see, how to try it and what to check before going live. The reference for the key, origins and every setting is [Web widget](https://vatio.ai/docs/channels/widget). It is the same agent that answers in your iOS, Android or Flutter app.

## Start from an agent

The widget talks to one environment, normally live. The app is the starting point:

1. **Fastest, from any chat.** Add the remote MCP server `https://vatio.ai/mcp` as a connector in Claude, ChatGPT or Cursor, sign in with a Vatio account and ask for the agent. It is created in a preview to try; nothing reaches users until you publish ([Vatio in Claude and ChatGPT](https://vatio.ai/docs/mcp)).
2. **To build the agent into your app's code.** In the project run `npx @vatio-ai/skills`, then `/vatio-init`, then ask your coding agent to add the [web widget](https://vatio.ai/docs/channels/widget), and `/vatio-improve` daily. [Build an AI agent](https://vatio.ai/docs/guides/build-an-ai-agent) walks through it.

Before anyone else sees the agent, try the preview with **Try the agent** in the inbox. Publishing is your decision: when it is right, ask to publish.

## Create a publishable key and paste the tag

Ask your coding agent to create the key for your app's origins, or run it yourself:

```bash
vatio keys create publishable --env live --label website \
  --origin https://ember-roasters.example --origin http://localhost:3000
```

`create` prints the full key once. Origins are exact `scheme://host[:port]` values, so `https://www.ember-roasters.example` is a second origin if your site answers there too. Apps built in Lovable, Replit, Bolt, v0 and Base44 work without listing their addresses. Never put a `vat_` developer token in a browser. Key rules, `origin_not_allowed`, listing and revoking are in [Web widget](https://vatio.ai/docs/channels/widget).

Paste the tag before `</body>` in the layout every page shares:

```html
<script async src="https://cdn.vatio.ai/v1/widget.js"
        data-workspace="ember-roasters"
        data-token="vatpub_REPLACE_ME"></script>
```

With a bundler (React, Next.js, Vue, Svelte) install it from npm instead: see [From your app's code](https://vatio.ai/docs/channels/widget#from-your-app-s-code).

On the web each reply is one message the widget renders progressively, in Markdown. When a request looks resolved, the widget asks the visitor once how the agent helped ([Visitor feedback](https://vatio.ai/docs/channels/widget#visitor-feedback)).

## Make it look like your brand

In the inbox, open **Web chat**. It holds the title, greeting, up to four suggestions, position, color scheme, corner radius, font, language and the theme colors.

The **Name and brand** card of the same page holds the business name, logo, brand color and **About**; what the business does goes in the agent's instructions. These are workspace settings: a save applies to every environment immediately, with nothing to push.

A page can override a setting on its own tag. On the subscription pages, for example:

```html
<script async src="https://cdn.vatio.ai/v1/widget.js"
        data-workspace="ember-roasters"
        data-token="vatpub_REPLACE_ME"
        data-greeting="Questions about your subscription? Ask away."
        data-suggestions="When is my next delivery?|Which coffee ships this month?|How do I brew a pour-over?"
        data-accent="#8A3B12"
        data-position="left"></script>
```

Every attribute is in the table under [Widget configuration](https://vatio.ai/docs/channels/widget#widget-configuration).

## Recognize signed-in customers

To tell the agent who is chatting, your backend signs a JWT and Vatio verifies it. Vatio holds only your public key, so it can check a token but never mint one. If your users sign in with Supabase Auth, as apps built with Lovable often do, an Edge Function can read their session and sign it ([Supabase apps](https://vatio.ai/docs/guides/ai-agent-for-supabase-apps)). Your coding agent creates a key pair:

```bash
vatio auth --new-key
```

`identity.pub` stays in the workspace and `vatio.yml` names it under `auth`. `identity.pem` goes into your backend as a secret, never into `vatio secrets set`. Your backend signs with RS256, with `sub` (your user id), `aud` (your workspace slug) and a short `exp`; `name` and `email` are optional:

```js
// Node (jsonwebtoken)
jwt.sign(
  { sub: String(user.id), name: user.name, email: user.email },
  process.env.VATIO_IDENTITY_PRIVATE_KEY,
  { algorithm: "RS256", audience: "ember-roasters", expiresIn: "1h" }
);
```

Render it onto the tag for signed-in visitors only:

```html
<script async src="https://cdn.vatio.ai/v1/widget.js"
        data-workspace="ember-roasters"
        data-token="vatpub_REPLACE_ME"
        data-visitor-token="SIGNED_JWT"></script>
```

Signed-out visitors get no token, and the agent answers them with public tools. When someone signs in without a reload, call `window.VatioWidget.identify(newToken)`; `null` signs them out. **The conversation survives the sign-in:** a question asked anonymously carries over, now with a name on it, so the agent can answer it without the customer typing it again. A token with a different `sub`, or signing out, starts a new conversation, so a shared laptop never shows one customer another's chat. Render a fresh token on each page load to outlive the first `exp`.

With a verified visitor, tools marked `access: private` run. `tools/my_subscription.yml`, with the API address stored by `vatio secrets set EMBER_API`:

```yaml
description: The signed-in customer's subscription and next delivery.
when_to_use: A signed-in customer asks about their own subscription, next box or delivery date.
access: private
request:
  method: GET
  base_url: "$env.EMBER_API"
  path: /me/subscription
  headers:
    Authorization: "Bearer $auth.token"
respond:
  data:
    subscription: "$.subscription"
```

```yaml
agent:
  instructions: |
    ...
    Subscription questions: call my_subscription. If the visitor is not
    signed in, send them the sign_in link and answer once they are back.
  tools: [my_subscription, knowledge_lookup]
  links:
    sign_in: https://ember-roasters.example/login
auth:
  public_key: identity.pub
```

Your endpoint verifies the same JWT with the public key and reads the subscription of its `sub`. A private tool never takes the user's id as a parameter, and the deploy fails if one does: the model fills parameters and could be talked into filling in someone else's. A rejected token leaves the visitor anonymous instead of failing. See [Authentication](https://vatio.ai/docs/authentication/) and [Tools](https://vatio.ai/docs/tools).

## Try changes on preview first

Preview is the next version of the agent, with its own knowledge changes, chats and publishable keys. After `vatio push`, `vatio env` prints preview's key, widget snippet and test link. `vatio keys create publishable --env preview --origin https://staging.ember-roasters.example` creates a token for your staging site on preview; for local work, use `--origin http://localhost:3000`.

The look is the exception: the settings in **Web chat** in the inbox are workspace-wide and apply everywhere the moment you save. To try a new look on staging only, use `data-*` attributes there.

## Your own interface with the browser SDK

When the chat is part of your product rather than a bubble in the corner, use the browser SDK with the same key and origins:

```sh
npm install @vatio-ai/sdk
```

```js
import { Vatio } from "@vatio-ai/sdk";

const chat = await Vatio.chat({ workspace: "ember-roasters", token: "vatpub_REPLACE_ME" });
chat.on("message", (message) => render(message)); // dedupe by message.id
chat.on("typing", (typing) => showTyping(typing));
for (const message of await chat.history()) render(message);
await chat.send("When is my next delivery?");
```

`visitorToken` takes the same signed JWT as `data-visitor-token`. `replyStyle` defaults to `stream`, one complete reply to render progressively, and `paced` delivers short messages with typing indicators instead. `chat.send` accepts up to four files: images, PDFs, text or audio. The [SDK reference](https://vatio.ai/docs/sdk/reference) lists every method and option.

## Watch it from the inbox

Every conversation, from the widget or the SDK, shows in the inbox with its tool calls. Add a `metadata` object to the visitor token, with a `profile` link to your admin and a few `badges` such as the customer's plan, and the inbox shows them in the conversation header, so whoever answers knows who they are talking to. Supervisors take a conversation over by replying, and report wrong replies with **Report this reply**, writing what should have happened. Running `/vatio-improve` in your coding agent fixes the cause, pushes preview and tests the reported cases; you approve publication, as [Improve](https://vatio.ai/docs/improve) explains.

## Frequently asked questions

### How do I add an AI chat widget to my web app or site?

Once your agent is published, your coding agent creates a publishable key that allows your app's origin, with vatio keys create publishable --origin or in Settings → API keys in the Vatio inbox, and pastes one script tag carrying data-workspace and data-token before the closing body tag, or mounts the @vatio-ai/widget package. The widget's look, with the business name, logo and brand color, is set on the inbox's Web chat page.

### Is it safe to put the chat widget's key in my page's HTML?

Yes, publishable vatpub_ keys are made for page source. Each one works only from the exact origins listed on it, plus Vatio's own pages and app builders like Lovable, and it cannot deploy anything or open the inbox. Never put a vat_ developer token in a browser.

### Can the AI chat recognize customers who are signed in to my site?

Yes. Your backend signs a short-lived RS256 JWT saying who the visitor is and renders it as data-visitor-token on the script tag. Vatio verifies it with your public key, and tools marked access: private then run for that customer. A conversation started before signing in carries over.

### Can I change the widget's colors and text on a specific page?

Yes. Shared defaults live in the inbox, in Web chat, brand color included, and apply to every environment as soon as you save. Any page can override them with attributes on its own script tag, such as data-accent, data-greeting or data-suggestions.

### Does the widget work in an app built with Lovable, Replit or Bolt?

Yes. Every publishable key accepts the preview and published addresses of Lovable, Replit, Bolt, v0 and Base44 without listing them, so the widget works there as soon as the snippet is in. Once the app moves to a domain of its own, add that origin to the key. For an iPhone, Android or Flutter app, use the iOS, Android or Flutter SDK instead.

### Can I build my own chat interface instead of using the widget?

Yes. The browser SDK, @vatio-ai/sdk on npm, gives you the conversation, its history and typing and status events with the same publishable key, and you draw the interface yourself.

## Related guides

- [Build an AI agent for your app](https://vatio.ai/docs/guides/build-an-ai-agent): The two-step entry, from a chat or in your app's project, and trying the preview before publishing.
- [An AI agent in your iOS app](https://vatio.ai/docs/guides/ai-agent-in-your-ios-app): The same agent inside your iPhone app with the iOS SDK.
- [Improve](https://vatio.ai/docs/improve): Report mistakes in the inbox, /vatio-improve fixes them, and you approve the update.
