aethexDocs

React SDK

Ship a production voice agent inside your React app in an afternoon, on the web or in React Native. useAethexCall() hides every line of WebRTC, signaling, ICE, and audio plumbing behind one headless hook, so you build the experience and we run the real-time transport. It is SSR-safe, StrictMode-safe, and ready for the Next.js App Router, and the same hook runs natively on iOS and Android via Expo.

Install

Add the package. react and react-dom are peer dependencies for the hook; the framework-agnostic ./core entry needs neither.

npm
bash
npm install @aethexai/react react react-dom

Your first call

This is the entire integration. No sockets to wire, no peer connections to babysit, no <audio> elements to manage. Call start() on a user gesture and the agent is live. start() never throws, so you drive your UI straight from status and error.

CallButton.tsx
tsx
"use client"
import { useAethexCall, useAudioLevel } from "@aethexai/react"

export function CallButton() {
  const { status, isConnected, start, stop, remoteStream, error } = useAethexCall({
    agentId: "8a1b2c3d-4e5f-6789-abcd-ef0123456789",
    apiBaseUrl: "https://your-proxy.example.com", // your proxy, never a key
    onConnected: () => console.log("live"),
    onError: (e) => console.error(e.code, e.recoverable),
  })
  const { level } = useAudioLevel(remoteStream)

  if (error) return <p>Call failed: {error.code}</p>
  return (
    <button onClick={isConnected ? stop : start}>
      {status === "connecting" ? "Connecting..." : isConnected ? `Hang up (${level.toFixed(2)})` : "Call"}
    </button>
  )
}

useAethexCall

One hook runs the whole call. Give it an agent and your proxy URL, get back reactive state plus a handful of controls.

Options

agentId is always required. Supply exactly one of apiBaseUrl (your server-side proxy) or getToken (ephemeral token per call). Everything else tunes lifecycle callbacks, resilience, and transport.

UseAethexCallOptions
ts
interface UseAethexCallOptions {
  agentId: string          // the agent to call
  apiBaseUrl?: string      // your proxy (holds the key server-side)...
  getToken?: () => string | Promise<string>  // ...or mint a token per call, no proxy

  // Lifecycle callbacks
  onConnected?: () => void
  onEnded?: () => void
  onError?: (error: AethexError) => void
  onMetrics?: (metrics: PipelineMetrics) => void

  // Resilience & transport
  iceRestart?: boolean     // auto-heal dropped calls (default true)
  maxIceRestarts?: number  // consecutive attempts (default 1)
  timeoutMs?: number       // signaling timeout in ms (default 15000)
  headers?: Record<string, string>  // e.g. a short-lived proxy token
  audioConstraints?: MediaTrackConstraints
}

Returns

Reactive state you can render straight into your UI, plus start, stop, and interrupt. status moves through five values: idle, connecting, connected, ended, and error.

UseAethexCallResult
ts
interface UseAethexCallResult {
  status: "idle" | "connecting" | "connected" | "ended" | "error"
  isConnecting: boolean
  isConnected: boolean
  remoteStream: MediaStream | null   // pass to useAudioLevel()
  metrics: PipelineMetrics | null
  error: AethexError | null          // .code and .recoverable
  sessionId: string | null

  start: () => Promise<void>         // never throws
  stop: () => void                   // idempotent
  interrupt: () => void              // barge in on the agent
  getRemoteStatus: () => Promise<SessionStatusResponse>
}

Live audio visualizer

Feed the agent's remoteStream to useAudioLevel for a smoothed 0-to-1 amplitude on every animation frame. Drive a bar, a ring, or a pulsing orb, whatever fits your brand.

useAudioLevel
tsx
import { useAudioLevel } from "@aethexai/react"

const { level } = useAudioLevel(remoteStream) // 0..1, smoothed per animation frame
// drive a bar, a ring, a pulsing orb, whatever fits your brand

Drop-in widgets

Would rather not build UI at all? Import a ready-made, fully accessible component and go live in one line. Native buttons with full keyboard support, a single aria-live status region, and a visualizer that gracefully degrades under prefers-reduced-motion. The floating AethexVoiceOrb gives you the classic bottom-right voice bubble with zero layout work.

widgets
tsx
import { AethexCallButton, AethexVoiceWidget, AethexVoiceOrb } from "@aethexai/react/widgets"

// A one-line call button
<AethexCallButton agentId={AGENT_ID} apiBaseUrl={PROXY_URL} />

// A full widget with a live visualizer and a title
<AethexVoiceWidget agentId={AGENT_ID} apiBaseUrl={PROXY_URL} title="Talk to support" />

// A floating voice orb (bottom-right by default)
<AethexVoiceOrb agentId={AGENT_ID} apiBaseUrl={PROXY_URL} />

Transcripts

Pull the full turn-by-turn transcript once a call has ended, keyed by its sessionId.

getTranscript
ts
import { getTranscript } from "@aethexai/react"

const turns = await getTranscript({ apiBaseUrl, sessionId })

Calls that survive the real world

Real conversations happen on real networks. When an established call's connection drops, say a walk from wifi to cellular, the SDK renegotiates and heals it automatically instead of hanging up. It is on by default; turn the dial up with maxIceRestarts, or set iceRestart: false to fail fast.

resilience
ts
useAethexCall({ agentId, apiBaseUrl })                    // auto-heal on (default)
useAethexCall({ agentId, apiBaseUrl, maxIceRestarts: 2 }) // recover harder
useAethexCall({ agentId, apiBaseUrl, iceRestart: false }) // fail fast instead

