> ## 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`, `tool_events`, `timezone`, and
  `world_context` belong in your backend's creation request. The SDK forwards
  these options (and its `language` shorthand for `overrides.language`) 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, up to 256 characters. Stored on the session and echoed in [webhook](/agents/monitor/webhooks) payloads and [custom LLM](/agents/build/custom-llm) requests.                                                                                             |
| `metadata`          | object, optional  | Your own key-value namespace. Stored and returned verbatim on session queries and webhooks, never read or interpreted by the platform.                                                                                                                                                    |
| `llm_extra_body`    | object, optional  | JSON object (at most 16 KB) forwarded verbatim to your [custom LLM](/agents/build/custom-llm) endpoint on every request as `fishaudio_extra_body`, for example the chat or thread the user is in. Not stored or returned on session reads; ignored when the agent uses a platform model.  |
| `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; 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:

| 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 4,000 tokens               | Replaces the configured system prompt entirely, under the same token budget. `{{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.

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.

## 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; 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 call terminates and 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`  | A keyless (public-agent) request sent an override [public sessions don't accept](#overrides), or a `name`.                                   |
| `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>
