Skip to main content

Documentation Index

Fetch the complete documentation index at: https://orbit-docs.devotel.io/llms.txt

Use this file to discover all available pages before exploring further.

Web SDK

The Devotel Web SDK (@devotel/orbit-web-sdk) is the browser-side SDK that powers the embeddable Orbit Chat widget, the WebRTC softphone, on-site personalisation, and Web Push (VAPID) re-engagement notifications. It ships as native ES module + CommonJS builds with subpath exports per surface (./softphone, ./personalization, ./push, ./push/service-worker) and has no peer-dependency on a framework (works in vanilla, React, Vue, Svelte, Solid).

Installation

npm install @devotel/orbit-web-sdk
Or via CDN:
<script type="module" src="https://cdn.jsdelivr.net/npm/@devotel/orbit-web-sdk/dist/index.mjs"></script>
The Softphone path additionally requires jssip as an optional peer dep:
npm install jssip
Chat-only consumers can skip it; the bundle is tree-shaken so you don’t pay the cost unless you import Softphone.

Quick Start

The SDK exports four product surfaces — pick only what you need.
<!doctype html>
<html>
  <head>
    <script type="module">
      import {
        OrbitChat,
        Softphone,
        OrbitPersonalization,
        OrbitPush,
      } from '@devotel/orbit-web-sdk';

      // Chat widget — drop-in inbox-backed widget
      const chat = new OrbitChat({
        publicKey: 'dv_live_pk_your_public_key_here',
        agent: 'agent_support',
        greeting: 'Hi! How can we help?',
        position: 'bottom-right',
      });
      chat.mount('#orbit-chat');

      // Softphone — server mints a short-lived JWT, browser registers a
      // SIP/WebRTC UA against it. NEVER ship a secret key in the browser.
      // `Softphone.fromToken` requires (jwt, opts) — `fromNumber` is the
      // E.164 caller-ID the softphone identifies as on outbound calls.
      const tokenResp = await fetch('/your-backend/softphone-token').then((r) => r.json());
      const softphone = Softphone.fromToken(tokenResp.token, {
        fromNumber: '+15551234567',
      });
      await softphone.register();

      document.querySelector('#call-sales').addEventListener('click', async () => {
        const session = softphone.call('+18005551234');
        session.on('answered', () => console.log('Answered'));
        session.on('ended', (reason) => console.log('Ended:', reason));
      });

      // Personalisation — fetches contact_scores.segment_label so
      // champions/at-risk/dormant visitors see different copy.
      const personalization = new OrbitPersonalization({
        publicKey: 'dv_live_pk_your_public_key_here',
      });
      await personalization.identify({ id: 'user_42', email: 'user@example.com' });
      personalization.track({ name: 'checkout_started', properties: { cart_total_cents: 14999 } });
    </script>
  </head>
  <body>
    <div id="orbit-chat"></div>
    <button id="call-sales">Call sales</button>
  </body>
</html>

Modules

Chat widget — OrbitChat

Embeds the same Inbox-backed live chat that operators see on the dashboard. The widget supports text, file uploads, typing indicators, AI-agent fallback, and human-handoff escalation.
import { OrbitChat } from '@devotel/orbit-web-sdk';

const chat = new OrbitChat({
  publicKey: 'dv_live_pk_your_public_key_here',
  agent: 'agent_support',
  greeting: 'Hi! How can we help?',
  position: 'bottom-right',
});
chat.mount('#orbit-chat');

Softphone (SIP / WebRTC voice) — Softphone

A JsSIP-backed softphone that places outbound PSTN calls or joins inbound voice flows. Call control hooks into Orbit’s voice routing rules (IVR, AI agent, human handoff). Two-step auth: your server exchanges a server-issued JWT (or, less commonly, an API key) for short-lived SIP credentials via POST /voice/softphone/register, then the browser opens a JsSIP WSS to the Orbit SBC.
import { Softphone } from '@devotel/orbit-web-sdk';

// Server hands the browser a short-lived JWT via your backend.
// `fromToken` is synchronous (don't `await` it); it requires a second
// `opts` argument containing at minimum the outbound caller-ID.
const softphone = Softphone.fromToken(jwt, {
  fromNumber: '+15551234567',
});
await softphone.register();