Server-side session status

getRemoteStatus() returns the platform's own view of the session, its duration, turn count, and lifecycle, distinct from the local status. It works during the call and after it ends.

getRemoteStatus
ts
const { getRemoteStatus } = useAethexCall({ agentId, apiBaseUrl })

// the server's own view, works during and after the call
const s = await getRemoteStatus()
// { session_id, status, duration_s, turn_count, ... }

Typed errors, no guesswork

Every failure is an AethexError with a stable, switchable code, so you never match on message strings. Check error.recoverable to decide whether to retry, and narrow with isAethexError().

AethexErrorCode
ts
import { isAethexError } from "@aethexai/react"

// Every failure is a typed AethexError with a stable .code:
type AethexErrorCode =
  | "unsupported_browser"  // no WebRTC / getUserMedia
  | "mic_denied"           // user or policy blocked the microphone
  | "mic_missing"          // no audio input device
  | "connect_failed"       // could not open the session
  | "offer_failed"         // could not negotiate media
  | "quota_exceeded"       // 429, honors Retry-After
  | "payment_required"     // 402, billing
  | "capacity"             // 503, no voice capacity right now
  | "peer_failed"          // the connection dropped and could not recover
  | "timeout"              // a signaling request timed out
  | "aborted"              // the call was stopped mid-flight
  | "network"              // offline, CORS, or DNS
  | "unknown"

if (isAethexError(error) && error.recoverable) retry()

Security

Your ae_live_... key lives on a proxy you host and never touches the browser; apiBaseUrl points at that proxy. WebRTC and microphone access require HTTPS (or localhost).

Heads up
The SDK throws if apiBaseUrl looks like an API key. Never ship a key to the client.

Ephemeral tokens (skip the proxy)

Instead of hosting a proxy, mint a short-lived token on your server and hand it to the client with getToken. Pass getToken and drop apiBaseUrl, and the SDK talks to the Aethex API directly, so your only server-side code is a one-line route that calls POST /conversation/token with your API key. The token is scoped to one agent and expires after a short TTL (at most the agent's configured maximum call duration), so it is safe in the browser. Mint a fresh token for each call to limit the exposure window. See the Conversation API for the mint endpoint.

getToken
tsx
useAethexCall({
  agentId,
  // Drop apiBaseUrl; the SDK talks to the Aethex API directly with this token.
  getToken: async () => {
    const res = await fetch("/api/aethex-token", { method: "POST" })
    if (!res.ok) throw new Error("Token mint failed")
    return (await res.json()).token
  },
})

React Native (Expo)

The same useAethexCall hook runs in React Native: @aethexai/react ships a native build that Metro resolves automatically, so the import is identical. On native it runs on react-native-webrtc and routes the agent's audio to the loudspeaker. React Native has no CORS, so connect directly with the ephemeral-token flow (getToken), no proxy at all. WebRTC is a native module, so this needs an Expo development build (it does not run in Expo Go). The AethexVoiceOrb widget renders natively via Skia, so you can drop it straight in; the button and panel widgets are web-only.

Expo setup
bash
# WebRTC is a native module, so this needs an Expo dev build (not Expo Go).
npx expo install react-native-webrtc @config-plugins/react-native-webrtc expo-dev-client

# app.json -> expo.plugins:
#   ["@config-plugins/react-native-webrtc",
#     { "microphonePermission": "Allow $(PRODUCT_NAME) to use your microphone." }]

# For the AethexVoiceOrb widget: Skia, its reanimated peer, and loudspeaker routing.
npx expo install @shopify/react-native-skia react-native-reanimated react-native-incall-manager
# babel.config.js: add "react-native-reanimated/plugin" (last in the plugins list)

npx expo prebuild --clean
npx expo run:ios --device   # iOS needs a real device (the Simulator has no mic)
npx expo run:android        # an emulator is fine with host-mic input

The voice orb, natively

AethexVoiceOrb is the fastest way to ship a native call: the same import as on the web renders a Skia orb on iOS and Android, with the loudspeaker routing, mute, hang-up, and an optional post-call rating handled for you. Seed its colour with agentName and pick a texture with orbType. On native the audio-level hooks read 0 (no Web Audio), so the orb self-animates during the call rather than reacting to the audio.

AethexVoiceOrb (native)
tsx
import { AethexVoiceOrb } from "@aethexai/react/widgets"

// Same import as web; Metro resolves the Skia-rendered native orb.
<AethexVoiceOrb
  agentId={AGENT_ID}
  agentName="Kora"       // seeds the orb's colour + texture
  getToken={getToken}    // mint on your server; no key on the device
  orbType="aurora"
  controls               // mute + hang up
  feedback               // post-call rating
/>

Framework-agnostic core

Not on React? The same engine ships as a pure-TypeScript VoiceCall class under @aethexai/react/core, callbacks instead of hooks, everything else identical.

VoiceCall
ts
import { VoiceCall } from "@aethexai/react/core"

const call = new VoiceCall({
  agentId: AGENT_ID,
  apiBaseUrl: PROXY_URL, // never the direct API or a key
  callbacks: {
    onStatusChange: (s) => console.log(s),
    onRemoteStream: (stream) => { /* auto-played via a managed <audio> */ },
    onError: (err) => console.error(err.code, err.recoverable),
    onClose: () => console.log("call ended"),
  },
})

await call.start()
call.stop() // idempotent teardown