# Vatio documentation

Vatio is a runtime for customer-facing AI agents. This file is the whole site (https://docs.vatio.ai) as one document. Every page also exists on its own, at its path plus `.md`.

## Documentation

### Introduction

`https://docs.vatio.ai/`

Vatio is a runtime for customer-facing AI agents.

Define your agent in `vatio.yml`, connect the tools and knowledge it needs, and
deploy it to web chat, WhatsApp, and Instagram. Vatio runs the conversations,
verifies visitor identity, and gives your team an inbox to supervise replies.

#### Get started

- [Deploy your first agent](https://docs.vatio.ai/quickstart) — Install the CLI, write six lines of YAML, and talk to a live preview.
- [Build with a coding agent](https://docs.vatio.ai/cli/mcp) — Point Claude Code, Codex or Cursor at Vatio over MCP and let it deploy.
- [CLI reference](https://docs.vatio.ai/cli/) — Every vatio command, what it does, and which flags it takes.

You need **Node.js 20 or newer** and a Vatio account.

```bash
npm install -g @vatio-ai/cli@latest
vatio init acme
vatio push
```

Commands written as `vatio` assume a [global CLI install](https://docs.vatio.ai/cli/). You can always
replace that prefix with `npx @vatio-ai/cli@latest`.

#### Build

- [Manifest](https://docs.vatio.ai/manifest) — The workspace contract: one vatio.yml, its root keys and what each one owns.
- [Tools](https://docs.vatio.ai/tools) — Give the agent your API. One HTTP request, described in YAML.
- [Knowledge](https://docs.vatio.ai/knowledge) — Entries you write and sites Vatio reads for you, re-read every night.
- [Authentication](https://docs.vatio.ai/authentication/) — You sign a JWT saying who the visitor is; Vatio verifies it and never mints one.

#### Connect

- [Web widget](https://docs.vatio.ai/channels/widget) — One script tag, themed from the manifest or per page.
- [WhatsApp](https://docs.vatio.ai/channels/whatsapp) — Test on the shared number in a minute; connect your own for live traffic.
- [Instagram](https://docs.vatio.ai/channels/instagram) — Answer DMs with the same agent, on a professional account.
- [Browser SDK](https://docs.vatio.ai/sdk/) — Build your own chat UI against a dependency-free npm package.

#### For coding agents

This entire site is also one Markdown file: [`/docs.md`](https://docs.vatio.ai/docs.md), or
`vatio docs` from the terminal. Every individual page is available as Markdown
at its own path plus `.md` — [`/quickstart.md`](https://docs.vatio.ai/quickstart.md), for instance —
so an agent can read the one page it needs rather than the whole contract. The
index of them is [`/llms.txt`](https://docs.vatio.ai/llms.txt).

```bash
vatio docs --save vatio-docs.md
```

That fetches the documentation from your configured Vatio host. Run it again to
refresh the file; the CLI keeps no offline cache. To give an agent the commands
themselves rather than the prose, connect [the MCP server](https://docs.vatio.ai/cli/mcp).

A starting prompt:

```text
Read https://docs.vatio.ai/docs.md. Inspect this repository for an existing
vatio.yml before creating a workspace. Build a customer-facing agent
using the product's existing APIs and content. Deploy to preview, test
representative conversations, and show me the preview link and results.
Publish when I ask you to make it live.
```

For an existing workspace, work from the directory containing `vatio.yml` or
one of its subdirectories, and preserve the existing `workspace` slug.

Release notes are in the [changelog](https://docs.vatio.ai/changelog).

### Quickstart

`https://docs.vatio.ai/quickstart`

You need **Node.js 20 or newer** and a Vatio account. The examples use `acme`
as a workspace slug; replace it with yours.

#### Install the CLI

```bash
npm install -g @vatio-ai/cli@latest
vatio --help
```

#### Create a workspace

Run these commands in the repository where you want to keep your agent:

```bash
mkdir support-agent
cd support-agent
vatio init acme
```

`init` creates the remote workspace and writes `vatio.yml` in the **current
directory**. It opens device authorization if you are not logged in. Complete
that step in your browser. If you already own the remote workspace, `init`
uses it.

#### Define the agent

Replace the starter contents of `vatio.yml` with:

```yaml
workspace: acme
business:
  name: Acme
  summary: Acme sells warehouse robotics.
agent:
  name: Acme Support
  instructions: |
    Help visitors understand Acme's warehouse robotics.
    Ask what they need help with. If you do not have the information
    to answer, say so. Never invent product details or prices.
  personality: Warm, concise, and clear.
```

`workspace` selects the remote workspace. `agent.instructions` defines what
its single agent does. The other fields in this example are optional.

#### Deploy and test

```bash
vatio push
vatio chat "What does Acme do?"
```

`push` validates your files and deploys to `preview`. Open the preview link it
prints, or continue the conversation with another `chat` command.

```bash
vatio chat "Can you tell me the price?"
vatio chat debug
vatio chat reset
```

Check that the agent answers from the supplied context and acknowledges what
it does not know. `debug` reads the current CLI chat; `reset` starts a new one.

#### Publish

When the preview is ready for customers:

```bash
vatio publish
```

Your agent is now available at `https://vatio.ai/w/acme`.
`vatio rollback` restores the previous live deployment.

Next, [add knowledge](https://docs.vatio.ai/knowledge), [connect your API](https://docs.vatio.ai/tools), or
[embed the widget](https://docs.vatio.ai/channels/widget).

### Workspace and manifest

`https://docs.vatio.ai/manifest`

A workspace contains one agent and its conversations, contacts, knowledge,
secrets, and channel connections. Its deployable configuration lives in a
directory containing `vatio.yml`:

```text
support-agent/
  vatio.yml          # required
  tools/*.yml        # tools
  identity.pub       # public key that verifies your JWT (see Authentication)
```

Only create the files you need. The CLI finds the nearest `vatio.yml` at or
above the current directory and reads `workspace` to select the remote.
Deployment commands have no `--workspace` flag or `VATIO_WORKSPACE` override.
Your user credentials live separately in `~/.vatio/config.json`.

The guides below show configuration fragments. Merge them into your existing
manifest, keeping one copy of each root key and preserving the instructions
and tool assignments you still need.

#### Root keys

These are the supported root keys. Unknown root keys fail validation.

| Key | Purpose |
|---|---|
| `workspace` | Remote slug: lowercase letters and digits separated by hyphens |
| `agent` | The agent definition; requires non-empty `instructions` |
| `agents` | Several agents, keyed by slug, instead of `agent` |
| `entry` | Which agent takes a conversation; see [Several agents](https://docs.vatio.ai/multi-agent) |
| `business` | Company name and context for the agent |
| `widget` | Web appearance, copy, locale, and allowed origins |
| `auth` | Public key that verifies your JWT; see [Authentication](https://docs.vatio.ai/authentication/) |

`knowledge` and `links` are not root keys: each agent declares its own, inside
its `agent:` or `agents:` block. `identity` is `auth`. A manifest still
carrying any of the three fails the push with the migration to make.

Use `agent: false` only for a workspace dedicated to the
[phone verification API](https://docs.vatio.ai/api/phone-verification). It cannot also declare widget,
authentication, knowledge, links, tools, or shared helpers.

### Agents

`https://docs.vatio.ai/agents`

The `agent` block describes an agent:

| Field | Required | Purpose |
|---|---|---|
| `instructions` | Yes | Tasks, decision rules, and when to use tools |
| `name` | No | Display name; defaults to the agent's key |
| `personality` | No | Voice and tone; defaults to warm, brief, and clear |
| `tools` | No | Tool keys the agent may call; defaults to an empty list |
| `knowledge` | No | Knowledge bases this agent reads |
| `links` | No | Urls this agent may hand out |

There is no `agent.key` to configure. A workspace written with `agent:` has one
agent, and the runtime calls it `main`.

```yaml
business:
  name: Acme
  summary: Acme sells warehouse robotics to distributors in Chile and Peru.
agent:
  instructions: |
    Answer product questions using knowledge_lookup.
    If the knowledge does not answer the question, say what is missing.
  tools: [knowledge_lookup]
  knowledge: [docs]
```

`knowledge:` and `links:` belong to the agent, not to the workspace — every
agent states what it reads and which urls it may hand out, even when two agents
say the same thing.

Create the `docs` knowledge base with `vatio kb create docs` before pushing this
example: a referenced base must already exist.

#### Business context

`business` belongs at the manifest root.
`business.name` accepts up to 100 characters; `business.summary` accepts 2,000.
Omitting either preserves its current workspace value.

Write instructions about your product and workflow. Vatio adds business
context, contact state, tool descriptions, and its platform instructions.
The agent normally replies in the visitor's language; specify a language in
`instructions` or `personality` when your product requires one.

### Several agents

`https://docs.vatio.ai/multi-agent`

One workspace can run more than one agent, each with its own instructions,
tools and knowledge. Which one a visitor gets is decided by the manifest, from
whether they are signed in and what their token says about them.

The case it is for: a marketing site and a customer portal that embed the same
widget. The anonymous visitor asking about prices and the signed-in customer
asking about their own records are not the same conversation, and one set of
instructions serving both ends up serving neither. Before this they had to be
two workspaces — two manifests, two knowledge bases, two tokens to keep in step.

Key the agents under `agents:` and add an `entry:` table:

```yaml
agents:
  home:
    name: Ana
    instructions: |
      Answer product and pricing questions using knowledge_lookup.
      Nobody here is signed in: never ask for account details.
    tools: [knowledge_lookup]
    knowledge: [public_docs]

  portal:
    name: Ana
    instructions: |
      You are talking to a signed-in customer. Use my_orders and my_documents
      to answer about their own account, and knowledge_lookup for anything else.
    tools: [my_orders, my_documents, knowledge_lookup]
    knowledge: [portal_kb]

  # Each agent names its own knowledge; there is nothing inherited.

entry:
  - { authenticated: false, agent: home }
  - { claims: { role: staff }, agent: staff }
  - { agent: portal }
```

`agent:` is the short form of `agents: { main: … }`, so a workspace with one
agent changes nothing and keeps working exactly as it does today.

Give every agent the same `name` when the split should be invisible. As far as
the visitor can tell they are talking to one person the whole time, which is
what the platform rules already assume.

#### The entry table

An ordered list. The first row whose conditions hold wins, and that is the whole
rule — there is no specificity, no scoring, nothing to work out. The policy
reads top to bottom, and someone who has never used Vatio can check it in a pull
request.

| Key | Meaning |
|---|---|
| `agent` | Required. The agent this row selects |
| `authenticated` | `true` or `false` — whether the visitor arrived with a valid token |
| `claims` | Claim values that must all match; a list means any of them |

A row with no conditions matches everyone. The last row must be one, because a
visitor who matches nothing would otherwise have nobody to talk to — and
anything written after it is unreachable.

Claims are compared as text, so `role: 2` in the manifest matches `"2"` in the
token. `vatio tools check` warns about an agent no row can ever select.

#### When it is decided

Before the first reply, on every channel. Vatio verifies the token as the
conversation opens — a signature check, no network — so the right agent is
chosen before the visitor's first message is answered rather than after.

If they sign in mid-conversation, the SDK's `identify()` runs the table again.
Someone who asked a question anonymously, signed in, and came back keeps
everything they typed: same thread, same history, and from the next message on,
the agent for who they now are.

#### What it is not

`entry:` is not a security control, and should not be used as one. A visitor
with no valid token has no verified identity, so no `access: private` tool runs
for them whatever agent they land on. A mistake in the table shows someone the
wrong prompt; it cannot show them someone else's data.

Protect data with `access: private` on the tool, always. The entry table is
about giving each audience the right conversation.

#### Knowledge and links per agent

Each agent states its own, even when two of them say the same thing:

```yaml
agents:
  home:
    instructions: …
    knowledge: [public_docs]
    links:
      pricing: "https://acme.test/pricing"
  portal:
    instructions: …
    knowledge: [portal_kb]
    links: {}                   # hands out no urls at all
```

There is no workspace-level default to inherit from, and the repetition is
deliberate. What an agent knows and which urls it may hand out are the two
things you check when it answers wrongly, and an inherited value means looking
somewhere else and then working out whether this agent overrode it. It also
matters in the prompt: every declared url is listed in it, so a portal agent
would otherwise carry a marketing agent's whole link list around without ever
handing one out.

### Tools

`https://docs.vatio.ai/tools`

Tools let the agent call your APIs. A tool is one HTTP request, described in
YAML. The filename is the tool key: `tools/check_stock.yml` becomes
`check_stock`.

Anything that needs branching, a second call, or a derived response is code,
and it belongs in the backend the tool already calls — give the agent one
endpoint that does the whole job.

Add each tool to `agent.tools`; creating its file alone does not enable it.

#### Declarative HTTP tools

Set your backend URL as a workspace secret:

```bash
vatio secrets set ACME_API https://api.acme.com
```

Create `tools/check_stock.yml`. This example expects the API to return
`{"data": [{"name": "Picker", "stock": 3}]}`:

```yaml
description: Check product stock.
when_to_use: The visitor asks whether a product is available.
parameters:
  type: object
  properties:
    product: { type: string }
  required: [product]
access: public
request:
  method: GET
  base_url: "$env.ACME_API"
  path: /products
  requires: [params.product]
  query:
    name: "$params.product"
respond:
  data:
    products: "$.data"
  message:
    when_empty: No products matched that name.
    default: "Found {{count}} matching products."
```

Merge this into your existing `agent` block:

```yaml
agent:
  instructions: Use check_stock to answer questions about product availability.
  tools: [check_stock]
```

The request supports `GET`, `POST`, `PATCH`, and `DELETE`. Supply `method`,
`base_url`, and `path`; the base URL must include its scheme and host.
`headers` and `query` are maps; `body` is a JSON map used for `POST` and `PATCH`.

##### Placeholders

Values in `base_url`, `path`, `headers`, `query`, and `body` support:

| Placeholder | Source |
|---|---|
| `$params.<name>` | Tool arguments |
| `$env.<KEY>` | Workspace secrets |
| `$auth.subject`, `$auth.token`, `$auth.claims.<key>` | The tool's authenticated principal |
| `$contact.name`, `$contact.email`, `$contact.phone_number` | Contact profile |

A single placeholder keeps its value's type. A placeholder within a string
is interpolated. `||` selects the first non-empty alternative, such as
`"$params.name || $contact.name"`.

##### Required values

Missing values are omitted from the request. List any value the endpoint needs
under `request.requires`, without the `$` prefix, to fail before sending an
incomplete request. A missing contact phone, email, or name produces
`missing_contact_phone`, `missing_contact_email`, or `missing_contact_name`;
other paths produce `missing_requirement`.

##### Response mapping

`respond.data` maps output keys to JSON paths: `$` is the entire body, `$.data`
is its `data` field. `respond.message` can be a literal string or the
`when_empty` / `default` mapping shown above. `{{count}}` is
the length of the first array in the mapped data. Without an array, `when_empty`
applies when all mapped values are blank.

Omit `respond.message` to use the API's own `message` field. HTTP 2xx produces
`result: "ok"`; non-2xx produces `result: "error"`, so return the status that
says what happened rather than encoding failures inside a 200.

#### Tool results

Every tool returns an object with `result` (`ok` or `error`) and a non-empty
`message`. Additional fields, such as `data` and `error_key`, are allowed.

```json
{ "result": "ok", "message": "This product is out of stock.", "data": { "stock": 0 } }
```

```json
{ "result": "error", "message": "Stock could not be checked.", "error_key": "backend_unavailable" }
```

Use `ok` when the operation completed, including an empty result or negative
answer. Use `error` when it could not complete. Invalid results become a
platform error. Tool results are visible to the model; never return credentials.

Validate with `vatio tools check`, deploy with `vatio push`, and exercise the
tool through `vatio chat`. There is no direct tool-invoke command.

#### Built-in tools

| Tool | Purpose |
|---|---|
| `knowledge_lookup` | Search the knowledge bases listed in `knowledge` |
| `identify_contact` | Save a name supplied by the visitor |
| `request_contact_info` | Ask a WhatsApp visitor to share their phone number |
| `handoff` | Hand the conversation to a person — see [Human in the loop](https://docs.vatio.ai/handoff) |

Enable built-in tools through `agent.tools` just like your own tools. The one
exception is `handoff`, which has no switch of its own: declaring `agent.handoff`
is what attaches it, so an agent can never be able to escalate with nowhere to
escalate to.
`request_contact_info` sends a sharing request; the number arrives only if the
visitor shares it in a later message. End the turn and retry the dependent
operation after that reply.

A tool that reads a signed-in visitor's own records needs
[`access: private`](https://docs.vatio.ai/authentication/tools) and an `auth:` block.

### Knowledge bases

`https://docs.vatio.ai/knowledge`

A knowledge base holds **entries**, and an entry is Markdown. Either you write
it, or a **site** writes it: a site is one URL — or one URL pattern — that
Vatio reads, converts to Markdown, and re-reads every night.

```bash
vatio kb create docs
vatio kb follow docs acme.com/help/**      # one entry per page that matches
vatio kb write docs horarios horarios.md   # or write one yourself
vatio kb show docs
```

Create one base per set of pages you want kept apart — anything only one agent
should read belongs in its own.

Reference the base on the agent that reads it, and give it `knowledge_lookup`:

```yaml
agent:
  instructions: |
    Search knowledge_lookup before answering questions about Acme's policies.
    If the results do not answer the question, say so.
  tools: [knowledge_lookup]
  knowledge: [docs]
```

Run `vatio push` after changing these references. A referenced base must
already exist — a misspelled name fails the push rather than quietly becoming
an empty base. Removing a reference does not delete its content.

#### Sites

A site is one field, and one rule: a URL is that URL, a pattern is every page
that matches it.

| What you write | What it reads |
| --- | --- |
| `acme.com` | the home page, and only it |
| `acme.com/help` | that one page |
| `acme.com/**` | every page of the site |
| `acme.com/help/**` | that section, however deep |
| `acme.com/blog/*` | the posts directly under `/blog` |

`*` matches within one path segment; `**` matches across segments. Nothing is
implied: `acme.com/help` never means "and everything under it". URLs must be
public HTTP or HTTPS.

Vatio discovers pages through an exact path, `llms.txt`, a sitemap, or links on
the site. It respects `robots.txt` and can render pages whose content needs
JavaScript. Content behind a click or a search is better written as an entry.

Every site is read again nightly, and `vatio kb refresh BASE [URL]` reads them
now. A page whose Markdown has not changed costs one request and nothing else —
it is not re-indexed and not re-embedded.

`vatio kb unfollow BASE URL` stops reading a site and deletes the entries it
wrote: they are a copy of pages that live somewhere else, and keeping them
would leave the base answering from something nothing refreshes any more.

#### Entries

```bash
vatio kb write docs refunds refunds.md   # creates it, or replaces what is there
vatio kb cat docs refunds                # exactly what Vatio holds
vatio kb cat docs refunds | edit | vatio kb write docs refunds
```

`vatio kb write BASE ENTRY [FILE]` puts Markdown under a name, reading stdin
when no file is given. The entry's title comes from its own `# heading`.
Headings divide an entry into searchable sections; write each one so it can
answer a question on its own.

An entry a site wrote is **not editable** — the next read would overwrite it
without a word. Change the site, or write your own entry instead.

Use `vatio kb show docs` to check each site's status and errors, and which
entries are ready to answer from. Reading and indexing are asynchronous: an
entry shows as ready once its sections have been embedded. Test retrieval in a
conversation afterwards.

`vatio kb rm-entry BASE ENTRY` deletes one entry. `vatio kb rm BASE` deletes a
base only when no deployed agent references it.

**Knowledge is shared by all environments.** Writing, refreshing, and deleting
content can change live answers immediately. Deployment rollback does not
restore knowledge content.

#### Allow VatioBot

If you own a site whose `robots.txt` blocks crawling, allow VatioBot in that
site's `robots.txt`:

```text
User-agent: VatioBot
Allow: /
```

Then run `vatio kb reindex BASE SOURCE`. There is no option to bypass the site's
crawl policy.

### Links

`https://docs.vatio.ai/links`

Declare static destinations on the agent that may hand them out:

```yaml
agent:
  instructions: …
  links:
    help: https://acme.com/help
    product:
      url: https://acme.com/products/{category}
      when: The visitor wants to browse a product category.
      values:
        category: [picking, packing, sorting]
```

A value can be a URL string or a mapping with `url`, optional `when`, and
`values` for every placeholder. URLs must be absolute HTTP or HTTPS URLs.
Placeholder values use letters, digits, `.`, `_`, `~`, or `-`.

Vatio expands every combination at deploy time, with a limit of 100 URLs.
Use a tool for dynamic destinations or larger catalogs. Links returned by a
tool are allowed for the rest of that conversation.

A URL written straight into `instructions` is shareable too, so a one-off
mention needs no entry here. `links` remains the better place for a
destination the agent should actually offer: it carries a `when` that tells
the agent when to reach for it, and it expands placeholders.

Which URLs a reply may carry at all is enforced by the
[safeguards](https://docs.vatio.ai/safeguards).

### Human in the loop

`https://docs.vatio.ai/handoff`

Two blocks on the agent decide when a person takes over.

```yaml
agent:
  instructions: ...
  hours:
    timezone: America/Santiago
    mon: 09:00-18:00
    tue: 09:00-13:00, 14:00-18:00
    sat: 10:00-14:00
  handoff:
    after_turns: 8
    when:
      - el cliente pide hablar con una persona
      - reclamo por un cobro
```

Declaring `handoff` is what gives the agent the tool to hand a conversation
over. Calling it marks the chat as waiting for a person; it shows up that way
in [the inbox](https://docs.vatio.ai/channels/inbox), and whoever replies there or from the WhatsApp
Business app takes it over. Vatio does not notify anyone yet.

The agent says so plainly. It is an assistant, it does not pretend otherwise,
and when it hands something over it tells the visitor it is checking with a
colleague and will come back with an answer — no invented errand to cover for
it. Several jurisdictions require disclosing that a person is talking to a
machine, and a single honest behavior is one less thing to get wrong.

#### When it escalates

**`handoff.when`** is your own list of conditions, in your words, and goes into
the prompt. **`handoff.after_turns`** is the deterministic floor under it: past
that many visitor messages the handoff happens whether or not the model asked
for one. A visitor who asks for a human in plain words always triggers one —
that never depends on the model's judgement. Declare neither and escalation is
entirely the model's judgement.

#### Hours

**`hours`** says when a person is on duty, in a real timezone, 24h ranges, one
line per day. A day you leave out is a day nobody is on. The hours do **not**
decide who answers — the agent triages and tries to resolve either way. They
decide what happens after it gives up:

| | Inside hours | Outside hours |
|---|---|---|
| The agent | Goes quiet, someone is there to pick it up | Keeps answering — it promised a person for the morning, and going silent on top of that is worse than helping |

Declare no `hours` and the agent is treated as always reachable, so a handoff
always quiets it. A handoff nobody picks up within an hour hands the
conversation back to the agent rather than leaving the visitor with a promise
and silence.

### Safeguards

`https://docs.vatio.ai/safeguards`

Vatio checks draft replies for empty content, generated media, and unauthorized
links. A failed check triggers a correction attempt. These checks run for every
agent and cannot be disabled through the manifest.

Replies are text. The agent can read supported inbound media but cannot generate
or send files. Web replies use Markdown; WhatsApp and Instagram use plain text.

A reply may only carry a URL the agent was already given: one declared in
[`links`](https://docs.vatio.ai/links), one written anywhere else that reaches the prompt
(`instructions`, `personality`, the business description, a tool's
`when_to_use`), or one a tool returned in the conversation. It must be copied
exactly; a URL the agent assembled or altered is stripped. Nothing a visitor
typed becomes allowed.

### Authentication

`https://docs.vatio.ai/authentication/`

**You sign a JWT saying who the visitor is. Vatio verifies it and passes it to
your tools.** That is the whole of authentication — there is one mechanism, on
every channel.

Vatio holds only your **public** key, so it can check a token but never mint
one. Your backend stays the only thing that can say who someone is.

#### Set it up

Generate the keypair:

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

That writes two files, and they go to different places.

**`identity.pub` stays in the workspace.** It is a public key, so it is
committed like any other file and `vatio.yml` names it:

```yaml
### vatio.yml
auth:
  public_key: identity.pub
```

**`identity.pem` goes into your own backend**, as a secret — an environment
variable, or whatever credential store you already use. Your backend is what
signs, so it is the only thing that ever needs it. Load it, then delete the
file from the workspace directory; it has no job there.

```bash
### environment variable
VATIO_IDENTITY_PRIVATE_KEY="$(cat identity.pem)"

### or Rails credentials
bin/rails credentials:edit     # vatio: { identity_private_key: "..." }
```

**Never `vatio secrets set` the private key.** That store is read by Vatio, to
let your tools call your API. A private key in there would let Vatio *mint*
tokens for your users instead of only checking them, which is the one thing
this design exists to prevent. Vatio holds the public half and nothing else.

Mark every tool that needs a signed-in user:

```yaml
### tools/my_bookings.yml
access: private
```

Tools without `access: private` are public and run for anyone.

#### The token

Sign it with `identity.pem` using **RS256**:

| Claim | Required | Value |
|---|---|---|
| `sub` | yes | Your user id. Becomes `$auth.subject`. |
| `aud` | yes | Your workspace slug, exactly. |
| `exp` | yes | Unix seconds. Keep it short — Vatio re-checks it on every tool call. |
| `name` | no | Fills the contact's name. |
| `email` | no | Fills the contact's email. |
| `phone_number` | no | Fills the contact's phone. |

Any other claim you add arrives as `$auth.claims.<name>`.

```ruby
### Ruby
JWT.encode(
  { sub: user.id.to_s, aud: "acme", exp: 1.hour.from_now.to_i,
    name: user.name, email: user.email },
  OpenSSL::PKey::RSA.new(ENV["VATIO_IDENTITY_PRIVATE_KEY"]), "RS256"
)
```

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

```python
### Python (PyJWT)
jwt.encode(
    {"sub": str(user.id), "aud": "acme",
     "exp": int(time.time()) + 3600, "name": user.name},
    os.environ["VATIO_IDENTITY_PRIVATE_KEY"], algorithm="RS256",
)
```

`aud` is required because a signature proves *who* signed, not *what for*.
Without it, any other JWT you sign with the same key — a password reset, a
download link — would be accepted here as an identity.

Next: how the token reaches Vatio on
[each channel](https://docs.vatio.ai/authentication/sessions), and what a
[private tool](https://docs.vatio.ai/authentication/tools) sees once it does.

### Sessions and channels

`https://docs.vatio.ai/authentication/sessions`

#### On the web

Render the token onto the widget's script tag. Signed-out visitors simply get
no token, and the agent answers with its public tools.

```erb
<script src="https://cdn.vatio.ai/v1/widget.js"
        data-workspace="acme"
        data-token="vatpub_..."
        <%= "data-visitor-token=\"#{visitor_jwt}\"".html_safe if current_user %>></script>
```

To sign someone in without reloading the page:

```js
window.VatioWidget.identify(newToken);   // null to sign out
```

#### Signing in mid-conversation

A visitor asks something, the agent tells them to sign in, they do, and they
come back. **The conversation survives.** Whether they signed in through
`identify()` or by navigating away and reloading the page with a token, the
chat they already had is carried over and now has a name on it — so the agent
can answer the question that prompted the login without them retyping it.

| Before | After | What happens |
|---|---|---|
| anonymous | signed in | Same conversation, now identified |
| user A | user A, newer token | Same conversation, token refreshed |
| user A | user B | **New conversation** |
| signed in | signed out | New conversation |

The last two are the shared-laptop case: a conversation that belonged to one
`sub` is never handed to another, so signing in as someone else looks exactly
like arriving for the first time. Vatio enforces that server-side — the page
cannot opt out of it.

Refreshing is also how a web session stays alive: render a fresh token on each
page load and a long-running conversation keeps working past the first token's
`exp`.

#### Channels with no session

On WhatsApp and Instagram there is no page to render a token onto, so Vatio
asks your backend for one. Add `mint:`:

```yaml
auth:
  public_key: identity.pub
  mint:
    url: $env.API_URL/api/vatio/identity
    headers:
      X-Api-Key: $env.API_KEY
```

Vatio posts the channel's evidence and expects a token back:

```http
POST /api/vatio/identity
Content-Type: application/json

{ "channel": "whatsapp", "phone_number": "+56912345678" }
```

```json
{ "token": "<the same kind of JWT you sign for the web>" }
```

Return any non-2xx for a number you do not recognize — the visitor stays
anonymous and the agent keeps its public tools. Set the secrets it needs with
`vatio secrets set API_KEY ...`.

Mint runs once per conversation, and again only if the token expires. On
WhatsApp that means a long conversation refreshes itself.

**Mint is never used on the web**, even when you declare it. On the web the
token is the evidence: the page had a session and said so in a signature. A web
visitor with no valid token is signed out, and stays that way until the page
calls `identify()` with a fresh one.

### Private tools

`https://docs.vatio.ai/authentication/tools`

#### What a tool sees

| Placeholder | Value |
|---|---|
| `$auth.subject` | The token's `sub` |
| `$auth.token` | The raw JWT, to forward to your API |
| `$auth.claims.<name>` | Any custom claim |

```yaml
### tools/my_bookings.yml
description: The visitor's upcoming bookings.
when_to_use: When they ask about their own bookings.
access: private
request:
  method: GET
  base_url: $env.API_URL
  path: /api/bookings
  headers:
    Authorization: Bearer $auth.token
respond:
  data:
    bookings: bookings
```

Your endpoint verifies the same JWT with the same public key, and scopes the
query to its `sub`.

#### Rules

**Never take the user's id as a parameter.** The model fills parameters, and it
can be talked into filling that one with someone else's id. A private tool
declaring `user_id`, `customer_id`, `account_id`, `member_id`, `patient_id` or
`subject` fails the deploy:

```text
tool my_bookings: a private tool cannot take "user_id" as a parameter — the model
fills parameters and can be talked into filling that one with someone else's id.
Use $auth.subject, which comes from the verified token
```

Derive the user from the token instead. An endpoint that accepts an id has to
remember to scope it every time, forever; one that reads the token cannot forget.

Other errors you may hit:

| Error | Fix |
|---|---|
| `access: private needs an auth: block` | Add `auth:` with `public_key:` |
| `auth.public_key ... is a PRIVATE key` | Point at `identity.pub`, not `identity.pem` |
| `auth.algorithm must be one of RS256...` | Sign with RS256/ES256, not HS256 — a shared secret cannot live in a manifest |
| `auth.mint.url must be https://` | Use `https://` or an `$env.` placeholder |
| `access must be "public" or "private"` | Scheme names are gone; use `private` |
| `auth/ holds JavaScript auth providers, which are gone` | Delete `auth/`, sign a JWT instead |
| `JavaScript tools are gone` | Delete `tools/*.js` and `lib/*.js`; describe the call in `tools/<key>.yml` |

A rejected token — wrong key, expired, wrong `aud` — leaves the visitor
anonymous rather than erroring, and the agent falls back to its public tools.
`vatio chat` shows the reason.

### Channels

`https://docs.vatio.ai/channels/`

The same agent serves web chat, WhatsApp, Instagram, and CLI conversations.
Use the actual channel when testing channel identity or message delivery.

- [Web widget](https://docs.vatio.ai/channels/widget) — One script tag on your own site, or the hosted page Vatio publishes.
- [WhatsApp](https://docs.vatio.ai/channels/whatsapp) — A shared test number, then your own business number for live traffic.
- [Instagram](https://docs.vatio.ai/channels/instagram) — DMs on a professional account, with the same agent behind them.
- [Inbox](https://docs.vatio.ai/channels/inbox) — Where your team watches conversations and takes one over.

#### Channel behavior

WhatsApp and Instagram continue the latest conversation until the last visitor
message is more than eight hours old. The next message then starts a new chat.

Replies are text-only. Supported inbound images, audio, and PDFs can be passed
to the model; supported files must fit the 8 MB per-file limit. Unsupported or
oversized attachments remain available for supervisors, and the agent receives
a notice that it cannot read them.

Web chat receives one complete reply, which the widget renders progressively.
WhatsApp and Instagram receive paced messages with typing indicators. CLI
replies are immediate when ready. These are delivery styles, not separate agents.

### Web widget

`https://docs.vatio.ai/channels/widget`

The hosted page at `https://vatio.ai/w/acme` works after publishing. To embed
chat in your own website, add its origin to `vatio.yml`:

```yaml
widget:
  allowed_origins:
    - https://acme.com
    - http://localhost:3000
  accent_color: "#3355FF"
  about: Ask Acme Support about products and orders.
  greeting: How can I help?
  suggestions:
    - What does Acme sell?
    - Where is my order?
```

Origins are exact `scheme://host[:port]` values, with no path or wildcard.
An empty list blocks external embeds; Vatio's own hosted pages still work.
Push the manifest, publish the agent if needed, and create a token:

```bash
vatio push
vatio publish
vatio tokens create --env live --label website
```

Copy the returned `vatpub_` token into your page:

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

Publishable tokens are intended for page source and select one workspace and
environment. They do not authorize deployment or access to the workspace inbox.
Never use a `vat_` developer token in a browser.

`vatio widget` reports the server's configuration and existing token prefixes.
`vatio tokens list` lists tokens; `vatio tokens revoke PREFIX` revokes one.
Revocation prevents new chats and conversation lists with that token; existing
chat credentials remain valid until expiry.

#### Widget configuration

Visual values resolve in this order: page override, workspace setting, platform
default. Use `widget` in `vatio.yml` for shared defaults and `data-*` attributes
for a specific page.

| Manifest key | Page attribute | Value / default |
|---|---|---|
| `accent_color` | `data-accent` | `#RRGGBB` brand color |
| `accent_ink` | `data-accent-ink` | `#RRGGBB`; otherwise automatic contrast |
| `surface`, `ink`, `muted`, `line` | `data-surface`, `data-ink`, `data-muted`, `data-line` | `#RRGGBB` theme colors |
| `scheme` | `data-scheme` | `auto`, `light`, or `dark`; default `auto` |
| `position` | `data-position` | `right` or `left`; default `right` |
| `font` | `data-font` | CSS font stack; `inherit` uses the host's font |
| `radius` | `data-radius` | CSS length such as `16px`; default `16px` |
| `title` | `data-title` | Up to 200 characters; defaults to the agent's name |
| `greeting` | `data-greeting` | Up to 200 characters; defaults to localized copy |
| `suggestions` | `data-suggestions` | Up to four prompts, 200 characters each; attribute uses pipe-separated text |
| `about` | `data-about` | Up to 2,000 characters of visitor-facing copy |
| `locale` | `data-locale` | `en`, `es`, or `pt`; default `en` |
| `logo` | — | Workspace-relative PNG, JPEG, WebP, or GIF, up to 2 MB |
| `allowed_origins` | — | Origins permitted to use the public API |

`about` is displayed to visitors; `business.summary` supplies model context.
`locale` sets interface labels; the agent's language follows the conversation.
Unknown widget keys fail validation.

For example, `data-suggestions="Products|Track an order"` sets two prompts.

#### Full-page chat

For a full-page chat, use a container and page-specific attributes:

```html
<div id="chat" style="height:100dvh"></div>
<script async src="https://cdn.vatio.ai/v1/widget.js"
        data-workspace="acme" data-token="vatpub_REPLACE_ME"
        data-display="page" data-mount="#chat"></script>
```

`data-display` defaults to `bubble`. `data-mount` selects the container in
page mode and defaults to the body. These attributes do not belong in the
manifest. The widget normally waits for page load and idle time;
`data-eager="true"` starts it immediately.

To tell the agent who is signed in, render a visitor token onto the same tag —
see [Sessions and channels](https://docs.vatio.ai/authentication/sessions). To build your own UI
instead of embedding this one, use the [browser SDK](https://docs.vatio.ai/sdk/).

### WhatsApp

`https://docs.vatio.ai/channels/whatsapp`

#### Test with the shared preview

You do not need your own Meta account:

```bash
vatio whatsapp numbers add +56912345678
vatio whatsapp numbers verify +56912345678 123456
```

Replace the number with your phone and `123456` with the code you receive.
Then message the shared number from that phone. It always reaches `preview`,
including when you have named previews or a live deployment.

You can register up to five test phones per workspace. Codes last 10 minutes;
resends have a 60-second cooldown. Manage registrations with
`vatio whatsapp numbers list`, `resend PHONE`, and `remove PHONE`.
A verified phone can route to only one workspace at a time; adding one already
verified on another workspace you own moves it to this workspace.

#### Connect your business number

For live traffic:

```bash
vatio whatsapp connect
```

Complete Meta's signup in the browser. Then inspect the connection and
activate replies after publishing your agent:

```bash
vatio whatsapp check
vatio whatsapp activate
```

A newly connected number is paused. Status reports credentials, webhook
reception, and activation separately. `vatio whatsapp deactivate` pauses
replies; `disconnect` removes the connection. Reconnecting the same number
renews its credentials. Disconnect first to change numbers.

The connection flow supports keeping your WhatsApp Business phone app.
A reply sent from that app starts human takeover on the conversation; release
the chat in [the inbox](https://docs.vatio.ai/channels/inbox) when the agent should resume.

Identifying a WhatsApp visitor means minting a token from your own backend —
see [Channels with no session](https://docs.vatio.ai/authentication/sessions#channels-with-no-session).

### Instagram

`https://docs.vatio.ai/channels/instagram`

#### Test with the shared preview

```bash
vatio instagram accounts add @yourhandle
```

DM the shared account printed by the command **from that handle**. Vatio sends
a code by DM. Confirm the received code:

```bash
vatio instagram accounts verify 123456
```

Verified test accounts always reach `preview`. Up to five are allowed per
workspace. `vatio instagram accounts list` shows each account's ID and status:
`declared`, `awaiting_code`, or `verified`. Use `resend ID` or `remove ID` to
manage a registration. The CLI does not wait for the first DM.

#### Connect your professional account

For live DMs:

```bash
vatio instagram connect
```

Complete Meta consent in the browser, then run `vatio instagram check`.
A workspace connects one account; an account connects to one workspace.
Vatio refreshes its access token automatically. If it expires, reconnect the
same account. Use `vatio instagram disconnect` before switching accounts.

### Inbox

`https://docs.vatio.ai/channels/inbox`

Invite supervisors from **Team** in the Vatio console. They sign in with a code
to their email and land on `/workspaces/<id>/inbox` — every conversation,
replies, human takeover, ratings, and the agent's tool calls. They see that and
nothing else: no deploy, no tools, no knowledge. Remove them from the same page
and the next request they make is refused, including a socket they already had
open.

There is nothing to embed and no token to mint. Being signed in to Vatio, as
someone this workspace has invited, is the whole access model.

Replying starts **human takeover**, which pauses agent replies on that chat —
from the inbox, or from the WhatsApp Business app on your own number, which
reaches Vatio as an echo and counts the same. Hand it back from the inbox when
you are finished, or leave it: a takeover expires **8 hours after the last
thing the person said**, and the agent picks the conversation up again the next
time the visitor writes. The clock restarts on every human reply, so a
conversation somebody is actively holding never expires underneath them.

To have the agent ask for a person itself, rather than waiting for one to
notice, declare [`handoff` and `hours`](https://docs.vatio.ai/handoff) on it.

### Deployments

`https://docs.vatio.ai/deployments`

#### Preview and live

A deployment is a saved configuration snapshot. An environment is a name
pointing to a deployment. `preview` is the default development target; `live`
is the customer-facing target.

```bash
vatio push
vatio status
vatio diff --env live
vatio publish
vatio rollback
```

`push` validates and updates a preview; it cannot target `live`. `publish`
promotes a preview. `rollback` restores the previous live configuration.
`diff --env live` compares your **local directory** with live, so push local
edits before publishing them.

Agents, tools, auth configuration, chats, and contacts are scoped by environment.
**Secrets, knowledge content, channel connections, business fields, and widget
settings are workspace-wide.** Applying a manifest can update business and
widget settings during a preview push. A preview tool can also call the same
external backend as live. Use a separate workspace when these resources must
be isolated.

Rollback restores deployment configuration, not knowledge content, secret
values, or actions already performed by a tool.

#### Named previews

Create an independent preview for a branch or pull request:

```bash
vatio push --env pr-42
vatio chat "What does Acme do?" --env pr-42
vatio tokens create --env pr-42
vatio publish --env pr-42
```

Names use lowercase letters and digits separated by hyphens or underscores,
up to 40 characters. Shared WhatsApp and Instagram previews always use the
literal `preview` environment.

The preview share URL printed by `push` opens without a login. Anyone with
that URL can talk to the preview, so share it with its intended testers.

#### Deploying from GitHub

Install the Vatio GitHub App and bind a repository to your workspace in the
console. The workspace owner chooses this binding; a repository's `workspace`
value must match it. Set the workspace directory if the repository contains
more than one `vatio.yml`.

| Event | Result |
|---|---|
| Pull request opened, updated, or reopened | A `pr-N` preview and a comment with its URL when the workspace directory changes |
| Pull request closed | Its preview is removed |
| Push to the default branch | Deployment and publication to live |

The CLI remains available for manual deployment and troubleshooting.

#### Secrets

```bash
vatio secrets set ACME_API https://api.acme.com
vatio secrets set ACME_API_KEY YOUR_BACKEND_KEY
vatio secrets list
vatio secrets rm ACME_API_KEY
```

Keys use uppercase letters, digits, and underscores, starting with a letter.
Tools read values with `$env.KEY`. Listing returns names, never
values. Changes take effect on subsequent calls without deploying and apply
to every environment. Rollback does not restore old values.

Keep credentials out of manifests, tool source, knowledge, and tool results.
The workspace upload includes ordinary files, so keep private signing keys
outside that directory.

### Troubleshooting

`https://docs.vatio.ai/troubleshooting`

| Symptom | Check |
|---|---|
| CLI cannot find the workspace | Run from the directory containing `vatio.yml` or below it; inspect with `vatio doctor` |
| CLI authentication fails | Run `vatio login`; verify the host with `vatio doctor` |
| Manifest or tool validation fails | Run `vatio tools check` and fix the reported file; unknown root keys and missing instructions are errors |
| Agent never calls a tool | Add its key to `agent.tools`, describe when to use it, push, and start a fresh test chat |
| Knowledge answers are missing | Check `vatio kb show BASE`, `knowledge` references, and the `knowledge_lookup` tool assignment |
| Agent cannot share a URL | Check it is written verbatim in `links`, in `instructions`, or in a tool result — an assembled or edited URL is stripped |
| Widget does not appear | Check `vatio widget`, the token's environment, a deployed agent, and the host's exact allowed origin |
| Signed-in visitor appears anonymous | Check `auth.public_key` matches the key you sign with, and the JWT's signature, `exp` and `aud` (the workspace slug) |
| WhatsApp or Instagram is connected but silent | Run the channel's `check`; confirm webhook reception and a live agent; WhatsApp must also be activated |
| Agent stopped replying to one chat | Check for human takeover in the inbox and release it when ready |

#### Issues

Send a bug report or feature request from the CLI:

```bash
vatio issue "Describe what happened and what you expected"
vatio issue --template > issue.md
vatio issue --file issue.md
vatio issue list
vatio issue show 42
vatio issue comment 42 "Additional details"
vatio issue "Push is rejected" --no-source
```

Run from a workspace, `vatio issue` **attaches the directory** — the same files
`vatio push` sends. Vatio keeps `vatio.yml` and everything under `tools/`,
lists the rest by name, and records what its own checks made of them, so a
report about a tool arrives with the tool. Any file a check complained about is
kept as well. The command prints what it is attaching before it sends;
`--no-source` sends the report alone. Hidden files are never included, so
`.env` and `.git/` stay on your machine.

Issue and comment commands **send immediately**. Review the text before running
them. Include the failing command, the observed error, and a `request_id` when
available. The CLI includes its version and workspace context; it does not
attach your previous command's output. Remove credentials and customer data
from any report. Follow up on the existing thread so the context stays together.

## CLI

### CLI

`https://docs.vatio.ai/cli/`

The npm package is `@vatio-ai/cli`; its installed executable is `vatio`. The
rest of this reference uses that shorter form:

```bash
npm install -g @vatio-ai/cli@latest
vatio --help
```

Alternatively, replace `vatio` in any command with `npx @vatio-ai/cli@latest`.
Specifying `@latest` explicitly requests the current release. Update a global
install by running the install command again.

Authorize the machine once, and check what the CLI thinks it is talking to:

```bash
vatio login
vatio doctor
```

Every command below finds the nearest `vatio.yml` at or above the current
directory and reads `workspace` from it. Start with the
[quickstart](https://docs.vatio.ai/quickstart) if you have not deployed an agent yet.

#### Commands

- [Workspace and account](https://docs.vatio.ai/cli/workspace) — init, login, doctor, docs, config, issue.
- [Deployments](https://docs.vatio.ai/cli/deploy) — tools check, push, publish, rollback, status, diff.
- [Chats](https://docs.vatio.ai/cli/chat) — chat, and the transcript and debug views of it.
- [Knowledge bases](https://docs.vatio.ai/cli/knowledge) — kb: bases, entries, and the sites that write them.
- [Secrets](https://docs.vatio.ai/cli/secrets) — secrets: the values tools read as $env.KEY.
- [Publishable tokens](https://docs.vatio.ai/cli/tokens) — tokens and widget: what a page carries.
- [Channels](https://docs.vatio.ai/cli/channels) — whatsapp and instagram, live and on the shared preview.
- [Authentication keys](https://docs.vatio.ai/cli/auth) — auth --new-key: the keypair that signs a visitor's JWT.

#### Using it

- [Configuration and uploads](https://docs.vatio.ai/cli/configuration) — Where credentials live, which env vars override them, what a push sends.
- [MCP server](https://docs.vatio.ai/cli/mcp) — vatio mcp exposes these same commands to a coding agent.

### Workspace and account

`https://docs.vatio.ai/cli/workspace`

| Command | Purpose |
|---|---|
| `vatio init [SLUG] [--name NAME]` | Create or use the remote workspace and write a starter manifest in the current directory |
| `vatio login [--base-url URL]` | Authorize this machine through the browser |
| `vatio logout` | Remove the saved token |
| `vatio doctor` | Show Node version, config location, workspace, and token presence |
| `vatio version` | Show CLI and Node versions |
| `vatio docs [--save [PATH]]` | Fetch the docs; print them or save to `vatio-docs.md` / `PATH` |
| `vatio config show` / `get KEY` / `set KEY VALUE` / `unset KEY` | Manage local `base_url` and `token` settings |

`init` writes `vatio.yml` in the **current directory** and opens device
authorization if you are not logged in. Every other command finds the nearest
`vatio.yml` at or above the current directory and reads `workspace` from it —
there is no `--workspace` flag and no `VATIO_WORKSPACE` override.

Reach for `doctor` before anything else when a command cannot find the
workspace or the token: it prints all four things that decide that, and prints
no secrets, so it is the safe one to paste into an issue.

#### Issues

| Command | Purpose |
|---|---|
| `vatio issue "message"` / `--file PATH` / `--template` / `--no-source` | Send a report or fetch the report template; run from a workspace it attaches the directory, `--no-source` does not |
| `vatio issue list` / `show ID` / `comment ID "reply"` | Read and reply to your support threads |

Issue and comment commands **send immediately**. Review the text before running
them, and see [Troubleshooting](https://docs.vatio.ai/troubleshooting#issues) for what to include.

### Deployments

`https://docs.vatio.ai/cli/deploy`

| Command | Purpose |
|---|---|
| `vatio tools check` | Validate files on the platform without deploying |
| `vatio push [--env NAME]` | Update a preview; default `preview` |
| `vatio publish [--env NAME]` | Promote the named preview to live; default `preview` |
| `vatio rollback` | Restore the previous live deployment |
| `vatio status` | Show deployment state |
| `vatio diff [--env NAME]` | Compare local files with a deployment; default `preview` |
| `vatio diff --stat` / `--name-only` / `--format json` / `--full` | Select diff detail or output format |

`push` cannot target `live`: publishing is a separate, deliberate step. `diff`
compares your **local directory** with a deployment, so push local edits before
publishing them.

`--env` takes a [named preview](https://docs.vatio.ai/deployments#named-previews) as well as
`preview`, which is what gives a branch or a pull request its own environment.

See [Deployments](https://docs.vatio.ai/deployments) for what an environment actually holds, and
what a rollback does and does not restore.

### Chats

`https://docs.vatio.ai/cli/chat`

| Command | Purpose |
|---|---|
| `vatio chat "message" [--env NAME] [--timeout N]` | Talk as the CLI token holder; default `preview`, timeout 120 seconds |
| `vatio chat transcript [--env NAME] [--last N]` | Read the current CLI chat as the visitor saw it; default 10 messages |
| `vatio chat debug [--env NAME] [--last N]` | The same chat with everything: tool calls, deleted messages, identity, delivery |
| `vatio chat reset [--env NAME]` | Start a new CLI conversation with a fresh contact |
| `vatio chat destroy CHAT_ID` | Delete a conversation |

`debug` is the one to reach for when an answer is wrong: it shows which tools
ran, what they returned, and whether the workspace recognized the visitor.

The CLI channel identifies the developer holding the token; it does not
simulate a customer's identity. Test on the real channel when identity or
delivery is what you are checking.

`chat --env live` creates a real live conversation — billed, and visible in the
inbox.

Current chats are remembered per environment in `.vatio-chat.json`. Add that
file to your repository's `.gitignore`.

### Knowledge bases

`https://docs.vatio.ai/cli/knowledge`

| Command | Purpose |
|---|---|
| `vatio kb [list]` | List knowledge bases and references |
| `vatio kb show NAME` | Inspect a base: its sites and its entries |
| `vatio kb create NAME` / `vatio kb rm NAME` | Create or delete an unreferenced base |
| `vatio kb write BASE ENTRY [FILE]` | Write an entry, from a file or stdin |
| `vatio kb cat BASE ENTRY` | Print an entry's stored Markdown |
| `vatio kb rm-entry BASE ENTRY` | Delete one entry |
| `vatio kb follow BASE URL` | Read a site into the base, and again every night |
| `vatio kb unfollow BASE URL` | Stop reading it, and drop the entries it wrote |
| `vatio kb refresh BASE [URL]` | Read the sites again now |
| `vatio kb reindex BASE [NAME]` | Re-crawl one source or all crawl sources |

`write` reads stdin when no file is given, which is what makes the edit loop
one line:

```bash
vatio kb cat docs refunds | edit | vatio kb write docs refunds
```

`show` is the one that answers "why is it not answering from this": it reports
each site's status and errors, and which entries are ready to be searched.

**Knowledge is shared by all environments**, so writing, refreshing and
deleting can change live answers immediately — and a deployment rollback does
not restore content. See [Knowledge bases](https://docs.vatio.ai/knowledge).

### Secrets

`https://docs.vatio.ai/cli/secrets`

| Command | Purpose |
|---|---|
| `vatio secrets list` / `set KEY VALUE` / `rm KEY` | Manage shared workspace secrets |

Keys use uppercase letters, digits and underscores, starting with a letter.
Tools read values with `$env.KEY`.

`list` returns names, never values. Changes take effect on subsequent tool
calls without deploying, and apply to every environment — a rollback does not
restore an old value. See [Secrets](https://docs.vatio.ai/deployments#secrets).

Never put a private signing key in here: that store is read by Vatio, and the
whole point of [authentication](https://docs.vatio.ai/authentication/) is that Vatio holds only your
public key.

### Publishable tokens and the widget

`https://docs.vatio.ai/cli/tokens`

| Command | Purpose |
|---|---|
| `vatio tokens list` / `create [--env NAME] [--label NAME]` / `revoke PREFIX` | Manage publishable tokens; `create` defaults to `live` |
| `vatio widget [--env NAME]` | Read widget configuration and show the token-creation command for that environment; default `live` |

`create` prints the full token once. `list` shows prefixes only, which is also
what a revoke takes.

Revoking prevents new chats and conversation lists with that token; chat
credentials already issued stay valid until they expire.

`widget` is read-only — `vatio.yml` owns every field it reports, so a
[`push`](https://docs.vatio.ai/cli/deploy) is what changes them. See
[Web widget](https://docs.vatio.ai/channels/widget) for the fields themselves.

Never use a developer token (`vat_…`) in a browser. Publishable tokens are the
ones meant for page source, and the origin allowlist is what scopes them.

### Channels

`https://docs.vatio.ai/cli/channels`

#### WhatsApp

| Command | Purpose |
|---|---|
| `vatio whatsapp [status]` | Inspect your live number |
| `vatio whatsapp connect` / `check` / `activate` / `deactivate` / `disconnect` | Connect, check, or manage the live number |
| `vatio whatsapp numbers list` / `add PHONE` / `verify PHONE CODE` / `resend PHONE` / `remove PHONE` | Manage phones on the shared preview |

A newly connected number is paused: `check` reports credentials, webhook
reception and activation separately, and `activate` is what starts live
replies. `numbers add` needs no Meta account — the shared preview number is how
you test in a minute. See [WhatsApp](https://docs.vatio.ai/channels/whatsapp).

#### Instagram

| Command | Purpose |
|---|---|
| `vatio instagram [status]` | Inspect your live account |
| `vatio instagram connect` / `check` / `disconnect` | Connect, check, or disconnect the live account |
| `vatio instagram accounts list` / `add @HANDLE` / `verify CODE` / `resend ID` / `remove ID` | Manage accounts on the shared preview |

A workspace connects one account and an account connects to one workspace;
`disconnect` before switching. See [Instagram](https://docs.vatio.ai/channels/instagram).

`connect` on either channel opens a browser for Meta's consent, so it needs the
account holder — it is the one part of this a coding agent cannot do for you.

### Authentication keys

`https://docs.vatio.ai/cli/auth`

| Command | Purpose |
|---|---|
| `vatio auth --new-key` | Generate the keypair that signs your JWT, and print the `auth:` block and claims to use |

It writes two files, and they go to different places. `identity.pub` stays in
the workspace and is committed like any other file; `identity.pem` goes into
your own backend as a secret, and should be deleted from the workspace
directory once it is loaded there.

The command prints the `auth:` block to paste into `vatio.yml` and the claims
to sign, so the output is most of the setup. [Authentication](https://docs.vatio.ai/authentication/)
has the rest, including why `aud` is required and what a private tool sees.

### Configuration and uploads

`https://docs.vatio.ai/cli/configuration`

`~/.vatio/config.json` stores `base_url` and `token`. Environment variables
override saved settings: `VATIO_BASE_URL`, `VATIO_TOKEN`, and `VATIO_HOME`
(the configuration directory). `config show` can print the saved token;
use `doctor` when sharing diagnostics.

`tools check`, `diff`, and `push` send workspace files to the platform for
validation. They require network access and authentication. `push` creates
the remote if missing; the other two require an existing workspace.

Uploads exclude hidden files and directories, `node_modules`, `tmp`, and `log`.
Limits are 500 files, 2 MB per file, and 8 MB total. These exclusions do not
make an ordinary file safe to store credentials in.

### Connect MCP

`https://docs.vatio.ai/cli/mcp`

Vatio provides an MCP server over stdio. In your MCP client's server settings,
use this command and arguments:

```json
{
  "mcpServers": {
    "vatio": {
      "command": "vatio",
      "args": ["mcp"]
    }
  }
}
```

Use your client's configuration format if it differs. If your client cannot
find `vatio` on its `PATH`, use `"command": "npx"` with
`"args": ["-y", "@vatio-ai/cli@latest", "mcp"]` instead. Log in with the CLI
first. Start the server in the workspace directory, or pass the absolute
workspace path as `workspace_dir` on tool calls:

```json
{
  "args": ["--env", "pr-42"],
  "workspace_dir": "/path/to/support-agent"
}
```

That is a call to `vatio_push`. Each MCP tool takes the arguments after its
CLI command as an array of strings. A message containing spaces is one array
element.

#### What it exposes

MCP exposes docs, diagnostics, validation, deployment, chat, knowledge,
secrets, tokens, widget inspection, and channel management. Login, workspace
creation, issue submission, configuration, channel connection/disconnection,
chat deletion, and knowledge-base deletion use the CLI directly. Browser
consent and verification codes require the account holder.

#### Limits

MCP command output is limited to 20,000 characters. For the full docs, use
`vatio_docs` with `args: ["--save", "/absolute/path/vatio-docs.md"]` and read
that file with your coding agent's file tools. Commands time out after
180 seconds.

## SDK

### Browser SDK

`https://docs.vatio.ai/sdk/`

The browser SDK is an npm package with no dependencies, and types included.
Create a publishable token and allow your page's origin as described in
[the widget guide](https://docs.vatio.ai/channels/widget).

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

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

const options = { workspace: "acme", token: "vatpub_REPLACE_ME" };
const chat = await Vatio.chat(options);
const messages = new Map();

function receive(message) {
  messages.set(message.id, message);
  // Render messages ordered by ID in your UI, escaping untrusted content.
}

chat.on("message", receive);
chat.on("typing", (typing) => { /* Update your typing indicator. */ });
chat.on("status", (status) => { /* Update your connection indicator. */ });
chat.on("error", (error) => console.error(error.code, error.message));

for (const message of await chat.history()) receive(message);
await chat.send("What does Acme do?");
// Call chat.close() when this view unmounts.
```

#### Without a bundler

Any npm CDN serves the same module:

```html
<script type="module">
  import { Vatio } from "https://cdn.jsdelivr.net/npm/@vatio-ai/sdk/+esm";
</script>
```

Pin the major the ordinary way — `"@vatio-ai/sdk": "^2.0.0"`. The SDK used to
ship from a versioned URL (`cdn.vatio.ai/v1/sdk.js`) for exactly this reason,
and a semver range says it better: a breaking change is a new major you upgrade
to when you choose, not a second URL to notice.

Next: the [reference](https://docs.vatio.ai/sdk/reference) for every method and option, and the
[limits](https://docs.vatio.ai/sdk/limits) every page hits.

### SDK reference

`https://docs.vatio.ai/sdk/reference`

`Vatio.chat` starts or resumes a conversation and waits for a subscription
before resolving. The SDK handles reconnects, history resync, and polling
fallback. Messages include `id`, `role`, and `content`. Deduplicate history
and event delivery by `id`.

| Method | Purpose |
|---|---|
| `Vatio.config({ workspace, token })` | Fetch public agent and widget configuration |
| `Vatio.chat(options)` | Start, resume, or reopen a conversation |
| `Vatio.conversations({ workspace, token, ... })` | List this browser visitor's conversations, newest first |
| `Vatio.resumable({ workspace, ... })` | Check for a locally stored, unexpired conversation |
| `Vatio.reset({ workspace, ... })` | Forget the locally stored conversation; does not delete server history |
| `chat.send(text)` | Send a message |
| `chat.history({ limit })` | Fetch messages |
| `chat.on(event, handler)` | Subscribe to `message`, `typing`, `status`, or `error` |
| `chat.close()` | Disconnect the client |

#### Options

`Vatio.chat` accepts `workspace` and `token`, plus optional `baseUrl`, `fresh`,
`scope`, `visitorToken`, `replyStyle`, and `conversation`. `fresh: true` starts
a new chat. `conversation` reopens an entry returned by `Vatio.conversations`.

Use a distinct `scope` when preview and live UIs share an origin. Pass the
same `scope` and `visitorToken` to `chat`, `conversations`, `resumable`, and
`reset`. Storage is separated by the complete visitor token, so issuing a new
token also changes its local conversation scope. It does not merge contacts
across devices.

#### Storage

The SDK stores the current conversation per tab and a visitor reference in
local storage. Chat credentials last 12 hours. Treat visitor references and
chat credentials as private: the reference can retrieve that visitor's
conversation list and fresh chat credentials, so store the reference where the
signed-in user cannot read another's, and never derive it from a public or
predictable user ID. Issue a new reference when the signed-in user changes.

### Limits and delivery

`https://docs.vatio.ai/sdk/limits`

#### Limits

Messages are limited to 4,000 characters. Per browser IP and workspace, per
minute: 10 conversations started, 30 messages sent, 60 configuration reads, and
60 conversation lists. Over the limit the SDK surfaces an `error` event and the
underlying request answers HTTP 429; back off rather than retrying immediately.

Every request the SDK makes is checked against the workspace's origin
allowlist, so a publishable token lifted from your HTML is useless from another
page. The allowlist is what keeps a token on your own site; it is not evidence
of who the visitor is. To tell the agent who is signed in, see
[Sessions and channels](https://docs.vatio.ai/authentication/sessions).

#### Reply style

| `replyStyle` | Delivery |
|---|---|
| `stream` | Default on web: one complete reply, without artificial delay |
| `paced` | Short messages with typing indicators and pauses |
| `instant` | One complete reply with no typing events |

`stream` is not token-by-token network streaming. Render the received reply
progressively if desired. The style is fixed at chat creation; use `fresh: true`
when changing it.

Use the SDK for realtime delivery. The underlying socket protocol is not a
public integration contract.

## API Reference

### API Reference

`https://docs.vatio.ai/api/`

Base URL: `https://vatio.ai`. All request bodies below are JSON; send
`Content-Type: application/json` on requests with a body. Replace `:slug`,
`:id`, and other path placeholders with the actual value.

Your customers do not use this API. A browser talks to Vatio through the
[SDK](https://docs.vatio.ai/sdk/) with a publishable token, over a separate surface that exposes one
conversation and nothing else about the workspace.

#### Getting a token

Every request below is authorized by a **developer token**, which looks like
`vat_…`. There is no token page in a console to copy one from: a developer
token is minted by device authorization, where a human approves this machine in
a browser once.

Start the flow, and read `device_code`, `user_code`, `verification_uri_complete`
and `interval` off the response:

```http
POST /cli/device_authorizations
Content-Type: application/json
```

Send the person to `verification_uri_complete`, then poll at `interval` seconds
until it answers with the token instead of `authorization_pending`:

```http
POST /cli/device_authorizations/token
Content-Type: application/json

{ "device_code": "..." }
```

Neither path is workspace-scoped and neither takes a token — they are how you
obtain one. [Device authorization](https://docs.vatio.ai/api/device-authorization) has the full
flow, including how to list and create workspaces with the result.

If you have a terminal in front of you, `vatio login` runs exactly these two
calls and writes the token to `~/.vatio/config.json`, where you can read it.

#### Credentials

Send the token as `Authorization: Bearer TOKEN`. Every path below is relative
to `/api/v1/:slug`, where `:slug` is the workspace.

One token reaches every workspace its user owns. It can deploy, read secrets,
start conversations and mint publishable tokens, so it belongs on a server or
in a CI secret — never in a page, a mobile app, or a repository.

Errors carry `error_key` and `error_message`, or `error` and
`error_description` on authentication failures. See [Errors](https://docs.vatio.ai/api/errors).

#### Endpoints

- [Deployments](https://docs.vatio.ai/api/deployments) — Check, deploy, publish, roll back, and read deployment history.
- [Secrets](https://docs.vatio.ai/api/secrets) — Write-only workspace secrets your tools read as $env.KEY.
- [Knowledge bases](https://docs.vatio.ai/api/knowledge) — Bases, entries and the sites that write them.
- [Publishable tokens](https://docs.vatio.ai/api/tokens) — Mint and revoke the vatpub_ tokens a page carries.
- [Channels](https://docs.vatio.ai/api/channels) — WhatsApp and Instagram connections, and shared-preview registrations.
- [Chats](https://docs.vatio.ai/api/chats) — Your own developer conversations, with the full debug view.
- [Phone verification](https://docs.vatio.ai/api/phone-verification) — WhatsApp OTP for your own backend, with no agent involved.

### Errors

`https://docs.vatio.ai/api/errors`

Authentication and access errors generally return:

```json
{
  "error": "workspace_forbidden",
  "error_description": "Token does not have access to this workspace",
  "request_id": "REQUEST_ID"
}
```

HTTP 401 means invalid credentials; 403 means missing permission or a disallowed
origin; 404 means a missing resource. Validation commonly uses 422.
Deploy and chat errors may use `error_key` and `error_message`.
Phone-verification service errors use `{ "error": { "code": "...", "message": "..." } }`.
Retain `request_id` when the response includes one.

### Device authorization

`https://docs.vatio.ai/api/device-authorization`

`vatio login` is the normal route, and these paths are how it works. They are
not workspace-scoped and take no developer token — they are how you obtain one.

A custom CLI starts device authorization with
`POST /cli/device_authorizations`. The response supplies `device_code`,
`user_code`, verification URLs, `interval`, and `expires_in`.

Have the user open `verification_uri_complete`, then poll
`POST /cli/device_authorizations/token` with `{ "device_code": "..." }` at
the supplied interval. `GET /cli/workspaces` lists workspaces and
`POST /cli/workspaces` creates one with `slug` and optional `name`, using the
resulting developer token.

### Deployments API

`https://docs.vatio.ai/api/deployments`

| Method | Path | Purpose |
|---|---|---|
| POST | `/deploy/check` | Validate a file bundle and return a manifest and diff |
| PATCH | `/deploy/preview` | Deploy the validated manifest to a preview |
| POST | `/deploy/publish` | Promote a preview to live |
| POST | `/deploy/rollback` | Restore previous live |
| GET | `/deploy/status` | Deployment pointers |
| GET | `/deploy/manifest?environment=preview` | A deployed manifest |
| GET | `/deploy/revisions` | Deployment history |
| GET | `/deploy/revisions/:id` | One deployment |

First call `POST /deploy/check` with the workspace files:

```json
{
  "environment": "preview",
  "files": [
    {
      "path": "vatio.yml",
      "encoding": "utf-8",
      "content": "workspace: acme\nagent:\n  instructions: Help visitors learn about Acme.\n"
    }
  ]
}
```

Each file has a relative `path`, `content`, and `encoding` (`utf-8` or `base64`).
The response contains `ok`, `manifest`, `errors`, `warnings`, and `diff`.
Diagnostics contain `message` and `path`. Validation failure is HTTP 200 with
`ok: false`; an unreadable or oversized bundle is 422.

When `ok` is true, send the returned `manifest` unchanged to
`PATCH /deploy/preview` with optional `git_sha` and `environment` (default
`preview`). Do not construct the normalized manifest yourself. The response
includes `deployment_id`, `environment`, `share_url`, and `preview_url`.

Publish with `{ "environment": "preview" }`, or name another preview.
Rollback needs no body. Neither operation uploads your local files.

### Secrets API

`https://docs.vatio.ai/api/secrets`

| Method | Path | Purpose |
|---|---|---|
| GET | `/deploy/secrets` | Secret names |
| PATCH | `/deploy/secrets/:key` | Set a secret with `{ "value": "..." }` |
| DELETE | `/deploy/secrets/:key` | Remove a secret |

Values are write-only: reading lists keys, never contents.

### Knowledge bases API

`https://docs.vatio.ai/api/knowledge`

| Method | Path | Body or result |
|---|---|---|
| GET / POST | `/knowledge_bases` | List bases / create with `{ "name": "docs" }` |
| GET / DELETE | `/knowledge_bases/:name` | Read a base / delete when unreferenced |
| PATCH | `/knowledge_bases/:name/entries/:entry` | `{ "content": "# Refunds\n..." }` — creates the entry or replaces it |
| GET | `/knowledge_bases/:name/entries/:entry` | The entry, with its stored Markdown |
| POST | `/knowledge_bases/:name/sites` | `{ "url": "acme.com/help/**" }` |
| DELETE | `/knowledge_bases/:name/sources/:source` | Delete a source and its entries |
| POST | `/knowledge_bases/:name/sources/:source/reindex` | Queue a crawl; uploads cannot be reindexed |

### Publishable tokens API

`https://docs.vatio.ai/api/tokens`

| Method | Path | Body or result |
|---|---|---|
| GET | `/publishable_tokens` | List active developer-created token prefixes |
| POST | `/publishable_tokens` | `{ "environment": "live", "label": "website" }`; returns the full token once |
| DELETE | `/publishable_tokens/:prefix` | Revoke a token |
| GET | `/widget` | Widget configuration, origin allowlist, agent availability, and token prefixes |

Publishable token environments accept named previews as well as `live` and
`preview`; omission defaults to `live`. `GET /widget` is read-only — `vatio.yml`
owns every field it reports, so a push is what changes them.

### Channels API

`https://docs.vatio.ai/api/channels`

| Method | Path | Purpose / body |
|---|---|---|
| GET / POST / DELETE | `/whatsapp_account` | Status / get browser connection URL / disconnect |
| POST | `/whatsapp_account/check` | Check credentials and repair webhook subscription |
| POST | `/whatsapp_account/activate` | Enable live replies |
| POST | `/whatsapp_account/deactivate` | Pause live replies |
| GET / POST / DELETE | `/instagram_account` | Status / get browser consent URL / disconnect |
| POST | `/instagram_account/check` | Check credentials and repair webhook subscription |
| GET / POST | `/test_phone_numbers` | List / register with `{ "phone_number": "+56912345678" }` |
| POST | `/test_phone_numbers/:wa_id/verify` | `{ "code": "123456" }` |
| POST | `/test_phone_numbers/:wa_id/resend` | Resend a code |
| DELETE | `/test_phone_numbers/:wa_id` | Remove a test phone |
| GET / POST | `/test_instagram_accounts` | List / declare with `{ "username": "yourhandle" }` |
| GET / DELETE | `/test_instagram_accounts/:id` | Read / remove a test account |
| POST | `/test_instagram_accounts/verify` | `{ "code": "123456" }` |
| POST | `/test_instagram_accounts/:id/resend_otp` | Resend after the account's first DM |

`:wa_id` is the phone's digits without `+`. Registering a phone already verified
on this workspace succeeds without sending a code. A verified phone on another
workspace you own can be moved here. Codes arrive through the channel and are
never returned by the API.

### Chats API

`https://docs.vatio.ai/api/chats`

A chat here is you, the developer holding the token, on channel `cli`. It is
not a way to impersonate a customer: `channel`, `from`, `as`, `session_id`,
`reply_style`, `email` and `phone_number` are all refused, and `environment` is
the only field create and reset accept.

| Method | Path | Body / query |
|---|---|---|
| POST | `/chats` | `{ "environment": "preview" }` → `chat_id` |
| GET | `/chats/:id` | The conversation and its identity state |
| POST | `/chats/:id/reset` | `{ "environment": "preview" }` → a new `chat_id` |
| DELETE | `/chats/:id` | Delete the conversation |
| POST | `/chats/:id/messages` | `{ "content": "Hello" }` → 202 with `user_message_id` |
| GET | `/chats/:id/messages` | `after` and `limit` |

**`environment` is required on create and reset**, and it is the one field in
this API with no default. Everywhere else an omitted environment falls back to
something harmless; here the fallback would be `live`, and a request missing one
word would be a real conversation with your published agent — billed, visible in
the inbox, and running your JS tools against live secrets. Send `preview` to
talk to a preview deployment, or `live` when you mean it. A name that is not an
environment is 422, not a guess.

Replies are asynchronous: post a message, then poll `GET /chats/:id/messages`
with `after` set to the last id you have seen.

Both read endpoints always return the developer view, because the token already
is the developer view — deleted messages, tool calls with their results,
attachments, channel delivery receipts, and an `identity` block saying whether
the workspace recognized the visitor and why not. There is no view parameter to
choose; print as much of the payload as your caller needs.

This API only sees the conversations it started. To read what real customers
said on the widget, WhatsApp or Instagram, use [the inbox](https://docs.vatio.ai/channels/inbox).

### Phone verification API

`https://docs.vatio.ai/api/phone-verification`

No CLI command: this one is for your own backend, to verify a phone through
WhatsApp OTP. An existing workspace and a WhatsApp sender are required; an agent
deployment is not. It does not authenticate a visitor inside an agent
conversation.

```http
POST /api/v1/:slug/phone_verifications
Content-Type: application/json
Authorization: Bearer VATIO_DEVELOPER_TOKEN

{ "phone_number": "+56912345678" }
```

The 201 response contains `verification_id` and `expires_at`. Delivery is
queued; 201 does not confirm that the code reached the phone. During the resend
cooldown, the response is 429 with `verification_id`, `expires_at`, and
`retry_after_seconds`.

```http
POST /api/v1/:slug/phone_verifications/:id/verify
Content-Type: application/json
Authorization: Bearer VATIO_DEVELOPER_TOKEN

{ "code": "123456" }
```

Success returns `{ "verified": true }`. An invalid code returns 422 with
`verified: false` and `attempts_left`; five failed attempts lock the verification.

A workspace dedicated to this API declares `agent: false` — see
[the manifest](https://docs.vatio.ai/manifest#root-keys).
