> ## Documentation Index
> Fetch the complete documentation index at: https://hanabiaiinc-chore-update-openapi-schema.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Public Agents

> Let visitors talk to your agent directly from the browser, no backend required

A public agent accepts sessions straight from the browser: the SDK calls the session endpoint with just an `agent_id`, no API key and no server of your own. The platform gates access with three controls: an agent-level public switch (off by default), a required origin allowlist, and rate limiting.

Use this mode for demos, marketing pages, and support bubbles where standing up a backend isn't worth it. For production apps with their own users, [authenticated sessions](/agents/deploy/authenticated-sessions) are usually the better fit. See [when to use session tokens instead](#when-to-use-session-tokens-instead).

## Enable public access

<Steps>
  <Step title="Publish the agent">
    Public sessions always run the agent's **published** configuration. An
    unpublished agent rejects session creation. See [Versions &
    publishing](/agents/deploy/versions-publishing).
  </Step>

  <Step title="Turn on the public switch">
    On the agent's **Deploy** page in the console, turn on **Public access**. It
    is **off by default**. Until you enable it, sessions require
    authentication.
  </Step>

  <Step title="Add allowed origins">
    On the same page, list every web origin that may start sessions under
    **Allowed origins** (`allowed_origins` in the API). The allowlist is
    **required**: a request whose `Origin` doesn't match any entry is rejected
    with `403`.
  </Step>
</Steps>

## Start a session from the browser

Pass `agentId` to the SDK and it creates the session directly with the Fish Audio API, then connects the audio.

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { AgentSession } from "@fishaudio/agent-client";

  const session = await AgentSession.start({
    agentId: "YOUR_AGENT_ID",
  });

  session.on("agentResponse", ({ text }) => console.log("Agent:", text));
  ```

  ```bash API (curl) theme={null}
  # No Authorization header. The Origin header is checked instead.
  # Browsers send Origin automatically; curl must set it explicitly.
  curl --request POST https://api.fish.audio/v1/agent/sessions \
    --header "Content-Type: application/json" \
    --header "Origin: https://app.example.com" \
    --data '{ "agent_id": "YOUR_AGENT_ID" }'
  ```
</CodeGroup>

The response is the same session token an authenticated request returns. The SDK consumes it internally. Everything else (events, methods, client tools) works exactly as in the [Web SDK](/agents/deploy/web-sdk).

## Origin matching

An entry matches only on **exact scheme + host + port**. The host is case-insensitive, and a trailing slash on an entry is tolerated.

| Allowlist entry           | Browser origin                 | Allowed                        |
| ------------------------- | ------------------------------ | ------------------------------ |
| `https://app.example.com` | `https://app.example.com`      | Yes                            |
| `https://app.example.com` | `https://APP.example.com`      | Yes (host is case-insensitive) |
| `https://app.example.com` | `http://app.example.com`       | No (scheme differs)            |
| `https://app.example.com` | `https://app.example.com:8443` | No (port differs)              |
| `http://localhost:5173`   | `http://127.0.0.1:5173`        | No (different hosts)           |

<Warning>
  `localhost` and `127.0.0.1` are **different origins**. If you develop against both, list both:

  ```text theme={null}
  http://localhost:5173
  http://127.0.0.1:5173
  ```
</Warning>

## Rate limiting

Public session creation is rate limited on two dimensions at once: per **client IP** and per **agent**. Either limit being exceeded rejects the request. Sessions started on a public agent are billed to the workspace that owns the agent.

<Note>
  If legitimate traffic outgrows these limits, or you need per-user quotas, move
  to [authenticated sessions](/agents/deploy/authenticated-sessions): your
  backend becomes the gate, and the platform-side public limits no longer apply.
</Note>

## Treat public input as untrusted

On the public path, the entire session request originates in the visitor's browser. The platform enforces its own guardrails (public sessions accept only the `language` and `voice_id` [overrides](/agents/deploy/authenticated-sessions#overrides), rejecting everything else with `400`), but anything that passes through verbatim is attacker-controllable:

* **`metadata`** is stored and returned exactly as sent, never interpreted by the platform. When you read it back in [conversation history](/agents/monitor/conversation-history) or [webhooks](/agents/monitor/webhooks), treat it as untrusted data, never as proof of who the visitor is.
* **`dynamic_variables`** are chosen by the page that starts the session. Don't inject anything through them that the visitor shouldn't control. See [Dynamic variables](/agents/build/dynamic-variables).

For trusted attribution (a verified `end_user_id`, server-set metadata), create sessions from your backend instead.

## Errors

Public-mode failures surface as `FishAgentError` codes on `AgentSession.start()`:

| Code                     | Meaning                                                                  |
| ------------------------ | ------------------------------------------------------------------------ |
| `agent_not_public`       | The agent's public switch is off.                                        |
| `origin_forbidden`       | The page's origin is not in `allowed_origins` (HTTP `403`).              |
| `session_request_failed` | Other creation failures, for example the agent has no published version. |

## When to use session tokens instead

Public mode trades control for zero setup. Prefer the `sessionToken` flow (your backend calls the session endpoint with an API key and hands the token to the browser) when any of these apply:

* The agent should stay **private** rather than callable by anyone who loads your page.
* You need **trusted attribution**: an `end_user_id` and `metadata` set by your server, not the browser.
* You want server-side control of `overrides`, `dynamic_variables`, or `tool_events` instead of accepting the browser's values.
* You already authenticate users and want **your own quotas** per account, not IP-based platform limits.

The SDK call changes by one field: pass `{ sessionToken }` instead of `{ agentId }`. See [Authenticated sessions](/agents/deploy/authenticated-sessions) for the full flow.

## Going further

<CardGroup cols={2}>
  <Card title="Authenticated sessions" icon="server" href="/agents/deploy/authenticated-sessions">
    The session token flow in full: request fields, lifetime, errors.
  </Card>

  <Card title="Web SDK" icon="js" href="/agents/deploy/web-sdk">
    Events, methods, and client tools, identical in both modes.
  </Card>

  <Card title="React SDK" icon="react" href="/agents/deploy/react-sdk">
    Hooks and components for React apps.
  </Card>

  <Card title="Conversation history" icon="clock-rotate-left" href="/agents/monitor/conversation-history">
    Review what public visitors said to your agent.
  </Card>
</CardGroup>
