@cemscale/voip-sdkTypeScript SDK for integrating CemScale VoIP into your CRM application. Provides API client, WebRTC softphone, React hooks, and real-time WebSocket events.
Installationnpm 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' ,
} ) ;
await client. adminLogin ( {
email: 'admin@yourtenant.example.com' ,
password: 'your-admin-password' ,
} ) ;
const { callUuid } = await client. originate ( {
fromExtension: '1001' ,
toNumber: '+17865551234' ,
} ) ;
await client. holdCall ( callUuid) ;
await client. holdCall ( callUuid, false ) ;
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' ,
} ) ;
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: '...' } ) ;
client. connectWebSocket ( ) ;
client. onWsEvent ( 'call_start' , ( event) => {
if ( event. data. direction === 'inbound' ) {
showIncomingCallPopup ( {
caller: event. data. caller,
destination: event. data. destination,
callUuid: event. data. callUuid,
} ) ;
}
} ) ;
client. onWsEvent ( 'presence_change' , ( event) => {
updateExtensionStatus ( event. data. extension, event. data. status) ;
} ) ;
client. onWsEvent ( 'call_end' , ( event) => {
logCallToCRM ( {
callUuid: event. data. callUuid,
duration: event. data. duration,
hangupCause: event. data. hangupCause,
} ) ;
} ) ; 4. Webhooks (Server-Side CRM Integration)
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' ,
} ) ;
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
await client. updateCrmMapping ( extId, {
crmUserId: 'crm-user-12345' ,
crmMetadata: { department: 'Sales' , role: 'Manager' } ,
} ) ;
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
await client. blockNumber ( {
number : '+15551234567' ,
reason: 'Spam caller' ,
direction: 'inbound' ,
} ) ;
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
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' ,
} ) ;
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
const { stats } = await client. getQueueStats ( queueId) ;
console . log ( ` Agents available: ${ stats. agents. available} / ${ stats. agents. total} ` ) ;
console . log ( ` Service level: ${ stats. today. serviceLevel} % ` ) ;
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
ArchitectureCRM (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)