Package Exports
- @cemscale-voip/voip-sdk
- @cemscale-voip/voip-sdk/hooks
Readme
@cemscale-voip/voip-sdk
TypeScript SDK for integrating CemScale VoIP into your application. Includes an HTTP/WebSocket API client, a WebRTC softphone, and React hooks.
Installation
npm install @cemscale-voip/voip-sdkAuthentication
Use an API Key to authenticate. Get one from the dashboard (API Keys page).
import { VoIPClient } from '@cemscale-voip/voip-sdk';
const voip = new VoIPClient({
apiUrl: 'https://voip-api.cemscale.com',
apiKey: 'csk_live_your_key_here',
});
// Ready. All methods work immediately.That's it. No login, no passwords, no tokens to manage.
Examples
Click-to-call
// Call a phone number from extension 1001
const { callUuid } = await voip.originate({
fromExtension: '1001',
toNumber: '+15551234567',
});
// Control the call
await voip.holdCall(callUuid);
await voip.transfer(callUuid, { targetExtension: '1002', type: 'blind' });
await voip.hangup(callUuid);Extensions
const { extensions } = await voip.listExtensions();
const { extension } = await voip.getExtension('ext-id');
await voip.createExtension({ extension: '1004', password: 'SecurePass1!', displayName: 'Dave' });CRM User Mapping
// Link a CRM user to a VoIP extension
await voip.updateCrmMapping('ext-id', {
crmUserId: 'crm-user-123',
crmMetadata: { department: 'Sales' },
});
// Look up extension by CRM user (click-to-call from contact page)
const { extension } = await voip.getExtensionByCrmUser('crm-user-123');
await voip.originate({ fromExtension: extension.extension, toNumber: '+15551234567' });Real-Time Events (WebSocket)
voip.connectWebSocket();
voip.onWsEvent('call_start', (event) => {
console.log('New call:', event.data);
});
voip.onWsEvent('presence_change', (event) => {
console.log(event.data.extension, event.data.status);
});
voip.onWsEvent('call_end', (event) => {
// Log to CRM
logCallToCRM(event.data);
});Webhooks
await voip.createWebhook({
name: 'CRM Events',
url: 'https://your-crm.com/api/voip-webhook',
events: ['call.started', 'call.answered', 'call.ended'],
});Call Queues
const { queues } = await voip.listQueues();
const { stats } = await voip.getQueueStats('queue-id');
console.log(`Agents: ${stats.agents.available}/${stats.agents.total}`);
console.log(`Service level: ${stats.today.serviceLevel}%`);Business Hours
await voip.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' },
],
afterHoursAction: 'voicemail',
});
const { isOpen } = await voip.getScheduleStatus('schedule-id');Blocklist
await voip.blockNumber({ number: '+15559999999', reason: 'Spam', direction: 'inbound' });
const { blocked } = await voip.checkBlocked('+15559999999'); // trueCDR Export
const csv = await voip.exportCalls({
dateFrom: '2026-03-01',
dateTo: '2026-03-31',
direction: 'inbound',
});API Key Management
// Create a key for a new integration
const { apiKey } = await voip.createApiKey({ name: 'Mobile App', role: 'readonly' });
console.log(apiKey.key); // csk_live_... — save this, shown only once
// List all keys
const { apiKeys } = await voip.listApiKeys();
// Revoke a key
await voip.revokeApiKey('key-id');Connecting a WebRTC Softphone
Your app doesn't need to know or store SIP passwords. Use the API key to fetch credentials on the fly:
// 1. Get SIP credentials for the extension (API key handles auth)
const { sipCredentials } = await voip.getSipCredentialsByNumber('1001');
// sipCredentials = {
// extension: "1001",
// displayName: "Alice",
// password: "auto-provided", <-- API returns it, your app doesn't store it
// sipDomain: "sip.cemscale.com",
// wsUri: "wss://sip.cemscale.com/ws",
// registrar: "sip:sip.cemscale.com"
// }
// 2. Also get TURN credentials for NAT traversal
const turn = await voip.getTurnCredentials();Two methods available:
| Method | Use when |
|---|---|
getSipCredentials(extensionId) |
You have the extension UUID |
getSipCredentialsByNumber('1001') |
You have the extension number |
WebRTC Softphone (Browser Only)
For building a browser-based softphone, use the WebRTCPhone class or the useVoIP React hook.
These require SIP credentials (extension + password), not an API key.
Check microphone access
Before starting the phone, check that the user has granted microphone permission:
import { WebRTCPhone } from '@cemscale-voip/voip-sdk';
const hasAccess = await WebRTCPhone.checkMicrophoneAccess();
if (!hasAccess) {
alert('Microphone access is required to make calls.');
}React hook example
import { useVoIP } from '@cemscale-voip/voip-sdk';
function PhoneWidget() {
const { isRegistered, currentCall, login, startPhone, call, answer, hangup, toggleHold, toggleMute, error } = useVoIP({
apiUrl: 'https://voip-api.cemscale.com',
sipDomain: 'sip.cemscale.com',
wsUri: 'wss://sip.cemscale.com/ws',
});
return (
<div>
<p>{isRegistered ? 'Online' : 'Offline'}</p>
{error && <p style={{ color: 'red' }}>{error}</p>}
{currentCall ? (
<>
<button onClick={hangup}>Hangup</button>
<button onClick={toggleHold}>{currentCall.held ? 'Resume' : 'Hold'}</button>
<button onClick={toggleMute}>{currentCall.muted ? 'Unmute' : 'Mute'}</button>
</>
) : (
<button onClick={() => call('+15551234567')}>Call</button>
)}
</div>
);
}Reliability features (built-in)
The WebRTCPhone handles these scenarios automatically:
| Feature | Behavior |
|---|---|
| SIP transport reconnection | Auto-reconnects up to 3 times (4s delay) if the WebSocket drops |
| Auto re-registration | Re-sends SIP REGISTER after transport reconnects |
| Media recovery | Reattaches remote audio after hold/unhold or renegotiation via pc.ontrack |
| ICE monitoring | Emits error event when ICE connection enters failed or disconnected state |
| Autoplay detection | Emits an error event (AUTOPLAY_BLOCKED) if the browser blocks audio playback |
| Hold guard | Prevents hold/unhold race conditions from rapid toggling |
| Busy call rejection | Incoming calls during active call are rejected with SIP 486 and emit callStateChanged |
WebRTCPhone events
Listen for events via the on() method:
const phone = new WebRTCPhone(config);
phone.on('registered', () => console.log('SIP registered'));
phone.on('registrationFailed', (error) => console.error('Registration failed:', error));
phone.on('callStateChanged', (callInfo) => console.log('Call state:', callInfo.state));
phone.on('error', (err) => console.error('Phone error:', err));| Event | Payload | When |
|---|---|---|
registered |
none | SIP registration succeeds (including after reconnect) |
registrationFailed |
Error |
SIP registration fails or re-registration after reconnect fails |
callStateChanged |
WebRTCCallInfo |
Call state changes (ringing, answered, held, terminated, etc.) |
error |
Error |
ICE failure, autoplay blocked, media error |
## All Methods
### Calls
| Method | Description |
|--------|-------------|
| `originate({ fromExtension, toNumber })` | Start a call |
| `hangup(uuid)` | Hang up |
| `transfer(uuid, { targetExtension, type })` | Transfer (blind/attended) |
| `holdCall(uuid, hold?)` | Hold / resume |
| `parkCall(uuid, slot?)` | Park a call |
| `getParkedCalls()` | List parked calls |
| `listCalls(params?)` | CDR history |
| `getCall(id)` | Single CDR record |
| `getActiveCalls()` | Active calls |
| `getCallStats(period?)` | Call statistics |
| `exportCalls(params?)` | Export CDR as CSV |
### Extensions
| Method | Description |
|--------|-------------|
| `listExtensions()` | List all |
| `getExtension(id)` | Get detail |
| `createExtension(params)` | Create |
| `updateExtension(id, params)` | Update |
| `deleteExtension(id)` | Delete |
| `setCallForward(id, params)` | Set forwarding |
| `getCallForward(id)` | Get forwarding |
| `getExtensionByCrmUser(crmUserId)` | Lookup by CRM user |
| `updateCrmMapping(id, params)` | Set CRM mapping |
| `getSipCredentials(id)` | SIP credentials by extension UUID |
| `getSipCredentialsByNumber('1001')` | SIP credentials by extension number |
### Conferences
| Method | Description |
|--------|-------------|
| `listConferences()` | List active |
| `getConference(name)` | Detail + members |
| `joinConference(name, callUuid)` | Add call to conference |
| `transferToConference(uuid, name?)` | Transfer call to conference |
| `kickFromConference(name, memberId)` | Remove member |
| `muteConferenceMember(name, memberId, mute)` | Mute/unmute |
| `deafConferenceMember(name, memberId, deaf)` | Deaf/undeaf |
| `lockConference(name, lock)` | Lock/unlock |
| `recordConference(name, action)` | Start/stop recording |
### Ring Groups
| Method | Description |
|--------|-------------|
| `listRingGroups()` | List all |
| `createRingGroup(params)` | Create |
| `updateRingGroup(id, params)` | Update |
| `deleteRingGroup(id)` | Delete |
### Call Queues
| Method | Description |
|--------|-------------|
| `listQueues()` | List all |
| `createQueue(params)` | Create |
| `updateQueue(id, params)` | Update |
| `deleteQueue(id)` | Delete |
| `pauseQueueAgent(queueId, agentId, paused)` | Pause/unpause |
| `loginQueueAgent(queueId, agentId, loggedIn)` | Login/logout |
| `getQueueStats(queueId)` | Real-time stats |
### IVR
| Method | Description |
|--------|-------------|
| `listIvrMenus()` | List all |
| `getIvrMenu(id)` | Get detail |
| `createIvrMenu(params)` | Create |
| `updateIvrMenu(id, params)` | Update |
| `deleteIvrMenu(id)` | Delete |
| `uploadIvrAudio(id, file, filename)` | Upload greeting audio |
| `uploadIvrInvalidAudio(id, file, filename)` | Upload invalid-input audio |
| `getIvrAudioUrl(id)` | Get greeting audio URL |
| `getIvrInvalidAudioUrl(id)` | Get invalid-input audio URL |
| `downloadIvrAudio(id)` | Download greeting audio |
| `downloadIvrInvalidAudio(id)` | Download invalid-input audio |
| `deleteIvrAudio(id)` | Delete greeting audio |
| `deleteIvrInvalidAudio(id)` | Delete invalid-input audio |
| `generateIvrTts(id, params)` | Generate TTS greeting |
| `listIvrVoices()` | List available TTS voices |
### Webhooks
| Method | Description |
|--------|-------------|
| `listWebhooks()` | List all |
| `createWebhook(params)` | Create |
| `updateWebhook(id, params)` | Update |
| `deleteWebhook(id)` | Delete |
| `testWebhook(id)` | Send test event |
| `listWebhookDeliveries(id)` | Delivery history |
### Voicemail
| Method | Description |
|--------|-------------|
| `listVoicemails(params?)` | List messages |
| `getVoicemail(id)` | Get single |
| `getVoicemailAudioUrl(id)` | Audio URL |
| `getVoicemailDownloadUrl(id)` | Download URL |
| `getVoicemailCount(extension?)` | Unread/total |
| `markVoicemailRead(id)` | Mark as read |
| `deleteVoicemail(id)` | Delete |
| `bulkDeleteVoicemails(params?)` | Bulk delete |
| `getVoicemailGreeting(extension?)` | Get greeting settings |
| `updateVoicemailGreeting(params)` | Update greeting settings |
| `uploadVoicemailGreetingAudio(file, ext?, name?)` | Upload custom greeting |
| `getVoicemailGreetingAudioUrl(extension)` | Greeting audio URL |
| `deleteVoicemailGreetingAudio(extension?)` | Delete greeting audio |
### Blocklist
| Method | Description |
|--------|-------------|
| `listBlockedNumbers()` | List blocked |
| `blockNumber(params)` | Block a number |
| `unblockNumber(id)` | Unblock |
| `checkBlocked(number)` | Check if blocked |
### Business Hours
| Method | Description |
|--------|-------------|
| `listSchedules()` | List schedules |
| `getSchedule(id)` | Get detail |
| `createSchedule(params)` | Create |
| `updateSchedule(id, params)` | Update |
| `deleteSchedule(id)` | Delete |
| `getScheduleStatus(id)` | Open or closed? |
### SIP Trunks
| Method | Description |
|--------|-------------|
| `listTrunks()` | List trunks |
| `getTrunk(id)` | Get detail |
| `createTrunk(params)` | Create |
| `updateTrunk(id, params)` | Update |
| `deleteTrunk(id)` | Delete |
| `getTrunkStatus(id)` | Check gateway status |
| `syncTrunk(id)` | Sync config to FreeSWITCH |
| `listActiveGateways()` | List active gateways |
### API Keys
| Method | Description |
|--------|-------------|
| `listApiKeys()` | List keys |
| `getApiKeyDetail(id)` | Get detail |
| `createApiKey(params)` | Create (returns full key once) |
| `updateApiKey(id, params)` | Update |
| `revokeApiKey(id)` | Delete |
| `regenerateApiKey(id)` | Regenerate (new key) |
### DIDs
| Method | Description |
|--------|-------------|
| `listDids()` | List numbers |
| `createDid(params)` | Assign DID |
| `updateDid(id, params)` | Update routing |
| `deleteDid(id)` | Remove |
### Tenants (superadmin)
| Method | Description |
|--------|-------------|
| `listTenants()` | List all tenants |
| `getTenant(id)` | Get detail |
| `createTenant(params)` | Create tenant |
| `updateTenant(id, params)` | Update tenant |
| `deleteTenant(id)` | Delete tenant |
| `getTenantStats(id)` | Tenant statistics |
### Recordings
| Method | Description |
|--------|-------------|
| `listRecordings(params?)` | List recordings |
| `getRecordingUrl(id)` | Get presigned S3 URL |
| `getRecordingAudioUrl(id)` | Direct audio stream URL |
| `getRecordingDownloadUrl(id)` | Download URL |
| `deleteRecording(id)` | Delete |
### Presence
| Method | Description |
|--------|-------------|
| `getPresence()` | Simple map |
| `getPresenceDetailed()` | Detailed with call info |
### Reports
| Method | Description |
|--------|-------------|
| `getDashboardStats()` | Dashboard numbers |
| `getCallsByDay(params?)` | Daily breakdown |
| `getTopExtensions(params?)` | Most active extensions |
## WebSocket Events
| Event | Data |
|-------|------|
| `presence_snapshot` | Full presence state on connect |
| `presence_change` | `{ extension, status, callUuid }` |
| `registration_change` | `{ extension, registered, ip, timestamp }` |
| `call_start` | `{ callUuid, caller, destination, direction }` |
| `call_answer` | `{ callUuid }` |
| `call_end` | `{ callUuid, duration, hangupCause }` |
### WebSocket reconnection
The `VoIPClient` WebSocket reconnects automatically with exponential backoff:
- Initial delay: **3 seconds**, max: **60 seconds**
- Max retries: **10** before emitting an error event and stopping
- Call `connectWebSocket()` again to reset the retry counter
## React Hooks
### `useVoIP`
All-in-one hook that combines `VoIPClient` + `WebRTCPhone`. Returns `client`, `phone`, `isRegistered`, `currentCall`, `error`, and action methods (`login`, `startPhone`, `call`, `answer`, `hangup`, `toggleHold`, `toggleMute`, `sendDtmf`, `blindTransfer`, `originate`, etc.).
### `usePresence`
Real-time extension presence via WebSocket with REST polling fallback. Automatically handles `presence_snapshot`, `presence_change`, and `registration_change` events.
```tsx
import { usePresence } from '@cemscale-voip/voip-sdk/hooks';
const { presence, getStatus, isRealtime } = usePresence(client);
// presence: Map<extension, status>
// getStatus('1001') => 'available' | 'on_call' | 'ringing' | 'offline'useCallStatus
Real-time active call tracking via WebSocket.
import { useCallStatus } from '@cemscale-voip/voip-sdk/hooks';
const { activeCalls, recentCalls, stats, activeCount } = useCallStatus(client);Feature Codes (Phone Dial Pad)
| Code | Function |
|---|---|
*3 |
Conference room |
*70 |
Park call |
*71XX |
Retrieve parked call from slot XX |
*97 |
Check voicemail |