JSPM

  • Created
  • Published
  • Downloads 1364
  • Score
    100M100P100Q132992F
  • License UNLICENSED

VoIP SDK for CemScale multi-tenant platform — API client, WebRTC, React hooks

Package Exports

  • @cemscale-voip/voip-sdk
  • @cemscale-voip/voip-sdk/hooks

Readme

@cemscale/voip-sdk

TypeScript SDK for integrating CemScale VoIP into your CRM application. Provides API client, WebRTC softphone, React hooks, and real-time WebSocket events.

Installation

npm install @cemscale/voip-sdk

Quick Start

1. API Client (Node.js or Browser)

import { VoIPClient } from '@cemscale/voip-sdk';

const client = new VoIPClient({
  apiUrl: 'https://voip-api.cemscale.com',
});

// Login as admin
await client.adminLogin({
  email: 'admin@yourtenant.example.com',
  password: 'your-admin-password',
});

// Click-to-call: originate a call from extension to PSTN
const { callUuid } = await client.originate({
  fromExtension: '1001',
  toNumber: '+17865551234',
});

// Monitor the call
await client.holdCall(callUuid);        // Hold
await client.holdCall(callUuid, false); // Resume
await client.transfer(callUuid, { targetExtension: '1002', type: 'blind' });
await client.hangup(callUuid);

2. React Integration (useVoIP hook)

import { useVoIP } from '@cemscale/voip-sdk';

function PhoneWidget() {
  const {
    isLoggedIn, isRegistered, currentCall, error,
    login, startPhone, call, answer, hangup, toggleHold, toggleMute,
  } = useVoIP({
    apiUrl: 'https://voip-api.cemscale.com',
    sipDomain: 'sip.cemscale.com',
    wsUri: 'wss://sip.cemscale.com:5065/ws',
  });

  // Login and start softphone
  async function init() {
    await login({ username: '1001', password: 'your-extension-password', tenantId: 'your-tenant-id' });
    await startPhone('1001', 'your-extension-password', 'Alice');
  }

  return (
    <div>
      <p>Status: {isRegistered ? 'Online' : 'Offline'}</p>
      {currentCall ? (
        <div>
          <p>{currentCall.direction}: {currentCall.remoteIdentity} ({currentCall.state})</p>
          {currentCall.state === 'ringing' && currentCall.direction === 'inbound' && (
            <button onClick={answer}>Answer</button>
          )}
          <button onClick={hangup}>Hangup</button>
          <button onClick={toggleHold}>{currentCall.held ? 'Resume' : 'Hold'}</button>
          <button onClick={toggleMute}>{currentCall.muted ? 'Unmute' : 'Mute'}</button>
        </div>
      ) : (
        <button onClick={() => call('+17865551234')}>Call</button>
      )}
    </div>
  );
}

3. Real-Time Events (WebSocket)

const client = new VoIPClient({ apiUrl: 'https://voip-api.cemscale.com' });
await client.adminLogin({ email: 'admin@demo.cemscale.com', password: '...' });

// Connect WebSocket for real-time events
client.connectWebSocket();

// Incoming call popup
client.onWsEvent('call_start', (event) => {
  if (event.data.direction === 'inbound') {
    showIncomingCallPopup({
      caller: event.data.caller,
      destination: event.data.destination,
      callUuid: event.data.callUuid,
    });
  }
});

// Live presence updates
client.onWsEvent('presence_change', (event) => {
  updateExtensionStatus(event.data.extension, event.data.status);
});

// Call ended — log to CRM
client.onWsEvent('call_end', (event) => {
  logCallToCRM({
    callUuid: event.data.callUuid,
    duration: event.data.duration,
    hangupCause: event.data.hangupCause,
  });
});

4. Webhooks (Server-Side CRM Integration)

// Register a webhook to receive call events
const { webhook } = await client.createWebhook({
  name: 'CRM Call Events',
  url: 'https://your-crm.com/api/voip-webhook',
  events: ['call.started', 'call.answered', 'call.ended', 'voicemail.new'],
  secret: 'your-hmac-secret',  // optional, auto-generated if omitted
});

// Your CRM receives POST requests like:
// {
//   "event": "call.ended",
//   "timestamp": "2026-03-28T00:30:00Z",
//   "data": {
//     "callUuid": "abc-123",
//     "direction": "inbound",
//     "caller": "+17865551234",
//     "destination": "1001",
//     "duration": 45,
//     "billsec": 42,
//     "hangupCause": "NORMAL_CLEARING",
//     "status": "completed",
//     "recordingFile": "/tmp/recordings/tenant-id/uuid.wav"
//   }
// }

