Chanl

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-sdk

Peer dependency: react ^18 or ^19 (optional — only required for /react).

Package exports

Import pathUse case
@chanl/widget-sdkcreateChanlClient() — headless chat + lazy voice
@chanl/widget-sdk/reactuseChat, useVoice, useChanl, ChanlWidget
@chanl/widget-sdk/voiceVoiceClient (browser only)
@chanl/widget-sdk/nextcomputeUserHash() for Next.js Server Components
@chanl/widget-sdk/embed/v1.jsBundled IIFE (also hosted at chat.channel.tel/widget/v1.js)

Auth

KeyWhereHeader
pub_xxxBrowser, embed scriptPublic key — origin allowlist enforced
ak_xxx.secretServer onlyNever 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

MethodDescription
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:

PrefixMeaning
0:Text delta
9:Tool call start
a:Tool result
e:Finish + usage

The SDK parses this automatically in streamMessage and React hooks.

Troubleshooting

SymptomFix
401 Origin not allowedAdd your site origin to the public key allowlist
Widget blankConfirm agent has Public embed enabled
Worker is not defined in testsImport voice only in browser code; use useChat in Node tests
Raw text instead of tool eventsBackend may not be sending the stream protocol — upgrade chanl-api

vs @chanl/sdk

@chanl/sdk@chanl/widget-sdk
Runs inNode, server, CIBrowser (voice: browser only)
Authak_xxx API keypub_xxx in browser
ScopeFull platform CRUDChat + 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.

On this page