Skip to main content

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.
If your publishable key is used from a browser, restrict it to your web origins and IP ranges in the Playground so it can’t be replayed elsewhere.

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

Filter your thread lists

An unfiltered list sorts every thread in the organization and can be slow. Scope by project_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 seq and resume — every event and message frame carries a monotonic seq; keep the highest one you’ve processed and, if the connection drops, call streamMessage again 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 an error event; 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, sendMessage is 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 one on() handler per event (a second call replaces the first). See WebSocket.

Troubleshooting

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.
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).
Unlike sendMessage, the stream request wraps the payload: streamMessage({ body: { user_id, text } }) — the top level also accepts the Last-Event-ID resume header.
Add filters (project_id, created range) and raise the per-call timeout: listThreads({ project_id }, { timeoutInSeconds: 120 }).
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.
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.runtime first. "v2" is the runtime’s own turn trace in your program’s vocabulary; "v1" is the typed runtime v1 record. The SDK narrows Apollo.Trace on that field — read nothing else before the branch.
  • Ask inline when you need it with the reply. include_trace: true on sendMessage / streamMessage / rerun returns the trace as message.trace_info in the same request. Leave it off on hot paths — a trace is large — and fetch history through threadTrace instead.
  • 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 store message.id.
  • Sources for end users are citations_used, with passages_used as the fallback — not sources[].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: no runtime_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.