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

# Secure Browser Setup

> Keep your API key out of the browser: a backend route that mints a 10-minute wst_ ticket from POST /ws-ticket, and browser code that passes it with the JavaScript SDK's auth option.

An API key in browser code can be read by anyone who opens the page, and spends your balance. Instead, keep the key on your server and give the page a **ticket**: a credential that starts with `wst_`, works anywhere a key does, and expires after 10 minutes ([Authentication](/authentication#short-lived-tickets)).

```
browser ──POST /api/lokutor-ticket──▶ your backend ──POST /ws-ticket (key in header)──▶ api.lokutor.com
browser ◀────────── { ticket } ────── your backend ◀──────── { ticket, expires_in } ────
browser ──wss://api.lokutor.com/ws/agent?api_key=wst_…──▶ api.lokutor.com
```

## 1. Backend: mint tickets

Put this route behind your own login, so only your signed-in users can get a ticket. Example with Node.js 22+ and Express:

```javascript theme={null}
// server.mjs — LOKUTOR_API_KEY stays on the server
import express from 'express';

const app = express();

app.post('/api/lokutor-ticket', requireSignedInUser, async (req, res) => {
  const r = await fetch('https://api.lokutor.com/ws-ticket', {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.LOKUTOR_API_KEY}` },
  });
  if (!r.ok) return res.status(502).json({ error: 'could not get a ticket' });
  const { ticket, expires_in } = await r.json();
  res.json({ ticket, expires_in });
});

function requireSignedInUser(req, res, next) {
  // Replace with your own session check.
  next();
}

app.listen(3000);
```

The key goes in the `Authorization` (or `X-API-Key`) header. `POST /ws-ticket` does not accept it in the URL.

## 2. Browser: pass the ticket with `auth`

`VoiceAgentClient` takes an `auth` callback instead of `apiKey`. The SDK calls it before every connection, including each automatic reconnect, so a session that reconnects after the first ticket expired still gets a valid one.

```typescript theme={null}
import { VoiceAgentClient } from '@lokutor/sdk';

async function getTicket(): Promise<string> {
  const res = await fetch('/api/lokutor-ticket', { method: 'POST', credentials: 'include' });
  if (!res.ok) throw new Error('No ticket');
  return (await res.json()).ticket;
}

const client = new VoiceAgentClient({
  auth: getTicket,
  agentId: 'your-agent-id', // or prompt / voice / language for an agent defined in code
});

startButton.addEventListener('click', () => client.startManaged());
```

`TTSClient`, `STTClient` and `SpeechToTextClient` take a credential as `apiKey`. Pass a ticket there, fetched just before use:

```typescript theme={null}
import { TTSClient } from '@lokutor/sdk';

const tts = new TTSClient({ apiKey: await getTicket() });
```

A ticket can be reused until it expires, so one ticket can serve several requests made within its 10 minutes.

## Without the SDK

Pass the ticket as `?api_key=`, since the browser `WebSocket` and `EventSource` cannot set headers:

```typescript theme={null}
const ticket = await getTicket();
const ws = new WebSocket(`wss://api.lokutor.com/ws/agent?agent_id=your-agent-id&api_key=${ticket}`);
```

## Checklist

* No `sk_` key in any file the browser downloads, including `.env` values your bundler inlines (for example `VITE_*` or `NEXT_PUBLIC_*` variables).
* The ticket route requires your own authentication; otherwise anyone can mint tickets on your balance.
* Rate-limit the ticket route per user if your app is public.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.