const session = softphone.call('+14155552671', { callerId: '+18005551234' });
session.on('ringing', () => console.log('Ringing'));
session.on('answered', () => console.log('Answered'));
session.on('ended', (reason) => console.log('Ended:', reason));
The SDK auto re-registers ~5 minutes before the SIP credential expires. Tear down with softphone.unregister() to stop the renewal loop. See Voice operations below for the full call-control surface (hangup, hold, mute, DTMF, transfer, conference, recording, webhooks).

Web Push subscriptions — OrbitPush

VAPID + RFC-8030/8291/8292 compliant push registration. Always feature-detect with OrbitPush.isSupported() first — Safari pre-16.4 and browsers without PushManager will throw otherwise.
import { OrbitPush } from '@devotel/orbit-web-sdk';

if (OrbitPush.isSupported()) {
  const push = new OrbitPush({
    publicKey: 'dv_live_pk_your_public_key_here',
    vapidPublicKey: 'BPa6...your-vapid-public-key',
    serviceWorkerPath: '/orbit-push-sw.js',
  });
  const registration = await push.register();
  console.log('Subscribed:', registration.endpoint);
}
The service-worker bootstrap lives at a separate subpath so you can importScripts it from your site root:
// public/orbit-push-sw.js
import { OrbitPushServiceWorker } from '@devotel/orbit-web-sdk/push/service-worker';

OrbitPushServiceWorker.install({
  apiBaseUrl: 'https://api.orbit.devotel.io',
  expectedTenantId: 'org_yourTenantId',
});
The SW must run with scope: "/" to receive pushes for any path; the expectedTenantId guard drops mismatched payloads to prevent cross-tenant leakage.

Personalisation — OrbitPersonalization

Identifies a visitor and fetches their contact_scores.segment_label server-side. Use the segment to swap copy, gate features, or trigger CTAs.
import { OrbitPersonalization } from '@devotel/orbit-web-sdk';

const personalization = new OrbitPersonalization({
  publicKey: 'dv_live_pk_your_public_key_here',
});

await personalization.identify({
  id: 'user_42',
  email: 'user@example.com',
  traits: { plan: 'business' },
});

personalization.track({
  name: 'checkout_started',
  properties: { cart_total_cents: 14999 },
});

const payload = await personalization.fetch();
// payload.segmentLabel ∈ 'champion' | 'at_risk' | 'dormant' | 'new' | …

Voice operations

The browser softphone is a thin client over the JsSIP WebRTC stack. Call control happens on the live CallSession object for the active call leg; for surface-area calls like recording, conferences, transfers to PSTN, transcripts, and dialer campaigns, your backend uses the Node SDK against the same call_id the browser exposes via session.id. All snippets below match the SDK source in packages/sdk-web/src/softphone/.

Make a call

const session = softphone.call('+14155552671', { callerId: '+18005551234' });
session.on('ringing',  () => console.log('Ringing'));
session.on('answered', () => console.log('Answered — call.id =', session.id));
session.on('ended',    (reason) => console.log('Ended:', reason));
session.on('media-level', (level) => updateVUMeter(level)); // 0..1 RMS

Get call details

Live state is on the CallSession. For historical details, query the server SDK with session.id:
const callId = session.id;
console.log(session.direction); // 'outgoing' | 'incoming'
console.log(session.isOnHold(), session.isMuted());

// From your backend (Node SDK):
// const detail = await orbit.voice.calls.get(callId);

Hang up a call

session.hangup();                          // normal hangup
session.hangup('caller-disconnected');     // optional SIP cause

Answer an incoming call

softphone.on('incoming', async (incoming) => {
  // Show a UI prompt; if the user accepts:
  incoming.answer();
});

Hold and mute

session.hold(true);    // put on hold (sends re-INVITE with a=sendonly)
session.hold(false);   // resume

session.mute(true);    // mute local mic
session.mute(false);   // unmute

Send DTMF

session.sendDTMF('1');         // single digit
session.sendDTMF('123#');      // sequence — RFC 4733 telephone-event
// digits must match /^[0-9*#ABCD]+$/

Transfer a call

