> ## 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.

# Error Handling

> A recipe for handling Lokutor errors: show a top-up prompt on 402 billing.* codes, back off on 429 and 503 (honouring Retry-After), and let the retryable flag decide everything else.

Every Lokutor error carries a `code` and a `retryable` flag ([Error Catalog](/api-reference/errors)). Three rules cover almost every case:

| What you get | What it means | What to do |
| :- | :- | :- |
| `402` / `billing.*` | The account cannot pay | Do not retry. Ask a person to top up at [app.lokutor.com/billing](https://app.lokutor.com/billing) (or turn on auto-recharge). |
| `429` / `auth.rate_limited` | Too many sessions open at once on the account | Close sessions you no longer need, then retry with backoff. |
| `503` | Capacity or a temporary failure | Retry after the `Retry-After` header when there is one, otherwise with exponential backoff. |
| Anything else | See `retryable` | `true`: retry with backoff. `false`: fix the request, key, voice or agent; retrying will not help. |

## REST

```typescript theme={null}
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

async function synthesize(body: object, attempts = 4): Promise<ArrayBuffer> {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch('https://api.lokutor.com/tts/synthesize', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.LOKUTOR_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    });
    if (res.ok) return res.arrayBuffer();

    const err = await res.json().catch(() => ({}));
    if (res.status === 402) {
      throw new Error(`Top up needed: ${err.message ?? err.code}`);
    }
    // The synthesis 503 has {error, voice, hint} and Retry-After; other errors have {code, retryable}.
    const retryable = res.status === 503 || res.status === 429 || err.retryable === true;
    if (!retryable || attempt >= attempts) {
      throw new Error(`${res.status} ${err.code ?? err.error ?? ''}: ${err.message ?? err.hint ?? ''}`);
    }
    const retryAfter = Number(res.headers.get('Retry-After'));
    await sleep(retryAfter > 0 ? retryAfter * 1000 : Math.min(1000 * 2 ** attempt, 10000));
  }
}
```

## JavaScript SDK

The SDK raises `LokutorError`, whose `retryable` is set from the code. Register a handler with `on('error', …)` or pass `onError` to the constructor; it is not a property to assign afterwards.

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

const client = new VoiceAgentClient({ auth: getTicket, agentId: 'your-agent-id' });

client.on('error', (error) => {
  if (error.code.startsWith('billing.')) {
    showTopUpPrompt();                       // billing.insufficient_balance, billing.topup_required, …
  } else if (error.code === 'auth.rate_limited') {
    showToast('Too many calls open. Try again in a moment.');
  } else if (error.code === 'session.ended') {
    showEnded(error.message);                // the server ended the call on purpose; no reconnect
  } else if (!isRetryable(error)) {
    showFatal(error.message);                // e.g. auth.invalid_key, agent.not_found, invalid voice
  }
  // Retryable errors on a dropped connection: the SDK reconnects by itself (up to 5 attempts).
});
```

`TTSClient.synthesize()` rejects with a `LokutorError` instead, so wrap it in `try`/`catch`:

```typescript theme={null}
try {
  await tts.synthesize({ text, voice: VoiceStyle.F1, language: Language.ENGLISH, onAudio });
} catch (error) {
  if (isRetryable(error)) {
    // e.g. tts.synthesis_failed, or internal.timeout when no audio arrived within 30 s
  }
}
```

## WebSockets

* A `402` refuses the handshake before the socket opens. A browser `WebSocket` only sees close code `1006`, so if a connection fails at once, check the balance.
* On `/ws/agent`, `429` refuses the handshake. On `/ws/tts`, the socket opens, sends an `auth.rate_limited` error frame and closes.
* Close code `1008` with reason `account balance exhausted` means a call ran the balance out: show the top-up prompt.
* Error frames on `/ws/agent` and `/ws/stt` can carry a plain string in `data` instead of the structured object. Treat a string as `internal.error`, retryable.


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