@cemscale-voip/voip-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,445 @@
1
+ # @cemscale/voip-sdk
2
+
3
+ TypeScript SDK for integrating CemScale VoIP into your CRM application. Provides API client, WebRTC softphone, React hooks, and real-time WebSocket events.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @cemscale/voip-sdk
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ### 1. API Client (Node.js or Browser)
14
+
15
+ ```typescript
16
+ import { VoIPClient } from '@cemscale/voip-sdk';
17
+
18
+ const client = new VoIPClient({
19
+ apiUrl: 'https://voip-api.cemscale.com',
20
+ });
21
+
22
+ // Login as admin
23
+ await client.adminLogin({
24
+ email: 'admin@yourtenant.example.com',
25
+ password: 'your-admin-password',
26
+ });
27
+
28
+ // Click-to-call: originate a call from extension to PSTN
29
+ const { callUuid } = await client.originate({
30
+ fromExtension: '1001',
31
+ toNumber: '+17865551234',
32
+ });
33
+
34
+ // Monitor the call
35
+ await client.holdCall(callUuid); // Hold
36
+ await client.holdCall(callUuid, false); // Resume
37
+ await client.transfer(callUuid, { targetExtension: '1002', type: 'blind' });
38
+ await client.hangup(callUuid);
39
+ ```
40
+
41
+ ### 2. React Integration (useVoIP hook)
42
+
43
+ ```tsx
44
+ import { useVoIP } from '@cemscale/voip-sdk';
45
+
46
+ function PhoneWidget() {
47
+ const {
48
+ isLoggedIn, isRegistered, currentCall, error,
49
+ login, startPhone, call, answer, hangup, toggleHold, toggleMute,
50
+ } = useVoIP({
51
+ apiUrl: 'https://voip-api.cemscale.com',
52
+ sipDomain: 'sip.cemscale.com',
53
+ wsUri: 'wss://sip.cemscale.com:5065/ws',
54
+ });
55
+
56
+ // Login and start softphone
57
+ async function init() {
58
+ await login({ username: '1001', password: 'your-extension-password', tenantId: 'your-tenant-id' });
59
+ await startPhone('1001', 'your-extension-password', 'Alice');
60
+ }
61
+
62
+ return (
63
+ <div>
64
+ <p>Status: {isRegistered ? 'Online' : 'Offline'}</p>
65
+ {currentCall ? (
66
+ <div>
67
+ <p>{currentCall.direction}: {currentCall.remoteIdentity} ({currentCall.state})</p>
68
+ {currentCall.state === 'ringing' && currentCall.direction === 'inbound' && (
69
+ <button onClick={answer}>Answer</button>
70
+ )}
71
+ <button onClick={hangup}>Hangup</button>
72
+ <button onClick={toggleHold}>{currentCall.held ? 'Resume' : 'Hold'}</button>
73
+ <button onClick={toggleMute}>{currentCall.muted ? 'Unmute' : 'Mute'}</button>
74
+ </div>
75
+ ) : (
76
+ <button onClick={() => call('+17865551234')}>Call</button>
77
+ )}
78
+ </div>
79
+ );
80
+ }
81
+ ```
82
+
83
+ ### 3. Real-Time Events (WebSocket)
84
+
85
+ ```typescript
86
+ const client = new VoIPClient({ apiUrl: 'https://voip-api.cemscale.com' });
87
+ await client.adminLogin({ email: 'admin@demo.cemscale.com', password: '...' });
88
+
89
+ // Connect WebSocket for real-time events
90
+ client.connectWebSocket();
91
+
92
+ // Incoming call popup
93
+ client.onWsEvent('call_start', (event) => {
94
+ if (event.data.direction === 'inbound') {
95
+ showIncomingCallPopup({
96
+ caller: event.data.caller,
97
+ destination: event.data.destination,
98
+ callUuid: event.data.callUuid,
99
+ });
100
+ }
101
+ });
102
+
103
+ // Live presence updates
104
+ client.onWsEvent('presence_change', (event) => {
105
+ updateExtensionStatus(event.data.extension, event.data.status);
106
+ });
107
+
108
+ // Call ended — log to CRM
109
+ client.onWsEvent('call_end', (event) => {
110
+ logCallToCRM({
111
+ callUuid: event.data.callUuid,
112
+ duration: event.data.duration,
113
+ hangupCause: event.data.hangupCause,
114
+ });
115
+ });
116
+ ```
117
+
118
+ ### 4. Webhooks (Server-Side CRM Integration)
119
+
120
+ ```typescript
121
+ // Register a webhook to receive call events
122
+ const { webhook } = await client.createWebhook({
123
+ name: 'CRM Call Events',
124
+ url: 'https://your-crm.com/api/voip-webhook',
125
+ events: ['call.started', 'call.answered', 'call.ended', 'voicemail.new'],
126
+ secret: 'your-hmac-secret', // optional, auto-generated if omitted
127
+ });
128
+
129
+ // Your CRM receives POST requests like:
130
+ // {
131
+ // "event": "call.ended",
132
+ // "timestamp": "2026-03-28T00:30:00Z",
133
+ // "data": {
134
+ // "callUuid": "abc-123",
135
+ // "direction": "inbound",
136
+ // "caller": "+17865551234",
137
+ // "destination": "1001",
138
+ // "duration": 45,
139
+ // "billsec": 42,
140
+ // "hangupCause": "NORMAL_CLEARING",
141
+ // "status": "completed",
142
+ // "recordingFile": "/tmp/recordings/tenant-id/uuid.wav"
143
+ // }
144
+ // }
145
+
146
+ // Verify webhook signature (Node.js):
147
+ const crypto = require('crypto');
148
+ function verifySignature(body, signature, secret) {
149
+ const expected = `sha256=${crypto.createHmac('sha256', secret).update(body).digest('hex')}`;
150
+ return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
151
+ }
152
+ ```
153
+
154
+ ## API Reference
155
+
156
+ ### Authentication
157
+
158
+ | Method | Description |
159
+ |--------|-------------|
160
+ | `client.login(params)` | Login as extension user |
161
+ | `client.adminLogin(params)` | Login as tenant admin |
162
+ | `client.me()` | Get current user info |
163
+ | `client.getTurnCredentials()` | Get TURN credentials for WebRTC |
164
+
165
+ ### Calls
166
+
167
+ | Method | Description |
168
+ |--------|-------------|
169
+ | `client.originate({ fromExtension, toNumber })` | Click-to-call via ESL |
170
+ | `client.hangup(uuid)` | Hang up active call |
171
+ | `client.transfer(uuid, { targetExtension, type })` | Transfer call (blind/attended) |
172
+ | `client.holdCall(uuid, hold?)` | Hold/resume call |
173
+ | `client.parkCall(uuid, slot?)` | Park call (*70) |
174
+ | `client.getParkedCalls()` | List parked calls |
175
+ | `client.transferToConference(uuid, name?)` | Move call to conference |
176
+ | `client.listCalls(params?)` | List CDR records |
177
+ | `client.getCall(id)` | Get single CDR |
178
+ | `client.getActiveCalls()` | List active calls from FS |
179
+ | `client.getCallStats(period?)` | Call statistics |
180
+ | `client.exportCalls(params?)` | Export CDR as CSV |
181
+
182
+ ### Extensions
183
+
184
+ | Method | Description |
185
+ |--------|-------------|
186
+ | `client.listExtensions()` | List all extensions |
187
+ | `client.getExtension(id)` | Get extension detail |
188
+ | `client.createExtension(params)` | Create extension + SIP subscriber |
189
+ | `client.updateExtension(id, params)` | Update extension (incl. CRM mapping) |
190
+ | `client.deleteExtension(id)` | Delete extension |
191
+ | `client.setCallForward(id, { enabled, number, type })` | Set call forward |
192
+ | `client.getCallForward(id)` | Get call forward settings |
193
+ | `client.getExtensionByCrmUser(crmUserId)` | Lookup extension by CRM user |
194
+ | `client.updateCrmMapping(id, params)` | Update CRM mapping |
195
+
196
+ ### CRM Mapping
197
+
198
+ ```typescript
199
+ // Map CRM user to extension
200
+ await client.updateCrmMapping(extId, {
201
+ crmUserId: 'crm-user-12345',
202
+ crmMetadata: { department: 'Sales', role: 'Manager' },
203
+ });
204
+
205
+ // Lookup extension by CRM user ID (click-to-call from CRM contact)
206
+ const { extension } = await client.getExtensionByCrmUser('crm-user-12345');
207
+ await client.originate({ fromExtension: extension.extension, toNumber: '+15551234567' });
208
+ ```
209
+
210
+ ### Conferences
211
+
212
+ | Method | Description |
213
+ |--------|-------------|
214
+ | `client.listConferences()` | List active conferences |
215
+ | `client.getConference(name)` | Conference details + members |
216
+ | `client.joinConference(name, callUuid)` | Add call to conference |
217
+ | `client.kickFromConference(name, memberId)` | Remove member |
218
+ | `client.muteConferenceMember(name, memberId, mute)` | Mute/unmute |
219
+ | `client.lockConference(name, lock)` | Lock/unlock |
220
+ | `client.recordConference(name, action)` | Start/stop recording |
221
+
222
+ ### Ring Groups
223
+
224
+ | Method | Description |
225
+ |--------|-------------|
226
+ | `client.listRingGroups()` | List ring groups |
227
+ | `client.createRingGroup(params)` | Create ring group |
228
+ | `client.updateRingGroup(id, params)` | Update ring group |
229
+ | `client.deleteRingGroup(id)` | Delete ring group |
230
+
231
+ **Strategies**: `simultaneous` (ring all at once), `sequential` (by priority), `random`
232
+
233
+ ### Call Queues (ACD)
234
+
235
+ | Method | Description |
236
+ |--------|-------------|
237
+ | `client.listQueues()` | List queues |
238
+ | `client.createQueue(params)` | Create queue |
239
+ | `client.updateQueue(id, params)` | Update queue |
240
+ | `client.deleteQueue(id)` | Delete queue |
241
+ | `client.pauseQueueAgent(queueId, agentId, paused)` | Pause/unpause agent |
242
+ | `client.loginQueueAgent(queueId, agentId, loggedIn)` | Login/logout agent |
243
+ | `client.getQueueStats(queueId)` | Real-time queue statistics |
244
+
245
+ **Strategies**: `round-robin`, `longest-idle`, `ring-all`, `least-calls`, `random`
246
+
247
+ ### IVR
248
+
249
+ | Method | Description |
250
+ |--------|-------------|
251
+ | `client.listIvrMenus()` | List IVR menus |
252
+ | `client.getIvrMenu(id)` | Get IVR detail |
253
+ | `client.createIvrMenu(params)` | Create IVR menu |
254
+ | `client.updateIvrMenu(id, params)` | Update IVR |
255
+ | `client.deleteIvrMenu(id)` | Delete IVR |
256
+
257
+ **IVR Option Actions**: `transfer` (to extension), `ivr` (sub-menu), `voicemail`, `queue`, `external` (PSTN), `ringgroup`, `hangup`
258
+
259
+ ### Webhooks
260
+
261
+ | Method | Description |
262
+ |--------|-------------|
263
+ | `client.listWebhooks()` | List webhooks + supported events |
264
+ | `client.createWebhook(params)` | Create webhook |
265
+ | `client.updateWebhook(id, params)` | Update webhook |
266
+ | `client.deleteWebhook(id)` | Delete webhook |
267
+ | `client.testWebhook(id)` | Send test event |
268
+ | `client.listWebhookDeliveries(id)` | Delivery history |
269
+
270
+ **Supported Events**:
271
+ - `call.started` — new call initiated
272
+ - `call.answered` — call answered
273
+ - `call.ended` — call ended (full CDR data)
274
+ - `call.transferred` — call transferred
275
+ - `call.held` / `call.resumed` — hold state
276
+ - `call.parked` — call parked
277
+ - `voicemail.new` — new voicemail
278
+ - `extension.registered` / `extension.unregistered` — registration
279
+ - `queue.caller_joined` / `queue.caller_left` — queue events
280
+ - `*` — all events
281
+
282
+ ### Voicemail
283
+
284
+ | Method | Description |
285
+ |--------|-------------|
286
+ | `client.listVoicemails(params?)` | List voicemails |
287
+ | `client.getVoicemailCount(extension?)` | Unread/total count |
288
+ | `client.getVoicemail(id)` | Get single voicemail |
289
+ | `client.getVoicemailAudioUrl(id)` | Get audio download URL |
290
+ | `client.markVoicemailRead(id)` | Mark as read |
291
+ | `client.deleteVoicemail(id)` | Delete voicemail |
292
+ | `client.bulkDeleteVoicemails(params?)` | Bulk delete voicemails |
293
+
294
+ ### Blocklist (Call Blocking)
295
+
296
+ | Method | Description |
297
+ |--------|-------------|
298
+ | `client.listBlockedNumbers()` | List all blocked numbers |
299
+ | `client.blockNumber(params)` | Block a phone number |
300
+ | `client.unblockNumber(id)` | Unblock a number by ID |
301
+ | `client.checkBlocked(number)` | Check if a number is blocked |
302
+
303
+ ```typescript
304
+ // Block a spam caller
305
+ await client.blockNumber({
306
+ number: '+15551234567',
307
+ reason: 'Spam caller',
308
+ direction: 'inbound', // 'inbound' | 'outbound' | 'both'
309
+ });
310
+
311
+ // Check before routing
312
+ const { blocked } = await client.checkBlocked('+15551234567');
313
+ ```
314
+
315
+ ### Business Hours / Schedules
316
+
317
+ | Method | Description |
318
+ |--------|-------------|
319
+ | `client.listSchedules()` | List business hour schedules |
320
+ | `client.getSchedule(id)` | Get schedule detail |
321
+ | `client.createSchedule(params)` | Create a schedule |
322
+ | `client.updateSchedule(id, params)` | Update a schedule |
323
+ | `client.deleteSchedule(id)` | Delete a schedule |
324
+ | `client.getScheduleStatus(id)` | Check if currently open/closed |
325
+
326
+ ```typescript
327
+ // Create business hours
328
+ await client.createSchedule({
329
+ name: 'Office Hours',
330
+ timezone: 'America/New_York',
331
+ schedules: [
332
+ { day: 'monday', enabled: true, startTime: '09:00', endTime: '17:00' },
333
+ { day: 'tuesday', enabled: true, startTime: '09:00', endTime: '17:00' },
334
+ // ...
335
+ ],
336
+ holidays: [{ date: '2026-12-25', name: 'Christmas' }],
337
+ afterHoursAction: 'voicemail',
338
+ });
339
+
340
+ // Check if office is open right now
341
+ const { isOpen } = await client.getScheduleStatus(scheduleId);
342
+ ```
343
+
344
+ ### SIP Trunks
345
+
346
+ | Method | Description |
347
+ |--------|-------------|
348
+ | `client.listTrunks()` | List SIP trunks (passwords masked) |
349
+ | `client.getTrunk(id)` | Get trunk detail |
350
+ | `client.createTrunk(params)` | Create a SIP trunk |
351
+ | `client.updateTrunk(id, params)` | Update a trunk |
352
+ | `client.deleteTrunk(id)` | Delete a trunk |
353
+
354
+ ```typescript
355
+ await client.createTrunk({
356
+ name: 'Twilio US',
357
+ provider: 'twilio',
358
+ gateway: 'your-trunk.pstn.twilio.com',
359
+ authType: 'ip',
360
+ priority: 1,
361
+ maxChannels: 50,
362
+ });
363
+ ```
364
+
365
+ ### Queue Stats & CDR Export
366
+
367
+ | Method | Description |
368
+ |--------|-------------|
369
+ | `client.getQueueStats(queueId)` | Real-time queue statistics |
370
+ | `client.exportCalls(params?)` | Export CDR as CSV text |
371
+
372
+ ```typescript
373
+ // Real-time queue dashboard
374
+ const { stats } = await client.getQueueStats(queueId);
375
+ console.log(`Agents available: ${stats.agents.available}/${stats.agents.total}`);
376
+ console.log(`Service level: ${stats.today.serviceLevel}%`);
377
+
378
+ // Export CDR for a date range
379
+ const csv = await client.exportCalls({
380
+ dateFrom: '2026-03-01',
381
+ dateTo: '2026-03-28',
382
+ direction: 'inbound',
383
+ limit: 10000,
384
+ });
385
+ ```
386
+
387
+ ### Presence
388
+
389
+ | Method | Description |
390
+ |--------|-------------|
391
+ | `client.getPresence()` | Simple presence map |
392
+ | `client.getPresenceDetailed()` | Detailed presence with call info |
393
+
394
+ ### WebSocket Events
395
+
396
+ | Event Type | Data |
397
+ |------------|------|
398
+ | `presence_snapshot` | Full presence state on connect |
399
+ | `presence_change` | `{ extension, status, callUuid, caller, direction }` |
400
+ | `call_start` | `{ callUuid, caller, destination, direction }` |
401
+ | `call_answer` | `{ callUuid }` |
402
+ | `call_end` | `{ callUuid, caller, destination, duration, hangupCause }` |
403
+ | `registration_change` | `{ extension, registered, ip }` |
404
+
405
+ ## Feature Codes (Phone Dial Pad)
406
+
407
+ | Code | Function |
408
+ |------|----------|
409
+ | `*3` | Enter conference room |
410
+ | `*70` | Park current call (auto-assign slot) |
411
+ | `*71XX` | Retrieve parked call from slot XX |
412
+ | `*97` | Check voicemail |
413
+ | `9196` | Echo test |
414
+ | `9197` | MOH test |
415
+
416
+ ## Architecture
417
+
418
+ ```
419
+ CRM (React + Node.js)
420
+ |
421
+ |--- @cemscale/voip-sdk (HTTP + WebSocket + WebRTC)
422
+ |
423
+ v
424
+ API (Fastify + TypeScript) --- port 3000
425
+ |
426
+ |--- ESL ---> FreeSWITCH (media, recording, conference, IVR, queues)
427
+ |--- SQL ---> PostgreSQL (tenants, extensions, CDR, webhooks, queues)
428
+ |--- Redis --> Presence, caching
429
+ |
430
+ v
431
+ Kamailio (SIP proxy) --- port 5060
432
+ |
433
+ |--- RTPEngine (media relay, NAT traversal)
434
+ |--- Twilio Elastic SIP Trunk (PSTN connectivity)
435
+ ```
436
+
437
+ ## Notes
438
+
439
+ - All endpoints require JWT authentication (except `/health` and `/api/internal/*`)
440
+ - Webhooks include HMAC-SHA256 signatures in `X-Webhook-Signature` header
441
+ - WebSocket requires auth message with JWT token as first message
442
+ - Conference rooms are tenant-isolated (`conf_{tenantId}`)
443
+ - Parking lots are tenant-isolated (`park_{tenantId}`, slots 701-750)
444
+ - All recordings saved to `/tmp/recordings/{tenantId}/`
445
+ - Twilio requires tenant DID as caller ID for outbound (carrier limitation)