// Verify webhook signature (Node.js):
const crypto = require('crypto');
function verifySignature(body, signature, secret) {
  const expected = `sha256=${crypto.createHmac('sha256', secret).update(body).digest('hex')}`;
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

API Reference

Authentication

Method Description
client.login(params) Login as extension user
client.adminLogin(params) Login as tenant admin
client.me() Get current user info
client.getTurnCredentials() Get TURN credentials for WebRTC

Calls

Method Description
client.originate({ fromExtension, toNumber }) Click-to-call via ESL
client.hangup(uuid) Hang up active call
client.transfer(uuid, { targetExtension, type }) Transfer call (blind/attended)
client.holdCall(uuid, hold?) Hold/resume call
client.parkCall(uuid, slot?) Park call (*70)
client.getParkedCalls() List parked calls
client.transferToConference(uuid, name?) Move call to conference
client.listCalls(params?) List CDR records
client.getCall(id) Get single CDR
client.getActiveCalls() List active calls from FS
client.getCallStats(period?) Call statistics
client.exportCalls(params?) Export CDR as CSV

Extensions

Method Description
client.listExtensions() List all extensions
client.getExtension(id) Get extension detail
client.createExtension(params) Create extension + SIP subscriber
client.updateExtension(id, params) Update extension (incl. CRM mapping)
client.deleteExtension(id) Delete extension
client.setCallForward(id, { enabled, number, type }) Set call forward
client.getCallForward(id) Get call forward settings
client.getExtensionByCrmUser(crmUserId) Lookup extension by CRM user
client.updateCrmMapping(id, params) Update CRM mapping

CRM Mapping

// Map CRM user to extension
await client.updateCrmMapping(extId, {
  crmUserId: 'crm-user-12345',
  crmMetadata: { department: 'Sales', role: 'Manager' },
});

// Lookup extension by CRM user ID (click-to-call from CRM contact)
const { extension } = await client.getExtensionByCrmUser('crm-user-12345');
await client.originate({ fromExtension: extension.extension, toNumber: '+15551234567' });

Conferences

Method Description
client.listConferences() List active conferences
client.getConference(name) Conference details + members
client.joinConference(name, callUuid) Add call to conference
client.kickFromConference(name, memberId) Remove member
client.muteConferenceMember(name, memberId, mute) Mute/unmute
client.lockConference(name, lock) Lock/unlock
client.recordConference(name, action) Start/stop recording

Ring Groups

Method Description
client.listRingGroups() List ring groups
client.createRingGroup(params) Create ring group
client.updateRingGroup(id, params) Update ring group
client.deleteRingGroup(id) Delete ring group

Strategies: simultaneous (ring all at once), sequential (by priority), random

Call Queues (ACD)

Method Description
client.listQueues() List queues
client.createQueue(params) Create queue
client.updateQueue(id, params) Update queue
client.deleteQueue(id) Delete queue
client.pauseQueueAgent(queueId, agentId, paused) Pause/unpause agent
client.loginQueueAgent(queueId, agentId, loggedIn) Login/logout agent
client.getQueueStats(queueId) Real-time queue statistics

Strategies: round-robin, longest-idle, ring-all, least-calls, random

IVR

Method Description
client.listIvrMenus() List IVR menus
client.getIvrMenu(id) Get IVR detail
client.createIvrMenu(params) Create IVR menu
client.updateIvrMenu(id, params) Update IVR
client.deleteIvrMenu(id) Delete IVR

IVR Option Actions: transfer (to extension), ivr (sub-menu), voicemail, queue, external (PSTN), ringgroup, hangup

Webhooks

Method Description
client.listWebhooks() List webhooks + supported events
client.createWebhook(params) Create webhook
client.updateWebhook(id, params) Update webhook
client.deleteWebhook(id) Delete webhook
client.testWebhook(id) Send test event
client.listWebhookDeliveries(id) Delivery history

Supported Events:

  • call.started — new call initiated
  • call.answered — call answered
  • call.ended — call ended (full CDR data)
  • call.transferred — call transferred
  • call.held / call.resumed — hold state
  • call.parked — call parked
  • voicemail.new — new voicemail
  • extension.registered / extension.unregistered — registration
  • queue.caller_joined / queue.caller_left — queue events
  • * — all events

Voicemail

Method Description
client.listVoicemails(params?) List voicemails
client.getVoicemailCount(extension?) Unread/total count
client.getVoicemail(id) Get single voicemail
client.getVoicemailAudioUrl(id) Get audio download URL
client.markVoicemailRead(id) Mark as read
client.deleteVoicemail(id) Delete voicemail
client.bulkDeleteVoicemails(params?) Bulk delete voicemails

Blocklist (Call Blocking)

Method Description
client.listBlockedNumbers() List all blocked numbers
client.blockNumber(params) Block a phone number
client.unblockNumber(id) Unblock a number by ID
client.checkBlocked(number) Check if a number is blocked
// Block a spam caller
await client.blockNumber({
  number: '+15551234567',
  reason: 'Spam caller',
  direction: 'inbound',  // 'inbound' | 'outbound' | 'both'
});

// Check before routing
const { blocked } = await client.checkBlocked('+15551234567');

Business Hours / Schedules

Method Description
client.listSchedules() List business hour schedules
client.getSchedule(id) Get schedule detail
client.createSchedule(params) Create a schedule
client.updateSchedule(id, params) Update a schedule
client.deleteSchedule(id) Delete a schedule
client.getScheduleStatus(id) Check if currently open/closed
// Create business hours
await client.createSchedule({
  name: 'Office Hours',
  timezone: 'America/New_York',
  schedules: [
    { day: 'monday', enabled: true, startTime: '09:00', endTime: '17:00' },
    { day: 'tuesday', enabled: true, startTime: '09:00', endTime: '17:00' },
    // ...
  ],
  holidays: [{ date: '2026-12-25', name: 'Christmas' }],
  afterHoursAction: 'voicemail',
});

// Check if office is open right now
const { isOpen } = await client.getScheduleStatus(scheduleId);

SIP Trunks

Method Description
client.listTrunks() List SIP trunks (passwords masked)
client.getTrunk(id) Get trunk detail
client.createTrunk(params) Create a SIP trunk
client.updateTrunk(id, params) Update a trunk
client.deleteTrunk(id) Delete a trunk
await client.createTrunk({
  name: 'Twilio US',
  provider: 'twilio',
  gateway: 'your-trunk.pstn.twilio.com',
  authType: 'ip',
  priority: 1,
  maxChannels: 50,
});

Queue Stats & CDR Export

Method Description
client.getQueueStats(queueId) Real-time queue statistics
client.exportCalls(params?) Export CDR as CSV text
// Real-time queue dashboard
const { stats } = await client.getQueueStats(queueId);
console.log(`Agents available: ${stats.agents.available}/${stats.agents.total}`);
console.log(`Service level: ${stats.today.serviceLevel}%`);

// Export CDR for a date range
const csv = await client.exportCalls({
  dateFrom: '2026-03-01',
  dateTo: '2026-03-28',
  direction: 'inbound',
  limit: 10000,
});

Presence

Method Description
client.getPresence() Simple presence map
client.getPresenceDetailed() Detailed presence with call info

WebSocket Events

Event Type Data
presence_snapshot Full presence state on connect
presence_change { extension, status, callUuid, caller, direction }
call_start { callUuid, caller, destination, direction }
call_answer { callUuid }
call_end { callUuid, caller, destination, duration, hangupCause }
registration_change { extension, registered, ip }

Feature Codes (Phone Dial Pad)

Code Function
*3 Enter conference room
*70 Park current call (auto-assign slot)
*71XX Retrieve parked call from slot XX
*97 Check voicemail
9196 Echo test
9197 MOH test

Architecture

CRM (React + Node.js)
  |
  |--- @cemscale/voip-sdk (HTTP + WebSocket + WebRTC)
  |
  v
API (Fastify + TypeScript) --- port 3000
  |
  |--- ESL ---> FreeSWITCH (media, recording, conference, IVR, queues)
  |--- SQL ---> PostgreSQL (tenants, extensions, CDR, webhooks, queues)
  |--- Redis --> Presence, caching
  |
  v
Kamailio (SIP proxy) --- port 5060
  |
  |--- RTPEngine (media relay, NAT traversal)
  |--- Twilio Elastic SIP Trunk (PSTN connectivity)

Notes

  • All endpoints require JWT authentication (except /health and /api/internal/*)
  • Webhooks include HMAC-SHA256 signatures in X-Webhook-Signature header
  • WebSocket requires auth message with JWT token as first message
  • Conference rooms are tenant-isolated (conf_{tenantId})
  • Parking lots are tenant-isolated (park_{tenantId}, slots 701-750)
  • All recordings saved to /tmp/recordings/{tenantId}/
  • Twilio requires tenant DID as caller ID for outbound (carrier limitation)