The browser softphone supports blind transfer (SIP REFER) directly on the call session. Warm (consult-first) transfer is a server-orchestrated flow — your backend calls voice.calls.warmTransfer(session.id, ...) while keeping the local leg alive.
// Cold / blind transfer (in-browser):
session.transfer('+14155552500');         // bare E.164 or full SIP URI

// Warm transfer (server-orchestrated):
await fetch('/your-backend/warm-transfer', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({
    callId: session.id,
    destination: '+14155552500',
    contextWhisper: true,
  }),
});
// Your backend handler then calls (Node SDK):
//   orbit.voice.calls.warmTransfer(callId, { destination, contextWhisper: true })

Conference call

Conferencing is server-orchestrated — the browser stays in its own leg while the backend bridges additional participants:
// From your backend (Node SDK):
// const conf = await orbit.voice.conferences.create({ name: 'support-room' });
// await orbit.voice.conferences.addParticipant(conf.data.id, {
//   callId: session.id, // the browser leg
// });
// await orbit.voice.conferences.addParticipant(conf.data.id, {
//   phoneNumber: '+14155552671',
//   coaching: false,
// });

Record a call

Recording is enabled at call setup — your backend sets record: true when minting the softphone JWT, or when triggering the outbound call. After hangup, fetch the recording from your backend:
// Browser: nothing to do once recording is enabled at the JWT layer.

// Backend (Node SDK), after the 'call.recording.ready' webhook fires:
// const recordings = await orbit.voice.recordings.list({ callId });
// const signed = await orbit.voice.recordings.getDownloadUrl(recordings.data[0].id);
// // Hand the signed URL to the browser to play in an <audio> element.

Listen to a recording

Once your backend has the signed URL, drop it into a standard HTML5 audio element — no SDK API required:
const { url } = await fetch(`/your-backend/recordings/${callId}/url`).then((r) => r.json());
const audio = document.querySelector<HTMLAudioElement>('#playback')!;
audio.src = url;
audio.play();

Voice webhook handling

Voice webhooks are server-side — the browser SDK should never accept inbound HTTPS. Wire your backend (see the Node SDK voice webhook guide) and forward state to the browser via your existing channel (WebSocket, SSE, or a BroadcastChannel):
// Browser-side: subscribe to a backend-pushed event stream.
const es = new EventSource('/your-backend/voice-events?callId=' + session.id);
es.addEventListener('call.recording.ready', (e) => {
  const { recordingId } = JSON.parse(e.data);
  showDownloadButton(recordingId);
});
es.addEventListener('call.transcript.ready', (e) => {
  const { transcript } = JSON.parse(e.data);
  renderTranscript(transcript);
});

Configuration

Each surface (OrbitChat, Softphone, OrbitPersonalization, OrbitPush) takes its own constructor options. Common options across surfaces:
OptionTypeDefaultDescription
publicKeystringYour Devotel public key (dv_live_pk_*) — required for OrbitChat, OrbitPersonalization, and OrbitPush. NEVER ship a secret key in browser code.
baseUrlstringhttps://api.orbit.devotel.ioAPI base URL.
theme'light' | 'dark' | 'system''system'Widget theme — OrbitChat only.
localestringbrowser defaultUI language — OrbitChat only.
Softphone is constructed via the Softphone.fromToken(jwt, opts) factory (where opts carries the outbound fromNumber and any per-call overrides), not a public key — see the Softphone module above.

Source

For server-side / private-key operations (sending messages on behalf of the user, accessing PII, etc.) use the Node.js SDK — never the Web SDK with a secret key.

Authentication

Web SDK surfaces (OrbitChat, OrbitPersonalization, OrbitPush) authenticate with public keys only (dv_live_pk_*). Public keys are scoped to surfaces that are safe to expose in browser code: chat widget submission, push subscription registration, personalisation events. They cannot send messages on behalf of arbitrary recipients, mutate billing, or access PII. Softphone does NOT take a public key directly — your backend mints a short-lived JWT (≈ 60 minutes) via POST /voice/softphone/register using your dv_live_sk_* server key, and the browser receives only the JWT. The SDK then opens the SIP/WebRTC session against that JWT. This keeps your secret key server-side while still letting end-users place calls. See the authentication guide for the full scope matrix.