---
title: Web SDK quickstart
description: Drive the whole avatar session from the browser with @avtr/web-sdk — session lifecycle, token handling, and error semantics in one client.
sidebar:
  label: Web SDK quickstart
  order: 5
---

The browser SDK packages the canonical `/v1/` flow — create, connect, refresh,
status, end — behind one client, so your frontend never hand-rolls REST calls
or token logic. The SDK is bound to the same generated contract as this site:
it never invents endpoints, fields, or error codes.

## Install and construct

```bash
npm install @avtr/web-sdk
```

```javascript
import { AvtrWebSdk } from "@avtr/web-sdk";

const sdk = new AvtrWebSdk({
  apiBaseUrl: "https://<orvyn-public-api-host>", // REST base (no trailing slash needed)
  wsBaseUrl: "wss://<orvyn-public-api-host>", // optional; derived from apiBaseUrl when omitted
  videoElement: document.querySelector("video"),
  apiKey: "<server-minted-key>", // only needed for SDK-initiated REST calls
  sessionConfig: {
    /* CreateSessionRequest body when the SDK creates the session */
  },
  diagnostic: (message) => console.log(message),
});
```

| Option | Purpose |
|---|---|
| `apiBaseUrl` | REST base URL for the `/v1/` control plane |
| `wsBaseUrl` | Media WebSocket base; derived from `apiBaseUrl` when omitted |
| `videoElement` | `<video>` element the bundled MSE player renders into |
| `apiKey` | API key — only needed when the SDK itself makes REST calls |
| `sessionConfig` | `CreateSessionRequest` body used when the SDK creates the session |
| `diagnostic` | Diagnostic log callback |

:::danger[Keep the API key out of the browser when you can]
Prefer the split-plane pattern from the
[quickstart](/quickstart): your backend creates the session and hands the
frontend only the `session_id` and `client_token`. Pass `apiKey` only when the
SDK must create sessions itself — and treat a key in browser code as
compromised-by-design.
:::

## Session lifecycle methods

| Method | Contract | Semantics |
|---|---|---|
| `createSession(apiBaseUrl, auth, body)` | `POST /v1/sessions` | Returns the bare `SessionResponse` — `client_token` is delivered here **exactly once** |
| `sdk.connect(sessionId, clientToken?)` | Media WebSocket `/v1/sessions/{session_id}/ws?token=` | Refreshes an expired token before connecting; on a pre-open failure with 401 semantics it performs exactly one refresh + one retry. Mic capture starts automatically |
| `sdk.refreshNow()` | `POST /v1/sessions/{session_id}/client-token` | Explicit refresh; returns the bare `ClientTokenResponse` (5-minute TTL). Single-flight — concurrent callers share one request |
| `sdk.status()` | `GET /v1/sessions/{session_id}` | Returns the bare `SessionStatusResponse`. **Never** contains a `client_token` |
| `sdk.endSession({ force? })` | `POST /v1/sessions/{session_id}/end` | Returns the bare `EndSessionResponse`; `force: true` appends `?force=true` to skip the graceful handshake |
| `sdk.disconnect()` | — | Local teardown only; no REST call |

## Token lifecycle

The `client_token` is a short-lived bearer credential with a **5-minute TTL**
(`expires_at` is epoch milliseconds). The SDK tracks the newest token and
handles four refresh triggers:

1. **Creation** — `createSession()` returns the first token in the
   `SessionResponse`.
2. **Expiry-triggered** — before opening the socket, a cached token whose
   `expires_at` has passed is refreshed via the client-token endpoint
   (single-flight, lazy — no timers).
3. **401-triggered** — the media WebSocket answers an expired or invalid
   `?token=` with HTTP 401. A pre-open connect failure while a token is known
   triggers exactly one refresh and one retry; a second failure propagates the
   original error.
4. **Manual** — `await sdk.refreshNow()` for reconnects, delayed connections,
   and browser sleep recovery.

Refreshing does **not** revoke previously issued tokens — outstanding tokens
simply age out at their own expiry (5-minute TTL).

## End semantics — the 409 resolution

`POST /end` returns **409** when the session is not `active`:

- `AVTR_SESSION_ENDED` — status was `ended` / `ended_with_warnings`.
- `AVTR_SESSION_CONFLICT` — status was `created` / `ending` / `failed`.

Both mean the end intent can no longer change the outcome, so the SDK resolves
them to `{ sessionId, status: "ended", endedAt: 0 }` instead of throwing
(`endedAt: 0` marks "already ended; server timestamp not re-issued"). All other
errors — 401, 403, 404, 429, 503 — throw `AvtrApiError`. See the
[lifecycle guide](/guides/session-lifecycle#ending-a-session) for the full
matrix.

## Error handling

REST failures throw `AvtrApiError`, which carries the flat contract error
fields:

| Property | Meaning |
|---|---|
| `code` | The `AVTR_*` error code from the flat error shape |
| `status` | HTTP status code |
| `requestId` | The `request_id` correlation UUID |
| `retryAfter` | `retry_after` in seconds — present on retryable codes only |
| `docUrl` | The `doc_url` documentation link |

```javascript
try {
  await sdk.endSession({ force: true });
} catch (err) {
  console.error(err.code, err.status, err.requestId, err.retryAfter);
}
```

Match on `code` (and `status`), never on `message` — see
[errors, limits & security](/guides/errors-limits-security) for the full
catalog and the retry rules.

## Media playback

The SDK bundles an MSE player (`MediaSource` + `SourceBuffer`, bounded segment
queue with backpressure handling) that renders fMP4 segments into your
`videoElement`. For the underlying media protocol — segments, microphone PCM
frames, and the message catalog — see the
[WebSocket & fMP4 guide](/guides/websocket-fmp4), including its
[MSE playback section](/guides/websocket-fmp4#receiving-the-media-stream).
