Errors
Full RealtimeErrorCode table — when each error fires and how to recover.
The server sends error frames over the data channel. Each frame has a code, message, and fatal flag. The SDKs additionally raise typed exceptions for local failures (session start, dialing, sends before connect).
Server error codes
These come from the wire protocol (RealtimeErrorCode).
| Code | Fatal | When it fires | Recovery |
|---|---|---|---|
auth_failed | yes | API key or minted token missing, expired, or invalid | Refresh the credential. Do not retry with the same one. |
workspace_forbidden | yes | The authenticated credential does not have access to the requested resource | Verify the key belongs to the workspace you think it does. |
voice_disabled | yes | The workspace does not have realtime voice enabled | Enable voice in the dashboard or contact support. |
upstream_disconnect | no | The upstream model session dropped (go-away or network error) | The server attempts transparent reconnect (you may see a reconnecting event). If fatal: false, the session may continue. If fatal: true, start a new session. |
internal_error | varies | Unexpected server-side failure | Retry. If persistent, file a support issue with the session_id. |
invalid_message | no | The client sent a malformed or unrecognized control message | Check SDK version. If hand-crafting messages, validate against the protocol schema. |
version_mismatch | yes | The client's version field is incompatible with the server's protocol version | Upgrade the SDK. |
Typed session-start rejections (e.g. unknown_server_tool, invalid_tool_config, version_mismatch) arrive as non-2xx REST responses instead, carried in the standard error envelope (error.type, error.code, error.message) — the SDKs surface them as start failures, not error frames.
TypeScript client-side error codes
The TypeScript SDK maps server codes and local failures to its own RealtimeErrorCode union:
| Code | Source | Meaning |
|---|---|---|
mic_denied | client | Browser refused microphone permission |
screen_denied | client | Browser refused screen share permission |
screen_start_failed | client | Screen share could not be started (no video track, etc.) |
session_start_failed | client | Session-start HTTP call failed |
session_rejected | client | Server rejected the session (4xx response) |
auth_error | mapped from auth_failed / workspace_forbidden | Authentication or authorization failure |
not_ready | client | sendText(), setMuted(), etc. called before ready |
transport_connect_timeout | client | LiveKit room join timed out |
transport_disconnect | mapped from upstream_disconnect | Transport closed unexpectedly |
unsupported_browser | client | WebRTC not available in this browser |
server_error | mapped from internal_error etc. | Generic server-side failure |
Subscribing to errors
TypeScript
session.on('error', (error) => {
if (error === null) return; // null = error cleared on recovery
console.error(`[${error.code}] ${error.message}`);
if (error.code === 'mic_denied') {
// Show mic permission instructions
} else if (error.code === 'auth_error') {
// Refresh the credential
} else if (error.code === 'transport_disconnect') {
// The session is gone — offer a "reconnect" action that starts a new one
}
});useRealtimeError() is the React hook equivalent:
import { useRealtimeError } from 'cosmo-ai';
function ErrorBanner() {
const error = useRealtimeError();
if (!error) return null;
return <div role="alert">{error.message}</div>;
}Python — error frames arrive as RealtimeError events on the session stream; local failures raise typed exceptions:
from cosmo_ai import (
DialError,
NotConnectedError,
RealtimeError,
SessionStartError,
)
try:
async with agent.start() as session:
async for event in session:
if isinstance(event, RealtimeError):
print(f"[{event.code}] {event.message} (fatal={event.fatal})")
if event.fatal:
break # the terminal session-ended item follows
except SessionStartError as exc:
print(f"session start rejected: [{exc.code}] {exc}")SessionStartError— the session-start REST call was rejected;codecarries the server's rejection code (VersionMismatchErroris theversion_mismatchsubclass).DialError— asession.dial(...)call failed;codecarries the rejection code.NotConnectedError— a send (send_text,set_muted, …) was attempted on a session that is not connected.
Swift — error frames are .error cases on the event stream; start failures throw from start(_:config:):
do {
let session = try await RealtimeSession.start(options, config: config)
for try await event in session.events {
if case .error(let e) = event {
print("server error [\(e.code.rawValue)] \(e.message) fatal=\(e.fatal)")
// e.fatal == true → the terminal .sessionEnded follows; start a new session
}
}
} catch let error as RealtimeSessionError {
// e.g. .handshakeFailed(status:code:detail:), .insecureBaseURL
print("start failed: \(error)")
}fatal flag
fatal: true means the session is dead and cannot continue — the stream will finish with its terminal session-ended item; start a new session. fatal: false means this turn or operation failed but the session itself is still alive — you may continue speaking.
Recovery recipes
auth_failed or workspace_forbidden
These are almost always configuration errors. Check:
- The API key (or minted token) is valid and not expired.
- The key has the
voice:usescope. - The key belongs to the workspace whose agents and tools you are referencing.
upstream_disconnect (non-fatal)
The server attempts a transparent upstream-session rotation. The client receives reconnecting on the data channel; the session and transport survive. If instead the frame was fatal: true, start a new session (optionally with resume_session_id).
version_mismatch
Upgrade the SDK. The protocol version is pinned at "1.0". If your SDK sends a different version, it is out of date. See Protocol version.
invalid_message
If you are using a supported SDK version and not hand-crafting messages, file a bug. If you are hand-crafting, validate your JSON against the OpenAPI schema at sdks/cosmo-realtime/external-openapi.json.
Pitfalls
- The
errorevent in TypeScript fires withnullwhen the error is cleared (e.g. on the next session start). Handlers must check for null:if (error === null) return;. transport_disconnectin TypeScript does not map 1:1 to the server'supstream_disconnect. The TS code maps bothupstream_disconnectand network-level closes totransport_disconnect.- The wire
RealtimeErrorCodeis the server enum; it does not include client-side codes likemic_denied(those only exist in the TypeScript SDK). - A non-fatal
errornever ends the session — do not tear down on every frame. Gate teardown onfatal, thesession_endedevent, or the lifecycle reachingdisconnected.
See also
- Sessions — the
versionfield onsession-configfor version negotiation - Lifecycle — when
errorfires in the event sequence, and the typed disconnect reasons - Protocol version — how breaking changes work