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

# Authenticated Sessions

> Create short-lived session tokens on your backend and run private agents in any client

An authenticated session starts on your server: your backend calls the session endpoint with your API key and receives a short-lived **session token**, which your client uses to connect. The API key never leaves your server, and every session parameter — user identity, overrides, dynamic variables — is set by code you trust. This is how private agents run in production.

The token is client-agnostic. Create it the same way regardless of which surface renders the conversation:

| Client                                         | Where the token goes                                                                             |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| [Web SDK](/agents/deploy/web-sdk)              | `AgentSession.start({ sessionToken })`                                                           |
| [React SDK](/agents/deploy/react-sdk)          | `startSession({ sessionToken })`                                                                 |
| [Widget](/agents/deploy/widget#private-agents) | Returned from `sessionTokenProvider`                                                             |
| [Custom client](/agents/deploy/protocol)       | Connect to the transport named in the response, per the [wire protocol](/agents/deploy/protocol) |

<Note>
  No backend, and anyone may talk to the agent? A [public
  agent](/agents/deploy/public-agents) lets the SDK create sessions with just an
  `agentId` — no token involved, gated by an origin allowlist and rate limits.
</Note>

## Create a token on your backend

Your backend exchanges your API key for a single-conversation token.

<Steps>
  <Step title="Create a session from your backend">
    Call `POST /v1/agent/sessions` with your API key. This is where you set
    per-session parameters — user identity, overrides, dynamic variables.
  </Step>

  <Step title="Return the response to your frontend">
    The response is the session token. Forward it verbatim.
  </Step>

  <Step title="Start the client with the token">
    Pass the object to `AgentSession.start({sessionToken})` unmodified. The SDK
    handles the connection from there.
  </Step>
</Steps>

<CodeGroup>
  ```bash API (curl) theme={null}
  curl --request POST https://api.fish.audio/v1/agent/sessions \
    --header "Authorization: Bearer $FISH_API_KEY" \
    --header "Content-Type: application/json" \
    --data '{
      "agent_id": "YOUR_AGENT_ID",
      "end_user_id": "user_42",
      "dynamic_variables": { "name": "Ada" }
    }'
  ```

  ```javascript Backend (Express) theme={null}
  app.post("/voice-session", async (req, res) => {
    const upstream = await fetch("https://api.fish.audio/v1/agent/sessions", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.FISH_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        agent_id: "YOUR_AGENT_ID",
        end_user_id: req.user.id,
        dynamic_variables: { name: req.user.name },
      }),
    });
    if (!upstream.ok) {
      console.error("session creation failed:", upstream.status);
      return res.status(502).json({ error: "session_unavailable" });
    }
    res.json(await upstream.json());
  });
  ```

  ```python Backend (Python) theme={null}
  import os
  import requests

  def create_voice_session(end_user_id: str, name: str) -> dict:
      response = requests.post(
          "https://api.fish.audio/v1/agent/sessions",
          headers={"Authorization": f"Bearer {os.environ['FISH_API_KEY']}"},
          json={
              "agent_id": "YOUR_AGENT_ID",
              "end_user_id": end_user_id,
              "dynamic_variables": {"name": name},
          },
      )
      response.raise_for_status()
      return response.json()
  ```
</CodeGroup>

On the client, fetch the token from your backend and pass it to the SDK unchanged:

```javascript Browser theme={null}
import { AgentSession } from "@fishaudio/agent-client";

const sessionToken = await fetch("/voice-session", { method: "POST" }).then(r =>
  r.json()
);

const session = await AgentSession.start({ sessionToken });
```

More on what the SDK can do once connected is in the [Web SDK](/agents/deploy/web-sdk) reference.

<Note>
  `overrides`, `dynamic_variables`, `language`, `tool_events`, `timezone`, and
  `world_context` belong in your backend's creation request — the SDK forwards
  these options only in [public agent](/agents/deploy/public-agents) mode.
</Note>

## Request fields

| Field               | Type              | Description                                                                                                                                                                                                                                                                               |
| ------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_id`          | string, required  | The agent to talk to. It must have a [published version](/agents/deploy/versions-publishing).                                                                                                                                                                                             |
| `name`              | string, optional  | Display name for this session in the console's Conversations list, up to 128 characters. Omit it to show the session's start time instead. API-key requests only — keyless (public) creation rejects it with `400`.                                                                       |
| `overrides`         | object, optional  | Replace parts of the published configuration for this session — see [Overrides](#overrides).                                                                                                                                                                                              |
| `dynamic_variables` | object, optional  | Up to 50 entries of string, number, or boolean values, substituted into `{{placeholders}}`. See [Dynamic variables](/agents/build/dynamic-variables).                                                                                                                                     |
| `tool_events`       | boolean, optional | Stream tool lifecycle events (`toolCallStarted` / `toolCallCompleted` / `toolCallFailed`) to the client. Default `true`; set `false` to keep tool inputs and outputs off the client.                                                                                                      |
| `end_user_id`       | string, optional  | Your identifier for the end user, for attribution in [conversation history](/agents/monitor/conversation-history).                                                                                                                                                                        |
| `metadata`          | object, optional  | Your own key-value namespace. Stored and returned verbatim on session queries and webhooks — never read or interpreted by the platform.                                                                                                                                                   |
| `record_audio`      | boolean, optional | Whether to record this session's audio. Overrides the agent's [recording setting](/agents/monitor/conversation-history#what-gets-stored) for this session only — it never changes the agent; omit it to use the agent's configuration.                                                    |
| `timezone`          | string, optional  | IANA timezone (like `Asia/Shanghai`) for the agent's sense of local time. Invalid names are rejected with `422`. See [Time & timezone](/agents/build/time-timezone).                                                                                                                      |
| `client_timezone`   | string, optional  | The end user's browser timezone, filled automatically by the SDK in public-agent mode. A hint, not a demand: it applies only when neither `timezone` nor the agent's configured timezone is set, and invalid values are ignored. See the [resolution order](/agents/build/time-timezone). |
| `world_context`     | boolean, optional | Whether the agent knows the current date and time. Default `true`; set `false` to withhold both from this session.                                                                                                                                                                        |

Unknown fields — top-level or inside `overrides` — are rejected with `422`.

## Overrides

`overrides` replaces parts of the agent's published configuration for one session — the agent itself never changes:

| Field                  | Type                                     | Effect for this session                                                                                                                                                                                           |
| ---------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `first_message`        | string, up to 10,000 characters          | The agent opens with exactly this text, spoken verbatim — whatever first-message mode the configuration sets. `{{placeholders}}` render inside it.                                                                |
| `first_message_prompt` | string, up to 10,000 characters          | Instructions the agent generates its opener from, replacing the configured first-message behavior. `{{placeholders}}` render inside it. Mutually exclusive with `first_message` — sending both is `422`.          |
| `system_prompt`        | string, up to 8,000 characters           | Replaces the configured system prompt entirely. `{{placeholders}}` render inside it.                                                                                                                              |
| `voice_id`             | string                                   | The voice the agent speaks with — any [voice model id](/agents/build/voice-language#use-any-voice-model) from the Voice Library. Voices bias pronunciation toward their own language, so pair it with `language`. |
| `language`             | `en`, `ja`, `zh`, `ko`, `es`, `fr`, `de` | Pins the conversation language, taking precedence over the configured [speaking language](/agents/build/voice-language#speaking-language).                                                                        |

```json theme={null}
{
  "agent_id": "YOUR_AGENT_ID",
  "overrides": {
    "first_message": "Welcome back, {{name}} — picking up where we left off.",
    "voice_id": "802e3bc2b27e49c2995d23ef70e6ac89",
    "language": "ja"
  }
}
```

On [public agents](/agents/deploy/public-agents), keyless (browser-created) sessions may override only `language` and `voice_id` — the prompt-shaping fields are rejected with `400` regardless of the agent. Create the session from your backend to use them.

The session record stores which overrides took effect.

Everything else on the request is not an override: `dynamic_variables`, `timezone`, `record_audio`, and `tool_events` are separate top-level fields. Session length is not settable per session — `max_duration_seconds` comes from the agent's [conversation configuration](/agents/build/configuration#call-duration).

## Response

```json JSON theme={null}
{
  "session_id": "abc123",
  "expires_at": "2026-07-23T12:00:00Z",
  "max_duration_seconds": 1800,
  "transport": "livekit",
  "livekit_url": "wss://example.livekit.cloud",
  "token": "<participant token>"
}
```

| Field                  | Description                                                                                                 |
| ---------------------- | ----------------------------------------------------------------------------------------------------------- |
| `session_id`           | The session's id — use it later to look up the [conversation record](/agents/monitor/conversation-history). |
| `expires_at`           | Deadline for the client to connect. Create the token right before starting, not ahead of time.              |
| `max_duration_seconds` | Hard cap on session length.                                                                                 |
| `transport`            | Which transport the SDK uses for this session; `livekit` today.                                             |
| `livekit_url`, `token` | Connection details for that transport, consumed by the SDK.                                                 |

Treat the response as opaque and pass it to `start()` unmodified. If the SDK does not recognize the `transport` value, it fails fast with an `unsupported_transport` error asking you to upgrade the SDK — it never silently degrades.

## Token lifetime

* A session token is **single-use**: `start()` consumes it once to establish the conversation.
* On network drops the SDK reconnects at the transport level using the same connection state — it never re-creates the session, so you never need a fresh token mid-call.
* Once a session ends, the token is spent. Create a new token for each conversation.

## Ending sessions from your backend

A session normally ends from the client side — the user disconnects, or the agent [hangs up](/agents/build/system-tools). To force-end a live session server-side, call the end endpoint with your API key and the `session_id` from the creation response:

```bash Request theme={null}
curl --request POST https://api.fish.audio/v1/agent/sessions/$SESSION_ID/end \
  --header "Authorization: Bearer $FISH_API_KEY"
```

The agent disconnects and the call terminates — the response is `204`. The session record stays readable in [conversation history](/agents/monitor/conversation-history), along with the transcript and recording when the agent [stores them](/agents/monitor/conversation-history#what-gets-stored). The same endpoint ends in-progress [phone calls](/agents/telephony/inbound-calls) too.

## Keep API keys on the server

<Warning>
  Your API key grants full access to every resource in your team — agents,
  sessions, tools, phone numbers. Never embed it in a browser, mobile app, or
  any code you ship to users. The only credential that belongs in a client is
  the session token, and the only credential-free path is a public agent ID.
</Warning>

## Error responses

Session creation fails with standard HTTP statuses; the SDK surfaces them as a `FishAgentError` with `statusCode` set.

| Status | Meaning                                                                                                                                       |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | An `overrides` field is not enabled for this agent, or a keyless request sent an override [public sessions don't accept](#overrides).         |
| `401`  | Invalid API key. A request with no `Authorization` header at all is treated as a public-agent request instead.                                |
| `402`  | Quota exceeded.                                                                                                                               |
| `403`  | Public-agent request rejected: the agent is not public, or the page's `Origin` is not on the allow-list.                                      |
| `409`  | Conflict — most commonly the agent has no published version yet. [Publish](/agents/deploy/versions-publishing) to resolve.                    |
| `422`  | The request body failed validation — an unknown field, an unsupported `language` code, an invalid `timezone`, or an invalid dynamic variable. |
| `429`  | Rate limited — a public-agent request exceeded the per-agent or per-IP limit.                                                                 |

The complete status-code reference for every `/v1/agent` endpoint — error shapes, `400` vs `422`, conflict semantics — is on [Agents API errors](/api-reference/agent-errors).

## Going further

<CardGroup cols={2}>
  <Card title="Public agents" icon="globe" href="/agents/deploy/public-agents">
    The credential-free alternative, with origin allow-lists.
  </Card>

  <Card title="Widget with private agents" icon="puzzle-piece" href="/agents/deploy/widget#private-agents">
    Feed the widget session tokens from your backend via `sessionTokenProvider`.
  </Card>

  <Card title="Web SDK" icon="js" href="/agents/deploy/web-sdk">
    Everything `AgentSession` can do once connected.
  </Card>

  <Card title="Dynamic variables" icon="brackets-curly" href="/agents/build/dynamic-variables">
    Personalize each session at creation time.
  </Card>
</CardGroup>
