@aquantinc/acp-web-sdk
v0.1.3
Published
Browser SDK for Aquant Conversation Platform — chat, voice, and session management
Downloads
52
Readme
@aquantinc/acp-web-sdk
Browser SDK for the Aquant Conversation Platform — voice calls, chat messaging, and session management from a single, framework-agnostic package.
Works with any JavaScript environment that runs in a browser: plain <script> tags, React, Vue, Angular, Svelte, or any bundler.
Install
With a package manager (React, Vue, Angular, etc.):
npm install @aquantinc/acp-web-sdkWithout a bundler (plain HTML/JS):
No install needed. Serve the SDK's dist/ folder or copy dist/index.js into your project and import it directly:
<script type="module">
import { AquantClient } from "./path-to/dist/index.js";
// ready to use
</script>The SDK handles all voice dependencies automatically at runtime — no extra
<script>tags or installs required.
Quick Start
With a bundler (React, Vue, etc.)
import { AquantClient } from "@aquantinc/acp-web-sdk";
const client = new AquantClient({
apiKey: "your-api-key",
apiSecret: "your-api-secret",
agentId: "your-agent-id",
});
await client.initialize();
const agent = client.voice.agentInfo;
console.log(`Agent: ${agent?.agentName} (${agent?.agentPhone})`);
await client.voice.startCall();
// Later…
await client.voice.endCall();
await client.destroy();Without a bundler (plain HTML)
<!DOCTYPE html>
<html>
<body>
<button id="call">Start Call</button>
<button id="mute" disabled>Mute</button>
<button id="end" disabled>End Call</button>
<script type="module">
import { AquantClient } from "./dist/index.js";
const client = new AquantClient({
apiKey: "your-api-key",
apiSecret: "your-api-secret",
agentId: "your-agent-id",
});
document.getElementById("call").onclick = async () => {
if (!client.isInitialized) await client.initialize();
await client.voice.startCall();
document.getElementById("mute").disabled = false;
document.getElementById("end").disabled = false;
};
document.getElementById("mute").onclick = () => {
if (client.voice.isMuted) client.voice.unmute();
else client.voice.mute();
};
document.getElementById("end").onclick = async () => {
await client.voice.endCall();
};
</script>
</body>
</html>Browser requirement: The browser will prompt the user for microphone access when a call starts. The SDK requires a modern browser with WebRTC support (Chrome, Edge, Firefox, Safari 14.1+).
Configuration
const client = new AquantClient({
apiKey: string; // API key (required)
apiSecret: string; // API secret (required)
agentId: string; // Agent identifier (required)
baseUrl?: string; // VoiceAI server URL (default: "https://voiceai-api.aquant.ai")
callerId?: string; // Caller identifier (default: "acp-sdk-user")
voiceAdapter?: VoiceAdapter; // Optional — swap VoIP backend (default: Twilio browser SDK)
chatAdapter?: ChatAdapter; // Optional — swap chat transport (default: VoiceAI POST /vss/web-chat SSE)
debug?: boolean; // Enable debug logging (default: false)
});| Property | Required | Default | Description |
|-----------------|----------|--------------------------------------|--------------------------------------|
| apiKey | Yes | — | Your API key |
| apiSecret | Yes | — | Your API secret |
| agentId | Yes | — | AI agent identifier |
| baseUrl | No | https://voiceai-api.aquant.ai | Server URL (override for dev/staging)|
| callerId | No | acp-sdk-user | Identifies the caller to the system |
| voiceAdapter | No | Twilio-based adapter | Custom VoiceAdapter implementation |
| chatAdapter | No | SSE web-chat adapter | Custom ChatAdapter implementation |
| debug | No | false | Enables verbose console logging |
Authentication
The SDK uses API key / secret authentication. During client.initialize(), the SDK exchanges your credentials via POST /acp/token to obtain an access token and agent metadata (name, phone number). Tokens are valid for 1 hour — to refresh, call destroy() and re-initialize.
Client Lifecycle
new AquantClient(config) → client.initialize() → use voice/chat/sessions → client.destroy()
↑ ↑ ↑
Creates the client. Fetches token, registers Cleans up voice device,
No network calls yet. voice device; initializes chat adapter, and internal
chat adapter. Required state.
before voice/chat APIs.You can check client.isInitialized and client.isDestroyed at any time.
Usage with React
The SDK is framework-agnostic, but here's the recommended React pattern:
import { useCallback, useEffect, useRef, useState } from "react";
import { AquantClient, type VoiceCallState } from "@aquantinc/acp-web-sdk";
export default function VoiceCall() {
const clientRef = useRef<AquantClient | null>(null);
const [callState, setCallState] = useState<VoiceCallState | null>(null);
const [calling, setCalling] = useState(false);
// Create client once, destroy on unmount
useEffect(() => {
const client = new AquantClient({
apiKey: "your-api-key",
apiSecret: "your-api-secret",
agentId: "your-agent-id",
});
clientRef.current = client;
client.on("voice.call.ended", () => {
setCalling(false);
setCallState(null);
});
return () => { client.destroy(); };
}, []);
const handleStartCall = async () => {
const client = clientRef.current!;
setCalling(true);
try {
if (!client.isInitialized) await client.initialize();
const state = await client.voice.startCall();
setCallState(state);
client.voice.onStateChange(setCallState);
} catch (e: any) {
setCalling(false);
console.error(e.message);
}
};
const handleEndCall = async () => {
await clientRef.current!.voice.endCall();
};
const handleToggleMute = () => {
const voice = clientRef.current!.voice;
if (voice.isMuted) voice.unmute(); else voice.mute();
setCallState({ ...voice.callState });
};
return (
<div>
<button onClick={handleStartCall} disabled={calling}>Start Call</button>
<button onClick={handleToggleMute} disabled={!callState}>
{callState?.muted ? "Unmute" : "Mute"}
</button>
<button onClick={handleEndCall} disabled={!callState}>End Call</button>
{callState && <p>Status: {callState.status}</p>}
</div>
);
}Key React tips:
- Store the client in a
useRef— it should not trigger re-renders. - Create the client in
useEffectand callclient.destroy()in the cleanup function. - Use
client.isInitializedto lazy-initialize on first action (avoids a separate "Initialize" button). - Use
onStateChangeto push call state updates into React state.
Imports
The SDK exposes a main entry point and subpath imports:
// Full SDK
import { AquantClient, AquantError } from "@aquantinc/acp-web-sdk";
// Chat module only
import { ChatClient } from "@aquantinc/acp-web-sdk/chat";
// Voice module only
import { VoiceClient } from "@aquantinc/acp-web-sdk/voice";Voice
How it works
client.initialize()exchanges your credentials for an access token and retrieves agent metadata (name, phone number).- The SDK sets up the underlying voice device automatically.
startCall()connects to the agent with optimized WebRTC audio (echo cancellation, noise suppression, auto gain control).- Audio flows: Browser ↔ VoiceAI ↔ AI Agent.
API
// Initialize (fetches token, registers voice device)
await client.initialize();
// Agent info is available after initialization
const agent = client.voice.agentInfo;
// { agentId: string, agentName: string, agentPhone: string }
// Start a call
const callState = await client.voice.startCall();
// Mute / unmute
client.voice.mute();
client.voice.unmute();
client.voice.isMuted; // boolean
// Listen for state changes
client.voice.onStateChange((state) => {
console.log(state.status); // "idle" | "connecting" | "ringing" | "connected" | "disconnected" | "failed"
console.log(state.muted); // boolean
});
// End the call
await client.voice.endCall();
// Full call state at any time
client.voice.callState;
// { status, muted, agentInfo? }Call Status Flow
idle → connecting → ringing → connected → disconnected
↓
failedAdvanced: Custom Voice Adapter
The voice module is provider-agnostic. You can swap the built-in provider for any VoIP backend by implementing the VoiceAdapter interface:
import type { VoiceAdapter } from "@aquantinc/acp-web-sdk/voice";
class MyCustomAdapter implements VoiceAdapter {
readonly name = "my-provider";
// Implement: initialize, startCall, endCall, mute, unmute,
// onStatusChange, destroy, status, isMuted, agentInfo
}
const client = new AquantClient({
apiKey: "...",
apiSecret: "...",
agentId: "...",
voiceAdapter: new MyCustomAdapter(),
});Sessions
const session = await client.sessions.create({
metadata: { source: "web-app" },
});
const existing = await client.sessions.get("sess_123");
await client.sessions.updateMetadata("sess_123", { resolved: true });
await client.sessions.end("sess_123", { reason: "resolved" });Chat
Chat uses POST {baseUrl}/vss/web-chat with JSON { agent_id, sender_id, message }. The server streams the assistant reply as Server-Sent Events (text/event-stream): each data: line carries JSON with a text delta, then { "done": true }. There is no WebSocket — the SDK consumes the stream with fetch and ReadableStream.
After initialize(), call connect({ sessionId }) once per conversation thread. The sessionId is sent as sender_id (stable id for that user/thread — often the id from sessions.create()).
await client.initialize();
client.chat.connect({ sessionId: session.id });
const unsubscribe = client.chat.onMessage((message) => {
// User message once; assistant messages update as deltas arrive (same id, growing `text`).
console.log(`[${message.role}]: ${message.text}`);
});
const reply = await client.chat.sendMessage({
sessionId: session.id,
text: "Can you help me?",
});
// `reply` is the final assistant message (full accumulated text).
client.chat.onConnectionStatusChange((status) => {
console.log("Chat:", status); // "disconnected" | "connected"
});
client.chat.disconnect();Advanced: Custom Chat Adapter
Implement ChatAdapter and pass chatAdapter if you need a non-default backend (the default streams VoiceAI web-chat SSE).
import type { ChatAdapter } from "@aquantinc/acp-web-sdk";
class MyChatAdapter implements ChatAdapter {
readonly name = "custom";
// initialize, connect, disconnect, streamAssistantReply, destroy, isConnected
}
const client = new AquantClient({
apiKey: "...",
apiSecret: "...",
agentId: "...",
chatAdapter: new MyChatAdapter(),
});Events
Subscribe to typed events across all modules:
// Voice
client.on("voice.call.started", (state) => { /* ... */ });
client.on("voice.call.connected", (state) => { /* ... */ });
client.on("voice.call.ended", (state) => { /* ... */ });
// Chat
client.on("chat.message.received", (msg) => { /* ... */ });
client.on("chat.message.sent", (msg) => { /* ... */ });
// Sessions
client.on("session.created", (session) => { /* ... */ });
client.on("session.ended", (session) => { /* ... */ });
// Errors
client.on("error", (error) => { /* ... */ });
// One-time listener
client.once("voice.call.connected", (state) => { /* ... */ });
// Unsubscribe
const unsub = client.on("error", handler);
unsub();Error Handling
All SDK errors extend AquantError with a typed error code:
import { AquantError, isAquantError } from "@aquantinc/acp-web-sdk";
try {
await client.initialize();
} catch (error) {
if (isAquantError(error)) {
console.error(error.code); // e.g. "AUTH_INVALID_CREDENTIALS"
console.error(error.message); // human-readable description
console.error(error.details); // optional extra context
}
}| Error Code | When |
|-----------------------------|---------------------------------------------------|
| AUTH_INVALID_CREDENTIALS | Invalid API key or secret (HTTP 401) |
| AGENT_NOT_FOUND | No agent found for the given agentId (HTTP 404) |
| AGENT_NOT_ACTIVE | Agent exists but is not active (HTTP 400) |
| VOICE_CONNECTION_FAILED | Voice device or call connection failed |
| VOICE_NOT_CONNECTED | Action requires an active call but none exists |
| CLIENT_NOT_INITIALIZED | Method called before initialize() |
| CLIENT_DESTROYED | Method called after destroy() |
| NETWORK_UNAVAILABLE | Network request failed |
| SESSION_NOT_FOUND | Session ID not found |
| SESSION_CREATE_FAILED | Session creation failed |
| CHAT_SEND_FAILED | Chat request failed (network, HTTP error, or SSE error payload) |
| CHAT_CONNECTION_FAILED | sendMessage without connect(), or sessionId mismatch |
Development
npm install # install dependencies
npm run build # build ESM + CJS + types
npm test # run tests (Vitest)
npm run lint # type-check with tscProject Structure
src/
index.ts # Root entry point & public exports
core/
client.ts # AquantClient — main entry point
config.ts # Config validation, defaults, headers
events.ts # Typed EventBus
errors.ts # AquantError + error codes
sessions.ts # SessionManager
types.ts # Shared type definitions
chat/
index.ts # Chat subpath entry
chatClient.ts # ChatClient (public API)
chatAdapter.ts # ChatAdapter interface + WebChatAdapter (SSE) + default stub
types.ts # Chat-specific types
voice/
index.ts # Voice subpath entry
voiceClient.ts # VoiceClient (public API)
voiceAdapter.ts # VoiceAdapter interface + DefaultVoiceAdapter (stub)
twilioVoiceAdapter.ts # Built-in Twilio browser SDK adapter
types.ts # Voice-specific types
utils/
logger.ts # Leveled logger
guards.ts # Runtime assertion helpersLicense
MIT
