@2chat/voice-sdk
v0.2.1
Published
2Chat Voice SDK — embeddable, framework-agnostic browser voice calling.
Downloads
388
Maintainers
Readme
@2chat/voice-sdk
Embeddable, framework-agnostic browser voice calling for the 2Chat platform.
JsSIP under the hood; a Device / Call public API on top.
- Outbound + inbound calls
- Mute, hold, DTMF
- Short-lived JWT auth — your long-lived
X-User-API-Keynever reaches the browser - ESM, CJS, and UMD/CDN builds; zero React (or any framework) dependency
Install
npm install @2chat/voice-sdkOr drop the UMD build directly into a <script> tag:
<script src="https://cdn.jsdelivr.net/npm/@2chat/voice-sdk/dist/index.global.js"></script>
<script>
const { Device } = TwoChatVoice;
</script>How auth works
- Your backend holds a long-lived 2Chat
X-User-API-Key. - Your backend calls
POST /open/sdk/access-tokenwith auser_uuid(and optionallabel) to mint a short-lived JWT. - You hand that JWT to the browser; the SDK uses it to fetch SIP credentials and register.
The JWT is scoped to one 2Chat User — running N concurrent agents means minting N tokens, one per real User your backend has provisioned.
Quick start
import { Device } from "@2chat/voice-sdk";
// token comes from your backend: POST /open/sdk/access-token
const device = new Device({
token: myJwt,
logLevel: "info",
});
device.on("registered", () => console.log("ready"));
device.on("incoming", (call) => {
if (confirm("Incoming call — accept?")) call.accept();
else call.reject();
});
device.on("tokenWillExpire", async () => {
const fresh = await fetch("/my/backend/voice-token").then((r) => r.text());
await device.updateToken(fresh);
});
device.on("error", (err) => console.warn(err.code, err.message));
await device.register();
const call = await device.connect({
to: "+15551234567",
from: "+15550000000", // caller ID
});
call.on("ringing", () => {});
call.on("accepted", () => {});
call.on("disconnect", ({ reason }) => console.log("ended:", reason));
call.mute(true);
call.hold(true);
call.sendDigits("1");
await call.disconnect();API
new Device(options)
| option | type | default | notes |
|---|---|---|---|
| token | string | — | Required. JWT minted by your backend. |
| logLevel | silent \| error \| warn \| info \| debug | warn | |
| allowIncomingWhileBusy | boolean | false | If false, extra incoming calls get rejected with 486. |
| iceServers | RTCIceServer[] | from credentials | Override ICE servers entirely. |
| media | { inputDeviceId?, outputDeviceId? } | — | Initial device preferences. |
| ringtoneUrl | string | — | Looped ringtone for inbound calls. |
| tokenExpiryLeadMs | number | 60000 | How far before exp to fire tokenWillExpire. |
| defaultExtraHeaders | string[] | — | Appended to every outbound call. |
Methods
register(): Promise<void>— fetch SIP creds, start the UA, send the initial REGISTER.connect(params): Promise<Call>— place an outbound call.updateToken(jwt): Promise<void>— swap the token in place (sameuser_uuidonly).unregister(): Promise<void>— send un-REGISTER and close the WS.destroy(): void— unrecoverable teardown; call on page unload.enumerateDevices(),setInputDevice(id),setOutputDevice(id)isRegistered(): boolean,identity: TokenClaims,label?: string,tokenExpiresAt: Date
Events
| name | payload |
|---|---|
| registered | void |
| unregistered | { reason?: string } |
| incoming | Call |
| tokenWillExpire | { expiresAt: Date } |
| reconnecting | { attempt: number } |
| reconnected | void |
| error | VoiceSDKError |
Call
call.accept(); // inbound only
call.reject(); // inbound only
call.disconnect();
call.mute(true);
call.hold(true);
call.sendDigits("1");
call.setInputDevice(id); // applies to this call only
call.setOutputDevice(id); // applies to this call only
call.direction // 'inbound' | 'outbound'
call.getStatus() // 'pending' | 'ringing' | 'accepted' | 'disconnected' | 'failed'
call.isMuted()
call.isOnHold()
call.session // raw JsSIP RTCSession escape hatchEvents: ringing, accepted, disconnect, error, mute, hold, quality.
Errors
All SDK errors are instances of VoiceSDKError with a typed .code:
import { VoiceSDKError } from "@2chat/voice-sdk";
device.on("error", (err: VoiceSDKError) => {
switch (err.code) {
case "TOKEN_EXPIRED":
case "AUTH_ERROR":
// re-mint the token
break;
case "REGISTRATION_FAILED":
// let the user know
break;
}
});Development
npm install
npm run typecheck
npm test
npm run build
# Try the vanilla example:
npm run build && npx serve .
# then navigate to examples/vanilla-htmlLicense
MIT
