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

# Best Practices

> Error handling, timeouts, key hygiene, and patterns that hold up in production.

## Pick the right client for the runtime

<CardGroup cols={2}>
  <Card title="ApolloMessagingClient" icon="paper-plane">
    Publishable key. Safe in browsers and mobile webviews — that's what makes
    the key "publishable". Use it for anything an end user drives.
  </Card>

  <Card title="ApolloManagementClient" icon="gear">
    Organization API key. **Server-side only** — backend services, CI, cron.
    Never ship this key in client code.
  </Card>
</CardGroup>

```ts theme={"dark"}
// Backend: both clients are fine
const management = new ApolloManagementClient({ organizationApiKey: process.env.AUI_ORG_API_KEY! });

// Browser: messaging only
const messaging = new ApolloMessagingClient({ publishableKey: 'pk_network_...' });
```

<Tip>
  If your publishable key is used from a browser, restrict it to your web
  origins and IP ranges in the Console so it can't be replayed elsewhere.
</Tip>

## Reuse one client instance

The messaging client caches its access token and refreshes it before expiry.
Constructing a new client per request throws that away and forces a token
exchange every time:

```ts theme={"dark"}
// Good — module-level singleton
export const apollo = new ApolloMessagingClient({ publishableKey: PK });

// Bad — new client (and token exchange) per call
async function send(text: string) {
  const client = new ApolloMessagingClient({ publishableKey: PK });
  return client.messaging.sendMessage({ user_id, text });
}
```

## Error handling

All failures throw typed errors. `ApolloError` and `ApolloTimeoutError` are
exported at the top level; per-status classes live under the `Apollo`
namespace (`BadRequestError`, `UnauthorizedError`, `ForbiddenError`,
`NotFoundError`, `ConflictError`, `UnprocessableEntityError`,
`InternalServerError`, `BadGatewayError`).

```ts theme={"dark"}
import { Apollo, ApolloError, ApolloTimeoutError } from '@aui.io/aui-client';

try {
  await client.agents.getAgent(agentId);
} catch (error) {
  if (error instanceof Apollo.NotFoundError) {
    // handle the specific case
  } else if (error instanceof ApolloTimeoutError) {
    // the request timed out — consider a longer timeoutInSeconds
  } else if (error instanceof ApolloError) {
    // any other API error: statusCode + the error envelope in body
    const body = error.body as { error?: { code?: string; request_id?: string } };
    console.error(error.statusCode, body?.error?.code, body?.error?.request_id);
  } else {
    throw error; // not an API error — don't swallow it
  }
}
```

`error.body` carries the API's [error envelope](/api/errors) — branch on the
stable `error.code`, and log `error.request_id` for support.

## Timeouts and retries

There is no client-wide timeout — set one per call where it matters, and tune
retries (default: 2) for the operation's cost:

```ts theme={"dark"}
// Long-running list over a large window
await client.threads.listThreads({ filters }, { timeoutInSeconds: 120 });

// Don't retry non-idempotent sends more than you must
await client.messaging.sendMessage(request, { maxRetries: 0 });

// Cancelable from the UI
const controller = new AbortController();
await client.messaging.sendMessage(request, { abortSignal: controller.signal });
```

<Warning>
  `sendMessage` triggers a billable agent turn. Automatic retries re-send the
  request — for user-driven sends prefer `maxRetries: 0` and surface the error,
  rather than risking duplicate turns.
</Warning>

## Filter your thread lists

An empty `filters` object sorts every thread in the organization and can be
slow. Scope by `project_id`, `agent_id`, or a `created` range, and paginate
with the returned cursors:

```ts theme={"dark"}
let page = await client.threads.listThreads({
  filters: { project_id, created: ['2026-07-01T00:00:00Z'] },
  'page[size]': 50,
});

while (page.meta?.has_more) {
  page = await client.threads.listThreads({
    filters: { project_id },
    'page[after]': page.meta.after_cursor!,
  });
}
```

## WebSocket resilience

* **Track `seq` and resume** — keep the highest `seq` processed; on reconnect
  send `{ type: 'resume', resume_after: lastSeq }` instead of re-submitting the
  turn (see [WebSocket](/sdk/websocket)).
* **One handler per event** — `socket.on()` replaces the previous handler for
  the same event; route inside one handler rather than registering several.
* **Persist `thread_id`** — keep it across reconnects (and page reloads, if the
  conversation should survive them) to stay in the same conversation.
* **Prefer REST for one-shot turns** — if you don't need live tokens or
  bidirectional traffic, `sendMessage`/`streamMessage` are simpler to operate.

## Troubleshooting

<AccordionGroup>
  <Accordion title="401 Unauthorized on every call">
    The credential doesn't match the client: messaging needs a publishable key
    (`pk_network_...`), management needs the organization API key. If the key
    has origin/IP restrictions, confirm the calling origin is allowlisted.
  </Accordion>

  <Accordion title="WebSocket closes immediately with code 1008">
    `1008` is an authentication failure on the upgrade — the key exchange
    failed or the token was rejected. Verify the publishable key; `1011`
    signals a server-side error instead.
  </Accordion>

  <Accordion title="streamMessage type error: 'body' is missing">
    Unlike `sendMessage`, the stream request wraps the payload:
    `streamMessage({ body: { user_id, text } })` — the top level also accepts
    the `Last-Event-ID` resume header.
  </Accordion>

  <Accordion title="Thread lists time out">
    Add filters (`project_id`, `created` range) and raise the per-call timeout:
    `listThreads({ filters }, { timeoutInSeconds: 120 })`.
  </Accordion>
</AccordionGroup>

## Migrating from v2 (`ia-controller`)

v3 is a full rewrite against the Apollo API; the v2 surface is gone. The
short version: every `task_id` becomes a `thread_id`, every
`client.controllerApi.X` moves onto one of the two new clients, and the
agent is now encoded in your key instead of passed per call.

<Card title="Migration guide (v2 → v3)" icon="arrow-right-arrow-left" href="/sdk/migration">
  The exact before/after for every call and response — method map, signature
  changes, WebSocket rewrite, response shapes, and a step-by-step checklist.
</Card>
