Widget SDK
@chanl/widget-sdk — embed script, React hooks, headless chat client, and voice for Chanl agents
@chanl/widget-sdk is the browser-facing package for embedding Chanl agents. It complements @chanl/sdk (server-side platform API). Install it when you need a chat bubble on your site, a custom React chat UI, or in-browser voice — not when you are managing agents from a backend script.
Installation
npm install @chanl/widget-sdkPeer dependency: react ^18 or ^19 (optional — only required for /react).
Package exports
| Import path | Use case |
|---|---|
@chanl/widget-sdk | createChanlClient() — headless chat + lazy voice |
@chanl/widget-sdk/react | useChat, useVoice, useChanl, ChanlWidget |
@chanl/widget-sdk/voice | VoiceClient (browser only) |
@chanl/widget-sdk/next | computeUserHash() for Next.js Server Components |
@chanl/widget-sdk/embed/v1.js | Bundled IIFE (also hosted at chat.channel.tel/widget/v1.js) |
Auth
| Key | Where | Header |
|---|---|---|
pub_xxx | Browser, embed script | Public key — origin allowlist enforced |
ak_xxx.secret | Server only | Never embed in client code |
Create keys in the dashboard under Settings → API keys.
Embed script (no bundler)
<script src="https://chat.channel.tel/widget/v1.js"></script>
<script>
ChanlChat.init({
agentId: 'ag_xxx',
apiKey: 'pub_xxx',
theme: 'dark',
proactiveMessages: [
{ text: 'Questions about pricing?', delay: 3000 },
],
});
</script>Embed API
| Method | Description |
|---|---|
ChanlChat.init(config) | Mount launcher + iframe |
ChanlChat.open() / close() / toggle() | Control panel |
ChanlChat.destroy() | Remove from DOM |
ChanlChat.setUser(user) | Pass visitor identity |
ChanlChat.setContext(ctx) | Page context (URL, product id, …) |
ChanlChat.on(event, fn) | open, close, message, … |
Full option list: see chat widget guide.
Headless client
Build your own UI without the iframe:
import { createChanlClient } from '@chanl/widget-sdk';
const client = createChanlClient({
agentId: 'ag_xxx',
apiKey: 'pub_xxx',
});
const session = await client.chat.createSession('ag_xxx');
await client.chat.streamMessage(
session.sessionId,
'What is your return policy?',
(chunk) => appendToUI(chunk),
(part) => console.log('Tool:', part),
);
await client.chat.endSession(session.sessionId);createSession returns sessionId and interactionId. Subsequent message calls use sessionId.
React: text chat
useChat is tree-shakeable and safe for SSR — no voice/audio deps.
'use client';
import { useChat } from '@chanl/widget-sdk/react';
export function SupportChat({ agentId, apiKey }: { agentId: string; apiKey: string }) {
const { messages, isTyping, send, agentConfig, error } = useChat({
agentId,
apiKey,
});
return (
<div>
<h2>{agentConfig?.agent?.name ?? 'Support'}</h2>
{messages.map((m) => (
<p key={m.id}><strong>{m.role}:</strong> {m.content}</p>
))}
{isTyping && <p>Agent is typing…</p>}
<button type="button" onClick={() => send('Hello')}>Send</button>
{error && <p>{error.message}</p>}
</div>
);
}Identified users
const { send } = useChat({
agentId,
apiKey,
user: {
externalId: 'user_42',
email: 'jane@example.com',
name: 'Jane Doe',
},
userHash: hashFromYourServer, // HMAC-SHA256 of externalId
variables: { plan: 'pro' },
metadata: { pageUrl: window.location.pathname },
});When user.externalId changes, the hook ends the stale session so the next message starts fresh with the new identity.
React: voice
useVoice imports browser audio codecs (~100 KB). Use only in client components.
'use client';
import { useVoice } from '@chanl/widget-sdk/react';
export function VoiceCall({ agentId, apiKey }: { agentId: string; apiKey: string }) {
const {
voiceState,
startVoice,
stopVoice,
toggleMute,
isMuted,
isAgentSpeaking,
duration,
error,
} = useVoice({ agentId, apiKey });
if (voiceState === 'idle') {
return <button type="button" onClick={() => startVoice()}>Start call</button>;
}
return (
<div>
<p>{isAgentSpeaking ? 'Agent speaking' : 'Listening…'} · {Math.floor(duration)}s</p>
<button type="button" onClick={toggleMute}>{isMuted ? 'Unmute' : 'Mute'}</button>
<button type="button" onClick={() => stopVoice()}>End</button>
{error && <p>{error.message}</p>}
</div>
);
}Use useChanl when one component switches between text and voice.
Next.js identity hash
// app/page.tsx — Server Component
import { computeUserHash } from '@chanl/widget-sdk/next';
import { ChanlWidget } from '@chanl/widget-sdk/react';
export default async function Page() {
const user = await getCurrentUser();
const userHash = computeUserHash(user.id, process.env.CHANL_IDENTITY_SECRET!);
return (
<ChanlWidget
agentId={process.env.NEXT_PUBLIC_CHANL_AGENT_ID!}
apiKey={process.env.NEXT_PUBLIC_CHANL_PUB_KEY!}
user={{ externalId: user.id, email: user.email, name: user.name }}
userHash={userHash}
/>
);
}CHANL_IDENTITY_SECRET must stay server-side. Only userHash crosses to the client.
Stream protocol
Chat streaming uses line-prefixed JSON:
| Prefix | Meaning |
|---|---|
0: | Text delta |
9: | Tool call start |
a: | Tool result |
e: | Finish + usage |
The SDK parses this automatically in streamMessage and React hooks.
Troubleshooting
| Symptom | Fix |
|---|---|
401 Origin not allowed | Add your site origin to the public key allowlist |
| Widget blank | Confirm agent has Public embed enabled |
Worker is not defined in tests | Import voice only in browser code; use useChat in Node tests |
| Raw text instead of tool events | Backend may not be sending the stream protocol — upgrade chanl-api |
vs @chanl/sdk
@chanl/sdk | @chanl/widget-sdk | |
|---|---|---|
| Runs in | Node, server, CI | Browser (voice: browser only) |
| Auth | ak_xxx API key | pub_xxx in browser |
| Scope | Full platform CRUD | Chat + voice sessions for one agent |
| npm | @chanl/sdk | @chanl/widget-sdk |
Use both in the same project: @chanl/sdk on your backend, @chanl/widget-sdk on your frontend.
Related
- Chat widget (product guide)
- SDK overview
- Chat module (
@chanl/sdk) — server-side session testing