CLIENT SDK
The Client SDK (@aether-stack-dev/client-sdk) runs in the browser and connects to a UE-hosted AStack gateway via WebSocket while your app embeds the signed renderer player separately.
INSTALLATION
$ npm install @aether-stack-dev/client-sdk
END-USER NOTICES
Show a processing notice before calling startCall() or requesting microphone access. Suggested wording:
This conversation uses your microphone. Your audio, messages and any images you share are processed to generate replies. The character's voice, face and replies are AI-generated and may be inaccurate. See our Privacy Policy for how data is handled.
Link to your own privacy policy, adapt the notice to your data flow and obtain any consent your application requires. For Deepgram BYOK, also disclose processing under that Deepgram account. Browser microphone permission alone does not replace these notices.
AStack account holders must accept the current Terms and Privacy Policy in the dashboard before purchases or paid sessions. The session API returns legal_acceptance_required (403) until they do.
CONFIGURATION
// Session credentials are returned by your backend after it calls the control planeconst session = await fetch('/api/start-session', { method: 'POST' }).then(r => r.json());const client = new AStackCSRClient({sessionId: session.sessionId,gatewayTransport: session.gatewayTransport,gatewayWsUrl: session.gatewayWsUrl, // Preferred per-session WebSocket URLgatewayUrl: session.gatewayUrl, // HTTPS gateway origin fallbacksessionToken: session.sessionToken, // Auth token from the control planerendererPlayerUrl: session.rendererPlayerUrl,sampleRate: 24000, // Audio sample rate (default: 24000)fps: 30, // Animation frame rate (default: 30)enableImageCapture: true, // Enable camera capture (default: true)imageCaptureInterval: 5000, // Capture interval ms (default: 5000)});
gatewayWsUrlPreferred session WebSocket URL returned by the control planegatewayUrlHTTPS gateway origin used when gatewayWsUrl is omittedsessionTokenSession token returned by the control planerendererPlayerUrlSigned Pixel Streaming URL for iframe or tab embeddingworkerUrlTemporary migration alias for gatewayUrl; avoid in new codeenableImageCaptureSend camera frames for vision processingautoReconnectAuto-reconnect on disconnect (default: true)maxRetriesMax reconnection attempts (default: 5)reconnectDelayBase delay between reconnects in ms, with exponential backoff (default: 1000)audioProcessorUrlOptional self-hosted AudioWorklet URL. The SDK bundles the worklet and loads it from a Blob URL; set this only when your CSP blocks blob: scripts.debugEnable debug logging (default: false)METHODS
connect(): Promise<void>Establish WebSocket connection to the gateway and authenticatestartCall(options?: CallOptions): Promise<void>Initialize audio context and start streaming. The voiceAgent option explicitly selects self-hosted inference or Deepgram BYOK without carrying a key.stopCall(): voidStop mic/camera capture and release local media resourcesdisconnect(): voidStop call and close WebSocket connectionsendText(message: string): voidSend text input to the AI (instead of voice)getGatewayStatus(): GatewayConnectionStatusReturn gateway connection/auth stategetRendererPlayerUrl(): string | nullReturn the signed renderer player URL, if providedgetRendererPlayerState(): RendererPlayerStateReturn renderer player URL/status state separately from gateway authgetCallStatus(): CallStatusReturns 'idle' | 'starting' | 'active' | 'stopping' | 'error'isConnected(): booleanReturns true if WebSocket is opendestroy(): Promise<void>Disconnect and clean up all resourcesgetReconnectAttempts(): numberReturns current reconnection attempt countSTARTCALL OPTIONS
With the upcoming server-configured session contract, persona fields are pinned by your backend. Send only fields permitted by its agentConfig, or use startCall() without private values. Browser configuration requires an explicit backend opt-in.
await client.startCall({voiceAgent: { mode: 'self_hosted' },providers: {asr: 'self', // 'self' | 'deepinfra' (default: 'self')llm: 'self', // 'self' | 'deepinfra' (default: 'self')tts: 'self', // 'self' | 'deepinfra' (default: 'self')},voice: 'selected-voice', // Only when the backend permits voice overrides.});
EVENTS
The client extends EventEmitter. Listen for events with client.on(event, callback).
connected() => voiddisconnected() => voidreconnecting(attempt: number) => voidreconnected() => voiderror(error: Error) => voidcallStarted() => voidcallStopped() => voidspeechStarted() => voidinterim(text: string) => voidtranscript(text: string) => voidutteranceEnd() => voidresponse(text: string) => voidresponseComplete() => voidasrError(message: string) => voidmodelStatus(status: { model_loaded?: boolean }) => voidcreditsExhausted() => voidsessionExpired(reason: string) => voidgatewayStatusChanged(status) => voidrendererPlayerStateChanged(state) => voidEXAMPLE
client.on('transcript', (text) => {console.log('User said:', text);});client.on('response', (text) => {console.log('AI:', text);});client.on('error', (error) => {console.error('AStack error:', error.message);});
AUDIO PLAYER
The AudioPlayer is a standalone PCM helper. Hosted call audio comes from the signed renderer player over WebRTC, not from the gateway WebSocket.
import { AudioPlayer } from '@aether-stack-dev/client-sdk';const player = new AudioPlayer(24000); // sample rateplayer.enqueue({ audio: arrayBuffer });player.clearQueue();await player.destroy();// Events: playbackStarted, playbackEnded, queueEmpty, error
REACT HOOKS
Import from @aether-stack-dev/client-sdk/react.
useAStackCSR
const {client, // AStackCSRClient | nullisConnected, // booleangatewayStatus, // gateway connection/auth staterendererPlayerUrl, // signed Pixel Streaming/player URLcallStatus, // 'idle' | 'starting' | 'active' | 'stopping' | 'error'transcript, // string — latest user speechresponse, // string — latest AI responseerror, // Error | nullconnect, // () => Promise<void>disconnect, // () => voidstartCall, // () => Promise<void>stopCall, // () => voidsendText, // (message: string) => void} = useAStackCSR({session, // Full session API responseautoConnect: false, // auto-connect on mount (default: false)});
RENDERER PLAYER
const { rendererPlayerUrl } = useAStackCSR({ session });return rendererPlayerUrl ? (<iframe title="AStack renderer" src={rendererPlayerUrl} />) : null;
ERROR HANDLING
import { AStackError, ErrorCodes } from '@aether-stack-dev/client-sdk';client.on('error', (error) => {if (error instanceof AStackError) {switch (error.code) {case ErrorCodes.CONNECTION_LOST:// Reconnect logicbreak;case ErrorCodes.MEDIA_ACCESS_DENIED:// Prompt user for mic/camera permissionsbreak;}}});
ERROR CODES
INVALID_API_KEYAPI key is invalid or missingINVALID_TOKENSession token is invalidAUTHENTICATION_FAILEDAuthentication rejected by serverSESSION_EXPIREDSession has expiredCONNECTION_LOSTWebSocket connection droppedNETWORK_ERRORNetwork connectivity issueMEDIA_ACCESS_DENIEDMic/camera permission deniedAUDIO_ERRORAudio processing failureRATE_LIMIT_EXCEEDEDToo many requestsINSUFFICIENT_CREDITSAccount has no remaining credits