@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 +445 -0
- package/dist/client.d.ts +421 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +651 -0
- package/dist/client.js.map +1 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/useCallStatus.d.ts +38 -0
- package/dist/hooks/useCallStatus.d.ts.map +1 -0
- package/dist/hooks/useCallStatus.js +98 -0
- package/dist/hooks/useCallStatus.js.map +1 -0
- package/dist/hooks/usePresence.d.ts +22 -0
- package/dist/hooks/usePresence.d.ts.map +1 -0
- package/dist/hooks/usePresence.js +81 -0
- package/dist/hooks/usePresence.js.map +1 -0
- package/dist/hooks/useVoIP.d.ts +49 -0
- package/dist/hooks/useVoIP.d.ts.map +1 -0
- package/dist/hooks/useVoIP.js +250 -0
- package/dist/hooks/useVoIP.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +628 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/webrtc.d.ts +64 -0
- package/dist/webrtc.d.ts.map +1 -0
- package/dist/webrtc.js +427 -0
- package/dist/webrtc.js.map +1 -0
- package/package.json +55 -0
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)
|