Pick the right client for where your code runs
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.Streaming resilience
- Track
seqand resume — everyeventandmessageframe carries a monotonicseq; keep the highest one you’ve processed and, if the connection drops, callstreamMessageagain with'Last-Event-ID': String(lastSeq)instead of re-submitting the turn — the missed frames replay without a second billable turn (see Stream a message). - Branch on
error— a turn that fails mid-stream arrives as anerrorevent; handle it in the same loop as the other frames. - 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,
sendMessageis simpler to operate. - Runtime v1 WebSocket sessions — for
runtime v1 agents, the same rules apply to the
socket: track
seq, send{ type: 'resume', resume_after: lastSeq }on reconnect, and register oneon()handler per event (a second call replaces the first). See WebSocket.
Troubleshooting
WebSocket closes immediately with code 1008 (runtime v1 agents)
WebSocket closes immediately with code 1008 (runtime v1 agents)
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. If the socket opens but answers with
an error envelope saying the session is SSE-only, the agent is on
runtime v2 — use streamMessage (see
Runtime v1 agents).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 read went to the wrong runtime. Thread reads take the
runtime_version
selector — the runtime version your agent runs on, e.g.
listMessages(threadId, { runtime_version: '0.8.0' }) (same for
threads.getThread / getThreadMessages / updateThread); omitting it
asks runtime v1. UUID thread ids are the tell. See
Threads and runtime versions.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.Traces
- Branch on
trace.runtimefirst."v2"is the runtime’s own turn trace in your program’s vocabulary;"v1"is the typed runtime v1 record. The SDK narrowsApollo.Traceon that field — read nothing else before the branch. - Ask inline when you need it with the reply.
include_trace: trueonsendMessage/streamMessage/rerunreturns the trace asmessage.trace_infoin the same request. Leave it off on hot paths — a trace is large — and fetch history throughthreadTraceinstead. - Keep the thread id next to the interaction id. A runtime v2 interaction
is read by both (
interactionTrace(id, { thread_id })); store them together when you storemessage.id. - Sources for end users are
citations_used, withpassages_usedas the fallback — notsources[].evidence, which is everything retrieval returned. - Treat sections as additive. New sections and fields may appear; existing
ones keep their names and types. Don’t parse
narrative— it’s for reading.
Runtime v1 agents
Agents created before August 1 2026 run on runtime v1: noruntime_version
on thread reads, WebSocket sessions available, the typed runtime v1 trace,
24-character hex thread ids. Everything that differs — and the checklist for moving an
integration to runtime v2 — lives on one page:
Runtime v1 agents
Tell the runtimes apart, read runtime v1 threads, and swap a WebSocket
session for
streamMessage.