Pick the right client for the runtime
ApolloMessagingClient
Publishable key. Safe in browsers and mobile webviews — that’s what makes
the key “publishable”. Use it for anything an end user drives.
ApolloManagementClient
Organization API key. Server-side only — backend services, CI, cron.
Never ship this key in client code.
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: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).
error.body carries the API’s error envelope — 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:Filter your thread lists
An unfiltered list sorts every thread in the organization and can be slow. Scope byproject_id, agent_id, or a created range — filter fields go
directly on the request — and paginate with the returned cursors:
Upgrading from 3.3.4 or earlier? List methods used to take a
filters: { … }
wrapper — drop it and pass the fields directly (the compiler points at every
call site). Older versions also didn’t apply list filters on the wire; from
3.3.5 they filter reliably.WebSocket resilience
- Track
seqand resume — keep the highestseqprocessed; on reconnect send{ type: 'resume', resume_after: lastSeq }instead of re-submitting the turn (see 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/streamMessageare simpler to operate.
Troubleshooting
WebSocket closes immediately with code 1008
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.streamMessage type error: 'body' is missing
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.Thread lists time out
Thread lists time out
Add filters (
project_id, created range) and raise the per-call timeout:
listThreads({ project_id }, { timeoutInSeconds: 120 }).Reading a thread returns 404 (but sending works)
Reading a thread returns 404 (but sending works)
The thread lives on the v2 runtime — reads take the
runtime_version
selector: listMessages(threadId, { runtime_version: '0.8.0' }) (same for
threads.getThread / getThreadMessages / updateThread). UUID thread
ids are the tell. See Agent runtimes.listThreads filters stopped compiling after an upgrade
listThreads filters stopped compiling after an upgrade
From 3.3.5, list methods take filter fields directly on the request —
replace
listThreads({ filters: { project_id } }) with
listThreads({ project_id }). The old wrapper was also silently not
applied on the wire, so this upgrade is when your filters start working.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.
Migration guide (v2 → v3)
The exact before/after for every call and response — method map, signature
changes, WebSocket rewrite, response shapes, and a step-by-step checklist.