@cemscale-voip/voip-sdk 2.0.11 → 2.0.13
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 +443 -46
- package/dist/client.d.ts +3531 -211
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3456 -136
- package/dist/client.js.map +1 -1
- package/dist/hooks/useVoIP.d.ts.map +1 -1
- package/dist/hooks/useVoIP.js +26 -4
- package/dist/hooks/useVoIP.js.map +1 -1
- package/dist/types.d.ts +1172 -32
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -25,7 +25,35 @@ export declare class VoIPClient {
|
|
|
25
25
|
setApiKey(apiKey: string): void;
|
|
26
26
|
/** Get the current API key */
|
|
27
27
|
getApiKey(): string | null;
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Set JWT token manually (only for browser login flows — most apps should use apiKey).
|
|
30
|
+
*
|
|
31
|
+
* @description
|
|
32
|
+
* Restores a previously-obtained JWT token into the client so all subsequent API calls
|
|
33
|
+
* use Bearer authentication. Use this to rehydrate a session after a page reload:
|
|
34
|
+
* get the token from localStorage/sessionStorage and call `voip.setToken(token)`.
|
|
35
|
+
*
|
|
36
|
+
* @param token — The JWT token string from a prior `login()`, `adminLogin()`, or `superadminLogin()` call.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* // Page reload: restore token from localStorage
|
|
41
|
+
* const savedToken = localStorage.getItem('voip-token');
|
|
42
|
+
* if (savedToken) {
|
|
43
|
+
* voip.setToken(savedToken);
|
|
44
|
+
* // Verify it's still valid
|
|
45
|
+
* try {
|
|
46
|
+
* const { user } = await voip.me();
|
|
47
|
+
* console.log('Session restored:', user.displayName);
|
|
48
|
+
* } catch {
|
|
49
|
+
* // Token expired — redirect to login
|
|
50
|
+
* localStorage.removeItem('voip-token');
|
|
51
|
+
* }
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @see login, adminLogin, superadminLogin
|
|
56
|
+
*/
|
|
29
57
|
setToken(token: string): void;
|
|
30
58
|
/** Get current JWT token */
|
|
31
59
|
getToken(): string | null;
|
|
@@ -56,40 +84,281 @@ export declare class VoIPClient {
|
|
|
56
84
|
*/
|
|
57
85
|
buildUrl(path: string): string;
|
|
58
86
|
private request;
|
|
59
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Log in as an extension user with username, password, and tenant ID.
|
|
89
|
+
*
|
|
90
|
+
* @description
|
|
91
|
+
* Authenticates a PBX extension user (e.g. 1001, 1002) and returns a JWT token.
|
|
92
|
+
* The token is automatically stored in the client instance for subsequent requests.
|
|
93
|
+
* This is the login flow for softphone users, desk phone users, and CRM integrations
|
|
94
|
+
* that authenticate as a specific extension rather than as a tenant admin.
|
|
95
|
+
*
|
|
96
|
+
* After successful login, the client:
|
|
97
|
+
* - Stores the JWT token for all future API calls
|
|
98
|
+
* - Sets the tenant ID from the `tenantId` parameter
|
|
99
|
+
*
|
|
100
|
+
* For admin login, use `adminLogin()`. For platform superadmin, use `superadminLogin()`.
|
|
101
|
+
*
|
|
102
|
+
* @param params — Login parameters
|
|
103
|
+
* @param params.username — The extension number (e.g. `'1001'` or `'2002'`)
|
|
104
|
+
* @param params.password — The extension's SIP password
|
|
105
|
+
* @param params.tenantId — The UUID of the tenant this extension belongs to
|
|
106
|
+
*
|
|
107
|
+
* @returns An object containing:
|
|
108
|
+
* - `token: string` — JWT token (also stored internally in the client)
|
|
109
|
+
* - `user?: object` — User info including `id`, `extension`, `displayName`, `tenantId`
|
|
110
|
+
*
|
|
111
|
+
* @throws {HttpError} 401 — Invalid credentials (wrong username, password, or tenant)
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* const voip = new VoIPClient({ apiUrl: 'https://voip-api.cemscale.com' });
|
|
116
|
+
*
|
|
117
|
+
* const { token, user } = await voip.login({
|
|
118
|
+
* username: '1001',
|
|
119
|
+
* password: 'sip-password-here',
|
|
120
|
+
* tenantId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
|
121
|
+
* });
|
|
122
|
+
*
|
|
123
|
+
* console.log(`Logged in as ${user?.displayName} (${user?.extension})`);
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* @see POST /api/auth/login
|
|
127
|
+
*/
|
|
60
128
|
login(params: LoginParams): Promise<AuthResponse>;
|
|
61
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* Log in as a tenant administrator with email and password.
|
|
131
|
+
*
|
|
132
|
+
* @description
|
|
133
|
+
* Authenticates a tenant admin user and returns a JWT token. The admin role has
|
|
134
|
+
* full CRUD access to all resources within their tenant: extensions, DIDs, IVR menus,
|
|
135
|
+
* ring groups, queues, webhooks, schedules, blocklist, recordings, voicemails, and reports.
|
|
136
|
+
*
|
|
137
|
+
* After successful login, the client:
|
|
138
|
+
* - Stores the JWT token for all future API calls
|
|
139
|
+
* - Automatically sets the tenant ID from the response (the admin's own tenant)
|
|
140
|
+
*
|
|
141
|
+
* Use this for dashboard/admin panel logins. For extension-level (softphone) login,
|
|
142
|
+
* use `login()`. For cross-tenant superadmin access, use `superadminLogin()`.
|
|
143
|
+
*
|
|
144
|
+
* **CRM UI guidance:** Store the returned token in localStorage/sessionStorage and
|
|
145
|
+
* rehydrate it on page load via `setToken(token)`. Redirect to the dashboard
|
|
146
|
+
* after successful login. Show a "Forgot password?" link and rate-limit the login
|
|
147
|
+
* button to prevent brute-force attempts (the API enforces rate limiting server-side).
|
|
148
|
+
*
|
|
149
|
+
* @param params — Login parameters
|
|
150
|
+
* @param params.email — The admin's email address
|
|
151
|
+
* @param params.password — The admin's password
|
|
152
|
+
*
|
|
153
|
+
* @returns An object containing:
|
|
154
|
+
* - `token: string` — JWT token (also stored internally in the client)
|
|
155
|
+
* - `user?: object` — User info including `id`, `extension`, `displayName`, `tenantId`
|
|
156
|
+
* - `tenant?: object` — Tenant info including `id`, `subdomain`, `companyName`
|
|
157
|
+
*
|
|
158
|
+
* @throws {HttpError} 401 — Invalid credentials (wrong email or password)
|
|
159
|
+
* @throws {HttpError} 403 — Account is disabled or suspended
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* const voip = new VoIPClient({ apiUrl: 'https://voip-api.cemscale.com' });
|
|
164
|
+
*
|
|
165
|
+
* const { token, user, tenant } = await voip.adminLogin({
|
|
166
|
+
* email: 'admin@acmecorp.com',
|
|
167
|
+
* password: 'secure-password',
|
|
168
|
+
* });
|
|
169
|
+
*
|
|
170
|
+
* console.log(`Logged in to ${tenant?.companyName} as ${user?.displayName}`);
|
|
171
|
+
*
|
|
172
|
+
* // CRM UI: store token for page reloads
|
|
173
|
+
* localStorage.setItem('voip-token', token);
|
|
174
|
+
* localStorage.setItem('voip-tenant', JSON.stringify(tenant));
|
|
175
|
+
* ```
|
|
176
|
+
*
|
|
177
|
+
* @see POST /api/auth/admin-login
|
|
178
|
+
*/
|
|
62
179
|
adminLogin(params: AdminLoginParams): Promise<AuthResponse>;
|
|
63
|
-
/**
|
|
180
|
+
/**
|
|
181
|
+
* Log in as a platform superadmin with email and password.
|
|
182
|
+
*
|
|
183
|
+
* @description
|
|
184
|
+
* Authenticates a platform superadmin — the highest privilege level that has access
|
|
185
|
+
* to ALL tenants and global resources. Superadmins can manage tenants, create global
|
|
186
|
+
* SIP trunks, administer API keys for any tenant, and view cross-tenant reports.
|
|
187
|
+
*
|
|
188
|
+
* Unlike `adminLogin()`, this does NOT auto-set a tenant ID because superadmins
|
|
189
|
+
* operate across all tenants. Use `setTenantId()` or the per-request `options.tenantId`
|
|
190
|
+
* parameter when acting on behalf of a specific tenant.
|
|
191
|
+
*
|
|
192
|
+
* Superadmin can also impersonate any tenant by setting the `X-Tenant-ID` header,
|
|
193
|
+
* which is passed automatically when you use `options.tenantId` on any SDK method.
|
|
194
|
+
*
|
|
195
|
+
* **CRM UI guidance:** After login, show the tenant selector dropdown so the superadmin
|
|
196
|
+
* can pick which tenant to manage. Store the selected tenant ID in state and pass it
|
|
197
|
+
* as `{ tenantId }` on every SDK call. Show "Superadmin" badge in the header.
|
|
198
|
+
*
|
|
199
|
+
* @param params — Login parameters
|
|
200
|
+
* @param params.email — The superadmin's email address
|
|
201
|
+
* @param params.password — The superadmin's password
|
|
202
|
+
*
|
|
203
|
+
* @returns An object containing:
|
|
204
|
+
* - `token: string` — JWT token (also stored internally in the client)
|
|
205
|
+
* - `user?: object` — User info including `id`, `extension`, `displayName`, `tenantId`
|
|
206
|
+
* - `tenant?: object` — Tenant info (typically null for superadmin)
|
|
207
|
+
*
|
|
208
|
+
* @throws {HttpError} 401 — Invalid credentials (wrong email or password)
|
|
209
|
+
* @throws {HttpError} 403 — Account is not a superadmin
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```typescript
|
|
213
|
+
* const voip = new VoIPClient({ apiUrl: 'https://voip-api.cemscale.com' });
|
|
214
|
+
*
|
|
215
|
+
* const { token, user } = await voip.superadminLogin({
|
|
216
|
+
* email: 'superadmin@cemscale.com',
|
|
217
|
+
* password: 'secure-password',
|
|
218
|
+
* });
|
|
219
|
+
*
|
|
220
|
+
* // Superadmin: list ALL tenants
|
|
221
|
+
* const { tenants } = await voip.listTenants();
|
|
222
|
+
* console.log(`Managing ${tenants.length} tenants`);
|
|
223
|
+
*
|
|
224
|
+
* // Superadmin: view calls for a specific tenant
|
|
225
|
+
* const { calls } = await voip.listCalls({ tenantId: 'a1b2c3d4-...' });
|
|
226
|
+
*
|
|
227
|
+
* // CRM UI: tenant selector
|
|
228
|
+
* tenants.forEach(t => {
|
|
229
|
+
* renderTenantOption({ value: t.id, label: `${t.companyName} (${t.subdomain})` });
|
|
230
|
+
* });
|
|
231
|
+
* ```
|
|
232
|
+
*
|
|
233
|
+
* @see POST /api/auth/superadmin-login
|
|
234
|
+
*/
|
|
64
235
|
superadminLogin(params: AdminLoginParams): Promise<AuthResponse>;
|
|
65
236
|
/** Get current user info */
|
|
66
237
|
me(): Promise<{
|
|
67
238
|
user: any;
|
|
68
239
|
}>;
|
|
69
|
-
/**
|
|
240
|
+
/**
|
|
241
|
+
* Get TURN/STUN credentials for establishing WebRTC media connections.
|
|
242
|
+
*
|
|
243
|
+
* @description
|
|
244
|
+
* Returns time-limited TURN credentials (username, credential, TURN server URLs) used
|
|
245
|
+
* to establish peer-to-peer WebRTC media connections through the coTURN server.
|
|
246
|
+
* These credentials are auto-generated per tenant and expire after the TTL period.
|
|
247
|
+
*
|
|
248
|
+
* **When to call this:**
|
|
249
|
+
* - Before creating a `WebRTCPhone` instance — pass the credentials to the SIP.js UA config
|
|
250
|
+
* - When TURN credentials expire during a long call — re-fetch and update the ICE transport
|
|
251
|
+
* - On page load in a WebRTC-based softphone app
|
|
252
|
+
*
|
|
253
|
+
* The TTL (time-to-live) indicates how many seconds the credential is valid.
|
|
254
|
+
* Re-fetch credentials before the TTL expires if the user is on a long call.
|
|
255
|
+
*
|
|
256
|
+
* **CRM UI guidance:** Call this once when the WebRTC softphone initializes.
|
|
257
|
+
* Show a "Connecting..." spinner while fetching. If it fails, show
|
|
258
|
+
* "Unable to connect to media server — please check your network" with a Retry button.
|
|
259
|
+
*
|
|
260
|
+
* @returns An object containing:
|
|
261
|
+
* - `urls: string[]` — Array of TURN server URLs (e.g. `['turn:3.20.219.41:3478?transport=udp']`)
|
|
262
|
+
* - `username: string` — Time-limited TURN username
|
|
263
|
+
* - `credential: string` — Time-limited TURN password
|
|
264
|
+
* - `ttl: number` — Time-to-live in seconds before the credential expires
|
|
265
|
+
*
|
|
266
|
+
* @throws {HttpError} 401 — Missing or invalid API key/token
|
|
267
|
+
* @throws {HttpError} 500 — TURN server configuration error
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```typescript
|
|
271
|
+
* // Fetch credentials for WebRTC phone setup
|
|
272
|
+
* const { urls, username, credential, ttl } = await voip.getTurnCredentials();
|
|
273
|
+
*
|
|
274
|
+
* // Pass to WebRTCPhone or SIP.js UA
|
|
275
|
+
* const phone = new WebRTCPhone({
|
|
276
|
+
* iceServers: [{ urls, username, credential }],
|
|
277
|
+
* // ...
|
|
278
|
+
* });
|
|
279
|
+
*
|
|
280
|
+
* // Refresh before TTL expires on long calls
|
|
281
|
+
* setTimeout(async () => {
|
|
282
|
+
* const newCreds = await voip.getTurnCredentials();
|
|
283
|
+
* phone.updateIceServers([{ urls: newCreds.urls, username: newCreds.username, credential: newCreds.credential }]);
|
|
284
|
+
* }, (ttl - 30) * 1000); // Refresh 30s before expiry
|
|
285
|
+
* ```
|
|
286
|
+
*
|
|
287
|
+
* @see GET /api/auth/turn-credentials
|
|
288
|
+
*/
|
|
70
289
|
getTurnCredentials(): Promise<TurnCredentials>;
|
|
71
290
|
/**
|
|
72
|
-
* List call records (CDR) with pagination and
|
|
291
|
+
* List historical call records (CDR — Call Detail Records) with pagination, filtering, and search.
|
|
292
|
+
*
|
|
293
|
+
* @description
|
|
294
|
+
* Retrieves a paginated list of completed/missed/failed call records for the current tenant.
|
|
295
|
+
* Think of this as your "call history" table — every call that went through the phone system
|
|
296
|
+
* appears here, with caller ID, destination, duration, status, and optional recording URLs.
|
|
297
|
+
*
|
|
298
|
+
* **Recording playback:** Each call with a recording includes `cdn_url` (CloudFront, instant
|
|
299
|
+
* playback, no auth required) and `audio_url` (API fallback, requires auth). The `cdn_url`
|
|
300
|
+
* is a permanent URL cached at 400+ global edge locations — you can store it in your database,
|
|
301
|
+
* send it in emails, or embed it in `<audio>` tags directly.
|
|
302
|
+
*
|
|
303
|
+
* **Superadmin mode:** When using a global superadmin API key, calls from ALL tenants are
|
|
304
|
+
* returned. Each call includes `company_name` and `subdomain` fields for tenant identification.
|
|
73
305
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
306
|
+
* @param params — Optional filters and pagination
|
|
307
|
+
* @param params.page — Page number (1-based, default 1)
|
|
308
|
+
* @param params.limit — Records per page (max 500, default 50). Example: `50`
|
|
309
|
+
* @param params.direction — Filter by call direction. `'inbound'` = someone called you,
|
|
310
|
+
* `'outbound'` = you called someone, `'internal'` = extension-to-extension, `'all'` = everything
|
|
311
|
+
* @param params.status — Filter by call outcome. `'completed'` = answered, `'failed'` = error,
|
|
312
|
+
* `'missed'` = no answer, `'voicemail'` = went to voicemail
|
|
313
|
+
* @param params.dateFrom — ISO date string: only calls on or after this date. Example: `'2026-04-01'`
|
|
314
|
+
* @param params.dateTo — ISO date string: only calls on or before this date. Example: `'2026-04-30'`
|
|
315
|
+
* @param params.extension — Filter by extension number (matches caller or destination). Example: `'1001'`
|
|
316
|
+
* @param params.number — Search by phone number or name (partial match, case-insensitive). Example: `'+1555123'`
|
|
317
|
+
* @param params.forwarded — `'true'` = only forwarded calls, `'false'` = only non-forwarded
|
|
318
|
+
* @param params.forwardedTo — Search by the external number a call was forwarded to. Example: `'+1555999'`
|
|
319
|
+
* @param params.tenantId — Filter by tenant (superadmin only, alternative to X-Tenant-ID header)
|
|
320
|
+
*
|
|
321
|
+
* @returns An object containing:
|
|
322
|
+
* - `calls: CallRecord[]` — Array of call records. Each record has `id`, `call_uuid`,
|
|
323
|
+
* `direction`, `caller_id_number`, `caller_id_name`, `destination`, `status`,
|
|
324
|
+
* `duration_seconds`, `billsec` (billed seconds), `forwarded_to`, `was_voicemail`,
|
|
325
|
+
* `cdn_url` (CloudFront recording URL, no auth), `audio_url` (API fallback),
|
|
326
|
+
* `started_at`, `answered_at`, `ended_at`, and optional `company_name`/`subdomain` (superadmin).
|
|
327
|
+
* - `pagination: PaginationInfo` — `{ page, limit, total, pages }`. Use `total` for
|
|
328
|
+
* "Showing 1-50 of 342 calls" and `pages` for page navigation.
|
|
329
|
+
*
|
|
330
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
331
|
+
* @throws {HttpError} 403 — API key doesn't have access to the tenant
|
|
76
332
|
*
|
|
77
333
|
* @example
|
|
78
334
|
* ```typescript
|
|
335
|
+
* // Basic list — show last 50 calls
|
|
336
|
+
* const { calls, pagination } = await voip.listCalls({ limit: 50 });
|
|
337
|
+
*
|
|
338
|
+
* // Filtered list — all inbound answered calls this month
|
|
79
339
|
* const { calls, pagination } = await voip.listCalls({
|
|
80
340
|
* direction: 'inbound',
|
|
81
341
|
* status: 'completed',
|
|
82
|
-
* dateFrom: '2026-
|
|
83
|
-
*
|
|
342
|
+
* dateFrom: '2026-06-01',
|
|
343
|
+
* dateTo: '2026-06-30',
|
|
344
|
+
* limit: 100,
|
|
84
345
|
* });
|
|
85
346
|
*
|
|
347
|
+
* // CRM UI: build a call history table
|
|
348
|
+
* const formatDuration = (s: number | null) => s ? `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}` : '—';
|
|
86
349
|
* calls.forEach(call => {
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* console.log(` Recording: ${call.cdn_url}`); // instant CDN playback
|
|
90
|
-
* }
|
|
350
|
+
* const recordingSrc = call.cdn_url ?? voip.buildUrl(`/api/recordings/${call.id}/audio`);
|
|
351
|
+
* console.log(`${call.started_at} | ${call.direction} | ${call.caller_id_number} → ${call.destination} | ${formatDuration(call.billsec)} | ${recordingSrc ? '🔊' : '—'}`);
|
|
91
352
|
* });
|
|
353
|
+
*
|
|
354
|
+
* // Pagination — page through all results
|
|
355
|
+
* for (let page = 1; page <= pagination.pages; page++) {
|
|
356
|
+
* const { calls } = await voip.listCalls({ page, limit: 50 });
|
|
357
|
+
* // render each page
|
|
358
|
+
* }
|
|
92
359
|
* ```
|
|
360
|
+
*
|
|
361
|
+
* @see GET /api/calls
|
|
93
362
|
*/
|
|
94
363
|
listCalls(params?: CallListParams): Promise<CallListResponse>;
|
|
95
364
|
/**
|
|
@@ -112,127 +381,1287 @@ export declare class VoIPClient {
|
|
|
112
381
|
getCall(id: string): Promise<{
|
|
113
382
|
call: CallRecord;
|
|
114
383
|
}>;
|
|
115
|
-
/**
|
|
384
|
+
/**
|
|
385
|
+
* Start a new outbound phone call from an extension to any destination number.
|
|
386
|
+
*
|
|
387
|
+
* @description
|
|
388
|
+
* Tells the phone system to dial an external or internal number FROM a specific extension.
|
|
389
|
+
* The extension's physical phone (desk phone, softphone, or WebRTC phone) will ring first,
|
|
390
|
+
* and when answered, the system connects it to the destination.
|
|
391
|
+
*
|
|
392
|
+
* This is the primary way to make outbound calls programmatically — also known as
|
|
393
|
+
* "click-to-call." The CRM clicks a phone number, the user's phone rings, they pick up,
|
|
394
|
+
* and the system dials the customer.
|
|
395
|
+
*
|
|
396
|
+
* **Superadmin mode:** Pass `{ tenantId: 'uuid-here' }` as the `options` parameter when
|
|
397
|
+
* using a global superadmin API key to originate calls on behalf of a specific tenant.
|
|
398
|
+
*
|
|
399
|
+
* @param params — Call parameters
|
|
400
|
+
* @param params.fromExtension — The extension number making the call. This extension's
|
|
401
|
+
* phone will ring first. Example: `'1001'`
|
|
402
|
+
* @param params.toNumber — The destination phone number in E.164 format (+country code +
|
|
403
|
+
* number) or an internal extension number. Examples: `'+17865551234'` (PSTN), `'1002'` (internal)
|
|
404
|
+
* @param options — Optional request overrides
|
|
405
|
+
* @param options.tenantId — Target tenant UUID (required for superadmin keys only).
|
|
406
|
+
* Example: `'a1b2c3d4-...'`
|
|
407
|
+
*
|
|
408
|
+
* @returns An object containing:
|
|
409
|
+
* - `message: string` — Human-readable confirmation (e.g. "Call originated successfully")
|
|
410
|
+
* - `callUuid: string` — The FreeSWITCH call UUID for tracking. Use this to monitor the
|
|
411
|
+
* call via WebSocket events (`call.answered`, `call.end`) and to call `hangup()`,
|
|
412
|
+
* `transfer()`, `holdCall()`, or `parkCall()`
|
|
413
|
+
*
|
|
414
|
+
* @throws {HttpError} 400 — `fromExtension` doesn't exist, `toNumber` is invalid, or extension is disabled
|
|
415
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
416
|
+
* @throws {HttpError} 500 — FreeSWITCH connection failed or originate error
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* ```typescript
|
|
420
|
+
* // Click-to-call from extension 1001 to customer +17865551234
|
|
421
|
+
* const { callUuid, message } = await voip.originate({
|
|
422
|
+
* fromExtension: '1001',
|
|
423
|
+
* toNumber: '+17865551234',
|
|
424
|
+
* });
|
|
425
|
+
* console.log(message); // "Call originated successfully"
|
|
426
|
+
* console.log(`Tracking UUID: ${callUuid}`);
|
|
427
|
+
*
|
|
428
|
+
* // CRM UI: Click a phone number in the contact card
|
|
429
|
+
* async function handleClickToCall(extension: string, customerPhone: string) {
|
|
430
|
+
* try {
|
|
431
|
+
* const { callUuid } = await voip.originate({
|
|
432
|
+
* fromExtension: extension,
|
|
433
|
+
* toNumber: customerPhone,
|
|
434
|
+
* });
|
|
435
|
+
* // Show "Dialing..." state in the UI
|
|
436
|
+
* // Listen for WebSocket 'call_start' event to confirm the call connected
|
|
437
|
+
* showCallBanner(callUuid, customerPhone, 'dialing');
|
|
438
|
+
* } catch (err) {
|
|
439
|
+
* if (err instanceof HttpError && err.status === 400) {
|
|
440
|
+
* showError('Invalid phone number — please check the format');
|
|
441
|
+
* }
|
|
442
|
+
* }
|
|
443
|
+
* }
|
|
444
|
+
*
|
|
445
|
+
* // Superadmin: originate for a specific tenant
|
|
446
|
+
* const { callUuid } = await voip.originate(
|
|
447
|
+
* { fromExtension: '1001', toNumber: '+17865551234' },
|
|
448
|
+
* { tenantId: 'a1b2c3d4-...' },
|
|
449
|
+
* );
|
|
450
|
+
* ```
|
|
451
|
+
*
|
|
452
|
+
* @see POST /api/calls/originate
|
|
453
|
+
*/
|
|
116
454
|
originate(params: OriginateParams, options?: RequestOptions): Promise<OriginateResponse>;
|
|
117
|
-
/**
|
|
455
|
+
/**
|
|
456
|
+
* End (hang up) an active call immediately.
|
|
457
|
+
*
|
|
458
|
+
* @description
|
|
459
|
+
* Terminates an in-progress call identified by its FreeSWITCH channel UUID.
|
|
460
|
+
* This is the server-side equivalent of physically hanging up the phone — both parties
|
|
461
|
+
* are disconnected. Works on any active call: WebRTC, desk phone, PSTN, or conference leg.
|
|
462
|
+
*
|
|
463
|
+
* After calling this, expect a `call_end` WebSocket event confirming the call terminated.
|
|
464
|
+
*
|
|
465
|
+
* @param uuid — The FreeSWITCH channel UUID of the active call to hang up.
|
|
466
|
+
* You get this from:
|
|
467
|
+
* - `originate()` → `callUuid`
|
|
468
|
+
* - `getActiveCalls()` → `activeCalls[].uuid`
|
|
469
|
+
* - `getParkedCalls()` → `parkedCalls[].uuid`
|
|
470
|
+
* - WebSocket `call_start` event → `call.uuid`
|
|
471
|
+
* Example: `'a1b2c3d4-e5f6-7890-abcd-ef1234567890'`
|
|
472
|
+
*
|
|
473
|
+
* @returns An object containing:
|
|
474
|
+
* - `message: string` — Confirmation message (e.g. "Call hung up successfully")
|
|
475
|
+
*
|
|
476
|
+
* @throws {HttpError} 400 — The call UUID is not found or the call is already terminated
|
|
477
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
478
|
+
* @throws {HttpError} 500 — FreeSWITCH communication error
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```typescript
|
|
482
|
+
* // Hang up the call that was just originated
|
|
483
|
+
* const { callUuid } = await voip.originate({
|
|
484
|
+
* fromExtension: '1001',
|
|
485
|
+
* toNumber: '+17865551234',
|
|
486
|
+
* });
|
|
487
|
+
*
|
|
488
|
+
* // ... later, when the user clicks "End Call" in the UI
|
|
489
|
+
* await voip.hangup(callUuid);
|
|
490
|
+
*
|
|
491
|
+
* // CRM UI: End Call button
|
|
492
|
+
* async function handleEndCall(callUuid: string) {
|
|
493
|
+
* await voip.hangup(callUuid);
|
|
494
|
+
* hideCallBanner();
|
|
495
|
+
* showNotification('Call ended');
|
|
496
|
+
* }
|
|
497
|
+
*
|
|
498
|
+
* // Hang up from getActiveCalls
|
|
499
|
+
* const { activeCalls } = await voip.getActiveCalls();
|
|
500
|
+
* const currentCall = activeCalls.find(c => c.caller_id_number === '+17865551234');
|
|
501
|
+
* if (currentCall) {
|
|
502
|
+
* await voip.hangup(currentCall.uuid);
|
|
503
|
+
* }
|
|
504
|
+
* ```
|
|
505
|
+
*
|
|
506
|
+
* @see POST /api/calls/{uuid}/hangup
|
|
507
|
+
*/
|
|
118
508
|
hangup(uuid: string): Promise<{
|
|
119
509
|
message: string;
|
|
120
510
|
}>;
|
|
121
|
-
/**
|
|
511
|
+
/**
|
|
512
|
+
* Transfer an active call to another extension.
|
|
513
|
+
*
|
|
514
|
+
* @description
|
|
515
|
+
* Moves an in-progress call from one extension to another. Two transfer modes:
|
|
516
|
+
*
|
|
517
|
+
* - **Blind transfer** (`type: 'blind'`, default): Immediately transfers the caller
|
|
518
|
+
* to the target extension without consulting the target first. The original extension
|
|
519
|
+
* is disconnected. Use this for "I'll transfer you now" — the caller hears hold music
|
|
520
|
+
* until the target picks up.
|
|
521
|
+
*
|
|
522
|
+
* - **Attended transfer** (`type: 'attended'`): The original extension calls the target
|
|
523
|
+
* first, talks to them ("I have Mr. Smith on the line about his invoice"), and then
|
|
524
|
+
* the call is connected. Requires a two-step UI flow with `addCallParticipant()`
|
|
525
|
+
* and `mergeCalls()` for the full attended transfer sequence.
|
|
526
|
+
*
|
|
527
|
+
* @param uuid — The FreeSWITCH channel UUID of the active call to transfer.
|
|
528
|
+
* Get this from `originate()` → `callUuid` or `getActiveCalls()`.
|
|
529
|
+
* Example: `'a1b2c3d4-e5f6-7890-abcd-ef1234567890'`
|
|
530
|
+
* @param params — Transfer parameters
|
|
531
|
+
* @param params.targetExtension — The extension to transfer the call TO.
|
|
532
|
+
* This is the recipient — the person who will receive the call.
|
|
533
|
+
* Example: `'1002'` (extension), `'+17865551234'` (external number)
|
|
534
|
+
* @param params.type — Transfer type: `'blind'` (default, immediate) or `'attended'`
|
|
535
|
+
* (talk to target first, then connect). Example: `'blind'`
|
|
536
|
+
*
|
|
537
|
+
* @returns An object containing:
|
|
538
|
+
* - `message: string` — Confirmation message (e.g. "Call transferred successfully")
|
|
539
|
+
*
|
|
540
|
+
* @throws {HttpError} 400 — Call UUID not found, target extension doesn't exist, or target is the same as caller
|
|
541
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
542
|
+
* @throws {HttpError} 500 — FreeSWITCH communication error
|
|
543
|
+
*
|
|
544
|
+
* @example
|
|
545
|
+
* ```typescript
|
|
546
|
+
* // Blind transfer — send the caller directly to extension 1002
|
|
547
|
+
* const { callUuid } = await voip.originate({
|
|
548
|
+
* fromExtension: '1001',
|
|
549
|
+
* toNumber: '+17865551234',
|
|
550
|
+
* });
|
|
551
|
+
* await voip.transfer(callUuid, { targetExtension: '1002', type: 'blind' });
|
|
552
|
+
*
|
|
553
|
+
* // CRM UI: Transfer button with extension picker
|
|
554
|
+
* async function handleTransfer(callUuid: string, targetExt: string) {
|
|
555
|
+
* await voip.transfer(callUuid, { targetExtension: targetExt, type: 'blind' });
|
|
556
|
+
* showNotification(`Call transferred to ${targetExt}`);
|
|
557
|
+
* hideCallBanner();
|
|
558
|
+
* }
|
|
559
|
+
*
|
|
560
|
+
* // Blind transfer to an external number
|
|
561
|
+
* await voip.transfer(callUuid, { targetExtension: '+17865559999', type: 'blind' });
|
|
562
|
+
* ```
|
|
563
|
+
*
|
|
564
|
+
* @see POST /api/calls/{uuid}/transfer
|
|
565
|
+
*/
|
|
122
566
|
transfer(uuid: string, params: TransferParams): Promise<{
|
|
123
567
|
message: string;
|
|
124
568
|
}>;
|
|
125
|
-
/**
|
|
569
|
+
/**
|
|
570
|
+
* Get all currently active (in-progress) calls in the phone system.
|
|
571
|
+
*
|
|
572
|
+
* @description
|
|
573
|
+
* Queries FreeSWITCH for every live call channel — calls that are ringing, connected,
|
|
574
|
+
* on hold, or in a conference. This is the real-time snapshot of what's happening
|
|
575
|
+
* on your phone system RIGHT NOW. Unlike `listCalls()` which shows historical CDRs,
|
|
576
|
+
* this shows only live calls.
|
|
577
|
+
*
|
|
578
|
+
* **CRM usage:** Build a "Live Calls" dashboard panel or operator console showing
|
|
579
|
+
* all active conversations. Poll this every 3-5 seconds for a near-real-time view,
|
|
580
|
+
* or use WebSocket events for true real-time updates.
|
|
581
|
+
*
|
|
582
|
+
* Each ActiveCall includes:
|
|
583
|
+
* - `uuid` — FreeSWITCH channel UUID. Required for `hangup()`, `transfer()`,
|
|
584
|
+
* `holdCall()`, `parkCall()`, and three-way operations
|
|
585
|
+
* - `call_uuid` — The unique call identifier (shared across both legs of the same call)
|
|
586
|
+
* - `caller_id_number` / `caller_id_name` — Who's calling
|
|
587
|
+
* - `destination` — Who's being called
|
|
588
|
+
* - `direction` — `'inbound'` (someone called you) or `'outbound'` (you called them)
|
|
589
|
+
* - `status` — Channel state: `'active'`, `'ringing'`, `'held'`
|
|
590
|
+
* - `answered` — `true` if the call was picked up, `false` if still ringing
|
|
591
|
+
* - `on_hold` — `true` if the call is currently on hold
|
|
592
|
+
* - `started_at` — When the call started (ISO timestamp)
|
|
593
|
+
*
|
|
594
|
+
* @returns An object containing:
|
|
595
|
+
* - `activeCalls: ActiveCall[]` — Array of live calls. Empty array `[]` if no calls are active.
|
|
596
|
+
*
|
|
597
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
598
|
+
* @throws {HttpError} 500 — FreeSWITCH connection error
|
|
599
|
+
*
|
|
600
|
+
* @example
|
|
601
|
+
* ```typescript
|
|
602
|
+
* // Build a live calls panel
|
|
603
|
+
* const { activeCalls } = await voip.getActiveCalls();
|
|
604
|
+
*
|
|
605
|
+
* // Show in a table
|
|
606
|
+
* activeCalls.forEach(call => {
|
|
607
|
+
* const status = call.answered ? (call.on_hold ? '⏸ On Hold' : '🗣 Active') : '📞 Ringing';
|
|
608
|
+
* console.log(`${status} | ${call.caller_id_number} → ${call.destination} | ${call.uuid}`);
|
|
609
|
+
* });
|
|
610
|
+
*
|
|
611
|
+
* // CRM UI: Live Calls widget — poll every 5 seconds
|
|
612
|
+
* setInterval(async () => {
|
|
613
|
+
* const { activeCalls } = await voip.getActiveCalls();
|
|
614
|
+
* updateLiveCallsTable(activeCalls);
|
|
615
|
+
* }, 5000);
|
|
616
|
+
*
|
|
617
|
+
* // CRM UI: Show/hide logic
|
|
618
|
+
* if (activeCalls.length === 0) {
|
|
619
|
+
* showEmptyState('No active calls right now');
|
|
620
|
+
* } else {
|
|
621
|
+
* const activeCount = activeCalls.filter(c => c.answered && !c.on_hold).length;
|
|
622
|
+
* const ringingCount = activeCalls.filter(c => !c.answered).length;
|
|
623
|
+
* showStats(`${activeCount} active, ${ringingCount} ringing`);
|
|
624
|
+
* }
|
|
625
|
+
*
|
|
626
|
+
* // Find a specific call by phone number
|
|
627
|
+
* const customerCall = activeCalls.find(c => c.caller_id_number === '+17865551234');
|
|
628
|
+
* if (customerCall) {
|
|
629
|
+
* // Show call controls: Hang Up, Transfer, Hold
|
|
630
|
+
* showCallControls(customerCall.uuid);
|
|
631
|
+
* }
|
|
632
|
+
* ```
|
|
633
|
+
*
|
|
634
|
+
* @see GET /api/calls/active
|
|
635
|
+
*/
|
|
126
636
|
getActiveCalls(): Promise<{
|
|
127
637
|
activeCalls: ActiveCall[];
|
|
128
638
|
}>;
|
|
129
639
|
/**
|
|
130
|
-
*
|
|
640
|
+
* Look up the FreeSWITCH channel UUID for a specific call by matching extension + remote number.
|
|
641
|
+
*
|
|
642
|
+
* @description
|
|
643
|
+
* When building three-way calling features, you need FreeSWITCH channel UUIDs — not the
|
|
644
|
+
* local SIP.js session IDs that the SDK's WebRTC phone uses internally. This helper
|
|
645
|
+
* searches the active calls list and returns the matching FS UUID.
|
|
646
|
+
*
|
|
647
|
+
* This is a **client-side convenience method** — it calls `getActiveCalls()` under the hood
|
|
648
|
+
* and does a fuzzy match on caller/destination numbers. It's best used in WebRTC-based
|
|
649
|
+
* applications where you have the extension number and the remote party's number but
|
|
650
|
+
* don't have the FreeSWITCH UUID.
|
|
651
|
+
*
|
|
652
|
+
* **Important:** This method only works for calls that are currently active in FreeSWITCH.
|
|
653
|
+
* It returns `null` if the call isn't found (e.g. the call already ended, or the numbers
|
|
654
|
+
* don't match any active channel).
|
|
655
|
+
*
|
|
656
|
+
* @param extension — The user's extension number. This is the LOCAL party — the one
|
|
657
|
+
* registered in your phone system. Example: `'1001'`
|
|
658
|
+
* @param remoteNumber — The remote party's phone number or extension. This is the OTHER
|
|
659
|
+
* party in the call. Example: `'+17865551234'` (external) or `'1002'` (internal)
|
|
660
|
+
* @param direction — The direction of the call relative to your system:
|
|
661
|
+
* - `'outbound'`: the extension called the remote number (extension is caller_id_number)
|
|
662
|
+
* - `'inbound'`: the remote number called the extension (extension is destination)
|
|
663
|
+
*
|
|
664
|
+
* @returns The FreeSWITCH channel UUID (e.g. `'a1b2c3d4-e5f6-7890-abcd-ef1234567890'`)
|
|
665
|
+
* or `null` if no matching active call was found.
|
|
666
|
+
*
|
|
667
|
+
* @example
|
|
668
|
+
* ```typescript
|
|
669
|
+
* // After originating a call, look up its FS UUID for three-way operations
|
|
670
|
+
* const { callUuid } = await voip.originate({
|
|
671
|
+
* fromExtension: '1001',
|
|
672
|
+
* toNumber: '+17865551234',
|
|
673
|
+
* });
|
|
674
|
+
*
|
|
675
|
+
* // Wait for the call to connect, then:
|
|
676
|
+
* const fsUuid = await voip.resolveCallFsUuid('1001', '+17865551234', 'outbound');
|
|
677
|
+
* if (fsUuid) {
|
|
678
|
+
* // Now we can add a third party
|
|
679
|
+
* const { newCallUuid } = await voip.addCallParticipant(fsUuid, { toNumber: '+17865559999' });
|
|
680
|
+
* // ... wait for B to answer, then merge
|
|
681
|
+
* await voip.mergeCalls({ callUuidA: fsUuid, callUuidB: newCallUuid });
|
|
682
|
+
* }
|
|
683
|
+
*
|
|
684
|
+
* // CRM UI: Three-way call button
|
|
685
|
+
* async function startThreeWay(extension: string, customerPhone: string, thirdParty: string) {
|
|
686
|
+
* const fsUuid = await voip.resolveCallFsUuid(extension, customerPhone, 'outbound');
|
|
687
|
+
* if (!fsUuid) {
|
|
688
|
+
* showError('Call not found — it may have ended');
|
|
689
|
+
* return;
|
|
690
|
+
* }
|
|
691
|
+
* const { newCallUuid } = await voip.addCallParticipant(fsUuid, { toNumber: thirdParty });
|
|
692
|
+
* showNotification(`Dialing ${thirdParty}...`);
|
|
693
|
+
* // Listen for WebSocket 'call_start' event on newCallUuid, then auto-merge
|
|
694
|
+
* }
|
|
695
|
+
* ```
|
|
696
|
+
*
|
|
697
|
+
* @see GET /api/calls/active (used internally)
|
|
698
|
+
*/
|
|
699
|
+
resolveCallFsUuid(extension: string, remoteNumber: string, direction: 'inbound' | 'outbound'): Promise<string | null>;
|
|
700
|
+
/**
|
|
701
|
+
* Get aggregate call statistics for a time period.
|
|
702
|
+
*
|
|
703
|
+
* @description
|
|
704
|
+
* Returns total call counts broken down by direction (inbound, outbound, internal)
|
|
705
|
+
* for the specified period. Use this for dashboard KPI cards showing "Calls Today",
|
|
706
|
+
* "Calls This Week", "Calls This Month", etc.
|
|
707
|
+
*
|
|
708
|
+
* **CRM UI guidance:** Display the stats as KPI cards at the top of the dashboard:
|
|
709
|
+
* - "Total Calls: 342" (large number, counts don't change often)
|
|
710
|
+
* - "Inbound: 210 | Outbound: 98 | Internal: 34" (breakdown rows)
|
|
711
|
+
* - Show a period selector: Today | 7 Days | 30 Days | 90 Days
|
|
712
|
+
* - Refresh every 60 seconds or on manual "Refresh" button click
|
|
713
|
+
* - Show a loading skeleton while fetching
|
|
714
|
+
*
|
|
715
|
+
* @param period — The time window for statistics:
|
|
716
|
+
* - `'today'` — Since midnight today
|
|
717
|
+
* - `'7d'` — Last 7 days (default if omitted)
|
|
718
|
+
* - `'30d'` — Last 30 days
|
|
719
|
+
* - `'90d'` — Last 90 days
|
|
720
|
+
*
|
|
721
|
+
* @returns An object containing:
|
|
722
|
+
* - `stats: object` — `{ total, inbound, outbound, internal, period }`
|
|
723
|
+
*
|
|
724
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
725
|
+
*
|
|
726
|
+
* @example
|
|
727
|
+
* ```typescript
|
|
728
|
+
* // Get today's stats for the dashboard
|
|
729
|
+
* const { stats } = await voip.getCallStats('today');
|
|
730
|
+
*
|
|
731
|
+
* // Render KPI cards
|
|
732
|
+
* console.log(`${stats.total} calls today`);
|
|
733
|
+
* console.log(`↘ ${stats.inbound} inbound | ↗ ${stats.outbound} outbound | ↔ ${stats.internal} internal`);
|
|
734
|
+
*
|
|
735
|
+
* // CRM UI: period selector
|
|
736
|
+
* const periods = [
|
|
737
|
+
* { value: 'today', label: 'Today' },
|
|
738
|
+
* { value: '7d', label: '7 Days' },
|
|
739
|
+
* { value: '30d', label: '30 Days' },
|
|
740
|
+
* { value: '90d', label: '90 Days' },
|
|
741
|
+
* ];
|
|
742
|
+
* // <select onChange={e => fetchStats(e.target.value)}>
|
|
743
|
+
* ```
|
|
744
|
+
*
|
|
745
|
+
* @see GET /api/calls/stats
|
|
746
|
+
*/
|
|
747
|
+
getCallStats(period?: 'today' | '7d' | '30d' | '90d'): Promise<CallStats>;
|
|
748
|
+
/**
|
|
749
|
+
* Place an active call on hold or resume it from hold.
|
|
750
|
+
*
|
|
751
|
+
* @description
|
|
752
|
+
* When you put a call on hold, the remote party hears hold music (or silence) and
|
|
753
|
+
* cannot hear anything from your end. This is useful when you need to consult with
|
|
754
|
+
* someone else, look up information, or set up a three-way call.
|
|
755
|
+
*
|
|
756
|
+
* Call `holdCall(uuid, false)` to resume the call — both parties can hear each other again.
|
|
757
|
+
* You can also use this as a toggle: `holdCall(uuid, !isCurrentlyOnHold)`.
|
|
758
|
+
*
|
|
759
|
+
* @param uuid — The FreeSWITCH channel UUID of the active call.
|
|
760
|
+
* Example: `'a1b2c3d4-e5f6-7890-abcd-ef1234567890'`
|
|
761
|
+
* @param hold — `true` to put the call ON hold, `false` to resume (take OFF hold).
|
|
762
|
+
* Default is `true`. Example: `true`
|
|
763
|
+
*
|
|
764
|
+
* @returns An object containing:
|
|
765
|
+
* - `message: string` — Confirmation (e.g. "Call placed on hold" or "Call resumed")
|
|
766
|
+
*
|
|
767
|
+
* @throws {HttpError} 400 — Call UUID not found or call already ended
|
|
768
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
769
|
+
*
|
|
770
|
+
* @example
|
|
771
|
+
* ```typescript
|
|
772
|
+
* // Put a call on hold
|
|
773
|
+
* await voip.holdCall(callUuid, true);
|
|
774
|
+
*
|
|
775
|
+
* // Resume a call
|
|
776
|
+
* await voip.holdCall(callUuid, false);
|
|
777
|
+
*
|
|
778
|
+
* // CRM UI: Hold/Resume toggle button
|
|
779
|
+
* let isOnHold = false;
|
|
780
|
+
*
|
|
781
|
+
* async function toggleHold(callUuid: string) {
|
|
782
|
+
* isOnHold = !isOnHold;
|
|
783
|
+
* await voip.holdCall(callUuid, isOnHold);
|
|
784
|
+
* updateHoldButton(isOnHold ? 'Resume' : 'Hold', isOnHold ? '▶' : '⏸');
|
|
785
|
+
* }
|
|
786
|
+
*
|
|
787
|
+
* // Put on hold before starting a three-way call
|
|
788
|
+
* await voip.holdCall(callUuid, true);
|
|
789
|
+
* const { newCallUuid } = await voip.addCallParticipant(callUuid, { toNumber: '+17865559999' });
|
|
790
|
+
* // ... when third party answers, merge
|
|
791
|
+
* ```
|
|
792
|
+
*
|
|
793
|
+
* @see POST /api/calls/{uuid}/hold
|
|
794
|
+
*/
|
|
795
|
+
holdCall(uuid: string, hold?: boolean): Promise<{
|
|
796
|
+
message: string;
|
|
797
|
+
}>;
|
|
798
|
+
/**
|
|
799
|
+
* Park an active call in a numbered parking slot so another extension can pick it up.
|
|
800
|
+
*
|
|
801
|
+
* @description
|
|
802
|
+
* Call parking is like putting a call "on hold in the cloud" — the caller hears hold
|
|
803
|
+
* music, and any extension in the tenant can retrieve the call by dialing the parking
|
|
804
|
+
* slot number. This is the telephony equivalent of "please hold while I transfer you"
|
|
805
|
+
* followed by an overhead page: "John, pick up on line 701."
|
|
806
|
+
*
|
|
807
|
+
* **How it works:**
|
|
808
|
+
* 1. Agent receives a call
|
|
809
|
+
* 2. Agent parks the call → system assigns a parking slot (e.g. `701`)
|
|
810
|
+
* 3. System announces "Call parked on 701"
|
|
811
|
+
* 4. Another agent dials `701` to pick up the call
|
|
812
|
+
*
|
|
813
|
+
* If you don't specify a slot number, the system auto-assigns the next available slot.
|
|
814
|
+
* Parked calls can be listed with `getParkedCalls()`.
|
|
815
|
+
*
|
|
816
|
+
* @param uuid — The FreeSWITCH channel UUID of the active call to park.
|
|
817
|
+
* Example: `'a1b2c3d4-e5f6-7890-abcd-ef1234567890'`
|
|
818
|
+
* @param slot — Optional parking slot number. If omitted, the system assigns the next
|
|
819
|
+
* available slot (recommended). If specified, this exact slot number is used.
|
|
820
|
+
* Example: `701`
|
|
821
|
+
*
|
|
822
|
+
* @returns An object containing:
|
|
823
|
+
* - `message: string` — Confirmation message (e.g. "Call parked in slot 701")
|
|
824
|
+
* - `slot: string` — The parking slot number where the call is parked.
|
|
825
|
+
* Display this prominently in the UI so agents know which slot to dial.
|
|
826
|
+
*
|
|
827
|
+
* @throws {HttpError} 400 — Call UUID not found, slot already occupied, or call already ended
|
|
828
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
829
|
+
*
|
|
830
|
+
* @example
|
|
831
|
+
* ```typescript
|
|
832
|
+
* // Park a call — let the system assign a slot
|
|
833
|
+
* const { slot } = await voip.parkCall(callUuid);
|
|
834
|
+
* console.log(`Call parked in slot ${slot} — tell the agent to dial ${slot}`);
|
|
835
|
+
*
|
|
836
|
+
* // Park in a specific slot
|
|
837
|
+
* const { slot } = await voip.parkCall(callUuid, 702);
|
|
838
|
+
*
|
|
839
|
+
* // CRM UI: Park Call button
|
|
840
|
+
* async function handleParkCall(callUuid: string) {
|
|
841
|
+
* const { slot } = await voip.parkCall(callUuid);
|
|
842
|
+
* showNotification(`Call parked on ${slot}`, 'success');
|
|
843
|
+
* showParkedCallBanner(slot, callUuid);
|
|
844
|
+
* }
|
|
845
|
+
*
|
|
846
|
+
* // List all parked calls (e.g. for a "Parked Calls" widget)
|
|
847
|
+
* const { parkedCalls } = await voip.getParkedCalls();
|
|
848
|
+
* parkedCalls.forEach(parked => {
|
|
849
|
+
* console.log(`Slot ${parked.slot}: ${parked.caller_id_number} → ${parked.destination}`);
|
|
850
|
+
* // Show "Pick Up" button next to each parked call
|
|
851
|
+
* });
|
|
852
|
+
*
|
|
853
|
+
* // CRM UI: Show/hide parked calls panel
|
|
854
|
+
* const { parkedCalls } = await voip.getParkedCalls();
|
|
855
|
+
* if (parkedCalls.length === 0) {
|
|
856
|
+
* hideParkedCallsPanel();
|
|
857
|
+
* } else {
|
|
858
|
+
* showParkedCallsPanel(parkedCalls); // Shows slot numbers and caller info
|
|
859
|
+
* }
|
|
860
|
+
* ```
|
|
861
|
+
*
|
|
862
|
+
* @see POST /api/calls/{uuid}/park
|
|
863
|
+
* @see GET /api/calls/parked (getParkedCalls)
|
|
864
|
+
*/
|
|
865
|
+
parkCall(uuid: string, slot?: number): Promise<{
|
|
866
|
+
message: string;
|
|
867
|
+
slot: string;
|
|
868
|
+
}>;
|
|
869
|
+
/**
|
|
870
|
+
* Eavesdrop on an active call. The specified extension will receive
|
|
871
|
+
* an incoming call. When answered, audio from the target call is bridged
|
|
872
|
+
* to the monitoring extension.
|
|
873
|
+
*
|
|
874
|
+
* @description
|
|
875
|
+
* Allows a supervisor or admin to silently monitor, coach, or intervene in an active
|
|
876
|
+
* call. Three modes are available:
|
|
877
|
+
*
|
|
878
|
+
* - **`listen`** (default): The supervisor hears the call audio but neither party
|
|
879
|
+
* hears the supervisor. Useful for quality monitoring and training.
|
|
880
|
+
* - **`whisper`**: The supervisor can speak to the agent (extension) privately
|
|
881
|
+
* without the remote party hearing. Used for real-time coaching.
|
|
882
|
+
* - **`barge`**: The supervisor enters the call — all three parties can hear each
|
|
883
|
+
* other. Used for escalations and intervention.
|
|
884
|
+
*
|
|
885
|
+
* After the monitoring extension answers the incoming call, DTMF controls are available:
|
|
886
|
+
* - Press `1` — whisper mode
|
|
887
|
+
* - Press `2` — barge mode
|
|
888
|
+
* - Press `3` — mute
|
|
889
|
+
* - Press `0` — toggle mute
|
|
890
|
+
*
|
|
891
|
+
* **CRM UI guidance:** Show an "Eavesdrop" button on active calls (visible to admins
|
|
892
|
+
* and supervisors only). Display a mode selector (Listen / Whisper / Barge). After
|
|
893
|
+
* initiating, show "Monitoring [caller]..." with mode indicator and DTMF instructions.
|
|
894
|
+
*
|
|
895
|
+
* @param callUuid — The FreeSWITCH channel UUID of the active call to monitor.
|
|
896
|
+
* Example: `'a1b2c3d4-e5f6-7890-abcd-ef1234567890'`
|
|
897
|
+
* @param extension — The extension number of the monitoring supervisor.
|
|
898
|
+
* This extension's phone will ring — when answered, monitoring begins.
|
|
899
|
+
* Example: `'1001'`
|
|
900
|
+
* @param mode — Eavesdrop mode: `'listen'` (default, silent), `'whisper'` (coach agent),
|
|
901
|
+
* or `'barge'` (join call). Example: `'listen'`
|
|
902
|
+
*
|
|
903
|
+
* @returns An object containing:
|
|
904
|
+
* - `message: string` — Confirmation message
|
|
905
|
+
* - `spyUuid: string` — The monitoring session UUID (used with `switchEavesdropMode()`)
|
|
906
|
+
*
|
|
907
|
+
* @throws {HttpError} 400 — Call UUID not found or extension is not registered
|
|
908
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
909
|
+
* @throws {HttpError} 403 — Extension doesn't have eavesdrop permission
|
|
910
|
+
* @throws {HttpError} 500 — FreeSWITCH operation failed
|
|
911
|
+
*
|
|
912
|
+
* @example
|
|
913
|
+
* ```typescript
|
|
914
|
+
* // Silent monitoring — supervisor hears but is not heard
|
|
915
|
+
* const { spyUuid } = await voip.eavesdropCall(callUuid, '1001', 'listen');
|
|
916
|
+
*
|
|
917
|
+
* // Coach the agent privately (whisper mode)
|
|
918
|
+
* await voip.eavesdropCall(callUuid, '1001', 'whisper');
|
|
919
|
+
*
|
|
920
|
+
* // CRM UI: Supervisor panel
|
|
921
|
+
* async function startMonitoring(callUuid: string, supervisorExt: string, mode: string) {
|
|
922
|
+
* showMonitoringBanner(`Connecting supervisor ${supervisorExt}...`);
|
|
923
|
+
* try {
|
|
924
|
+
* const { spyUuid } = await voip.eavesdropCall(callUuid, supervisorExt, mode);
|
|
925
|
+
* showMonitoringActive(spyUuid, mode);
|
|
926
|
+
* showDtmfInstructions(mode); // "Press 1 for whisper, 2 for barge"
|
|
927
|
+
* } catch (err) {
|
|
928
|
+
* if (err instanceof HttpError && err.status === 400) {
|
|
929
|
+
* showError('Call not found — it may have ended');
|
|
930
|
+
* }
|
|
931
|
+
* }
|
|
932
|
+
* }
|
|
933
|
+
* ```
|
|
934
|
+
*
|
|
935
|
+
* @see POST /api/calls/{uuid}/eavesdrop
|
|
936
|
+
* @see switchEavesdropMode (change mode during monitoring)
|
|
937
|
+
*/
|
|
938
|
+
eavesdropCall(callUuid: string, extension: string, mode?: 'listen' | 'whisper' | 'barge'): Promise<EavesdropResponse>;
|
|
939
|
+
/** Switch eavesdrop mode (server-side DTMF). */
|
|
940
|
+
switchEavesdropMode(callUuid: string, spyUuid: string, mode: 'listen' | 'whisper' | 'barge'): Promise<{
|
|
941
|
+
message: string;
|
|
942
|
+
spyUuid: string;
|
|
943
|
+
mode: string;
|
|
944
|
+
}>;
|
|
945
|
+
/** List parked calls */
|
|
946
|
+
getParkedCalls(): Promise<{
|
|
947
|
+
parkedCalls: ParkedCall[];
|
|
948
|
+
}>;
|
|
949
|
+
/**
|
|
950
|
+
* Export CDR (Call Detail Records) as CSV text.
|
|
951
|
+
*
|
|
952
|
+
* @description
|
|
953
|
+
* Generates a CSV file containing filtered call records for the specified date range.
|
|
954
|
+
* The response is raw CSV text — you need to handle downloading or displaying it.
|
|
955
|
+
* This is useful for:
|
|
956
|
+
* - "Export to CSV" buttons in the call history UI
|
|
957
|
+
* - Downloading call records for billing/invoicing
|
|
958
|
+
* - Importing into external analytics tools
|
|
959
|
+
*
|
|
960
|
+
* **CRM UI guidance:** Show an "Export CSV" button near the call history table.
|
|
961
|
+
* When clicked, filter by the current date range and download. Create a Blob from
|
|
962
|
+
* the response text and trigger a browser download via a temporary anchor element.
|
|
963
|
+
*
|
|
964
|
+
* @param params — Optional filters to narrow the export
|
|
965
|
+
* @param params.dateFrom — ISO date string: only calls on or after this date. Example: `'2026-04-01'`
|
|
966
|
+
* @param params.dateTo — ISO date string: only calls on or before this date. Example: `'2026-04-30'`
|
|
967
|
+
* @param params.direction — Filter by direction: `'inbound'`, `'outbound'`, `'internal'`, or `'all'` (default)
|
|
968
|
+
* @param params.limit — Maximum number of records to export (default: no limit)
|
|
969
|
+
*
|
|
970
|
+
* @returns Raw CSV text string with headers and call records
|
|
971
|
+
*
|
|
972
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
973
|
+
*
|
|
974
|
+
* @example
|
|
975
|
+
* ```typescript
|
|
976
|
+
* // Export last month's calls
|
|
977
|
+
* const csv = await voip.exportCalls({
|
|
978
|
+
* dateFrom: '2026-05-01',
|
|
979
|
+
* dateTo: '2026-05-31',
|
|
980
|
+
* direction: 'outbound',
|
|
981
|
+
* });
|
|
982
|
+
*
|
|
983
|
+
* // Trigger browser download
|
|
984
|
+
* const blob = new Blob([csv], { type: 'text/csv' });
|
|
985
|
+
* const url = URL.createObjectURL(blob);
|
|
986
|
+
* const a = document.createElement('a');
|
|
987
|
+
* a.href = url;
|
|
988
|
+
* a.download = 'calls-may-2026.csv';
|
|
989
|
+
* a.click();
|
|
990
|
+
* URL.revokeObjectURL(url);
|
|
991
|
+
*
|
|
992
|
+
* // CRM UI: Export button in call history
|
|
993
|
+
* async function handleExport(dateFrom: string, dateTo: string) {
|
|
994
|
+
* showExportingSpinner();
|
|
995
|
+
* try {
|
|
996
|
+
* const csv = await voip.exportCalls({ dateFrom, dateTo });
|
|
997
|
+
* downloadFile(csv, `calls-${dateFrom}-to-${dateTo}.csv`);
|
|
998
|
+
* showNotification('Export complete');
|
|
999
|
+
* } catch {
|
|
1000
|
+
* showError('Export failed — please try again');
|
|
1001
|
+
* } finally {
|
|
1002
|
+
* hideExportingSpinner();
|
|
1003
|
+
* }
|
|
1004
|
+
* }
|
|
1005
|
+
* ```
|
|
1006
|
+
*
|
|
1007
|
+
* @see GET /api/calls/export
|
|
1008
|
+
*/
|
|
1009
|
+
exportCalls(params?: ExportCallsParams): Promise<string>;
|
|
1010
|
+
/**
|
|
1011
|
+
* Add a third participant to an existing two-party call (Step 1 of three-way calling).
|
|
1012
|
+
*
|
|
1013
|
+
* @description
|
|
1014
|
+
* This is the first step in setting up a three-way conference call. It:
|
|
1015
|
+
* 1. Places the existing call on hold (the remote party hears hold music)
|
|
1016
|
+
* 2. Originates a NEW call from you to a third party (the `toNumber` or `toExtension`)
|
|
1017
|
+
* 3. Returns the new call UUID so you can track when the third party answers
|
|
1018
|
+
*
|
|
1019
|
+
* **Three-way call flow (3 steps):**
|
|
1020
|
+
* 1. `addCallParticipant()` — Hold person A, dial person B
|
|
1021
|
+
* 2. Wait for WebSocket `call_start` / `call_answer` event on `newCallUuid` (person B answers)
|
|
1022
|
+
* 3. `mergeCalls()` — Join A + you + B into a conference room
|
|
1023
|
+
*
|
|
1024
|
+
* **CRM UI guidance:**
|
|
1025
|
+
* - After calling this, show "Dialing [name/number]..." in the three-way panel
|
|
1026
|
+
* - Gray out the Merge button until the WebSocket confirms person B answered
|
|
1027
|
+
* - Show an inline timer ("Dialing... 10s")
|
|
1028
|
+
* - If person B doesn't answer within 30s, show "No answer" and an "End" button
|
|
1029
|
+
*
|
|
1030
|
+
* @param callUuid — The FreeSWITCH channel UUID of YOUR leg in the active two-party call.
|
|
1031
|
+
* This is your own WebRTC/desk phone channel, NOT the remote party's channel.
|
|
1032
|
+
* Get it from `originate()` → `callUuid`, `resolveCallFsUuid()`, or `getActiveCalls()`.
|
|
1033
|
+
* Example: `'a1b2c3d4-e5f6-7890-abcd-ef1234567890'`
|
|
1034
|
+
* @param params — Who to invite into the call
|
|
1035
|
+
* @param params.toNumber — An external PSTN phone number to call.
|
|
1036
|
+
* Example: `'+17865559999'`. Use this OR `toExtension`, not both.
|
|
1037
|
+
* @param params.toExtension — An internal extension to add.
|
|
1038
|
+
* Example: `'1002'`. Use this OR `toNumber`, not both.
|
|
1039
|
+
* @param options — Optional request overrides
|
|
1040
|
+
* @param options.tenantId — Target tenant UUID (required for superadmin keys only)
|
|
1041
|
+
*
|
|
1042
|
+
* @returns An object containing:
|
|
1043
|
+
* - `message: string` — Confirmation message
|
|
1044
|
+
* - `newCallUuid: string` — The UUID of the NEW call leg (person B). Watch for
|
|
1045
|
+
* WebSocket `call_start` and `call_answer` events with this UUID to know when
|
|
1046
|
+
* person B answers. This UUID goes into `mergeCalls({ callUuidB })`.
|
|
1047
|
+
* - `originalBridgedLeg: string` — The UUID of person A's PSTN leg (the original
|
|
1048
|
+
* remote party). Pass this to `mergeCalls({ bridgedLegA })` for optimal routing.
|
|
1049
|
+
*
|
|
1050
|
+
* @throws {HttpError} 400 — `callUuid` not found, both `toNumber` and `toExtension` missing, or extension is disabled
|
|
1051
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
1052
|
+
* @throws {HttpError} 500 — FreeSWITCH originate failed
|
|
1053
|
+
*
|
|
1054
|
+
* @example
|
|
1055
|
+
* ```typescript
|
|
1056
|
+
* // Full three-way call flow
|
|
1057
|
+
* // 1. Start the add
|
|
1058
|
+
* const { newCallUuid, originalBridgedLeg } = await voip.addCallParticipant(
|
|
1059
|
+
* currentCallUuid,
|
|
1060
|
+
* { toNumber: '+17865559999' },
|
|
1061
|
+
* );
|
|
1062
|
+
*
|
|
1063
|
+
* // 2. Listen for the WebSocket event that person B answered
|
|
1064
|
+
* voip.on('call_answer', (data) => {
|
|
1065
|
+
* if (data.call_uuid === newCallUuid) {
|
|
1066
|
+
* // 3. Now merge all three parties
|
|
1067
|
+
* voip.mergeCalls({
|
|
1068
|
+
* callUuidA: currentCallUuid,
|
|
1069
|
+
* callUuidB: newCallUuid,
|
|
1070
|
+
* bridgedLegA: originalBridgedLeg,
|
|
1071
|
+
* });
|
|
1072
|
+
* }
|
|
1073
|
+
* });
|
|
1074
|
+
*
|
|
1075
|
+
* // CRM UI: Three-way call with an internal extension
|
|
1076
|
+
* async function startThreeWayWithColleague(callUuid: string, colleagueExt: string) {
|
|
1077
|
+
* showThreeWayPanel('dialing');
|
|
1078
|
+
* try {
|
|
1079
|
+
* const { newCallUuid, originalBridgedLeg } = await voip.addCallParticipant(
|
|
1080
|
+
* callUuid,
|
|
1081
|
+
* { toExtension: colleagueExt },
|
|
1082
|
+
* );
|
|
1083
|
+
* // Store these for the merge step
|
|
1084
|
+
* pendingThreeWay = { callUuidA: callUuid, callUuidB: newCallUuid, bridgedLegA: originalBridgedLeg };
|
|
1085
|
+
* showNotification(`Dialing ${colleagueExt}...`);
|
|
1086
|
+
* } catch (err) {
|
|
1087
|
+
* showError('Could not add participant — extension may be busy or offline');
|
|
1088
|
+
* hideThreeWayPanel();
|
|
1089
|
+
* }
|
|
1090
|
+
* }
|
|
1091
|
+
* ```
|
|
1092
|
+
*
|
|
1093
|
+
* @see POST /api/calls/{uuid}/add-participant
|
|
1094
|
+
* @see mergeCalls (step 2 — join all three parties)
|
|
1095
|
+
* @see swapCallParticipant (go private with one party)
|
|
1096
|
+
*/
|
|
1097
|
+
addCallParticipant(callUuid: string, params: AddParticipantParams, options?: RequestOptions): Promise<AddParticipantResponse>;
|
|
1098
|
+
/**
|
|
1099
|
+
* Merge two active call legs into a three-way conference room (Step 2 of three-way calling).
|
|
1100
|
+
*
|
|
1101
|
+
* @description
|
|
1102
|
+
* After using `addCallParticipant()` to dial a third party and waiting for them to answer,
|
|
1103
|
+
* call this method to join all three parties (you + person A + person B) into a single
|
|
1104
|
+
* conference room. Everyone hears everyone.
|
|
1105
|
+
*
|
|
1106
|
+
* **Behind the scenes:** This creates a FreeSWITCH conference, moves your call leg and
|
|
1107
|
+
* both remote parties into it, and returns the conference name and participant details.
|
|
1108
|
+
* The conference stays alive as long as you (the moderator) are in it.
|
|
1109
|
+
*
|
|
1110
|
+
* **Three-way call flow (3 steps):**
|
|
1111
|
+
* 1. `addCallParticipant()` — Hold A, dial B
|
|
1112
|
+
* 2. Wait for person B to answer
|
|
1113
|
+
* 3. `mergeCalls()` — Join everyone into a conference ← **YOU ARE HERE**
|
|
1114
|
+
*
|
|
1115
|
+
* **After merging, you can:**
|
|
1116
|
+
* - `swapCallParticipant()` — Talk privately with one person while the other is on hold
|
|
1117
|
+
* - `kickFromConference()` — Remove someone from the call
|
|
1118
|
+
* - `muteConferenceMember()` — Mute/unmute a participant
|
|
1119
|
+
*
|
|
1120
|
+
* **CRM UI guidance:**
|
|
1121
|
+
* - After merging, show a "Conference Active" panel with all 3 participant avatars/names
|
|
1122
|
+
* - Show per-participant controls: Mute, Hold (private swap), Kick
|
|
1123
|
+
* - The merge button should be disabled until person B answers
|
|
1124
|
+
* - If merge fails, show "Could not merge calls" and offer "End" or "Retry"
|
|
1125
|
+
*
|
|
1126
|
+
* @param params — Merge parameters
|
|
1127
|
+
* @param params.callUuidA — Your own WebRTC/desk phone UUID (the moderator's channel).
|
|
1128
|
+
* This is the same UUID you passed to `addCallParticipant()` as `callUuid`.
|
|
1129
|
+
* Example: `'a1b2c3d4-e5f6-7890-abcd-ef1234567890'`
|
|
1130
|
+
* @param params.callUuidB — The third party's UUID, returned by `addCallParticipant()`
|
|
1131
|
+
* as `newCallUuid`. Example: `'b2c3d4e5-f6a7-8901-bcde-f12345678901'`
|
|
1132
|
+
* @param params.bridgedLegA — Optional. The original remote party's PSTN leg UUID,
|
|
1133
|
+
* returned by `addCallParticipant()` as `originalBridgedLeg`. Passing this avoids
|
|
1134
|
+
* an extra server-side lookup and ensures the correct leg is used.
|
|
1135
|
+
* Example: `'c3d4e5f6-a7b8-9012-cdef-123456789012'`
|
|
1136
|
+
* @param params.conferenceName — Optional custom name for the conference room.
|
|
1137
|
+
* Auto-generated if omitted (recommended). Example: `'support-three-way'`
|
|
1138
|
+
* @param options — Optional request overrides
|
|
1139
|
+
* @param options.tenantId — Target tenant UUID (required for superadmin keys only)
|
|
1140
|
+
*
|
|
1141
|
+
* @returns An object containing:
|
|
1142
|
+
* - `message: string` — Confirmation message
|
|
1143
|
+
* - `conferenceName: string` — The conference room name. Use this for subsequent
|
|
1144
|
+
* operations (`swapCallParticipant`, `kickFromConference`, `muteConferenceMember`).
|
|
1145
|
+
* - `participants: ThreeWayParticipant[]` — Array of all 3 participants in the conference.
|
|
1146
|
+
* Each has `uuid` (FS channel UUID), `memberId` (needed for kick/mute),
|
|
1147
|
+
* `callerIdNumber`, `callerIdName`, `muted` (boolean), and `status` (`'active'` or `'held'`).
|
|
1148
|
+
*
|
|
1149
|
+
* @throws {HttpError} 400 — One or both call UUIDs not found, or calls already terminated
|
|
1150
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
1151
|
+
* @throws {HttpError} 500 — FreeSWITCH conference creation failed
|
|
1152
|
+
*
|
|
1153
|
+
* @example
|
|
1154
|
+
* ```typescript
|
|
1155
|
+
* // Complete three-way call flow
|
|
1156
|
+
* // Step 1: Add third party
|
|
1157
|
+
* const { newCallUuid, originalBridgedLeg } = await voip.addCallParticipant(
|
|
1158
|
+
* currentCallUuid,
|
|
1159
|
+
* { toNumber: '+17865559999' },
|
|
1160
|
+
* );
|
|
1161
|
+
*
|
|
1162
|
+
* // Step 2: Wait for answer (via WebSocket or polling)
|
|
1163
|
+
* voip.on('call_answer', async (data) => {
|
|
1164
|
+
* if (data.call_uuid === newCallUuid) {
|
|
1165
|
+
* // Step 3: Merge — all three parties now connected
|
|
1166
|
+
* const { conferenceName, participants } = await voip.mergeCalls({
|
|
1167
|
+
* callUuidA: currentCallUuid,
|
|
1168
|
+
* callUuidB: newCallUuid,
|
|
1169
|
+
* bridgedLegA: originalBridgedLeg,
|
|
1170
|
+
* });
|
|
1171
|
+
*
|
|
1172
|
+
* console.log(`Conference "${conferenceName}" is live`);
|
|
1173
|
+
* participants.forEach(p => {
|
|
1174
|
+
* console.log(` ${p.callerIdName} (${p.callerIdNumber}) — ${p.status}`);
|
|
1175
|
+
* });
|
|
1176
|
+
* }
|
|
1177
|
+
* });
|
|
1178
|
+
*
|
|
1179
|
+
* // CRM UI: Build participant list after merge
|
|
1180
|
+
* function renderParticipants(participants: ThreeWayParticipant[]) {
|
|
1181
|
+
* participants.forEach(p => {
|
|
1182
|
+
* // Show avatar, name, number, and controls
|
|
1183
|
+
* renderParticipantCard({
|
|
1184
|
+
* name: p.callerIdName || p.callerIdNumber,
|
|
1185
|
+
* number: p.callerIdNumber,
|
|
1186
|
+
* isMuted: p.muted,
|
|
1187
|
+
* onMute: () => voip.muteConferenceMember(conferenceName, p.memberId, !p.muted),
|
|
1188
|
+
* onKick: () => voip.kickFromConference(conferenceName, p.memberId),
|
|
1189
|
+
* });
|
|
1190
|
+
* });
|
|
1191
|
+
* }
|
|
1192
|
+
* ```
|
|
1193
|
+
*
|
|
1194
|
+
* @see POST /api/calls/three-way/merge
|
|
1195
|
+
* @see addCallParticipant (step 1)
|
|
1196
|
+
* @see swapCallParticipant (go private with one party)
|
|
1197
|
+
* @see mergeDirectCalls (skip addCallParticipant — merge two existing calls)
|
|
1198
|
+
*/
|
|
1199
|
+
mergeCalls(params: MergeCallsParams, options?: RequestOptions): Promise<MergeCallsResponse>;
|
|
1200
|
+
/**
|
|
1201
|
+
* Swap which participant is active in a three-way conference — go "private"
|
|
1202
|
+
* with one person while the other is placed on hold.
|
|
1203
|
+
*
|
|
1204
|
+
* @description
|
|
1205
|
+
* In a three-way conference call, you might need to have a private conversation with
|
|
1206
|
+
* just one participant while the other waits on hold. This method removes one
|
|
1207
|
+
* participant from the conference (they hear hold music) and keeps the other
|
|
1208
|
+
* connected. The held participant can be brought back by calling `mergeCalls()` again.
|
|
1209
|
+
*
|
|
1210
|
+
* **Use case:** You're on a call with a customer (A) and your manager (B).
|
|
1211
|
+
* You need to discuss something privately with your manager:
|
|
1212
|
+
* 1. `swapCallParticipant({ keepUuid: managerUuid, holdUuid: customerUuid })`
|
|
1213
|
+
* 2. Now you and your manager can talk privately — the customer hears hold music
|
|
1214
|
+
* 3. `mergeCalls()` — Bring all three back together
|
|
1215
|
+
*
|
|
1216
|
+
* **CRM UI guidance:**
|
|
1217
|
+
* - Show a "Private" or "Talk Privately" button next to each participant
|
|
1218
|
+
* - After swapping, the held participant's card should show "On Hold" with a "Rejoin" button
|
|
1219
|
+
* - The active participant's card should show "Private Conversation"
|
|
1220
|
+
* - Show a "Rejoin All" button that calls `mergeCalls()` to restore the full conference
|
|
1221
|
+
*
|
|
1222
|
+
* @param params — Swap parameters
|
|
1223
|
+
* @param params.conferenceName — The conference room name, returned by `mergeCalls()`
|
|
1224
|
+
* or `mergeDirectCalls()`. Example: `'conf-abc123'`
|
|
1225
|
+
* @param params.keepUuid — The FreeSWITCH channel UUID of the participant to KEEP
|
|
1226
|
+
* active in the conference (talk privately with this person).
|
|
1227
|
+
* Get this from `mergeCalls()` → `participants[].uuid`.
|
|
1228
|
+
* Example: `'b2c3d4e5-f6a7-8901-bcde-f12345678901'`
|
|
1229
|
+
* @param params.holdUuid — The FreeSWITCH channel UUID of the participant to PLACE
|
|
1230
|
+
* ON HOLD (remove from conference — they hear hold music).
|
|
1231
|
+
* Get this from `mergeCalls()` → `participants[].uuid`.
|
|
1232
|
+
* Example: `'a1b2c3d4-e5f6-7890-abcd-ef1234567890'`
|
|
1233
|
+
* @param options — Optional request overrides
|
|
1234
|
+
* @param options.tenantId — Target tenant UUID (required for superadmin keys only)
|
|
1235
|
+
*
|
|
1236
|
+
* @returns An object containing:
|
|
1237
|
+
* - `message: string` — Confirmation message
|
|
1238
|
+
* - `conferenceName: string` — The same conference name (for subsequent operations)
|
|
1239
|
+
* - `activeUuid: string` — The UUID of the participant still in the conference (the one you kept)
|
|
1240
|
+
* - `heldUuid: string` — The UUID of the participant now on hold (the one you held)
|
|
1241
|
+
*
|
|
1242
|
+
* @throws {HttpError} 400 — Conference not found, UUIDs don't match participants, or invalid state
|
|
1243
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
1244
|
+
* @throws {HttpError} 500 — FreeSWITCH operation failed
|
|
1245
|
+
*
|
|
1246
|
+
* @example
|
|
1247
|
+
* ```typescript
|
|
1248
|
+
* // After mergeCalls, talk privately with the manager
|
|
1249
|
+
* const { conferenceName, participants } = await voip.mergeCalls({
|
|
1250
|
+
* callUuidA: selfUuid,
|
|
1251
|
+
* callUuidB: newCallUuid,
|
|
1252
|
+
* });
|
|
1253
|
+
*
|
|
1254
|
+
* const customer = participants.find(p => p.callerIdNumber === '+17865551234');
|
|
1255
|
+
* const manager = participants.find(p => p.callerIdNumber === '1002');
|
|
1256
|
+
*
|
|
1257
|
+
* // Go private with the manager — customer goes on hold
|
|
1258
|
+
* await voip.swapCallParticipant({
|
|
1259
|
+
* conferenceName,
|
|
1260
|
+
* keepUuid: manager.uuid,
|
|
1261
|
+
* holdUuid: customer.uuid,
|
|
1262
|
+
* });
|
|
1263
|
+
*
|
|
1264
|
+
* // CRM UI: Private conversation with manager
|
|
1265
|
+
* showPrivateBanner('Talking privately with Manager');
|
|
1266
|
+
* showOnHoldBanner(customer.callerIdNumber);
|
|
1267
|
+
*
|
|
1268
|
+
* // Bring everyone back
|
|
1269
|
+
* document.getElementById('rejoin-all-btn').onclick = async () => {
|
|
1270
|
+
* await voip.mergeCalls({
|
|
1271
|
+
* callUuidA: selfUuid,
|
|
1272
|
+
* callUuidB: manager.uuid,
|
|
1273
|
+
* });
|
|
1274
|
+
* showConferenceActiveBanner();
|
|
1275
|
+
* };
|
|
1276
|
+
*
|
|
1277
|
+
* // CRM UI: Swap to talk privately with customer while manager waits
|
|
1278
|
+
* await voip.swapCallParticipant({
|
|
1279
|
+
* conferenceName,
|
|
1280
|
+
* keepUuid: customer.uuid,
|
|
1281
|
+
* holdUuid: manager.uuid,
|
|
1282
|
+
* });
|
|
1283
|
+
* ```
|
|
1284
|
+
*
|
|
1285
|
+
* @see POST /api/calls/three-way/swap
|
|
1286
|
+
* @see mergeCalls (restore full three-way)
|
|
1287
|
+
*/
|
|
1288
|
+
swapCallParticipant(params: SwapParticipantParams, options?: RequestOptions): Promise<SwapParticipantResponse>;
|
|
1289
|
+
/**
|
|
1290
|
+
* Merge two independently-created calls directly into a conference — no
|
|
1291
|
+
* `addCallParticipant()` step required.
|
|
1292
|
+
*
|
|
1293
|
+
* @description
|
|
1294
|
+
* Unlike `mergeCalls()` which requires the `addCallParticipant()` → merge flow,
|
|
1295
|
+
* this method works with ANY two active call UUIDs from any source. Use this when:
|
|
1296
|
+
*
|
|
1297
|
+
* - A desk phone user puts call A on hold (using the phone's Hold button),
|
|
1298
|
+
* dials call B manually, and then wants to merge them
|
|
1299
|
+
* - Two WebRTC calls were originated separately via `originate()`
|
|
1300
|
+
* - You have two active call UUIDs from `getActiveCalls()` and want to merge them
|
|
1301
|
+
*
|
|
1302
|
+
* This is the lower-level "merge any two calls" endpoint. For the guided three-way
|
|
1303
|
+
* flow with auto-hold and dialing, use `addCallParticipant()` + `mergeCalls()` instead.
|
|
1304
|
+
*
|
|
1305
|
+
* **CRM UI guidance:**
|
|
1306
|
+
* - Use this for a "Merge" button in a multi-call view where the user has two
|
|
1307
|
+
* separate active calls
|
|
1308
|
+
* - Show both calls side by side with a "Merge Calls" button between them
|
|
1309
|
+
* - After merge, show the same conference participant panel as `mergeCalls()`
|
|
1310
|
+
*
|
|
1311
|
+
* @param params — Merge parameters
|
|
1312
|
+
* @param params.callUuidA — UUID of the first active call leg.
|
|
1313
|
+
* Example: `'a1b2c3d4-e5f6-7890-abcd-ef1234567890'`
|
|
1314
|
+
* @param params.callUuidB — UUID of the second active call leg.
|
|
1315
|
+
* Example: `'b2c3d4e5-f6a7-8901-bcde-f12345678901'`
|
|
1316
|
+
* @param params.conferenceName — Optional custom conference name. Auto-generated if omitted.
|
|
1317
|
+
* Example: `'manual-merge'`
|
|
1318
|
+
* @param options — Optional request overrides
|
|
1319
|
+
* @param options.tenantId — Target tenant UUID (required for superadmin keys only)
|
|
1320
|
+
*
|
|
1321
|
+
* @returns An object containing:
|
|
1322
|
+
* - `message: string` — Confirmation message
|
|
1323
|
+
* - `conferenceName: string` — The conference room name for subsequent operations
|
|
1324
|
+
* - `participants: ThreeWayParticipant[]` — All participants now in the conference.
|
|
1325
|
+
* Each has `uuid`, `memberId`, `callerIdNumber`, `callerIdName`, `muted`, and `status`.
|
|
1326
|
+
*
|
|
1327
|
+
* @throws {HttpError} 400 — One or both call UUIDs not found or already terminated
|
|
1328
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
1329
|
+
* @throws {HttpError} 500 — FreeSWITCH conference creation failed
|
|
1330
|
+
*
|
|
1331
|
+
* @example
|
|
1332
|
+
* ```typescript
|
|
1333
|
+
* // Desk phone user: held call A, dialed call B, now wants to merge
|
|
1334
|
+
* const { activeCalls } = await voip.getActiveCalls();
|
|
1335
|
+
*
|
|
1336
|
+
* // Find the two calls (one on hold, one active)
|
|
1337
|
+
* const heldCall = activeCalls.find(c => c.on_hold);
|
|
1338
|
+
* const activeCall = activeCalls.find(c => !c.on_hold && c.uuid !== heldCall?.uuid);
|
|
1339
|
+
*
|
|
1340
|
+
* if (heldCall && activeCall) {
|
|
1341
|
+
* const { conferenceName, participants } = await voip.mergeDirectCalls({
|
|
1342
|
+
* callUuidA: heldCall.uuid,
|
|
1343
|
+
* callUuidB: activeCall.uuid,
|
|
1344
|
+
* });
|
|
1345
|
+
* console.log(`Merged into conference: ${conferenceName}`);
|
|
1346
|
+
* }
|
|
1347
|
+
*
|
|
1348
|
+
* // CRM UI: Merge button for two active calls
|
|
1349
|
+
* async function mergeTwoCalls(uuid1: string, uuid2: string) {
|
|
1350
|
+
* try {
|
|
1351
|
+
* const { conferenceName, participants } = await voip.mergeDirectCalls({
|
|
1352
|
+
* callUuidA: uuid1,
|
|
1353
|
+
* callUuidB: uuid2,
|
|
1354
|
+
* });
|
|
1355
|
+
* showConferencePanel(conferenceName, participants);
|
|
1356
|
+
* hideMergeButton();
|
|
1357
|
+
* } catch (err) {
|
|
1358
|
+
* showError('Could not merge calls — one may have ended');
|
|
1359
|
+
* }
|
|
1360
|
+
* }
|
|
1361
|
+
*
|
|
1362
|
+
* // After merge, use the same conference operations
|
|
1363
|
+
* await voip.muteConferenceMember(conferenceName, participants[1].memberId, true);
|
|
1364
|
+
* await voip.kickFromConference(conferenceName, participants[2].memberId);
|
|
1365
|
+
* ```
|
|
1366
|
+
*
|
|
1367
|
+
* @see POST /api/calls/three-way/merge-direct
|
|
1368
|
+
* @see mergeCalls (guided three-way flow with addCallParticipant)
|
|
1369
|
+
* @see swapCallParticipant (go private with one party)
|
|
1370
|
+
*/
|
|
1371
|
+
mergeDirectCalls(params: MergeDirectParams, options?: RequestOptions): Promise<MergeDirectResponse>;
|
|
1372
|
+
/**
|
|
1373
|
+
* List all extensions for the current tenant.
|
|
1374
|
+
*
|
|
1375
|
+
* @description
|
|
1376
|
+
* Retrieves every PBX extension in the tenant, including SIP registration details,
|
|
1377
|
+
* voicemail configuration, call forwarding settings, and real-time presence status.
|
|
1378
|
+
* This is the primary method for populating extension lists in admin dashboards,
|
|
1379
|
+
* user directories, and CRM extension pickers.
|
|
1380
|
+
*
|
|
1381
|
+
* Each extension includes:
|
|
1382
|
+
* - `id` — UUID (used for update/delete/SIP credential lookups)
|
|
1383
|
+
* - `extension` — The extension number (e.g. `'1001'`)
|
|
1384
|
+
* - `display_name` — Human-readable name (e.g. `'John Smith'`)
|
|
1385
|
+
* - `presence` — Real-time status: `'available'`, `'on_call'`, `'offline'`, `'do_not_disturb'`
|
|
1386
|
+
* - `voicemail_enabled`, `recording_enabled`, `do_not_disturb`, `status`
|
|
1387
|
+
* - `crm_user_id` — Linked CRM user ID (for mapping extensions to CRM records)
|
|
1388
|
+
*
|
|
1389
|
+
* **Superadmin mode:** Returns extensions from ALL tenants. Each extension includes
|
|
1390
|
+
* a `tenants` field with `{ id, company_name, subdomain }`.
|
|
1391
|
+
*
|
|
1392
|
+
* **CRM UI guidance:** Show this as a table with columns: Extension, Display Name,
|
|
1393
|
+
* Presence (colored dot), Status (active/disabled), Voicemail (on/off), Actions
|
|
1394
|
+
* (Edit, Delete, SIP Credentials). Add a search bar and presence filter.
|
|
1395
|
+
*
|
|
1396
|
+
* @returns An object containing:
|
|
1397
|
+
* - `extensions: Extension[]` — Array of extension objects
|
|
1398
|
+
*
|
|
1399
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
1400
|
+
*
|
|
1401
|
+
* @example
|
|
1402
|
+
* ```typescript
|
|
1403
|
+
* const { extensions } = await voip.listExtensions();
|
|
1404
|
+
*
|
|
1405
|
+
* // Show extensions table
|
|
1406
|
+
* extensions.forEach(ext => {
|
|
1407
|
+
* const presenceColor = {
|
|
1408
|
+
* available: 'green', on_call: 'orange',
|
|
1409
|
+
* offline: 'gray', do_not_disturb: 'red',
|
|
1410
|
+
* }[ext.presence || 'offline'];
|
|
1411
|
+
* console.log(`${presenceColor} ${ext.extension} | ${ext.display_name} | ${ext.presence}`);
|
|
1412
|
+
* });
|
|
1413
|
+
*
|
|
1414
|
+
* // CRM UI: build a call transfer extension picker
|
|
1415
|
+
* const onlineExtensions = extensions.filter(e =>
|
|
1416
|
+
* e.presence === 'available' && e.status === 'active'
|
|
1417
|
+
* );
|
|
1418
|
+
* // Show in a dropdown for blind transfer
|
|
1419
|
+
* ```
|
|
1420
|
+
*
|
|
1421
|
+
* @see GET /api/extensions
|
|
1422
|
+
*/
|
|
1423
|
+
listExtensions(): Promise<{
|
|
1424
|
+
extensions: Extension[];
|
|
1425
|
+
}>;
|
|
1426
|
+
/**
|
|
1427
|
+
* Get a single extension by its UUID.
|
|
1428
|
+
*
|
|
1429
|
+
* @description
|
|
1430
|
+
* Returns full details for one extension including SIP settings, voicemail config,
|
|
1431
|
+
* call forwarding, CRM mapping, and real-time presence status. Use this to populate
|
|
1432
|
+
* an "Extension Detail" panel or edit form in the admin dashboard.
|
|
1433
|
+
*
|
|
1434
|
+
* **CRM UI guidance:** Show all fields in a detail view with an "Edit" button.
|
|
1435
|
+
* Display presence as a colored dot with status text. Show voicemail settings
|
|
1436
|
+
* (enabled/disabled, PIN, greeting type). Show call forwarding destination if active.
|
|
1437
|
+
*
|
|
1438
|
+
* @param id — The UUID of the extension (from `listExtensions()` → `extensions[].id`)
|
|
1439
|
+
*
|
|
1440
|
+
* @returns An object containing:
|
|
1441
|
+
* - `extension: Extension` — The full extension object
|
|
1442
|
+
*
|
|
1443
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
1444
|
+
* @throws {HttpError} 404 — Extension not found
|
|
1445
|
+
*
|
|
1446
|
+
* @example
|
|
1447
|
+
* ```typescript
|
|
1448
|
+
* const { extension } = await voip.getExtension('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
|
|
1449
|
+
*
|
|
1450
|
+
* console.log(`Extension: ${extension.extension}`);
|
|
1451
|
+
* console.log(`Name: ${extension.display_name}`);
|
|
1452
|
+
* console.log(`Presence: ${extension.presence}`);
|
|
1453
|
+
* console.log(`Voicemail: ${extension.voicemail_enabled ? 'On' : 'Off'}`);
|
|
1454
|
+
* console.log(`CRM User: ${extension.crm_user_id || 'Not mapped'}`);
|
|
1455
|
+
* ```
|
|
1456
|
+
*
|
|
1457
|
+
* @see GET /api/extensions/{id}
|
|
1458
|
+
*/
|
|
1459
|
+
getExtension(id: string): Promise<{
|
|
1460
|
+
extension: Extension;
|
|
1461
|
+
}>;
|
|
1462
|
+
/**
|
|
1463
|
+
* Create a new extension (also creates a Kamailio SIP subscriber for registration).
|
|
1464
|
+
*
|
|
1465
|
+
* @description
|
|
1466
|
+
* Provisions a new PBX extension for the tenant. This creates:
|
|
1467
|
+
* - An extension record in the PostgreSQL database
|
|
1468
|
+
* - A Kamailio SIP subscriber entry so the extension can register (softphone/desk phone)
|
|
1469
|
+
* - Optional voicemail box with PIN protection
|
|
1470
|
+
*
|
|
1471
|
+
* The returned `sip` object contains the provisioning info needed to configure
|
|
1472
|
+
* a SIP softphone or desk phone: `username`, `domain`, and `server`.
|
|
1473
|
+
*
|
|
1474
|
+
* **Superadmin mode:** Pass `{ tenantId: 'uuid' }` as the second parameter when
|
|
1475
|
+
* using a global superadmin API key to create an extension in a specific tenant.
|
|
1476
|
+
*
|
|
1477
|
+
* **CRM UI guidance:** Show a form with fields: Extension Number (required),
|
|
1478
|
+
* Display Name (required), Password (required), Voicemail Enabled (checkbox),
|
|
1479
|
+
* Voicemail PIN (optional, 4-6 digits), Recording Enabled (checkbox, default on),
|
|
1480
|
+
* Outbound Caller ID (dropdown of tenant DIDs). Validate before submit.
|
|
1481
|
+
*
|
|
1482
|
+
* @param params — Extension creation parameters
|
|
1483
|
+
* @param params.extension — The extension number (e.g. `'2001'`). Must be unique within the tenant
|
|
1484
|
+
* @param params.password — SIP password for registration (min 8 chars recommended)
|
|
1485
|
+
* @param params.displayName — Human-readable display name (e.g. `'Alice Johnson'`)
|
|
1486
|
+
* @param params.voicemailEnabled — Enable voicemail for this extension (default `false`)
|
|
1487
|
+
* @param params.voicemailPin — 4-6 digit PIN for voicemail access via *97 (optional)
|
|
1488
|
+
* @param params.doNotDisturb — Enable Do Not Disturb mode (default `false`)
|
|
1489
|
+
* @param params.recordingEnabled — Enable call recording (default `true`)
|
|
1490
|
+
* @param params.noiseCancellationEnabled — Enable server-side RNNoise noise cancellation (default `true`)
|
|
1491
|
+
* @param params.outboundCallerId — Outbound caller ID DID number (must be a tenant DID).
|
|
1492
|
+
* Set to `null` to fall back to the tenant's first active DID
|
|
1493
|
+
* @param options — Optional request overrides
|
|
1494
|
+
* @param options.tenantId — Target tenant UUID (required for superadmin keys only)
|
|
1495
|
+
*
|
|
1496
|
+
* @returns An object containing:
|
|
1497
|
+
* - `extension: Extension` — The created extension object
|
|
1498
|
+
* - `sip: SipProvisioningInfo` — SIP provisioning info with `username`, `domain`, `server`
|
|
1499
|
+
*
|
|
1500
|
+
* @throws {HttpError} 400 — Extension number already exists, invalid outbound caller ID, or missing required fields
|
|
1501
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
1502
|
+
*
|
|
1503
|
+
* @example
|
|
1504
|
+
* ```typescript
|
|
1505
|
+
* // Create a new extension for the tenant
|
|
1506
|
+
* const { extension, sip } = await voip.createExtension({
|
|
1507
|
+
* extension: '2001',
|
|
1508
|
+
* password: 'secure-sip-password',
|
|
1509
|
+
* displayName: 'Alice Johnson',
|
|
1510
|
+
* voicemailEnabled: true,
|
|
1511
|
+
* voicemailPin: '1234',
|
|
1512
|
+
* recordingEnabled: true,
|
|
1513
|
+
* });
|
|
1514
|
+
*
|
|
1515
|
+
* console.log(`Created extension ${sip.username}@${sip.domain} (server: ${sip.server})`);
|
|
1516
|
+
*
|
|
1517
|
+
* // Superadmin: create extension in a specific tenant
|
|
1518
|
+
* const { extension } = await voip.createExtension(
|
|
1519
|
+
* { extension: '3001', password: 'pass123', displayName: 'Bob' },
|
|
1520
|
+
* { tenantId: 'a1b2c3d4-...' },
|
|
1521
|
+
* );
|
|
1522
|
+
*
|
|
1523
|
+
* // CRM UI: handle form submission
|
|
1524
|
+
* async function handleCreateExtension(formData: CreateExtensionParams) {
|
|
1525
|
+
* try {
|
|
1526
|
+
* const { extension, sip } = await voip.createExtension(formData);
|
|
1527
|
+
* showNotification(`Extension ${extension.extension} created successfully`);
|
|
1528
|
+
* showProvisioningInfo(sip); // Show SIP config for the user
|
|
1529
|
+
* } catch (err) {
|
|
1530
|
+
* if (err instanceof HttpError && err.status === 400) {
|
|
1531
|
+
* showError('Extension number already exists — choose another');
|
|
1532
|
+
* }
|
|
1533
|
+
* }
|
|
1534
|
+
* }
|
|
1535
|
+
* ```
|
|
1536
|
+
*
|
|
1537
|
+
* @see POST /api/extensions
|
|
1538
|
+
*/
|
|
1539
|
+
createExtension(params: CreateExtensionParams, options?: RequestOptions): Promise<{
|
|
1540
|
+
extension: Extension;
|
|
1541
|
+
sip: SipProvisioningInfo;
|
|
1542
|
+
}>;
|
|
1543
|
+
/**
|
|
1544
|
+
* Update an existing extension's settings.
|
|
1545
|
+
*
|
|
1546
|
+
* @description
|
|
1547
|
+
* Modifies one or more fields on an extension. All parameters are optional —
|
|
1548
|
+
* only the fields you provide will be updated. Common use cases:
|
|
1549
|
+
* - Change display name when an employee's name changes
|
|
1550
|
+
* - Reset SIP password for a user who forgot it
|
|
1551
|
+
* - Enable/disable voicemail or change the PIN
|
|
1552
|
+
* - Toggle Do Not Disturb for vacation/after-hours
|
|
1553
|
+
* - Enable/disable recording per extension
|
|
1554
|
+
* - Activate or disable an extension
|
|
131
1555
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
1556
|
+
* **CRM UI guidance:** Show an edit form pre-populated with current values from `getExtension()`.
|
|
1557
|
+
* Use a modal or slide-over panel. Show a "Save" button that only lights up when a value
|
|
1558
|
+
* has changed. For password changes, show a "Show/Hide" toggle button.
|
|
135
1559
|
*
|
|
136
|
-
*
|
|
137
|
-
* to
|
|
1560
|
+
* @param id — The UUID of the extension to update (from `listExtensions()` → `extensions[].id`)
|
|
1561
|
+
* @param params — Fields to update (all optional, omit unchanged fields)
|
|
1562
|
+
* @param params.displayName — New display name. Example: `'Alicia Johnson'`
|
|
1563
|
+
* @param params.password — New SIP password. Example: `'new-secure-password'`
|
|
1564
|
+
* @param params.voicemailEnabled — Enable or disable voicemail
|
|
1565
|
+
* @param params.voicemailPin — New 4-6 digit voicemail PIN. Set to `null` to remove PIN protection
|
|
1566
|
+
* @param params.doNotDisturb — Enable or disable Do Not Disturb
|
|
1567
|
+
* @param params.status — `'active'` or `'disabled'`. Disabled extensions cannot make/receive calls
|
|
1568
|
+
* @param params.recordingEnabled — Enable or disable call recording
|
|
1569
|
+
* @param params.noiseCancellationEnabled — Enable or disable server-side noise cancellation
|
|
1570
|
+
* @param options — Optional request overrides
|
|
1571
|
+
* @param options.tenantId — Target tenant UUID (required for superadmin keys only)
|
|
138
1572
|
*
|
|
139
|
-
* @
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* @
|
|
1573
|
+
* @returns An object containing:
|
|
1574
|
+
* - `message: string` — Confirmation message (e.g. `'Extension updated'`)
|
|
1575
|
+
*
|
|
1576
|
+
* @throws {HttpError} 400 — Invalid field value or extension not found
|
|
1577
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
143
1578
|
*
|
|
144
1579
|
* @example
|
|
145
1580
|
* ```typescript
|
|
146
|
-
* //
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
1581
|
+
* // Update display name and enable voicemail
|
|
1582
|
+
* await voip.updateExtension('ext-uuid-here', {
|
|
1583
|
+
* displayName: 'Alicia Johnson',
|
|
1584
|
+
* voicemailEnabled: true,
|
|
1585
|
+
* voicemailPin: '5678',
|
|
1586
|
+
* });
|
|
1587
|
+
*
|
|
1588
|
+
* // Disable an extension (temporarily)
|
|
1589
|
+
* await voip.updateExtension('ext-uuid-here', { status: 'disabled' });
|
|
1590
|
+
*
|
|
1591
|
+
* // Re-enable
|
|
1592
|
+
* await voip.updateExtension('ext-uuid-here', { status: 'active' });
|
|
1593
|
+
*
|
|
1594
|
+
* // CRM UI: Edit form submit
|
|
1595
|
+
* async function handleUpdateExtension(id: string, changes: UpdateExtensionParams) {
|
|
1596
|
+
* if (Object.keys(changes).length === 0) return; // Nothing changed
|
|
1597
|
+
* try {
|
|
1598
|
+
* await voip.updateExtension(id, changes);
|
|
1599
|
+
* showNotification('Extension updated');
|
|
1600
|
+
* closeEditModal();
|
|
1601
|
+
* refreshExtensionList();
|
|
1602
|
+
* } catch (err) {
|
|
1603
|
+
* showError('Update failed — please try again');
|
|
1604
|
+
* }
|
|
150
1605
|
* }
|
|
151
1606
|
* ```
|
|
152
|
-
*/
|
|
153
|
-
resolveCallFsUuid(extension: string, remoteNumber: string, direction: 'inbound' | 'outbound'): Promise<string | null>;
|
|
154
|
-
/** Get call statistics for a period */
|
|
155
|
-
getCallStats(period?: 'today' | '7d' | '30d' | '90d'): Promise<CallStats>;
|
|
156
|
-
/** Place a call on hold or resume */
|
|
157
|
-
holdCall(uuid: string, hold?: boolean): Promise<{
|
|
158
|
-
message: string;
|
|
159
|
-
}>;
|
|
160
|
-
/** Park an active call */
|
|
161
|
-
parkCall(uuid: string, slot?: number): Promise<{
|
|
162
|
-
message: string;
|
|
163
|
-
slot: string;
|
|
164
|
-
}>;
|
|
165
|
-
/**
|
|
166
|
-
* Eavesdrop on an active call. The specified extension will receive
|
|
167
|
-
* an incoming call. When answered, audio from the target call is bridged
|
|
168
|
-
* to the monitoring extension.
|
|
169
1607
|
*
|
|
170
|
-
*
|
|
171
|
-
* 1 = whisper, 2 = barge, 3 = mute, 0 = toggle mute
|
|
1608
|
+
* @see PUT /api/extensions/{id}
|
|
172
1609
|
*/
|
|
173
|
-
|
|
174
|
-
/** Switch eavesdrop mode (server-side DTMF). */
|
|
175
|
-
switchEavesdropMode(callUuid: string, spyUuid: string, mode: 'listen' | 'whisper' | 'barge'): Promise<{
|
|
1610
|
+
updateExtension(id: string, params: UpdateExtensionParams, options?: RequestOptions): Promise<{
|
|
176
1611
|
message: string;
|
|
177
|
-
spyUuid: string;
|
|
178
|
-
mode: string;
|
|
179
|
-
}>;
|
|
180
|
-
/** List parked calls */
|
|
181
|
-
getParkedCalls(): Promise<{
|
|
182
|
-
parkedCalls: ParkedCall[];
|
|
183
1612
|
}>;
|
|
184
|
-
/** Export CDR as CSV. Returns raw CSV text. */
|
|
185
|
-
exportCalls(params?: ExportCallsParams): Promise<string>;
|
|
186
1613
|
/**
|
|
187
|
-
*
|
|
188
|
-
* The caller should watch for a `call.answered` WebSocket event on `newCallUuid`,
|
|
189
|
-
* then call `mergeCalls()` to bring all three into a conference.
|
|
1614
|
+
* Delete an extension permanently.
|
|
190
1615
|
*
|
|
191
|
-
* @
|
|
192
|
-
*
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
*
|
|
197
|
-
*
|
|
1616
|
+
* @description
|
|
1617
|
+
* Removes the extension from the database and cleans up associated resources:
|
|
1618
|
+
* - Extension record (PostgreSQL)
|
|
1619
|
+
* - Kamailio SIP subscriber (can no longer register)
|
|
1620
|
+
* - Voicemail messages and greeting audio files
|
|
1621
|
+
* - Ring group / queue / paging group memberships
|
|
1622
|
+
* - CRM mapping associations
|
|
198
1623
|
*
|
|
199
|
-
*
|
|
200
|
-
|
|
201
|
-
mergeCalls(params: MergeCallsParams, options?: RequestOptions): Promise<MergeCallsResponse>;
|
|
202
|
-
/**
|
|
203
|
-
* Swap — go "private" with one participant by removing the other from the
|
|
204
|
-
* conference and placing them on hold. Call `mergeCalls()` again to restore.
|
|
1624
|
+
* This operation is irreversible. Call records (CDRs) belonging to this extension
|
|
1625
|
+
* are preserved for audit purposes.
|
|
205
1626
|
*
|
|
206
|
-
*
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
1627
|
+
* **CRM UI guidance:** Show a confirmation dialog before deleting:
|
|
1628
|
+
* "Are you sure you want to delete extension [number] ([name])? This will also
|
|
1629
|
+
* delete all associated voicemail messages, greetings, and group memberships."
|
|
1630
|
+
* Use a red "Delete" button. After deletion, remove the row from the table.
|
|
1631
|
+
*
|
|
1632
|
+
* @param id — The UUID of the extension to delete (from `listExtensions()` → `extensions[].id`)
|
|
1633
|
+
*
|
|
1634
|
+
* @returns An object containing:
|
|
1635
|
+
* - `message: string` — Confirmation message (e.g. `'Extension deleted'`)
|
|
1636
|
+
*
|
|
1637
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
1638
|
+
* @throws {HttpError} 404 — Extension not found
|
|
1639
|
+
*
|
|
1640
|
+
* @example
|
|
1641
|
+
* ```typescript
|
|
1642
|
+
* // Delete extension 2001
|
|
1643
|
+
* await voip.deleteExtension('ext-uuid-here');
|
|
1644
|
+
*
|
|
1645
|
+
* // CRM UI: Delete button with confirmation
|
|
1646
|
+
* async function handleDeleteExtension(ext: Extension) {
|
|
1647
|
+
* const confirmed = confirm(
|
|
1648
|
+
* `Delete extension ${ext.extension} (${ext.display_name})?\n\n` +
|
|
1649
|
+
* 'This will also delete all voicemail messages and greetings.'
|
|
1650
|
+
* );
|
|
1651
|
+
* if (!confirmed) return;
|
|
1652
|
+
*
|
|
1653
|
+
* try {
|
|
1654
|
+
* await voip.deleteExtension(ext.id);
|
|
1655
|
+
* removeExtensionRow(ext.id);
|
|
1656
|
+
* showNotification(`Extension ${ext.extension} deleted`);
|
|
1657
|
+
* } catch (err) {
|
|
1658
|
+
* showError('Delete failed — please try again');
|
|
1659
|
+
* }
|
|
1660
|
+
* }
|
|
1661
|
+
* ```
|
|
214
1662
|
*
|
|
215
|
-
* @
|
|
1663
|
+
* @see DELETE /api/extensions/{id}
|
|
216
1664
|
*/
|
|
217
|
-
mergeDirectCalls(params: MergeDirectParams, options?: RequestOptions): Promise<MergeDirectResponse>;
|
|
218
|
-
/** List extensions for the current tenant */
|
|
219
|
-
listExtensions(): Promise<{
|
|
220
|
-
extensions: Extension[];
|
|
221
|
-
}>;
|
|
222
|
-
/** Get a single extension */
|
|
223
|
-
getExtension(id: string): Promise<{
|
|
224
|
-
extension: Extension;
|
|
225
|
-
}>;
|
|
226
|
-
/** Create an extension (also creates Kamailio subscriber). Pass `options.tenantId` when using a global superadmin key. */
|
|
227
|
-
createExtension(params: CreateExtensionParams, options?: RequestOptions): Promise<{
|
|
228
|
-
extension: Extension;
|
|
229
|
-
sip: SipProvisioningInfo;
|
|
230
|
-
}>;
|
|
231
|
-
/** Update an extension */
|
|
232
|
-
updateExtension(id: string, params: UpdateExtensionParams, options?: RequestOptions): Promise<{
|
|
233
|
-
message: string;
|
|
234
|
-
}>;
|
|
235
|
-
/** Delete an extension */
|
|
236
1665
|
deleteExtension(id: string): Promise<{
|
|
237
1666
|
message: string;
|
|
238
1667
|
}>;
|
|
@@ -254,26 +1683,122 @@ export declare class VoIPClient {
|
|
|
254
1683
|
crmMetadata: any;
|
|
255
1684
|
}>;
|
|
256
1685
|
/**
|
|
257
|
-
* Get SIP credentials for connecting a WebRTC softphone.
|
|
258
|
-
* Returns everything needed to register: extension, password, SIP domain, WebSocket URI.
|
|
259
|
-
* Use this so your app never needs to store SIP passwords.
|
|
1686
|
+
* Get SIP credentials for connecting a WebRTC softphone or SIP device.
|
|
260
1687
|
*
|
|
261
|
-
* @
|
|
262
|
-
*
|
|
1688
|
+
* @description
|
|
1689
|
+
* Returns everything needed to register a softphone or desk phone with the SIP server:
|
|
1690
|
+
* extension number, SIP password, SIP domain, WebSocket URI (for WebRTC), and registrar address.
|
|
1691
|
+
* Use this so your app never needs to store SIP passwords — fetch them on demand
|
|
1692
|
+
* when the user opens the softphone page.
|
|
1693
|
+
*
|
|
1694
|
+
* **Two lookup modes:**
|
|
1695
|
+
* - **By UUID** (default): `getSipCredentials('extension-uuid')` — use the `id` from `listExtensions()`
|
|
1696
|
+
* - **By number**: `getSipCredentials('1001', { byNumber: true, tenantId: 'uuid' })` — look up
|
|
1697
|
+
* by extension number string. `tenantId` is required for by-number lookup (the extension
|
|
1698
|
+
* number is only unique within a tenant).
|
|
1699
|
+
*
|
|
1700
|
+
* **CRM UI guidance:** Call this when the user clicks "Connect Softphone" or opens the
|
|
1701
|
+
* WebRTC dialer. Pass the credentials to the `WebRTCPhone` constructor. Never display
|
|
1702
|
+
* the password in the UI — use a "Copy" button with `type="password"` input.
|
|
1703
|
+
*
|
|
1704
|
+
* @param target — Extension UUID (default mode), or extension number string when `byNumber: true`
|
|
1705
|
+
* @param options — Optional overrides. Include `{ byNumber: true, tenantId: 'uuid' }` to look
|
|
1706
|
+
* up by extension number instead of UUID. `tenantId` is required for by-number lookups.
|
|
1707
|
+
*
|
|
1708
|
+
* @returns An object containing:
|
|
1709
|
+
* - `sipCredentials: SipCredentials` — Object with `extension`, `displayName`, `password`,
|
|
1710
|
+
* `sipDomain`, `wsUri` (WebSocket URI for WebRTC), and `registrar`
|
|
1711
|
+
*
|
|
1712
|
+
* @throws {HttpError} 400 — `tenantId` is required when using `byNumber: true`
|
|
1713
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
1714
|
+
* @throws {HttpError} 404 — Extension not found
|
|
263
1715
|
*
|
|
264
1716
|
* @example
|
|
265
|
-
*
|
|
1717
|
+
* ```typescript
|
|
1718
|
+
* // By UUID (default) — pass id from listExtensions()
|
|
266
1719
|
* const { sipCredentials } = await voip.getSipCredentials('extension-uuid');
|
|
267
1720
|
*
|
|
268
|
-
* // By extension number
|
|
269
|
-
* const { sipCredentials } = await voip.getSipCredentials('1001', {
|
|
1721
|
+
* // By extension number — requires tenantId
|
|
1722
|
+
* const { sipCredentials } = await voip.getSipCredentials('1001', {
|
|
1723
|
+
* byNumber: true,
|
|
1724
|
+
* tenantId: 'a1b2c3d4-...',
|
|
1725
|
+
* });
|
|
1726
|
+
*
|
|
1727
|
+
* // Use credentials with WebRTCPhone
|
|
1728
|
+
* const phone = new WebRTCPhone({
|
|
1729
|
+
* extension: sipCredentials.extension,
|
|
1730
|
+
* password: sipCredentials.password,
|
|
1731
|
+
* sipDomain: sipCredentials.sipDomain,
|
|
1732
|
+
* wsUri: sipCredentials.wsUri,
|
|
1733
|
+
* });
|
|
1734
|
+
*
|
|
1735
|
+
* // CRM UI: "Connect Phone" button
|
|
1736
|
+
* async function connectSoftphone(extensionId: string) {
|
|
1737
|
+
* showConnectingSpinner();
|
|
1738
|
+
* try {
|
|
1739
|
+
* const { sipCredentials } = await voip.getSipCredentials(extensionId);
|
|
1740
|
+
* const phone = new WebRTCPhone({
|
|
1741
|
+
* extension: sipCredentials.extension,
|
|
1742
|
+
* password: sipCredentials.password,
|
|
1743
|
+
* sipDomain: sipCredentials.sipDomain,
|
|
1744
|
+
* wsUri: sipCredentials.wsUri,
|
|
1745
|
+
* audioElement: document.getElementById('remote-audio') as HTMLAudioElement,
|
|
1746
|
+
* });
|
|
1747
|
+
* await phone.connect();
|
|
1748
|
+
* showPhoneReady(sipCredentials.extension);
|
|
1749
|
+
* } catch (err) {
|
|
1750
|
+
* showError('Failed to connect softphone');
|
|
1751
|
+
* }
|
|
1752
|
+
* }
|
|
1753
|
+
* ```
|
|
1754
|
+
*
|
|
1755
|
+
* @see GET /api/extensions/{id}/sip-credentials
|
|
1756
|
+
* @see GET /api/extensions/by-number/{number}/sip-credentials
|
|
270
1757
|
*/
|
|
271
1758
|
getSipCredentials(target: string, options?: RequestOptions & {
|
|
272
1759
|
byNumber?: boolean;
|
|
273
1760
|
}): Promise<{
|
|
274
1761
|
sipCredentials: SipCredentials;
|
|
275
1762
|
}>;
|
|
276
|
-
/**
|
|
1763
|
+
/**
|
|
1764
|
+
* List all tenants (superadmin only).
|
|
1765
|
+
*
|
|
1766
|
+
* @description
|
|
1767
|
+
* Returns every tenant in the platform. This is a superadmin-only endpoint — tenant
|
|
1768
|
+
* admins cannot see other tenants. Each tenant includes company name, subdomain,
|
|
1769
|
+
* SIP domain, Twilio integration settings, and feature flags.
|
|
1770
|
+
*
|
|
1771
|
+
* **CRM UI guidance (superadmin):** Show as a table with columns: Company Name,
|
|
1772
|
+
* Subdomain, Extensions Count (use `getTenantStats()`), Status (active/disabled),
|
|
1773
|
+
* and an Actions menu (View, Edit, Delete). Add a search bar to filter by company
|
|
1774
|
+
* name. Show a "Create Tenant" button at the top.
|
|
1775
|
+
*
|
|
1776
|
+
* @returns An object containing:
|
|
1777
|
+
* - `tenants: Tenant[]` — Array of tenant objects
|
|
1778
|
+
*
|
|
1779
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
1780
|
+
* @throws {HttpError} 403 — Not a superadmin
|
|
1781
|
+
*
|
|
1782
|
+
* @example
|
|
1783
|
+
* ```typescript
|
|
1784
|
+
* const { tenants } = await voip.listTenants();
|
|
1785
|
+
*
|
|
1786
|
+
* tenants.forEach(t => {
|
|
1787
|
+
* console.log(`${t.companyName} (${t.subdomain}) — ${t.status}`);
|
|
1788
|
+
* });
|
|
1789
|
+
*
|
|
1790
|
+
* // CRM UI: superadmin tenant list
|
|
1791
|
+
* const { tenants } = await voip.listTenants();
|
|
1792
|
+
* renderTenantTable(tenants.map(t => ({
|
|
1793
|
+
* id: t.id,
|
|
1794
|
+
* name: t.companyName,
|
|
1795
|
+
* subdomain: t.subdomain,
|
|
1796
|
+
* status: t.status,
|
|
1797
|
+
* })));
|
|
1798
|
+
* ```
|
|
1799
|
+
*
|
|
1800
|
+
* @see GET /api/tenants
|
|
1801
|
+
*/
|
|
277
1802
|
listTenants(): Promise<{
|
|
278
1803
|
tenants: Tenant[];
|
|
279
1804
|
}>;
|
|
@@ -331,7 +1856,53 @@ export declare class VoIPClient {
|
|
|
331
1856
|
totalCalls: number;
|
|
332
1857
|
};
|
|
333
1858
|
}>;
|
|
334
|
-
/**
|
|
1859
|
+
/**
|
|
1860
|
+
* List all DID (Direct Inward Dialing) phone numbers for the current tenant.
|
|
1861
|
+
*
|
|
1862
|
+
* @description
|
|
1863
|
+
* Retrieves every phone number assigned to the tenant. Each DID includes:
|
|
1864
|
+
* - The phone number itself (the DID)
|
|
1865
|
+
* - Inbound routing configuration (IVR, extension, queue, ring group, AI agent, etc.)
|
|
1866
|
+
* - Route target (e.g. IVR menu name, extension number)
|
|
1867
|
+
* - Emergency caller ID flag
|
|
1868
|
+
* - Status (active/inactive)
|
|
1869
|
+
*
|
|
1870
|
+
* DIDs are how external callers reach your phone system — someone dials the DID
|
|
1871
|
+
* number, and the system routes the call according to the inbound route settings.
|
|
1872
|
+
*
|
|
1873
|
+
* **CRM UI guidance:** Show as a table with columns: Phone Number, Route Type
|
|
1874
|
+
* (Extension/IVR/Queue/etc.), Route Target, Status (active/inactive), Actions
|
|
1875
|
+
* (Edit, Delete). Show a "Create DID" button. Use a badge/tag for the route type.
|
|
1876
|
+
*
|
|
1877
|
+
* @returns An object containing:
|
|
1878
|
+
* - `dids: DidNumber[]` — Array of DID number objects
|
|
1879
|
+
*
|
|
1880
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
1881
|
+
*
|
|
1882
|
+
* @example
|
|
1883
|
+
* ```typescript
|
|
1884
|
+
* const { dids } = await voip.listDids();
|
|
1885
|
+
*
|
|
1886
|
+
* dids.forEach(did => {
|
|
1887
|
+
* console.log(`${did.number} → ${did.inbound_route} / ${did.route_target}`);
|
|
1888
|
+
* });
|
|
1889
|
+
*
|
|
1890
|
+
* // CRM UI: DID list with route badges
|
|
1891
|
+
* dids.forEach(did => {
|
|
1892
|
+
* const routeLabel = {
|
|
1893
|
+
* extension: '📞 Extension',
|
|
1894
|
+
* ivr: '🌳 IVR',
|
|
1895
|
+
* queue: '👥 Queue',
|
|
1896
|
+
* ring_group: '🔔 Ring Group',
|
|
1897
|
+
* ai_agent: '🤖 AI Agent',
|
|
1898
|
+
* whatsapp: '💬 WhatsApp',
|
|
1899
|
+
* }[did.inbound_route] || did.inbound_route;
|
|
1900
|
+
* renderDidRow(did.number, routeLabel, did.route_target, did.status);
|
|
1901
|
+
* });
|
|
1902
|
+
* ```
|
|
1903
|
+
*
|
|
1904
|
+
* @see GET /api/dids
|
|
1905
|
+
*/
|
|
335
1906
|
listDids(): Promise<{
|
|
336
1907
|
dids: DidNumber[];
|
|
337
1908
|
}>;
|
|
@@ -664,89 +2235,467 @@ export declare class VoIPClient {
|
|
|
664
2235
|
*/
|
|
665
2236
|
generateIvrTts(id: string, params: IvrTtsParams): Promise<{
|
|
666
2237
|
message: string;
|
|
667
|
-
menu: IvrMenu;
|
|
668
|
-
}>;
|
|
669
|
-
/** List available TTS voices for IVR greetings */
|
|
670
|
-
listIvrVoices(): Promise<{
|
|
671
|
-
voices: IvrVoice[];
|
|
2238
|
+
menu: IvrMenu;
|
|
2239
|
+
}>;
|
|
2240
|
+
/** List available TTS voices for IVR greetings */
|
|
2241
|
+
listIvrVoices(): Promise<{
|
|
2242
|
+
voices: IvrVoice[];
|
|
2243
|
+
}>;
|
|
2244
|
+
/**
|
|
2245
|
+
* List call recordings with pagination and filtering.
|
|
2246
|
+
*
|
|
2247
|
+
* @description
|
|
2248
|
+
* Retrieves a paginated list of call recordings for the current tenant. Each recording
|
|
2249
|
+
* includes `cdn_url` (CloudFront CDN, instant, no auth) and `audio_url` (API fallback).
|
|
2250
|
+
* Always prefer `cdn_url` when non-null for the fastest playback from the nearest
|
|
2251
|
+
* CloudFront edge location.
|
|
2252
|
+
*
|
|
2253
|
+
* **CRM UI guidance:** Show a "Recordings" table with columns: Date/Time, Caller,
|
|
2254
|
+
* Destination, Duration, and a Play button. The Play button should use `cdn_url` if
|
|
2255
|
+
* available (no API call needed), falling back to `buildUrl(audio_url)`. Show a
|
|
2256
|
+
* loading spinner state. If recordings exist, show pagination controls at the bottom.
|
|
2257
|
+
*
|
|
2258
|
+
* **Playback decision tree:**
|
|
2259
|
+
* ```
|
|
2260
|
+
* if (rec.cdn_url) → use cdn_url directly (<audio src={rec.cdn_url} />)
|
|
2261
|
+
* else if (rec.audio_url) → use voip.buildUrl(`/api/recordings/${rec.id}/audio`)
|
|
2262
|
+
* else → "No recording" state
|
|
2263
|
+
* ```
|
|
2264
|
+
*
|
|
2265
|
+
* @param params — Optional filters and pagination
|
|
2266
|
+
* @param params.page — Page number (1-based, default 1)
|
|
2267
|
+
* @param params.limit — Records per page (default 50)
|
|
2268
|
+
* @param params.direction — Filter by call direction: `'inbound'`, `'outbound'`, `'internal'`, or `'all'`
|
|
2269
|
+
* @param params.dateFrom — ISO date string: recordings on or after this date
|
|
2270
|
+
* @param params.dateTo — ISO date string: recordings on or before this date
|
|
2271
|
+
* @param params.extension — Filter by extension number
|
|
2272
|
+
*
|
|
2273
|
+
* @returns An object containing:
|
|
2274
|
+
* - `recordings: Recording[]` — Array of recording objects with `id`, `cdn_url`,
|
|
2275
|
+
* `audio_url`, `caller_id_number`, `destination`, `duration_seconds`, `started_at`,
|
|
2276
|
+
* `format`, and optional `company_name`/`subdomain` (superadmin)
|
|
2277
|
+
* - `pagination: PaginationInfo` — `{ page, limit, total, pages }`
|
|
2278
|
+
*
|
|
2279
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
2280
|
+
*
|
|
2281
|
+
* @example
|
|
2282
|
+
* ```typescript
|
|
2283
|
+
* const { recordings, pagination } = await voip.listRecordings({
|
|
2284
|
+
* page: 1,
|
|
2285
|
+
* limit: 20,
|
|
2286
|
+
* dateFrom: '2026-06-01',
|
|
2287
|
+
* dateTo: '2026-06-30',
|
|
2288
|
+
* direction: 'inbound',
|
|
2289
|
+
* });
|
|
2290
|
+
*
|
|
2291
|
+
* console.log(`Found ${pagination.total} recordings`);
|
|
2292
|
+
*
|
|
2293
|
+
* recordings.forEach(rec => {
|
|
2294
|
+
* const src = rec.cdn_url ?? voip.buildUrl(`/api/recordings/${rec.id}/audio`);
|
|
2295
|
+
* // Render <audio src={src} preload="none" controls />
|
|
2296
|
+
* });
|
|
2297
|
+
*
|
|
2298
|
+
* // CRM UI: pagination through all recordings
|
|
2299
|
+
* for (let page = 1; page <= pagination.pages; page++) {
|
|
2300
|
+
* const { recordings } = await voip.listRecordings({ page, limit: 50 });
|
|
2301
|
+
* renderRecordingPage(recordings, page);
|
|
2302
|
+
* }
|
|
2303
|
+
* ```
|
|
2304
|
+
*
|
|
2305
|
+
* @see GET /api/recordings
|
|
2306
|
+
*/
|
|
2307
|
+
listRecordings(params?: RecordingListParams): Promise<RecordingListResponse>;
|
|
2308
|
+
/**
|
|
2309
|
+
* Get the best available URL to play a recording, with source and format metadata.
|
|
2310
|
+
*
|
|
2311
|
+
* @description
|
|
2312
|
+
* Resolves the optimal recording URL by checking S3/CloudFront availability first,
|
|
2313
|
+
* then falling back to local disk streaming. Returns metadata about which source
|
|
2314
|
+
* was selected so you can optimize future calls.
|
|
2315
|
+
*
|
|
2316
|
+
* **For most CRM use cases**, you don't need this method. Just read `cdn_url` from
|
|
2317
|
+
* `listCalls()` or `getCall()` directly — it's the same CDN URL with zero API calls.
|
|
2318
|
+
* Use this only when you need to programmatically resolve the URL before rendering
|
|
2319
|
+
* and need to know the source and format.
|
|
2320
|
+
*
|
|
2321
|
+
* @param id — The recording ID (from `listCalls()` → `calls[].id` or `listRecordings()` → `recordings[].id`)
|
|
2322
|
+
*
|
|
2323
|
+
* @returns An object containing:
|
|
2324
|
+
* - `url: string` — The full URL to play this recording (CDN or API)
|
|
2325
|
+
* - `source: 'cdn' | 'local'` — `'cdn'` = CloudFront (fast, ~5-20ms) or `'local'` = disk stream (slower)
|
|
2326
|
+
* - `format: 'mp3' | 'wav'` — Audio file format
|
|
2327
|
+
*
|
|
2328
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
2329
|
+
* @throws {HttpError} 404 — Recording not found
|
|
2330
|
+
*
|
|
2331
|
+
* @example
|
|
2332
|
+
* ```typescript
|
|
2333
|
+
* const { url, source, format } = await voip.getRecordingUrl(callId);
|
|
2334
|
+
*
|
|
2335
|
+
* if (source === 'cdn') {
|
|
2336
|
+
* console.log(`Playing from CDN: ${url} (${format})`);
|
|
2337
|
+
* } else {
|
|
2338
|
+
* console.log(`Playing from local disk: ${url} (${format})`);
|
|
2339
|
+
* }
|
|
2340
|
+
*
|
|
2341
|
+
* // Pass to audio element
|
|
2342
|
+
* audioElement.src = url;
|
|
2343
|
+
* audioElement.load();
|
|
2344
|
+
* audioElement.play();
|
|
2345
|
+
* ```
|
|
2346
|
+
*
|
|
2347
|
+
* @see GET /api/recordings/{id}/url
|
|
2348
|
+
*/
|
|
2349
|
+
getRecordingUrl(id: string): Promise<RecordingUrlResponse>;
|
|
2350
|
+
/**
|
|
2351
|
+
* Delete a recording permanently from all storage.
|
|
2352
|
+
*
|
|
2353
|
+
* @description
|
|
2354
|
+
* Removes the audio file from S3 cloud storage (if uploaded), local server disk, and
|
|
2355
|
+
* Redis cache. The `recording_url`, `s3_key`, `cdn_url`, and `audio_url` fields for
|
|
2356
|
+
* this call record will become null after deletion. The call record (CDR) itself is
|
|
2357
|
+
* preserved — only the audio file is removed.
|
|
2358
|
+
*
|
|
2359
|
+
* This is irreversible. Use with care, especially when complying with data retention
|
|
2360
|
+
* policies or GDPR deletion requests.
|
|
2361
|
+
*
|
|
2362
|
+
* **CRM UI guidance:** Show a confirmation dialog: "Permanently delete this recording?
|
|
2363
|
+
* The call history entry will remain, but the audio file cannot be recovered."
|
|
2364
|
+
* Use a red "Delete" button with a trash icon. After deletion, update the row to show
|
|
2365
|
+
* "Recording deleted" or remove the Play button.
|
|
2366
|
+
*
|
|
2367
|
+
* @param id — The recording/call ID (from `listCalls()` → `calls[].id` or `listRecordings()` → `recordings[].id`)
|
|
2368
|
+
*
|
|
2369
|
+
* @returns An object containing:
|
|
2370
|
+
* - `message: string` — Confirmation message (e.g. `'Recording deleted'`)
|
|
2371
|
+
*
|
|
2372
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
2373
|
+
* @throws {HttpError} 404 — Recording not found
|
|
2374
|
+
*
|
|
2375
|
+
* @example
|
|
2376
|
+
* ```typescript
|
|
2377
|
+
* // Delete a specific recording
|
|
2378
|
+
* await voip.deleteRecording('recording-id-here');
|
|
2379
|
+
*
|
|
2380
|
+
* // CRM UI: Delete button with confirmation
|
|
2381
|
+
* async function handleDeleteRecording(recordingId: string) {
|
|
2382
|
+
* if (!confirm('Permanently delete this recording? The audio file cannot be recovered.')) {
|
|
2383
|
+
* return;
|
|
2384
|
+
* }
|
|
2385
|
+
* try {
|
|
2386
|
+
* await voip.deleteRecording(recordingId);
|
|
2387
|
+
* showNotification('Recording deleted');
|
|
2388
|
+
* removePlayButton(recordingId); // Remove Play button from the row
|
|
2389
|
+
* } catch (err) {
|
|
2390
|
+
* showError('Failed to delete recording');
|
|
2391
|
+
* }
|
|
2392
|
+
* }
|
|
2393
|
+
* ```
|
|
2394
|
+
*
|
|
2395
|
+
* @see DELETE /api/recordings/{id}
|
|
2396
|
+
*/
|
|
2397
|
+
deleteRecording(id: string): Promise<{
|
|
2398
|
+
message: string;
|
|
2399
|
+
}>;
|
|
2400
|
+
/**
|
|
2401
|
+
* List all active conferences in the phone system.
|
|
2402
|
+
*
|
|
2403
|
+
* @description
|
|
2404
|
+
* Returns every live conference room currently running in FreeSWITCH. Each
|
|
2405
|
+
* conference includes:
|
|
2406
|
+
* - `name` — Conference room name (used for join/kick/mute operations)
|
|
2407
|
+
* - `memberCount` — Number of participants
|
|
2408
|
+
* - `locked` — Whether the conference is locked (no new participants)
|
|
2409
|
+
* - `runTime` — How long the conference has been active (HH:MM:SS)
|
|
2410
|
+
* - `members` — Array of conference member objects with caller info
|
|
2411
|
+
*
|
|
2412
|
+
* Conferences are created automatically by three-way calling (`mergeCalls()`,
|
|
2413
|
+
* `mergeDirectCalls()`) or manually via `transferToConference()`.
|
|
2414
|
+
*
|
|
2415
|
+
* **CRM UI guidance:** Show as a "Active Conferences" panel or monitor view.
|
|
2416
|
+
* Each conference row shows: Name, Participant Count, Duration, Lock Status.
|
|
2417
|
+
* Expand a row to show member details with per-participant controls (Mute, Kick).
|
|
2418
|
+
* Poll every 3-5 seconds for a near-real-time view.
|
|
2419
|
+
*
|
|
2420
|
+
* @returns An object containing:
|
|
2421
|
+
* - `conferences: Conference[]` — Array of active conference objects
|
|
2422
|
+
*
|
|
2423
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
2424
|
+
* @throws {HttpError} 500 — FreeSWITCH connection error
|
|
2425
|
+
*
|
|
2426
|
+
* @example
|
|
2427
|
+
* ```typescript
|
|
2428
|
+
* const { conferences } = await voip.listConferences();
|
|
2429
|
+
*
|
|
2430
|
+
* if (conferences.length === 0) {
|
|
2431
|
+
* console.log('No active conferences');
|
|
2432
|
+
* } else {
|
|
2433
|
+
* conferences.forEach(conf => {
|
|
2434
|
+
* console.log(`${conf.name}: ${conf.memberCount} participants, ${conf.runTime}`);
|
|
2435
|
+
* });
|
|
2436
|
+
* }
|
|
2437
|
+
*
|
|
2438
|
+
* // CRM UI: live conference monitor
|
|
2439
|
+
* setInterval(async () => {
|
|
2440
|
+
* const { conferences } = await voip.listConferences();
|
|
2441
|
+
* updateConferenceList(conferences);
|
|
2442
|
+
* }, 5000);
|
|
2443
|
+
* ```
|
|
2444
|
+
*
|
|
2445
|
+
* @see GET /api/conferences
|
|
2446
|
+
*/
|
|
2447
|
+
listConferences(): Promise<{
|
|
2448
|
+
conferences: Conference[];
|
|
2449
|
+
}>;
|
|
2450
|
+
/**
|
|
2451
|
+
* Get detailed information about a specific conference by name.
|
|
2452
|
+
*
|
|
2453
|
+
* @description
|
|
2454
|
+
* Returns full conference details including the member list with per-participant
|
|
2455
|
+
* caller ID, mute status, and other metadata. Use this to populate a conference
|
|
2456
|
+
* detail panel in an operator console or monitoring dashboard.
|
|
2457
|
+
*
|
|
2458
|
+
* **CRM UI guidance:** Show as a detail view with conference name, runtime, lock
|
|
2459
|
+
* button, and participant list with individual controls (Mute/Unmute toggle,
|
|
2460
|
+
* Kick button). Show a "Record" button if recording is supported. Display each
|
|
2461
|
+
* participant's caller ID and mute status.
|
|
2462
|
+
*
|
|
2463
|
+
* @param name — The conference room name (from `listConferences()` → `conferences[].name`
|
|
2464
|
+
* or returned by `mergeCalls()` → `conferenceName`)
|
|
2465
|
+
*
|
|
2466
|
+
* @returns An object containing:
|
|
2467
|
+
* - `conference: Conference` — Full conference object with `members` array
|
|
2468
|
+
*
|
|
2469
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
2470
|
+
* @throws {HttpError} 404 — Conference not found
|
|
2471
|
+
* @throws {HttpError} 500 — FreeSWITCH communication error
|
|
2472
|
+
*
|
|
2473
|
+
* @example
|
|
2474
|
+
* ```typescript
|
|
2475
|
+
* const { conference } = await voip.getConference('conf-abc123');
|
|
2476
|
+
*
|
|
2477
|
+
* console.log(`Conference: ${conference.name}`);
|
|
2478
|
+
* console.log(`Participants: ${conference.memberCount}`);
|
|
2479
|
+
* console.log(`Locked: ${conference.locked}`);
|
|
2480
|
+
*
|
|
2481
|
+
* conference.members.forEach(member => {
|
|
2482
|
+
* console.log(` ${member.caller_id_number} — ${member.muted ? 'Muted' : 'Unmuted'}`);
|
|
2483
|
+
* });
|
|
2484
|
+
* ```
|
|
2485
|
+
*
|
|
2486
|
+
* @see GET /api/conferences/{name}
|
|
2487
|
+
*/
|
|
2488
|
+
getConference(name: string): Promise<{
|
|
2489
|
+
conference: Conference;
|
|
2490
|
+
}>;
|
|
2491
|
+
/** Join a call to a conference */
|
|
2492
|
+
joinConference(conferenceName: string, callUuid: string): Promise<{
|
|
2493
|
+
message: string;
|
|
2494
|
+
}>;
|
|
2495
|
+
/**
|
|
2496
|
+
* Transfer an active call into a conference room.
|
|
2497
|
+
*
|
|
2498
|
+
* @description
|
|
2499
|
+
* Moves an existing active call into a FreeSWITCH conference room. If a
|
|
2500
|
+
* `conferenceName` is specified, the call is added to that existing conference.
|
|
2501
|
+
* If omitted, a new conference room is created automatically.
|
|
2502
|
+
*
|
|
2503
|
+
* Use this for:
|
|
2504
|
+
* - Adding a caller to a scheduled conference call
|
|
2505
|
+
* - Transferring a support call from an agent to a team conference
|
|
2506
|
+
* - Creating a "join conference" button in the call controls
|
|
2507
|
+
*
|
|
2508
|
+
* **CRM UI guidance:** Show a "Transfer to Conference" button in the call controls.
|
|
2509
|
+
* On click, show a selector: pick from existing conferences (`listConferences()`) or
|
|
2510
|
+
* create a new one. After transfer, show "Call moved to conference [name]".
|
|
2511
|
+
*
|
|
2512
|
+
* @param uuid — The FreeSWITCH channel UUID of the active call to transfer.
|
|
2513
|
+
* Example: `'a1b2c3d4-e5f6-7890-abcd-ef1234567890'`
|
|
2514
|
+
* @param conferenceName — Optional conference room to join. If omitted, a new
|
|
2515
|
+
* conference is created automatically. Example: `'support-team'` or omitted
|
|
2516
|
+
*
|
|
2517
|
+
* @returns An object containing:
|
|
2518
|
+
* - `message: string` — Confirmation message
|
|
2519
|
+
*
|
|
2520
|
+
* @throws {HttpError} 400 — Call UUID not found or already ended
|
|
2521
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
2522
|
+
* @throws {HttpError} 500 — FreeSWITCH operation failed
|
|
2523
|
+
*
|
|
2524
|
+
* @example
|
|
2525
|
+
* ```typescript
|
|
2526
|
+
* // Transfer a call to an existing conference
|
|
2527
|
+
* await voip.transferToConference(callUuid, 'support-team');
|
|
2528
|
+
*
|
|
2529
|
+
* // Transfer to a new auto-created conference
|
|
2530
|
+
* await voip.transferToConference(callUuid);
|
|
2531
|
+
*
|
|
2532
|
+
* // CRM UI: transfer to conference button
|
|
2533
|
+
* async function handleTransferToConference(callUuid: string) {
|
|
2534
|
+
* const { conferences } = await voip.listConferences();
|
|
2535
|
+
* const chosenConf = showConferencePicker(conferences);
|
|
2536
|
+
* if (chosenConf) {
|
|
2537
|
+
* await voip.transferToConference(callUuid, chosenConf);
|
|
2538
|
+
* showNotification(`Call transferred to conference "${chosenConf}"`);
|
|
2539
|
+
* }
|
|
2540
|
+
* }
|
|
2541
|
+
* ```
|
|
2542
|
+
*
|
|
2543
|
+
* @see POST /api/calls/{uuid}/conference
|
|
2544
|
+
*/
|
|
2545
|
+
transferToConference(uuid: string, conferenceName?: string): Promise<{
|
|
2546
|
+
message: string;
|
|
672
2547
|
}>;
|
|
673
2548
|
/**
|
|
674
|
-
*
|
|
2549
|
+
* Remove (kick) a participant from a conference room.
|
|
2550
|
+
*
|
|
2551
|
+
* @description
|
|
2552
|
+
* Forcefully removes a single participant from an active conference. The kicked
|
|
2553
|
+
* participant is disconnected immediately. This is used for moderator control in
|
|
2554
|
+
* three-way calls and multi-party conferences — for example, removing a disruptive
|
|
2555
|
+
* participant or ending a consult call while keeping the main call active.
|
|
2556
|
+
*
|
|
2557
|
+
* **CRM UI guidance:**
|
|
2558
|
+
* - Show a "Remove" or "Kick" button/icon next to each participant
|
|
2559
|
+
* - Show a confirmation dialog: "Remove [name] from the call?"
|
|
2560
|
+
* - After kicking, remove their card from the participant list immediately
|
|
2561
|
+
* - If only 2 participants remain, automatically leave the conference (three-way → two-way)
|
|
2562
|
+
*
|
|
2563
|
+
* @param conferenceName — The conference room name, returned by `mergeCalls()`,
|
|
2564
|
+
* `mergeDirectCalls()`, or `listConferences()`.
|
|
2565
|
+
* Example: `'conf-abc123'`
|
|
2566
|
+
* @param memberId — The conference member ID to kick (NOT the FS channel UUID).
|
|
2567
|
+
* Get this from `mergeCalls()` → `participants[].memberId`.
|
|
2568
|
+
* Example: `'1'` or `'2'`
|
|
675
2569
|
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
2570
|
+
* @returns An object containing:
|
|
2571
|
+
* - `message: string` — Confirmation message (e.g. "Member kicked from conference")
|
|
2572
|
+
*
|
|
2573
|
+
* @throws {HttpError} 400 — Conference or member not found, or member ID is invalid
|
|
2574
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
2575
|
+
* @throws {HttpError} 500 — FreeSWITCH operation failed
|
|
678
2576
|
*
|
|
679
2577
|
* @example
|
|
680
2578
|
* ```typescript
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
2579
|
+
* // After mergeCalls, kick the third party
|
|
2580
|
+
* const { conferenceName, participants } = await voip.mergeCalls({
|
|
2581
|
+
* callUuidA: selfUuid,
|
|
2582
|
+
* callUuidB: newCallUuid,
|
|
685
2583
|
* });
|
|
686
2584
|
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
2585
|
+
* const thirdParty = participants.find(p => p.callerIdNumber === '+17865559999');
|
|
2586
|
+
* if (thirdParty) {
|
|
2587
|
+
* await voip.kickFromConference(conferenceName, thirdParty.memberId);
|
|
2588
|
+
* }
|
|
2589
|
+
*
|
|
2590
|
+
* // CRM UI: Kick button per participant
|
|
2591
|
+
* function renderParticipantControls(participant: ThreeWayParticipant) {
|
|
2592
|
+
* const kickBtn = document.createElement('button');
|
|
2593
|
+
* kickBtn.textContent = 'Remove';
|
|
2594
|
+
* kickBtn.onclick = async () => {
|
|
2595
|
+
* if (confirm(`Remove ${participant.callerIdName || participant.callerIdNumber} from the call?`)) {
|
|
2596
|
+
* await voip.kickFromConference(conferenceName, participant.memberId);
|
|
2597
|
+
* removeParticipantCard(participant.memberId);
|
|
2598
|
+
* }
|
|
2599
|
+
* };
|
|
2600
|
+
* return kickBtn;
|
|
2601
|
+
* }
|
|
2602
|
+
*
|
|
2603
|
+
* // CRM UI: Show/hide kick button
|
|
2604
|
+
* // Only show kick for non-self participants
|
|
2605
|
+
* participants
|
|
2606
|
+
* .filter(p => p.uuid !== selfUuid)
|
|
2607
|
+
* .forEach(p => showKickButton(p));
|
|
692
2608
|
* ```
|
|
2609
|
+
*
|
|
2610
|
+
* @see POST /api/conferences/{conferenceName}/kick
|
|
2611
|
+
* @see mergeCalls (get conferenceName and memberId)
|
|
693
2612
|
*/
|
|
694
|
-
|
|
2613
|
+
kickFromConference(conferenceName: string, memberId: string): Promise<{
|
|
2614
|
+
message: string;
|
|
2615
|
+
}>;
|
|
695
2616
|
/**
|
|
696
|
-
*
|
|
2617
|
+
* Mute or unmute a participant in a conference room.
|
|
697
2618
|
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
2619
|
+
* @description
|
|
2620
|
+
* Toggles the microphone for a single conference participant. When muted, the
|
|
2621
|
+
* participant can still hear everyone, but nobody can hear them. This is the
|
|
2622
|
+
* moderator control for managing who can speak in a three-way or multi-party call.
|
|
702
2623
|
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
2624
|
+
* Common use cases:
|
|
2625
|
+
* - Mute a noisy participant (background noise, typing, echoes)
|
|
2626
|
+
* - Mute yourself temporarily while looking up information
|
|
2627
|
+
* - Mute a third party during a private consultation with the other participant
|
|
2628
|
+
*
|
|
2629
|
+
* **CRM UI guidance:**
|
|
2630
|
+
* - Show a microphone icon next to each participant
|
|
2631
|
+
* - Filled mic = unmuted, crossed-out mic = muted
|
|
2632
|
+
* - Click to toggle: `muteConferenceMember(name, memberId, !currentMutedState)`
|
|
2633
|
+
* - For your own card, label it "Mute Me" instead of just "Mute"
|
|
2634
|
+
* - After muting, show a pulsing "Muted" indicator on their card
|
|
2635
|
+
*
|
|
2636
|
+
* @param conferenceName — The conference room name, returned by `mergeCalls()`,
|
|
2637
|
+
* `mergeDirectCalls()`, or `listConferences()`.
|
|
2638
|
+
* Example: `'conf-abc123'`
|
|
2639
|
+
* @param memberId — The conference member ID to mute/unmute (NOT the FS channel UUID).
|
|
2640
|
+
* Get this from `mergeCalls()` → `participants[].memberId`.
|
|
2641
|
+
* Example: `'1'` or `'2'`
|
|
2642
|
+
* @param mute — `true` to mute (silence the participant's microphone), `false` to unmute
|
|
2643
|
+
* (allow them to speak again). Default is `true`.
|
|
2644
|
+
* Example: `true`
|
|
2645
|
+
*
|
|
2646
|
+
* @returns An object containing:
|
|
2647
|
+
* - `message: string` — Confirmation message (e.g. "Member muted" or "Member unmuted")
|
|
2648
|
+
*
|
|
2649
|
+
* @throws {HttpError} 400 — Conference or member not found, or member ID is invalid
|
|
2650
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
2651
|
+
* @throws {HttpError} 500 — FreeSWITCH operation failed
|
|
706
2652
|
*
|
|
707
2653
|
* @example
|
|
708
2654
|
* ```typescript
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
*
|
|
2655
|
+
* // Mute a noisy participant
|
|
2656
|
+
* const { conferenceName, participants } = await voip.mergeCalls({
|
|
2657
|
+
* callUuidA: selfUuid,
|
|
2658
|
+
* callUuidB: newCallUuid,
|
|
2659
|
+
* });
|
|
2660
|
+
*
|
|
2661
|
+
* const noisyParticipant = participants.find(p => p.callerIdNumber === '+17865559999');
|
|
2662
|
+
* if (noisyParticipant) {
|
|
2663
|
+
* await voip.muteConferenceMember(conferenceName, noisyParticipant.memberId, true);
|
|
2664
|
+
* }
|
|
2665
|
+
*
|
|
2666
|
+
* // Unmute them when the noise is gone
|
|
2667
|
+
* await voip.muteConferenceMember(conferenceName, noisyParticipant.memberId, false);
|
|
717
2668
|
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
2669
|
+
* // CRM UI: Mute/Unmute toggle button per participant
|
|
2670
|
+
* function renderMuteButton(participant: ThreeWayParticipant, conferenceName: string) {
|
|
2671
|
+
* let isMuted = participant.muted;
|
|
2672
|
+
* const btn = document.createElement('button');
|
|
2673
|
+
*
|
|
2674
|
+
* function updateButton() {
|
|
2675
|
+
* btn.textContent = isMuted ? '🔇 Unmute' : '🎤 Mute';
|
|
2676
|
+
* btn.className = isMuted ? 'btn-warning' : 'btn-default';
|
|
2677
|
+
* }
|
|
2678
|
+
*
|
|
2679
|
+
* btn.onclick = async () => {
|
|
2680
|
+
* isMuted = !isMuted;
|
|
2681
|
+
* updateButton();
|
|
2682
|
+
* await voip.muteConferenceMember(conferenceName, participant.memberId, isMuted);
|
|
2683
|
+
* };
|
|
2684
|
+
*
|
|
2685
|
+
* updateButton();
|
|
2686
|
+
* return btn;
|
|
2687
|
+
* }
|
|
2688
|
+
*
|
|
2689
|
+
* // Mute yourself (the local participant)
|
|
2690
|
+
* async function muteMyself(conferenceName: string, myMemberId: string) {
|
|
2691
|
+
* await voip.muteConferenceMember(conferenceName, myMemberId, true);
|
|
2692
|
+
* showSelfMutedIndicator();
|
|
2693
|
+
* }
|
|
2694
|
+
* ```
|
|
722
2695
|
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
2696
|
+
* @see POST /api/conferences/{conferenceName}/mute
|
|
2697
|
+
* @see mergeCalls (get conferenceName and memberId)
|
|
725
2698
|
*/
|
|
726
|
-
deleteRecording(id: string): Promise<{
|
|
727
|
-
message: string;
|
|
728
|
-
}>;
|
|
729
|
-
/** List active conferences */
|
|
730
|
-
listConferences(): Promise<{
|
|
731
|
-
conferences: Conference[];
|
|
732
|
-
}>;
|
|
733
|
-
/** Get conference details */
|
|
734
|
-
getConference(name: string): Promise<{
|
|
735
|
-
conference: Conference;
|
|
736
|
-
}>;
|
|
737
|
-
/** Join a call to a conference */
|
|
738
|
-
joinConference(conferenceName: string, callUuid: string): Promise<{
|
|
739
|
-
message: string;
|
|
740
|
-
}>;
|
|
741
|
-
/** Transfer a call to conference */
|
|
742
|
-
transferToConference(uuid: string, conferenceName?: string): Promise<{
|
|
743
|
-
message: string;
|
|
744
|
-
}>;
|
|
745
|
-
/** Kick a member from conference */
|
|
746
|
-
kickFromConference(conferenceName: string, memberId: string): Promise<{
|
|
747
|
-
message: string;
|
|
748
|
-
}>;
|
|
749
|
-
/** Mute/unmute a conference member */
|
|
750
2699
|
muteConferenceMember(conferenceName: string, memberId: string, mute?: boolean): Promise<{
|
|
751
2700
|
message: string;
|
|
752
2701
|
}>;
|
|
@@ -763,7 +2712,44 @@ export declare class VoIPClient {
|
|
|
763
2712
|
message: string;
|
|
764
2713
|
filePath?: string;
|
|
765
2714
|
}>;
|
|
766
|
-
/**
|
|
2715
|
+
/**
|
|
2716
|
+
* List all ring groups for the current tenant.
|
|
2717
|
+
*
|
|
2718
|
+
* @description
|
|
2719
|
+
* Returns every ring group configured for the tenant. A ring group rings multiple
|
|
2720
|
+
* extensions simultaneously or sequentially when a DID is dialed. Common strategies:
|
|
2721
|
+
*
|
|
2722
|
+
* - **`simultaneous`** — All extensions ring at once. First to answer gets the call.
|
|
2723
|
+
* - **`sequential`** — Extensions ring one at a time in priority order.
|
|
2724
|
+
* - **`random`** — A random extension is selected to receive the call.
|
|
2725
|
+
*
|
|
2726
|
+
* Each ring group includes:
|
|
2727
|
+
* - Name, strategy, ring timeout, no-answer action
|
|
2728
|
+
* - Member list with extensions, priorities, and delay seconds
|
|
2729
|
+
*
|
|
2730
|
+
* **CRM UI guidance:** Show as a table with columns: Name, Strategy,
|
|
2731
|
+
* Members (count), No Answer Action, Status, and Actions (Edit, Delete).
|
|
2732
|
+
* Each row should expand to show member details.
|
|
2733
|
+
*
|
|
2734
|
+
* @returns An object containing:
|
|
2735
|
+
* - `ringGroups: RingGroup[]` — Array of ring group objects with members
|
|
2736
|
+
*
|
|
2737
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
2738
|
+
*
|
|
2739
|
+
* @example
|
|
2740
|
+
* ```typescript
|
|
2741
|
+
* const { ringGroups } = await voip.listRingGroups();
|
|
2742
|
+
*
|
|
2743
|
+
* ringGroups.forEach(rg => {
|
|
2744
|
+
* console.log(`${rg.name} — ${rg.strategy} — ${rg.members.length} members`);
|
|
2745
|
+
* rg.members.forEach(m => {
|
|
2746
|
+
* console.log(` ${m.extension} (priority: ${m.priority}, delay: ${m.delay_seconds}s)`);
|
|
2747
|
+
* });
|
|
2748
|
+
* });
|
|
2749
|
+
* ```
|
|
2750
|
+
*
|
|
2751
|
+
* @see GET /api/ring-groups
|
|
2752
|
+
*/
|
|
767
2753
|
listRingGroups(): Promise<{
|
|
768
2754
|
ringGroups: RingGroup[];
|
|
769
2755
|
}>;
|
|
@@ -771,7 +2757,84 @@ export declare class VoIPClient {
|
|
|
771
2757
|
getRingGroup(id: string): Promise<{
|
|
772
2758
|
ringGroup: RingGroup;
|
|
773
2759
|
}>;
|
|
774
|
-
/**
|
|
2760
|
+
/**
|
|
2761
|
+
* Create a new ring group.
|
|
2762
|
+
*
|
|
2763
|
+
* @description
|
|
2764
|
+
* Configures a group of extensions that ring together when a DID or internal number
|
|
2765
|
+
* is dialed. This is the primary way to implement "ring all phones in the sales
|
|
2766
|
+
* department" or "try John, then Jane, then voicemail" routing patterns.
|
|
2767
|
+
*
|
|
2768
|
+
* **Strategies:**
|
|
2769
|
+
* - `'simultaneous'` (default) — All extensions ring at the same time
|
|
2770
|
+
* - `'sequential'` — One at a time, in priority order
|
|
2771
|
+
* - `'random'` — Randomly pick one extension to ring
|
|
2772
|
+
*
|
|
2773
|
+
* **No-answer handling:** When no member answers within `ringTimeout` seconds,
|
|
2774
|
+
* the call is routed according to `noAnswerAction` and `noAnswerTarget`.
|
|
2775
|
+
*
|
|
2776
|
+
* **Superadmin mode:** Pass `{ tenantId: 'uuid' }` when using a global superadmin
|
|
2777
|
+
* key to create a ring group for a specific tenant.
|
|
2778
|
+
*
|
|
2779
|
+
* **CRM UI guidance:** Show a form with: Name (required), Strategy (dropdown),
|
|
2780
|
+
* Ring Timeout (seconds slider, 5-60), No Answer Action (dropdown),
|
|
2781
|
+
* No Answer Target (dynamic based on action), and Member picker (multi-select
|
|
2782
|
+
* from extension list with priority and delay settings per member).
|
|
2783
|
+
*
|
|
2784
|
+
* @param params — Ring group creation parameters
|
|
2785
|
+
* @param params.name — Ring group name. Example: `'Sales Team'`
|
|
2786
|
+
* @param params.strategy — Ring strategy: `'simultaneous'` (default), `'sequential'`, or `'random'`
|
|
2787
|
+
* @param params.ringTimeout — Max seconds to ring before no-answer action (default: 30).
|
|
2788
|
+
* Example: `20`
|
|
2789
|
+
* @param params.noAnswerAction — What to do when nobody answers:
|
|
2790
|
+
* `'voicemail'`, `'ivr'`, `'extension'`, `'hangup'`, `'ai_agent'`
|
|
2791
|
+
* @param params.noAnswerTarget — Target for no-answer action
|
|
2792
|
+
* (extension number, IVR name, etc.)
|
|
2793
|
+
* @param params.members — Array of member extensions.
|
|
2794
|
+
* Each has: `extension` (string), `priority` (number, lower = rings first for sequential),
|
|
2795
|
+
* `delaySeconds` (seconds to delay before ringing this member)
|
|
2796
|
+
* @param options — Optional request overrides
|
|
2797
|
+
* @param options.tenantId — Target tenant UUID (required for superadmin keys only)
|
|
2798
|
+
*
|
|
2799
|
+
* @returns An object containing:
|
|
2800
|
+
* - `ringGroup: RingGroup` — The created ring group with members
|
|
2801
|
+
*
|
|
2802
|
+
* @throws {HttpError} 400 — Duplicate name, invalid extension in members, or missing required fields
|
|
2803
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
2804
|
+
*
|
|
2805
|
+
* @example
|
|
2806
|
+
* ```typescript
|
|
2807
|
+
* // Create a sales team ring group — ring all at once
|
|
2808
|
+
* const { ringGroup } = await voip.createRingGroup({
|
|
2809
|
+
* name: 'Sales Team',
|
|
2810
|
+
* strategy: 'simultaneous',
|
|
2811
|
+
* ringTimeout: 20,
|
|
2812
|
+
* noAnswerAction: 'voicemail',
|
|
2813
|
+
* noAnswerTarget: '1001',
|
|
2814
|
+
* members: [
|
|
2815
|
+
* { extension: '1001', priority: 1, delaySeconds: 0 },
|
|
2816
|
+
* { extension: '1002', priority: 1, delaySeconds: 0 },
|
|
2817
|
+
* { extension: '1003', priority: 1, delaySeconds: 0 },
|
|
2818
|
+
* ],
|
|
2819
|
+
* });
|
|
2820
|
+
*
|
|
2821
|
+
* // Sequential ring group — try primary first, then backup
|
|
2822
|
+
* const { ringGroup } = await voip.createRingGroup({
|
|
2823
|
+
* name: 'Support Escalation',
|
|
2824
|
+
* strategy: 'sequential',
|
|
2825
|
+
* ringTimeout: 15,
|
|
2826
|
+
* noAnswerAction: 'extension',
|
|
2827
|
+
* noAnswerTarget: '2000',
|
|
2828
|
+
* members: [
|
|
2829
|
+
* { extension: '1001', priority: 1, delaySeconds: 0 },
|
|
2830
|
+
* { extension: '1002', priority: 2, delaySeconds: 5 },
|
|
2831
|
+
* { extension: '1003', priority: 3, delaySeconds: 10 },
|
|
2832
|
+
* ],
|
|
2833
|
+
* });
|
|
2834
|
+
* ```
|
|
2835
|
+
*
|
|
2836
|
+
* @see POST /api/ring-groups
|
|
2837
|
+
*/
|
|
775
2838
|
createRingGroup(params: CreateRingGroupParams, options?: RequestOptions): Promise<{
|
|
776
2839
|
ringGroup: RingGroup;
|
|
777
2840
|
}>;
|
|
@@ -783,7 +2846,46 @@ export declare class VoIPClient {
|
|
|
783
2846
|
deleteRingGroup(id: string): Promise<{
|
|
784
2847
|
message: string;
|
|
785
2848
|
}>;
|
|
786
|
-
/**
|
|
2849
|
+
/**
|
|
2850
|
+
* List all paging groups for the current tenant.
|
|
2851
|
+
*
|
|
2852
|
+
* @description
|
|
2853
|
+
* Returns every paging/intercom group configured for the tenant. A paging group
|
|
2854
|
+
* enables one-way audio broadcast (PA system) to multiple extensions simultaneously
|
|
2855
|
+
* with auto-answer. Use this for announcements, emergency alerts, or overhead paging.
|
|
2856
|
+
*
|
|
2857
|
+
* Each paging group includes:
|
|
2858
|
+
* - `name` — Group name (e.g. "All Employees")
|
|
2859
|
+
* - `extension_code` — The dial code that triggers paging (e.g. `*801`)
|
|
2860
|
+
* - `members` — List of member extensions that receive the broadcast
|
|
2861
|
+
*
|
|
2862
|
+
* **CRM UI guidance:** Show as a table with columns: Name, Code, Members (count),
|
|
2863
|
+
* Status, and Actions (Broadcast, Edit, Delete). The Broadcast button opens a
|
|
2864
|
+
* modal to select a caller extension and sends the page. Show "No paging groups
|
|
2865
|
+
* configured" empty state with a Create button if the list is empty.
|
|
2866
|
+
*
|
|
2867
|
+
* @returns An object containing:
|
|
2868
|
+
* - `pagingGroups: PagingGroup[]` — Array of paging group objects with members
|
|
2869
|
+
*
|
|
2870
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
2871
|
+
*
|
|
2872
|
+
* @example
|
|
2873
|
+
* ```typescript
|
|
2874
|
+
* const { pagingGroups } = await voip.listPagingGroups();
|
|
2875
|
+
*
|
|
2876
|
+
* pagingGroups.forEach(pg => {
|
|
2877
|
+
* console.log(`${pg.name} (${pg.extension_code}) — ${pg.members.length} members`);
|
|
2878
|
+
* });
|
|
2879
|
+
*
|
|
2880
|
+
* // CRM UI: show broadcast button
|
|
2881
|
+
* pagingGroups.forEach(pg => {
|
|
2882
|
+
* renderPagingRow(pg.name, pg.extension_code, pg.members.length);
|
|
2883
|
+
* renderBroadcastButton(pg.id, pg.name);
|
|
2884
|
+
* });
|
|
2885
|
+
* ```
|
|
2886
|
+
*
|
|
2887
|
+
* @see GET /api/paging-groups
|
|
2888
|
+
*/
|
|
787
2889
|
listPagingGroups(): Promise<{
|
|
788
2890
|
pagingGroups: PagingGroup[];
|
|
789
2891
|
}>;
|
|
@@ -791,7 +2893,76 @@ export declare class VoIPClient {
|
|
|
791
2893
|
getPagingGroup(id: string): Promise<{
|
|
792
2894
|
pagingGroup: PagingGroup;
|
|
793
2895
|
}>;
|
|
794
|
-
/**
|
|
2896
|
+
/**
|
|
2897
|
+
* Create a new paging group for one-way audio broadcast.
|
|
2898
|
+
*
|
|
2899
|
+
* @description
|
|
2900
|
+
* Sets up a group of extensions that receive auto-answer audio broadcasts when
|
|
2901
|
+
* someone dials the `extensionCode` (e.g. `*801`). All online members hear the
|
|
2902
|
+
* caller through their speaker — useful for announcements, emergency alerts,
|
|
2903
|
+
* and overhead paging.
|
|
2904
|
+
*
|
|
2905
|
+
* **How it works:**
|
|
2906
|
+
* 1. Admin creates a paging group with a dial code and member extensions
|
|
2907
|
+
* 2. A user dials the code (e.g. `*801`) from their phone
|
|
2908
|
+
* 3. All online members auto-answer — the caller's audio is broadcast
|
|
2909
|
+
* 4. When the caller hangs up, all member calls end
|
|
2910
|
+
*
|
|
2911
|
+
* **Superadmin mode:** Pass `{ tenantId: 'uuid' }` when using a global superadmin
|
|
2912
|
+
* key to create a paging group for a specific tenant.
|
|
2913
|
+
*
|
|
2914
|
+
* **CRM UI guidance:** Show a form with: Name (required, e.g. "All Employees"),
|
|
2915
|
+
* Extension Code (required, e.g. `*801` — must be unique per tenant),
|
|
2916
|
+
* Description (optional), and Members (multi-select from extension list).
|
|
2917
|
+
* Show a preview of the dial code with "Users will dial *801 to page" hint.
|
|
2918
|
+
*
|
|
2919
|
+
* @param params — Paging group creation parameters
|
|
2920
|
+
* @param params.name — Group name. Must be unique within the tenant.
|
|
2921
|
+
* Example: `'All Employees'`
|
|
2922
|
+
* @param params.description — Optional description. Example: `'Broadcast to all office phones'`
|
|
2923
|
+
* @param params.extensionCode — Dial code that triggers the page. Must start with `*`.
|
|
2924
|
+
* Example: `'*801'`. Must be unique within the tenant
|
|
2925
|
+
* @param params.members — Array of member extensions to page.
|
|
2926
|
+
* Each has: `extension` (string, the extension number).
|
|
2927
|
+
* Example: `[{ extension: '1001' }, { extension: '1002' }]`
|
|
2928
|
+
* @param options — Optional request overrides
|
|
2929
|
+
* @param options.tenantId — Target tenant UUID (required for superadmin keys only)
|
|
2930
|
+
*
|
|
2931
|
+
* @returns An object containing:
|
|
2932
|
+
* - `pagingGroup: PagingGroup` — The created paging group with members
|
|
2933
|
+
*
|
|
2934
|
+
* @throws {HttpError} 400 — Duplicate name or extension code, invalid code format, or extension doesn't exist
|
|
2935
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
2936
|
+
*
|
|
2937
|
+
* @example
|
|
2938
|
+
* ```typescript
|
|
2939
|
+
* // Create an All Employees paging group
|
|
2940
|
+
* const { pagingGroup } = await voip.createPagingGroup({
|
|
2941
|
+
* name: 'All Employees',
|
|
2942
|
+
* description: 'Emergency broadcast to all employees',
|
|
2943
|
+
* extensionCode: '*801',
|
|
2944
|
+
* members: [
|
|
2945
|
+
* { extension: '1001' },
|
|
2946
|
+
* { extension: '1002' },
|
|
2947
|
+
* { extension: '1003' },
|
|
2948
|
+
* ],
|
|
2949
|
+
* });
|
|
2950
|
+
*
|
|
2951
|
+
* console.log(`Paging group created: dial ${pagingGroup.extension_code} to page ${pagingGroup.members.length} members`);
|
|
2952
|
+
*
|
|
2953
|
+
* // Create a department paging group
|
|
2954
|
+
* const { pagingGroup } = await voip.createPagingGroup({
|
|
2955
|
+
* name: 'Support Floor',
|
|
2956
|
+
* extensionCode: '*802',
|
|
2957
|
+
* members: [
|
|
2958
|
+
* { extension: '2001' },
|
|
2959
|
+
* { extension: '2002' },
|
|
2960
|
+
* ],
|
|
2961
|
+
* });
|
|
2962
|
+
* ```
|
|
2963
|
+
*
|
|
2964
|
+
* @see POST /api/paging-groups
|
|
2965
|
+
*/
|
|
795
2966
|
createPagingGroup(params: CreatePagingGroupParams, options?: RequestOptions): Promise<{
|
|
796
2967
|
pagingGroup: PagingGroup;
|
|
797
2968
|
}>;
|
|
@@ -803,9 +2974,126 @@ export declare class VoIPClient {
|
|
|
803
2974
|
deletePagingGroup(id: string): Promise<{
|
|
804
2975
|
message: string;
|
|
805
2976
|
}>;
|
|
806
|
-
/**
|
|
2977
|
+
/**
|
|
2978
|
+
* Broadcast a paging announcement to all online members of a paging group via the API.
|
|
2979
|
+
*
|
|
2980
|
+
* @description
|
|
2981
|
+
* Triggers a one-way audio broadcast to all currently online members of the specified
|
|
2982
|
+
* paging group. The caller extension initiates the broadcast — all online members
|
|
2983
|
+
* auto-answer and hear the caller through their speaker.
|
|
2984
|
+
*
|
|
2985
|
+
* This is the API-based alternative to dialing the paging code (`*801`) from a phone.
|
|
2986
|
+
* Use this for programmatic paging — e.g. a "Broadcast" button in the admin dashboard,
|
|
2987
|
+
* automated emergency alerts, or scheduled announcements.
|
|
2988
|
+
*
|
|
2989
|
+
* **How it works:**
|
|
2990
|
+
* 1. The API creates a conference room with the caller as moderator
|
|
2991
|
+
* 2. All online members are auto-called into the conference (listen-only, muted)
|
|
2992
|
+
* 3. The caller speaks — all members hear through their speaker
|
|
2993
|
+
* 4. When the caller hangs up, the conference auto-destroys
|
|
2994
|
+
*
|
|
2995
|
+
* **CRM UI guidance:** Show a "Broadcast" button next to each paging group.
|
|
2996
|
+
* On click, open a modal: select the caller extension (dropdown of online extensions),
|
|
2997
|
+
* and a "Start Broadcast" button. During broadcast, show a "Broadcasting..." indicator
|
|
2998
|
+
* with the number of members reached vs offline. After completion, show results
|
|
2999
|
+
* (membersOnline, membersContacted, status per extension).
|
|
3000
|
+
*
|
|
3001
|
+
* @param id — The UUID of the paging group (from `listPagingGroups()` → `pagingGroups[].id`)
|
|
3002
|
+
* @param callerExtension — The extension number initiating the broadcast.
|
|
3003
|
+
* This extension will be the speaker. Example: `'1001'`
|
|
3004
|
+
*
|
|
3005
|
+
* @returns An object containing:
|
|
3006
|
+
* - `jobId: string` — Unique broadcast job ID for tracking
|
|
3007
|
+
* - `groupName: string` — The paging group name
|
|
3008
|
+
* - `conferenceName: string` — The conference room name created for this broadcast
|
|
3009
|
+
* - `membersContacted: number` — Number of members successfully reached
|
|
3010
|
+
* - `membersOnline: number` — Number of members that were online at broadcast time
|
|
3011
|
+
* - `membersTotal: number` — Total number of members in the group
|
|
3012
|
+
* - `results: Array<{ extension: string; status: 'online' | 'offline' }>` — Per-member status
|
|
3013
|
+
*
|
|
3014
|
+
* @throws {HttpError} 400 — Paging group not found, caller extension doesn't exist, or no members online
|
|
3015
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
3016
|
+
* @throws {HttpError} 500 — FreeSWITCH conference creation failed
|
|
3017
|
+
*
|
|
3018
|
+
* @example
|
|
3019
|
+
* ```typescript
|
|
3020
|
+
* // Broadcast an emergency alert to all employees
|
|
3021
|
+
* const result = await voip.broadcastPaging('paging-group-uuid', '1001');
|
|
3022
|
+
*
|
|
3023
|
+
* console.log(`Broadcasting to ${result.groupName}`);
|
|
3024
|
+
* console.log(`Reached ${result.membersContacted}/${result.membersTotal} members`);
|
|
3025
|
+
* console.log(`Online: ${result.membersOnline}, Offline: ${result.membersTotal - result.membersOnline}`);
|
|
3026
|
+
*
|
|
3027
|
+
* result.results.forEach(r => {
|
|
3028
|
+
* console.log(` ${r.extension}: ${r.status}`);
|
|
3029
|
+
* });
|
|
3030
|
+
*
|
|
3031
|
+
* // CRM UI: broadcast button with results modal
|
|
3032
|
+
* async function handleBroadcast(pagingGroupId: string, callerExt: string) {
|
|
3033
|
+
* showBroadcastingModal(callerExt);
|
|
3034
|
+
* try {
|
|
3035
|
+
* const result = await voip.broadcastPaging(pagingGroupId, callerExt);
|
|
3036
|
+
* showBroadcastResultsModal({
|
|
3037
|
+
* reached: result.membersContacted,
|
|
3038
|
+
* online: result.membersOnline,
|
|
3039
|
+
* total: result.membersTotal,
|
|
3040
|
+
* results: result.results,
|
|
3041
|
+
* });
|
|
3042
|
+
* } catch (err) {
|
|
3043
|
+
* if (err instanceof HttpError && err.status === 400) {
|
|
3044
|
+
* showError('No members are online to receive the broadcast');
|
|
3045
|
+
* }
|
|
3046
|
+
* }
|
|
3047
|
+
* }
|
|
3048
|
+
* ```
|
|
3049
|
+
*
|
|
3050
|
+
* @see POST /api/paging-groups/{id}/broadcast
|
|
3051
|
+
*/
|
|
807
3052
|
broadcastPaging(id: string, callerExtension: string): Promise<BroadcastResult>;
|
|
808
|
-
/**
|
|
3053
|
+
/**
|
|
3054
|
+
* List all call queues for the current tenant.
|
|
3055
|
+
*
|
|
3056
|
+
* @description
|
|
3057
|
+
* Retrieves every call queue configured for the tenant. Each queue includes:
|
|
3058
|
+
* - Queue name, strategy (round-robin, ring-all, longest-idle, etc.)
|
|
3059
|
+
* - Music on hold, announcements, timeout settings
|
|
3060
|
+
* - Agent list with priorities and penalties
|
|
3061
|
+
* - Fallback actions when no agents are available or the caller times out
|
|
3062
|
+
*
|
|
3063
|
+
* Call queues are used for call center / ACD (Automatic Call Distribution) scenarios
|
|
3064
|
+
* where multiple agents handle incoming calls.
|
|
3065
|
+
*
|
|
3066
|
+
* **CRM UI guidance:** Show as a table with columns: Queue Name, Strategy,
|
|
3067
|
+
* Agents (count), Active Status, and Actions (Edit, Delete, Stats). Show a
|
|
3068
|
+
* "Create Queue" button. Each row should link to a detail view showing agents,
|
|
3069
|
+
* real-time stats, and queue configuration.
|
|
3070
|
+
*
|
|
3071
|
+
* @returns An object containing:
|
|
3072
|
+
* - `queues: CallQueue[]` — Array of call queue objects with agents
|
|
3073
|
+
*
|
|
3074
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
3075
|
+
*
|
|
3076
|
+
* @example
|
|
3077
|
+
* ```typescript
|
|
3078
|
+
* const { queues } = await voip.listQueues();
|
|
3079
|
+
*
|
|
3080
|
+
* queues.forEach(q => {
|
|
3081
|
+
* console.log(`${q.name} — ${q.strategy} — ${q.agents.length} agents`);
|
|
3082
|
+
* });
|
|
3083
|
+
*
|
|
3084
|
+
* // CRM UI: queue overview with agent counts
|
|
3085
|
+
* queues.forEach(q => {
|
|
3086
|
+
* const agentStatus = {
|
|
3087
|
+
* available: q.agents.filter(a => a.status === 'available').length,
|
|
3088
|
+
* busy: q.agents.filter(a => a.status === 'busy').length,
|
|
3089
|
+
* paused: q.agents.filter(a => a.status === 'paused').length,
|
|
3090
|
+
* };
|
|
3091
|
+
* renderQueueRow(q.name, q.strategy, agentStatus);
|
|
3092
|
+
* });
|
|
3093
|
+
* ```
|
|
3094
|
+
*
|
|
3095
|
+
* @see GET /api/queues
|
|
3096
|
+
*/
|
|
809
3097
|
listQueues(): Promise<{
|
|
810
3098
|
queues: CallQueue[];
|
|
811
3099
|
}>;
|
|
@@ -837,7 +3125,50 @@ export declare class VoIPClient {
|
|
|
837
3125
|
getQueueStats(queueId: string): Promise<{
|
|
838
3126
|
stats: QueueStats;
|
|
839
3127
|
}>;
|
|
840
|
-
/**
|
|
3128
|
+
/**
|
|
3129
|
+
* List all webhook subscriptions for the current tenant, along with supported event types.
|
|
3130
|
+
*
|
|
3131
|
+
* @description
|
|
3132
|
+
* Returns every webhook endpoint the tenant has configured, plus the list of all
|
|
3133
|
+
* supported event types that can be subscribed to. Each webhook includes:
|
|
3134
|
+
* - Name, URL, subscribed events, active status
|
|
3135
|
+
* - Whether a signing secret is configured (`hasSecret`)
|
|
3136
|
+
* - Custom headers, max retries, retry delay
|
|
3137
|
+
*
|
|
3138
|
+
* Webhooks are HTTP callbacks that fire when specific events occur in the phone system
|
|
3139
|
+
* (call started, call ended, voicemail received, etc.). Use them to integrate the
|
|
3140
|
+
* VoIP platform with external CRMs, ticketing systems, or analytics tools.
|
|
3141
|
+
*
|
|
3142
|
+
* **CRM UI guidance:** Show as a table with columns: Name, URL, Events (badge list),
|
|
3143
|
+
* Status (active/inactive), and Actions (Edit, Delete, Test). Show the supported
|
|
3144
|
+
* events list in a sidebar or tooltip for reference when creating/editing webhooks.
|
|
3145
|
+
*
|
|
3146
|
+
* @returns An object containing:
|
|
3147
|
+
* - `webhooks: Webhook[]` — Array of webhook subscription objects
|
|
3148
|
+
* - `supportedEvents: string[]` — All event types available for subscription.
|
|
3149
|
+
* Examples: `'call.start'`, `'call.end'`, `'call.answer'`, `'voicemail.received'`,
|
|
3150
|
+
* `'recording.ready'`, `'presence.change'`, `'queue.agent.login'`
|
|
3151
|
+
*
|
|
3152
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
3153
|
+
*
|
|
3154
|
+
* @example
|
|
3155
|
+
* ```typescript
|
|
3156
|
+
* const { webhooks, supportedEvents } = await voip.listWebhooks();
|
|
3157
|
+
*
|
|
3158
|
+
* console.log(`Available events: ${supportedEvents.join(', ')}`);
|
|
3159
|
+
*
|
|
3160
|
+
* webhooks.forEach(wh => {
|
|
3161
|
+
* console.log(`${wh.name} → ${wh.url} [${wh.events.join(', ')}] — ${wh.active ? 'Active' : 'Inactive'}`);
|
|
3162
|
+
* });
|
|
3163
|
+
*
|
|
3164
|
+
* // CRM UI: populate event checkboxes when creating a webhook
|
|
3165
|
+
* supportedEvents.forEach(event => {
|
|
3166
|
+
* renderEventCheckbox(event, `${event}.description`);
|
|
3167
|
+
* });
|
|
3168
|
+
* ```
|
|
3169
|
+
*
|
|
3170
|
+
* @see GET /api/webhooks
|
|
3171
|
+
*/
|
|
841
3172
|
listWebhooks(): Promise<{
|
|
842
3173
|
webhooks: Webhook[];
|
|
843
3174
|
supportedEvents: string[];
|
|
@@ -847,7 +3178,76 @@ export declare class VoIPClient {
|
|
|
847
3178
|
webhook: Webhook;
|
|
848
3179
|
recentDeliveries: WebhookDelivery[];
|
|
849
3180
|
}>;
|
|
850
|
-
/**
|
|
3181
|
+
/**
|
|
3182
|
+
* Create a new webhook subscription.
|
|
3183
|
+
*
|
|
3184
|
+
* @description
|
|
3185
|
+
* Registers a URL to receive HTTP POST callbacks when specific events occur in the
|
|
3186
|
+
* phone system. The response includes the signing secret (`secret`) — store this
|
|
3187
|
+
* immediately because it is only returned once (during creation).
|
|
3188
|
+
*
|
|
3189
|
+
* **Signing secret:** The secret is used to verify that incoming webhook requests
|
|
3190
|
+
* actually came from the VoIP platform. The platform sends an `X-Webhook-Signature`
|
|
3191
|
+
* header with each delivery — your endpoint should compute the HMAC-SHA256 of the
|
|
3192
|
+
* request body using this secret and compare it to the header value.
|
|
3193
|
+
*
|
|
3194
|
+
* **Superadmin mode:** Pass `{ tenantId: 'uuid' }` when using a global superadmin
|
|
3195
|
+
* key to create a webhook for a specific tenant.
|
|
3196
|
+
*
|
|
3197
|
+
* **CRM UI guidance:** Show a form with fields: Name (required), URL (required),
|
|
3198
|
+
* Events (checkboxes — use `supportedEvents` from `listWebhooks()`), Secret
|
|
3199
|
+
* (optional, auto-generate button), Custom Headers (optional, key-value editor),
|
|
3200
|
+
* Max Retries, Retry Delay. After creation, show the secret prominently:
|
|
3201
|
+
* "Copy this secret now — it will not be shown again" with a Copy button.
|
|
3202
|
+
*
|
|
3203
|
+
* @param params — Webhook creation parameters
|
|
3204
|
+
* @param params.name — Human-readable name for this webhook. Example: `'CRM Call Logger'`
|
|
3205
|
+
* @param params.url — The HTTPS URL to send events to. Example: `'https://my-crm.example.com/api/webhooks/voip'`
|
|
3206
|
+
* @param params.events — Array of event types to subscribe to.
|
|
3207
|
+
* Example: `['call.start', 'call.end', 'voicemail.received', 'recording.ready']`
|
|
3208
|
+
* @param params.secret — Optional signing secret (auto-generated if omitted, recommended)
|
|
3209
|
+
* @param params.headers — Optional custom HTTP headers to include in each delivery.
|
|
3210
|
+
* Example: `{ 'X-Custom-Header': 'value' }`
|
|
3211
|
+
* @param params.maxRetries — Max delivery retry attempts (default: 5)
|
|
3212
|
+
* @param params.retryDelaySeconds — Delay between retries in seconds (default: 60)
|
|
3213
|
+
* @param options — Optional request overrides
|
|
3214
|
+
* @param options.tenantId — Target tenant UUID (required for superadmin keys only)
|
|
3215
|
+
*
|
|
3216
|
+
* @returns An object containing:
|
|
3217
|
+
* - `webhook: Webhook & { secret: string }` — The created webhook object with
|
|
3218
|
+
* the signing secret (only returned once!)
|
|
3219
|
+
*
|
|
3220
|
+
* @throws {HttpError} 400 — Invalid URL, duplicate name, or invalid event types
|
|
3221
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
3222
|
+
*
|
|
3223
|
+
* @example
|
|
3224
|
+
* ```typescript
|
|
3225
|
+
* // Create a webhook for call logging
|
|
3226
|
+
* const { webhook } = await voip.createWebhook({
|
|
3227
|
+
* name: 'CRM Call Logger',
|
|
3228
|
+
* url: 'https://my-crm.example.com/api/webhooks/voip',
|
|
3229
|
+
* events: ['call.start', 'call.end', 'call.answer'],
|
|
3230
|
+
* maxRetries: 3,
|
|
3231
|
+
* });
|
|
3232
|
+
*
|
|
3233
|
+
* console.log(`Webhook secret (save this!): ${webhook.secret}`);
|
|
3234
|
+
*
|
|
3235
|
+
* // CRM UI: handle webhook creation
|
|
3236
|
+
* async function handleCreateWebhook(formData: CreateWebhookParams) {
|
|
3237
|
+
* try {
|
|
3238
|
+
* const { webhook } = await voip.createWebhook(formData);
|
|
3239
|
+
* showSecretModal(webhook.secret); // "Copy and save this secret!"
|
|
3240
|
+
* showNotification(`Webhook "${formData.name}" created`);
|
|
3241
|
+
* } catch (err) {
|
|
3242
|
+
* if (err instanceof HttpError && err.status === 400) {
|
|
3243
|
+
* showError('Invalid URL or duplicate webhook name');
|
|
3244
|
+
* }
|
|
3245
|
+
* }
|
|
3246
|
+
* }
|
|
3247
|
+
* ```
|
|
3248
|
+
*
|
|
3249
|
+
* @see POST /api/webhooks
|
|
3250
|
+
*/
|
|
851
3251
|
createWebhook(params: CreateWebhookParams, options?: RequestOptions): Promise<{
|
|
852
3252
|
webhook: Webhook & {
|
|
853
3253
|
secret: string;
|
|
@@ -872,17 +3272,245 @@ export declare class VoIPClient {
|
|
|
872
3272
|
deliveries: WebhookDelivery[];
|
|
873
3273
|
pagination: PaginationInfo;
|
|
874
3274
|
}>;
|
|
875
|
-
/**
|
|
3275
|
+
/**
|
|
3276
|
+
* List voicemail messages with pagination and optional filters.
|
|
3277
|
+
*
|
|
3278
|
+
* @description
|
|
3279
|
+
* Retrieves a paginated list of voicemail messages for the current tenant. Each message
|
|
3280
|
+
* includes caller ID, duration, read status, urgency, and an `audio_url` for playback.
|
|
3281
|
+
* Use this to populate a voicemail inbox UI — similar to an email inbox but for voice.
|
|
3282
|
+
*
|
|
3283
|
+
* Each voicemail includes:
|
|
3284
|
+
* - `id` — UUID for read/delete operations
|
|
3285
|
+
* - `caller_id` — Phone number or extension that left the message
|
|
3286
|
+
* - `caller_name` — Name if Caller ID lookup was performed
|
|
3287
|
+
* - `duration_seconds` — Length of the message
|
|
3288
|
+
* - `is_read` — Whether the message has been heard
|
|
3289
|
+
* - `is_urgent` — Whether the caller marked it urgent (pressing * after recording)
|
|
3290
|
+
* - `audio_url` — URL to stream the audio (append `?apiKey=...` for browser playback)
|
|
3291
|
+
* - `created_at` — ISO timestamp when the message was left
|
|
3292
|
+
*
|
|
3293
|
+
* **CRM UI guidance:** Show as an inbox-style list with columns: Status (unread dot),
|
|
3294
|
+
* Caller, Duration, Date, and a Play button. Unread messages should be bold or have
|
|
3295
|
+
* a blue dot indicator. Urgent messages should have a red exclamation mark. Show
|
|
3296
|
+
* "Mark Read" button per message and a "Delete" button with confirmation.
|
|
3297
|
+
* Show pagination at the bottom.
|
|
3298
|
+
*
|
|
3299
|
+
* @param params — Optional filters and pagination
|
|
3300
|
+
* @param params.page — Page number (1-based, default 1)
|
|
3301
|
+
* @param params.limit — Records per page (default 50)
|
|
3302
|
+
* @param params.unreadOnly — Show only unread messages when `true`
|
|
3303
|
+
* @param params.extension — Filter by extension number (e.g. `'1001'`)
|
|
3304
|
+
*
|
|
3305
|
+
* @returns An object containing:
|
|
3306
|
+
* - `messages: VoicemailMessage[]` — Array of voicemail messages
|
|
3307
|
+
* - `unread: number` — Total count of unread messages (for badge)
|
|
3308
|
+
* - `pagination: PaginationInfo` — `{ page, limit, total, pages }`
|
|
3309
|
+
*
|
|
3310
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
3311
|
+
*
|
|
3312
|
+
* @example
|
|
3313
|
+
* ```typescript
|
|
3314
|
+
* // List all voicemails
|
|
3315
|
+
* const { messages, unread, pagination } = await voip.listVoicemails({ limit: 25 });
|
|
3316
|
+
*
|
|
3317
|
+
* console.log(`${unread} unread voicemails (${pagination.total} total)`);
|
|
3318
|
+
*
|
|
3319
|
+
* // Show unread messages first
|
|
3320
|
+
* const { messages } = await voip.listVoicemails({ unreadOnly: true });
|
|
3321
|
+
*
|
|
3322
|
+
* // Play a voicemail in the browser
|
|
3323
|
+
* messages.forEach(msg => {
|
|
3324
|
+
* const audioUrl = voip.buildUrl(msg.audio_url);
|
|
3325
|
+
* // Render: <audio src={audioUrl} preload="none" controls />
|
|
3326
|
+
* });
|
|
3327
|
+
*
|
|
3328
|
+
* // CRM UI: voicemail inbox with unread badge
|
|
3329
|
+
* const { messages, unread } = await voip.listVoicemails({ page: 1, limit: 50 });
|
|
3330
|
+
* updateUnreadBadge(unread); // Show "3" badge on Voicemail tab
|
|
3331
|
+
* renderVoicemailInbox(messages);
|
|
3332
|
+
* ```
|
|
3333
|
+
*
|
|
3334
|
+
* @see GET /api/voicemail
|
|
3335
|
+
*/
|
|
876
3336
|
listVoicemails(params?: VoicemailListParams): Promise<VoicemailListResponse>;
|
|
877
|
-
/**
|
|
3337
|
+
/**
|
|
3338
|
+
* Get voicemail count (total and unread) for an extension.
|
|
3339
|
+
*
|
|
3340
|
+
* @description
|
|
3341
|
+
* Returns the total number of voicemail messages and the unread count for the specified
|
|
3342
|
+
* extension. This is ideal for displaying a badge counter on the voicemail tab or
|
|
3343
|
+
* menu item — poll this periodically to keep the badge up to date.
|
|
3344
|
+
*
|
|
3345
|
+
* If no extension is specified, returns counts for the authenticated user's extension
|
|
3346
|
+
* (when using JWT/extension-level auth). When using an API key with admin scope,
|
|
3347
|
+
* you must specify the extension.
|
|
3348
|
+
*
|
|
3349
|
+
* **CRM UI guidance:** Call this every 15-30 seconds via `setInterval` to keep the
|
|
3350
|
+
* unread badge updated. Show the count as a red circle badge: "Voicemail (3⦁)".
|
|
3351
|
+
* When counts are zero, hide the badge or show a gray "(0)" indicator.
|
|
3352
|
+
*
|
|
3353
|
+
* @param extension — Optional extension number (e.g. `'1001'`). If not specified,
|
|
3354
|
+
* uses the authenticated user's extension
|
|
3355
|
+
*
|
|
3356
|
+
* @returns An object containing:
|
|
3357
|
+
* - `total: number` — Total number of voicemail messages
|
|
3358
|
+
* - `unread: number` — Number of unread messages (for badge display)
|
|
3359
|
+
* - `extension: string` — The extension these counts belong to
|
|
3360
|
+
*
|
|
3361
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
3362
|
+
*
|
|
3363
|
+
* @example
|
|
3364
|
+
* ```typescript
|
|
3365
|
+
* // Check unread count for extension 1001
|
|
3366
|
+
* const { total, unread } = await voip.getVoicemailCount('1001');
|
|
3367
|
+
* console.log(`${unread} unread / ${total} total voicemails for 1001`);
|
|
3368
|
+
*
|
|
3369
|
+
* // CRM UI: update badge every 30 seconds
|
|
3370
|
+
* setInterval(async () => {
|
|
3371
|
+
* const { unread } = await voip.getVoicemailCount('1001');
|
|
3372
|
+
* updateVoicemailBadge(unread > 0 ? unread : null);
|
|
3373
|
+
* }, 30000);
|
|
3374
|
+
* ```
|
|
3375
|
+
*
|
|
3376
|
+
* @see GET /api/voicemail/count
|
|
3377
|
+
*/
|
|
878
3378
|
getVoicemailCount(extension?: string): Promise<VoicemailCountResponse>;
|
|
879
|
-
/**
|
|
3379
|
+
/**
|
|
3380
|
+
* Get a single voicemail message by its UUID.
|
|
3381
|
+
*
|
|
3382
|
+
* @description
|
|
3383
|
+
* Returns full details for one voicemail message including caller ID, duration,
|
|
3384
|
+
* read status, urgency, audio URL for playback, and any linked call record.
|
|
3385
|
+
* Use this to show a detailed voicemail view with playback controls and metadata.
|
|
3386
|
+
*
|
|
3387
|
+
* @param id — The UUID of the voicemail message (from `listVoicemails()` → `messages[].id`)
|
|
3388
|
+
*
|
|
3389
|
+
* @returns A `VoicemailMessage` object containing:
|
|
3390
|
+
* - `id` — UUID for read/delete operations
|
|
3391
|
+
* - `caller_id` — Phone number or extension that left the message
|
|
3392
|
+
* - `caller_name` — Name if Caller ID lookup was performed
|
|
3393
|
+
* - `duration_seconds` — Length of the message in seconds
|
|
3394
|
+
* - `is_read` — Whether the message has been heard
|
|
3395
|
+
* - `is_urgent` — Whether the caller marked it urgent
|
|
3396
|
+
* - `audio_url` — URL to stream the audio (use `buildUrl()` for browser playback)
|
|
3397
|
+
* - `call` — Linked call record (null if no matching call found)
|
|
3398
|
+
* - `created_at` — ISO timestamp when the message was left
|
|
3399
|
+
*
|
|
3400
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
3401
|
+
* @throws {HttpError} 404 — Voicemail not found
|
|
3402
|
+
*
|
|
3403
|
+
* @example
|
|
3404
|
+
* ```typescript
|
|
3405
|
+
* const msg = await voip.getVoicemail('voicemail-uuid-here');
|
|
3406
|
+
*
|
|
3407
|
+
* console.log(`From: ${msg.caller_id}`);
|
|
3408
|
+
* console.log(`Name: ${msg.caller_name || 'Unknown'}`);
|
|
3409
|
+
* console.log(`Duration: ${msg.duration_seconds}s`);
|
|
3410
|
+
* console.log(`Urgent: ${msg.is_urgent ? 'Yes' : 'No'}`);
|
|
3411
|
+
*
|
|
3412
|
+
* // Play the audio
|
|
3413
|
+
* const audioUrl = voip.buildUrl(msg.audio_url);
|
|
3414
|
+
* audioElement.src = audioUrl;
|
|
3415
|
+
* audioElement.play();
|
|
3416
|
+
* ```
|
|
3417
|
+
*
|
|
3418
|
+
* @see GET /api/voicemail/{id}
|
|
3419
|
+
*/
|
|
880
3420
|
getVoicemail(id: string): Promise<VoicemailMessage>;
|
|
881
|
-
/**
|
|
3421
|
+
/**
|
|
3422
|
+
* Mark a voicemail message as read (heard).
|
|
3423
|
+
*
|
|
3424
|
+
* @description
|
|
3425
|
+
* Sets the `is_read` flag to `true` and records the `read_at` timestamp for the
|
|
3426
|
+
* specified voicemail message. This also decrements the unread count for the
|
|
3427
|
+
* associated extension.
|
|
3428
|
+
*
|
|
3429
|
+
* **CRM UI guidance:** Call this automatically when the user clicks "Play" on a
|
|
3430
|
+
* voicemail — don't wait for them to finish listening. Also show a "Mark as Read"
|
|
3431
|
+
* button for messages the user has already heard but wants to acknowledge.
|
|
3432
|
+
* After marking, update the row to remove the bold/unread indicator and update
|
|
3433
|
+
* the unread badge count.
|
|
3434
|
+
*
|
|
3435
|
+
* @param id — The UUID of the voicemail message (from `listVoicemails()` → `messages[].id`)
|
|
3436
|
+
*
|
|
3437
|
+
* @returns An object containing:
|
|
3438
|
+
* - `message: string` — Confirmation message (e.g. `'Voicemail marked as read'`)
|
|
3439
|
+
*
|
|
3440
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
3441
|
+
* @throws {HttpError} 404 — Voicemail not found
|
|
3442
|
+
*
|
|
3443
|
+
* @example
|
|
3444
|
+
* ```typescript
|
|
3445
|
+
* // Mark as read when user clicks Play
|
|
3446
|
+
* async function handlePlayVoicemail(msg: VoicemailMessage) {
|
|
3447
|
+
* const audioUrl = voip.buildUrl(msg.audio_url);
|
|
3448
|
+
* audioElement.src = audioUrl;
|
|
3449
|
+
* audioElement.play();
|
|
3450
|
+
*
|
|
3451
|
+
* // Mark as read immediately on play
|
|
3452
|
+
* if (!msg.is_read) {
|
|
3453
|
+
* await voip.markVoicemailRead(msg.id);
|
|
3454
|
+
* markRowAsRead(msg.id); // Remove bold/unread dot
|
|
3455
|
+
* decrementUnreadBadge();
|
|
3456
|
+
* }
|
|
3457
|
+
* }
|
|
3458
|
+
*
|
|
3459
|
+
* // CRM UI: "Mark Read" button
|
|
3460
|
+
* async function handleMarkRead(voicemailId: string) {
|
|
3461
|
+
* await voip.markVoicemailRead(voicemailId);
|
|
3462
|
+
* markRowAsRead(voicemailId);
|
|
3463
|
+
* showNotification('Marked as read');
|
|
3464
|
+
* }
|
|
3465
|
+
* ```
|
|
3466
|
+
*
|
|
3467
|
+
* @see PUT /api/voicemail/{id}/read
|
|
3468
|
+
*/
|
|
882
3469
|
markVoicemailRead(id: string): Promise<{
|
|
883
3470
|
message: string;
|
|
884
3471
|
}>;
|
|
885
|
-
/**
|
|
3472
|
+
/**
|
|
3473
|
+
* Permanently delete a voicemail message (hard delete — removes audio file from disk).
|
|
3474
|
+
*
|
|
3475
|
+
* @description
|
|
3476
|
+
* Deletes the voicemail record from the database AND removes the audio file from
|
|
3477
|
+
* disk storage. This is irreversible. The voicemail will disappear from the inbox
|
|
3478
|
+
* and cannot be recovered.
|
|
3479
|
+
*
|
|
3480
|
+
* **CRM UI guidance:** Show a confirmation dialog before deleting: "Permanently
|
|
3481
|
+
* delete this voicemail from [caller]? This cannot be undone." After deletion,
|
|
3482
|
+
* remove the row from the list immediately to avoid showing a stale entry.
|
|
3483
|
+
*
|
|
3484
|
+
* @param id — The UUID of the voicemail message (from `listVoicemails()` → `messages[].id`)
|
|
3485
|
+
*
|
|
3486
|
+
* @returns An object containing:
|
|
3487
|
+
* - `message: string` — Confirmation message (e.g. `'Voicemail deleted'`)
|
|
3488
|
+
*
|
|
3489
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
3490
|
+
* @throws {HttpError} 404 — Voicemail not found
|
|
3491
|
+
*
|
|
3492
|
+
* @example
|
|
3493
|
+
* ```typescript
|
|
3494
|
+
* // Delete a single voicemail
|
|
3495
|
+
* await voip.deleteVoicemail('voicemail-uuid-here');
|
|
3496
|
+
*
|
|
3497
|
+
* // CRM UI: Delete button with confirmation
|
|
3498
|
+
* async function handleDeleteVoicemail(msg: VoicemailMessage) {
|
|
3499
|
+
* if (!confirm(`Delete voicemail from ${msg.caller_id}? This cannot be undone.`)) {
|
|
3500
|
+
* return;
|
|
3501
|
+
* }
|
|
3502
|
+
* try {
|
|
3503
|
+
* await voip.deleteVoicemail(msg.id);
|
|
3504
|
+
* removeVoicemailRow(msg.id); // Remove from list immediately
|
|
3505
|
+
* showNotification('Voicemail deleted');
|
|
3506
|
+
* } catch (err) {
|
|
3507
|
+
* showError('Failed to delete voicemail');
|
|
3508
|
+
* }
|
|
3509
|
+
* }
|
|
3510
|
+
* ```
|
|
3511
|
+
*
|
|
3512
|
+
* @see DELETE /api/voicemail/{id}
|
|
3513
|
+
*/
|
|
886
3514
|
deleteVoicemail(id: string): Promise<{
|
|
887
3515
|
message: string;
|
|
888
3516
|
}>;
|
|
@@ -945,40 +3573,367 @@ export declare class VoIPClient {
|
|
|
945
3573
|
message: string;
|
|
946
3574
|
}>;
|
|
947
3575
|
/**
|
|
948
|
-
* Get presence for all extensions.
|
|
949
|
-
*
|
|
3576
|
+
* Get presence status for all extensions in the tenant.
|
|
3577
|
+
*
|
|
3578
|
+
* @description
|
|
3579
|
+
* Retrieves real-time presence information for every extension. Two modes:
|
|
3580
|
+
*
|
|
3581
|
+
* - **Simple** (default): Returns a map of extension number → status string.
|
|
3582
|
+
* Status values: `'available'`, `'on_call'`, `'offline'`, `'do_not_disturb'`
|
|
3583
|
+
* - **Detailed** (`{ detailed: true }`): Returns rich objects with call info when
|
|
3584
|
+
* the extension is on a call, including `callUuid`, `caller`, `direction`, `since`.
|
|
3585
|
+
*
|
|
3586
|
+
* Use simple mode for status indicators (colored dots) and detailed mode for
|
|
3587
|
+
* call-aware features like "Busy — on call with +1555..." tooltips.
|
|
3588
|
+
*
|
|
3589
|
+
* **CRM UI guidance (simple):** Show presence as a colored dot next to each
|
|
3590
|
+
* extension name. Green = available, orange = on call, gray = offline,
|
|
3591
|
+
* red = do not disturb. Poll every 5-10 seconds.
|
|
3592
|
+
*
|
|
3593
|
+
* **CRM UI guidance (detailed):** Hover over the colored dot to show a tooltip:
|
|
3594
|
+
* "On call with +17865551234 (3:42)". Use this for the extension detail panel
|
|
3595
|
+
* or operator console.
|
|
3596
|
+
*
|
|
3597
|
+
* @param options — Optional mode selector
|
|
3598
|
+
* @param options.detailed — When `true`, returns detailed presence with call info.
|
|
3599
|
+
* Each value is a `PresenceDetail` object instead of a string
|
|
3600
|
+
*
|
|
3601
|
+
* @returns An object containing:
|
|
3602
|
+
* - `presence: PresenceMap | PresenceDetailMap` — Map of `extension_number → status_string`
|
|
3603
|
+
* (simple) or `extension_number → PresenceDetail` (detailed)
|
|
3604
|
+
*
|
|
3605
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
950
3606
|
*
|
|
951
3607
|
* @example
|
|
952
|
-
*
|
|
3608
|
+
* ```typescript
|
|
3609
|
+
* // Simple presence — extension list with colored dots
|
|
953
3610
|
* const { presence } = await voip.getPresence();
|
|
954
|
-
* // { '1001': 'available', '1002': 'on_call' }
|
|
3611
|
+
* // { '1001': 'available', '1002': 'on_call', '1003': 'offline' }
|
|
3612
|
+
*
|
|
3613
|
+
* Object.entries(presence).forEach(([ext, status]) => {
|
|
3614
|
+
* const color = { available: 'green', on_call: 'orange', offline: 'gray', do_not_disturb: 'red' }[status];
|
|
3615
|
+
* console.log(`${ext}: ${color} ${status}`);
|
|
3616
|
+
* });
|
|
955
3617
|
*
|
|
956
3618
|
* // Detailed presence with call info
|
|
957
3619
|
* const { presence } = await voip.getPresence({ detailed: true });
|
|
958
|
-
* // { '
|
|
3620
|
+
* // { '1002': { status: 'on_call', callUuid: '...', caller: '+1555...', direction: 'inbound' } }
|
|
3621
|
+
*
|
|
3622
|
+
* if (presence['1002'].status === 'on_call') {
|
|
3623
|
+
* console.log(`1002 is on call with ${presence['1002'].caller}`);
|
|
3624
|
+
* }
|
|
3625
|
+
* ```
|
|
3626
|
+
*
|
|
3627
|
+
* @see GET /api/presence
|
|
3628
|
+
* @see GET /api/presence/detailed
|
|
959
3629
|
*/
|
|
960
3630
|
getPresence(options?: {
|
|
961
3631
|
detailed?: boolean;
|
|
962
3632
|
}): Promise<{
|
|
963
3633
|
presence: PresenceMap | PresenceDetailMap;
|
|
964
3634
|
}>;
|
|
965
|
-
/**
|
|
3635
|
+
/**
|
|
3636
|
+
* Set manual presence status for an extension.
|
|
3637
|
+
*
|
|
3638
|
+
* @description
|
|
3639
|
+
* Overrides the automatic presence detection (which is based on SIP registration and
|
|
3640
|
+
* call activity) with a manual status. Use this when an agent wants to appear
|
|
3641
|
+
* "busy" even though they're not on a call, or "offline" when they're leaving for
|
|
3642
|
+
* the day.
|
|
3643
|
+
*
|
|
3644
|
+
* Manual presence is sticky — it remains until the extension changes it again
|
|
3645
|
+
* or until the extension's call activity forces a natural status update (e.g.
|
|
3646
|
+
* the extension goes on a call, which sets it to `'on_call'`).
|
|
3647
|
+
*
|
|
3648
|
+
* **CRM UI guidance:** Show a presence selector dropdown next to the user's name:
|
|
3649
|
+
* Available (green dot), Busy (orange dot), Offline (gray dot). Store the selected
|
|
3650
|
+
* value in local state. On change, call `setPresence()` and update the local UI.
|
|
3651
|
+
*
|
|
3652
|
+
* @param extensionId — The UUID of the extension (from `listExtensions()` → `extensions[].id`)
|
|
3653
|
+
* @param status — The desired manual presence:
|
|
3654
|
+
* - `'available'` — Available to take calls (green)
|
|
3655
|
+
* - `'busy'` — Not accepting calls (orange)
|
|
3656
|
+
* - `'offline'` — Signed out / away (gray)
|
|
3657
|
+
*
|
|
3658
|
+
* @returns An object containing:
|
|
3659
|
+
* - `ok: boolean` — `true` if the status was set
|
|
3660
|
+
* - `extension: string` — The extension number
|
|
3661
|
+
* - `status: string` — The new presence status
|
|
3662
|
+
*
|
|
3663
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
3664
|
+
* @throws {HttpError} 404 — Extension not found
|
|
3665
|
+
*
|
|
3666
|
+
* @example
|
|
3667
|
+
* ```typescript
|
|
3668
|
+
* // Set yourself as busy — don't route calls to me
|
|
3669
|
+
* await voip.setPresence('extension-uuid', 'busy');
|
|
3670
|
+
*
|
|
3671
|
+
* // Set yourself as available again
|
|
3672
|
+
* await voip.setPresence('extension-uuid', 'available');
|
|
3673
|
+
*
|
|
3674
|
+
* // Go offline at end of day
|
|
3675
|
+
* await voip.setPresence('extension-uuid', 'offline');
|
|
3676
|
+
*
|
|
3677
|
+
* // CRM UI: presence selector dropdown
|
|
3678
|
+
* async function handlePresenceChange(extId: string, newStatus: string) {
|
|
3679
|
+
* try {
|
|
3680
|
+
* await voip.setPresence(extId, newStatus);
|
|
3681
|
+
* updatePresenceIndicator(newStatus);
|
|
3682
|
+
* showNotification(`Status changed to ${newStatus}`);
|
|
3683
|
+
* } catch (err) {
|
|
3684
|
+
* showError('Failed to update presence');
|
|
3685
|
+
* }
|
|
3686
|
+
* }
|
|
3687
|
+
* ```
|
|
3688
|
+
*
|
|
3689
|
+
* @see PUT /api/extensions/{id}/presence
|
|
3690
|
+
*/
|
|
966
3691
|
setPresence(extensionId: string, status: 'busy' | 'available' | 'offline'): Promise<{
|
|
967
3692
|
ok: boolean;
|
|
968
3693
|
extension: string;
|
|
969
3694
|
status: string;
|
|
970
3695
|
}>;
|
|
971
|
-
/**
|
|
3696
|
+
/**
|
|
3697
|
+
* Get dashboard summary statistics for the current tenant.
|
|
3698
|
+
*
|
|
3699
|
+
* @description
|
|
3700
|
+
* Returns high-level KPIs for the admin dashboard landing page:
|
|
3701
|
+
* - Total extensions configured
|
|
3702
|
+
* - Total DIDs (phone numbers) assigned
|
|
3703
|
+
* - Calls today (since midnight)
|
|
3704
|
+
* - Calls this month
|
|
3705
|
+
*
|
|
3706
|
+
* **CRM UI guidance:** Display as 4 KPI cards at the top of the dashboard.
|
|
3707
|
+
* Each card shows a large number with a label below, using icons:
|
|
3708
|
+
* - 📞 Extensions (phone icon)
|
|
3709
|
+
* - 📱 DIDs (smartphone icon)
|
|
3710
|
+
* - 📊 Calls Today (chart icon)
|
|
3711
|
+
* - 📅 Calls This Month (calendar icon)
|
|
3712
|
+
* Refresh every 60 seconds or on page reload.
|
|
3713
|
+
*
|
|
3714
|
+
* @returns An object containing:
|
|
3715
|
+
* - `dashboard: object` — `{ extensions, dids, callsToday, callsThisMonth }`
|
|
3716
|
+
*
|
|
3717
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
3718
|
+
*
|
|
3719
|
+
* @example
|
|
3720
|
+
* ```typescript
|
|
3721
|
+
* const { dashboard } = await voip.getDashboardStats();
|
|
3722
|
+
*
|
|
3723
|
+
* console.log(`Extensions: ${dashboard.extensions}`);
|
|
3724
|
+
* console.log(`DIDs: ${dashboard.dids}`);
|
|
3725
|
+
* console.log(`Calls Today: ${dashboard.callsToday}`);
|
|
3726
|
+
* console.log(`Calls This Month: ${dashboard.callsThisMonth}`);
|
|
3727
|
+
*
|
|
3728
|
+
* // CRM UI: render KPI cards
|
|
3729
|
+
* function renderDashboardStats(stats: DashboardStats['dashboard']) {
|
|
3730
|
+
* showKpiCard('Extensions', stats.extensions, '📞');
|
|
3731
|
+
* showKpiCard('DIDs', stats.dids, '📱');
|
|
3732
|
+
* showKpiCard('Calls Today', stats.callsToday, '📊');
|
|
3733
|
+
* showKpiCard('Calls This Month', stats.callsThisMonth, '📅');
|
|
3734
|
+
* }
|
|
3735
|
+
* ```
|
|
3736
|
+
*
|
|
3737
|
+
* @see GET /api/reports/dashboard
|
|
3738
|
+
*/
|
|
972
3739
|
getDashboardStats(): Promise<DashboardStats>;
|
|
973
|
-
/**
|
|
3740
|
+
/**
|
|
3741
|
+
* Get call counts grouped by day for charting and trend analysis.
|
|
3742
|
+
*
|
|
3743
|
+
* @description
|
|
3744
|
+
* Returns a date → stats map showing call volume per day, broken down by direction
|
|
3745
|
+
* (inbound, outbound, internal) with total duration. Use this to render a bar chart
|
|
3746
|
+
* or line chart showing call trends over time.
|
|
3747
|
+
*
|
|
3748
|
+
* **CRM UI guidance:** Render as a bar chart with days on the X-axis and call counts
|
|
3749
|
+
* on the Y-axis. Use stacked bars for inbound/outbound/internal. Hover over a bar
|
|
3750
|
+
* to show a tooltip: "Mon: 45 calls (28 inbound, 12 outbound, 5 internal)".
|
|
3751
|
+
* Use a chart library like Chart.js, Recharts, or visx.
|
|
3752
|
+
*
|
|
3753
|
+
* @param days — Number of recent days to include. Default: 7. Example: `30` for a monthly chart
|
|
3754
|
+
*
|
|
3755
|
+
* @returns An object containing:
|
|
3756
|
+
* - `callsByDay: Record<string, DayStats>` — Map of `'YYYY-MM-DD'` → `{ total, inbound, outbound, internal, totalDuration }`
|
|
3757
|
+
*
|
|
3758
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
3759
|
+
*
|
|
3760
|
+
* @example
|
|
3761
|
+
* ```typescript
|
|
3762
|
+
* // Get last 7 days for a weekly trend chart
|
|
3763
|
+
* const { callsByDay } = await voip.getCallsByDay(7);
|
|
3764
|
+
*
|
|
3765
|
+
* // Format for charting
|
|
3766
|
+
* const chartData = Object.entries(callsByDay).map(([date, stats]) => ({
|
|
3767
|
+
* date,
|
|
3768
|
+
* inbound: stats.inbound,
|
|
3769
|
+
* outbound: stats.outbound,
|
|
3770
|
+
* internal: stats.internal,
|
|
3771
|
+
* total: stats.total,
|
|
3772
|
+
* }));
|
|
3773
|
+
*
|
|
3774
|
+
* // Last 30 days for monthly trend
|
|
3775
|
+
* const { callsByDay } = await voip.getCallsByDay(30);
|
|
3776
|
+
*
|
|
3777
|
+
* // CRM UI: chart rendering
|
|
3778
|
+
* const labels = chartData.map(d => d.date.slice(5)); // '06-01', '06-02', ...
|
|
3779
|
+
* const inboundData = chartData.map(d => d.inbound);
|
|
3780
|
+
* const outboundData = chartData.map(d => d.outbound);
|
|
3781
|
+
* renderStackedBarChart(labels, [
|
|
3782
|
+
* { label: 'Inbound', data: inboundData, color: '#4CAF50' },
|
|
3783
|
+
* { label: 'Outbound', data: outboundData, color: '#2196F3' },
|
|
3784
|
+
* ]);
|
|
3785
|
+
* ```
|
|
3786
|
+
*
|
|
3787
|
+
* @see GET /api/reports/calls-by-day
|
|
3788
|
+
*/
|
|
974
3789
|
getCallsByDay(days?: number): Promise<CallsByDayResponse>;
|
|
975
|
-
/**
|
|
3790
|
+
/**
|
|
3791
|
+
* Get top extensions ranked by call volume.
|
|
3792
|
+
*
|
|
3793
|
+
* @description
|
|
3794
|
+
* Returns the busiest extensions in the tenant, ranked by total call count and
|
|
3795
|
+
* total call duration. Use this for:
|
|
3796
|
+
* - "Top Performers" leaderboard on the dashboard
|
|
3797
|
+
* - Identifying overloaded extensions that need backup
|
|
3798
|
+
* - Call center agent performance reports
|
|
3799
|
+
*
|
|
3800
|
+
* **CRM UI guidance:** Render as a horizontal bar chart or ranked table.
|
|
3801
|
+
* Show: Rank, Extension Number, Display Name, Total Calls, Total Duration.
|
|
3802
|
+
* Use a podium icon for the top 3. Show a period selector (7d, 30d, 90d).
|
|
3803
|
+
*
|
|
3804
|
+
* @param days — Number of recent days to analyze. Default: 7. Example: `30`
|
|
3805
|
+
* @param limit — Maximum number of extensions to return. Default: 10. Example: `5`
|
|
3806
|
+
*
|
|
3807
|
+
* @returns An object containing:
|
|
3808
|
+
* - `topExtensions: TopExtension[]` — Array of `{ extension, totalCalls, totalDuration }` sorted by call count descending
|
|
3809
|
+
*
|
|
3810
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
3811
|
+
*
|
|
3812
|
+
* @example
|
|
3813
|
+
* ```typescript
|
|
3814
|
+
* // Top 10 extensions in last 7 days
|
|
3815
|
+
* const { topExtensions } = await voip.getTopExtensions(7, 10);
|
|
3816
|
+
*
|
|
3817
|
+
* topExtensions.forEach((ext, i) => {
|
|
3818
|
+
* const hours = Math.floor(ext.totalDuration / 3600);
|
|
3819
|
+
* const mins = Math.floor((ext.totalDuration % 3600) / 60);
|
|
3820
|
+
* console.log(`#${i + 1} ${ext.extension}: ${ext.totalCalls} calls (${hours}h ${mins}m)`);
|
|
3821
|
+
* });
|
|
3822
|
+
*
|
|
3823
|
+
* // CRM UI: leaderboard
|
|
3824
|
+
* function renderTopExtensions(extensions: TopExtension[]) {
|
|
3825
|
+
* extensions.forEach((ext, i) => {
|
|
3826
|
+
* const rank = i + 1;
|
|
3827
|
+
* const medal = rank <= 3 ? ['🥇', '🥈', '🥉'][i] : `#${rank}`;
|
|
3828
|
+
* const duration = formatDuration(ext.totalDuration); // "3h 24m"
|
|
3829
|
+
* renderLeaderboardRow(medal, ext.extension, ext.totalCalls, duration);
|
|
3830
|
+
* });
|
|
3831
|
+
* }
|
|
3832
|
+
* ```
|
|
3833
|
+
*
|
|
3834
|
+
* @see GET /api/reports/top-extensions
|
|
3835
|
+
*/
|
|
976
3836
|
getTopExtensions(days?: number, limit?: number): Promise<TopExtensionsResponse>;
|
|
977
|
-
/**
|
|
3837
|
+
/**
|
|
3838
|
+
* List all blocked phone numbers for the tenant.
|
|
3839
|
+
*
|
|
3840
|
+
* @description
|
|
3841
|
+
* Returns every number that has been added to the tenant's call blocklist.
|
|
3842
|
+
* Inbound calls from blocked numbers are automatically rejected with a "Number
|
|
3843
|
+
* not in service" tone or a custom message. Outbound calls to blocked numbers
|
|
3844
|
+
* are prevented.
|
|
3845
|
+
*
|
|
3846
|
+
* Each blocked number includes:
|
|
3847
|
+
* - `number` — The blocked phone number
|
|
3848
|
+
* - `reason` — Optional reason for blocking (e.g. "Spam caller")
|
|
3849
|
+
* - `direction` — `'inbound'` (block incoming), `'outbound'` (block outgoing), or `'both'`
|
|
3850
|
+
* - `created_at` — When the number was blocked
|
|
3851
|
+
*
|
|
3852
|
+
* **CRM UI guidance:** Show as a table with columns: Phone Number, Direction,
|
|
3853
|
+
* Reason, Blocked Date, and an "Unblock" button. Add a search bar to find
|
|
3854
|
+
* specific numbers. Show a "Block Number" button that opens a form with
|
|
3855
|
+
* number, direction selector, and reason field.
|
|
3856
|
+
*
|
|
3857
|
+
* @returns An object containing:
|
|
3858
|
+
* - `blocked: BlockedNumber[]` — Array of blocked number entries
|
|
3859
|
+
*
|
|
3860
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
3861
|
+
*
|
|
3862
|
+
* @example
|
|
3863
|
+
* ```typescript
|
|
3864
|
+
* const { blocked } = await voip.listBlockedNumbers();
|
|
3865
|
+
*
|
|
3866
|
+
* blocked.forEach(b => {
|
|
3867
|
+
* console.log(`${b.number} — ${b.direction || 'both'} — ${b.reason || 'No reason'}`);
|
|
3868
|
+
* });
|
|
3869
|
+
* ```
|
|
3870
|
+
*
|
|
3871
|
+
* @see GET /api/blocklist
|
|
3872
|
+
*/
|
|
978
3873
|
listBlockedNumbers(): Promise<{
|
|
979
3874
|
blocked: BlockedNumber[];
|
|
980
3875
|
}>;
|
|
981
|
-
/**
|
|
3876
|
+
/**
|
|
3877
|
+
* Block a phone number from calling or being called.
|
|
3878
|
+
*
|
|
3879
|
+
* @description
|
|
3880
|
+
* Adds a number to the tenant's call blocklist. Once blocked:
|
|
3881
|
+
* - Inbound calls from this number are automatically rejected
|
|
3882
|
+
* - Outbound calls to this number are prevented (when direction is `'outbound'` or `'both'`)
|
|
3883
|
+
*
|
|
3884
|
+
* **Superadmin mode:** Pass `{ tenantId: 'uuid' }` when using a global superadmin
|
|
3885
|
+
* key to block a number in a specific tenant.
|
|
3886
|
+
*
|
|
3887
|
+
* **CRM UI guidance:** Show a "Block Number" button on call records, voicemail
|
|
3888
|
+
* messages, and the blocklist page. On click, open a form with: Number (pre-filled
|
|
3889
|
+
* if coming from a call record), Direction (inbound/outbound/both), Reason (optional
|
|
3890
|
+
* text — "Spam", "Harassment", "Wrong number", etc.). After blocking, show a
|
|
3891
|
+
* confirmation and refresh the blocklist.
|
|
3892
|
+
*
|
|
3893
|
+
* @param params — Block parameters
|
|
3894
|
+
* @param params.number — The phone number to block (E.164 format recommended).
|
|
3895
|
+
* Example: `'+17865559999'`
|
|
3896
|
+
* @param params.reason — Optional reason for blocking. Example: `'Spam caller'`
|
|
3897
|
+
* @param params.direction — Which direction to block:
|
|
3898
|
+
* `'inbound'` — Calls FROM this number are blocked,
|
|
3899
|
+
* `'outbound'` — Calls TO this number are blocked,
|
|
3900
|
+
* `'both'` — Both directions (default)
|
|
3901
|
+
* @param options — Optional request overrides
|
|
3902
|
+
* @param options.tenantId — Target tenant UUID (required for superadmin keys only)
|
|
3903
|
+
*
|
|
3904
|
+
* @returns An object containing:
|
|
3905
|
+
* - `blocked: BlockedNumber` — The created blocklist entry with `id`, `number`, `reason`, `direction`, `created_at`
|
|
3906
|
+
*
|
|
3907
|
+
* @throws {HttpError} 400 — Number is already blocked or format is invalid
|
|
3908
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
3909
|
+
*
|
|
3910
|
+
* @example
|
|
3911
|
+
* ```typescript
|
|
3912
|
+
* // Block a spam caller
|
|
3913
|
+
* await voip.blockNumber({ number: '+17865559999', reason: 'Spam caller', direction: 'inbound' });
|
|
3914
|
+
*
|
|
3915
|
+
* // Block outgoing calls to a premium number
|
|
3916
|
+
* await voip.blockNumber({ number: '+19005551234', reason: 'Premium rate', direction: 'outbound' });
|
|
3917
|
+
*
|
|
3918
|
+
* // Block both directions
|
|
3919
|
+
* await voip.blockNumber({ number: '+17865558888', reason: 'Harassment', direction: 'both' });
|
|
3920
|
+
*
|
|
3921
|
+
* // CRM UI: Block button on call detail
|
|
3922
|
+
* async function blockCaller(call: CallRecord) {
|
|
3923
|
+
* if (!confirm(`Block ${call.caller_id_number}? They will no longer be able to call you.`)) {
|
|
3924
|
+
* return;
|
|
3925
|
+
* }
|
|
3926
|
+
* await voip.blockNumber({
|
|
3927
|
+
* number: call.caller_id_number,
|
|
3928
|
+
* reason: 'Blocked by user',
|
|
3929
|
+
* direction: 'inbound',
|
|
3930
|
+
* });
|
|
3931
|
+
* showNotification(`${call.caller_id_number} has been blocked`);
|
|
3932
|
+
* }
|
|
3933
|
+
* ```
|
|
3934
|
+
*
|
|
3935
|
+
* @see POST /api/blocklist
|
|
3936
|
+
*/
|
|
982
3937
|
blockNumber(params: CreateBlockedNumberParams, options?: RequestOptions): Promise<{
|
|
983
3938
|
blocked: BlockedNumber;
|
|
984
3939
|
}>;
|
|
@@ -986,9 +3941,103 @@ export declare class VoIPClient {
|
|
|
986
3941
|
unblockNumber(id: string): Promise<{
|
|
987
3942
|
message: string;
|
|
988
3943
|
}>;
|
|
989
|
-
/**
|
|
3944
|
+
/**
|
|
3945
|
+
* Check if a specific phone number is blocked.
|
|
3946
|
+
*
|
|
3947
|
+
* @description
|
|
3948
|
+
* Looks up a phone number in the tenant's blocklist and returns whether it's blocked
|
|
3949
|
+
* along with the blocklist entry details. Use this for:
|
|
3950
|
+
* - Incoming call screen pop — check if the caller is blocked before answering
|
|
3951
|
+
* - Warning indicator on call detail pages — show "Number is blocked" banner
|
|
3952
|
+
* - Pre-dial validation — check if a number is blocked before originating a call
|
|
3953
|
+
*
|
|
3954
|
+
* **CRM UI guidance:** Call this when a call comes in (in the WebSocket `call_start`
|
|
3955
|
+
* handler) and show a red "BLOCKED" banner if the number is on the blocklist.
|
|
3956
|
+
* Also call this before originating outbound calls — show a warning if the
|
|
3957
|
+
* destination is blocked.
|
|
3958
|
+
*
|
|
3959
|
+
* @param number — The phone number to check (E.164 format recommended).
|
|
3960
|
+
* Example: `'+17865559999'`
|
|
3961
|
+
*
|
|
3962
|
+
* @returns An object containing:
|
|
3963
|
+
* - `blocked: boolean` — `true` if the number is in the blocklist
|
|
3964
|
+
* - `entry: BlockedNumber | null` — The blocklist entry if blocked, `null` otherwise.
|
|
3965
|
+
* Contains `id`, `number`, `reason`, `direction`, `created_at`
|
|
3966
|
+
*
|
|
3967
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
3968
|
+
*
|
|
3969
|
+
* @example
|
|
3970
|
+
* ```typescript
|
|
3971
|
+
* // Check a caller before answering
|
|
3972
|
+
* const { blocked, entry } = await voip.checkBlocked('+17865559999');
|
|
3973
|
+
* if (blocked) {
|
|
3974
|
+
* console.log(`Blocked: ${entry.number} — Reason: ${entry.reason}`);
|
|
3975
|
+
* showBlockedBanner(entry);
|
|
3976
|
+
* } else {
|
|
3977
|
+
* showIncomingCallBanner('+17865559999');
|
|
3978
|
+
* }
|
|
3979
|
+
*
|
|
3980
|
+
* // Pre-dial check before originating a call
|
|
3981
|
+
* async function safeOriginate(fromExt: string, toNumber: string) {
|
|
3982
|
+
* const { blocked, entry } = await voip.checkBlocked(toNumber);
|
|
3983
|
+
* if (blocked) {
|
|
3984
|
+
* showWarning(`Cannot call ${toNumber} — it is blocked (${entry.reason || 'no reason given'})`);
|
|
3985
|
+
* return;
|
|
3986
|
+
* }
|
|
3987
|
+
* return voip.originate({ fromExtension: fromExt, toNumber });
|
|
3988
|
+
* }
|
|
3989
|
+
*
|
|
3990
|
+
* // CRM UI: screen pop on incoming call
|
|
3991
|
+
* voip.on('call_start', async (data) => {
|
|
3992
|
+
* const { blocked, entry } = await voip.checkBlocked(data.caller_id_number);
|
|
3993
|
+
* if (blocked) {
|
|
3994
|
+
* showScreenPop({ type: 'blocked', caller: data.caller_id_number, reason: entry.reason });
|
|
3995
|
+
* } else {
|
|
3996
|
+
* showScreenPop({ type: 'incoming', caller: data.caller_id_number, name: data.caller_id_name });
|
|
3997
|
+
* }
|
|
3998
|
+
* });
|
|
3999
|
+
* ```
|
|
4000
|
+
*
|
|
4001
|
+
* @see GET /api/blocklist/check/{number}
|
|
4002
|
+
*/
|
|
990
4003
|
checkBlocked(number: string): Promise<BlocklistCheckResponse>;
|
|
991
|
-
/**
|
|
4004
|
+
/**
|
|
4005
|
+
* List all business hour schedules for the tenant.
|
|
4006
|
+
*
|
|
4007
|
+
* @description
|
|
4008
|
+
* Returns every business hours schedule configured for the tenant. Each schedule
|
|
4009
|
+
* defines open hours per day of the week, holidays, and after-hours call routing.
|
|
4010
|
+
*
|
|
4011
|
+
* Each schedule includes:
|
|
4012
|
+
* - `name` — Schedule name (e.g. "Standard Business Hours")
|
|
4013
|
+
* - `timezone` — IANA timezone (e.g. `'America/Chicago'`)
|
|
4014
|
+
* - `schedules` — Array of day definitions with enabled flag and start/end times
|
|
4015
|
+
* - `holidays` — Holiday dates with names
|
|
4016
|
+
* - `after_hours_action` — What happens when the schedule is closed
|
|
4017
|
+
* (`'voicemail'`, `'ivr'`, `'extension'`, `'hangup'`, `'ai_agent'`)
|
|
4018
|
+
* - `after_hours_target` — The target for after-hours routing
|
|
4019
|
+
*
|
|
4020
|
+
* **CRM UI guidance:** Show as a table with columns: Name, Timezone, Open Days
|
|
4021
|
+
* (icons for each day), After-Hours Action, Status, Actions (Edit, Delete).
|
|
4022
|
+
* Add a "Check Status" button that calls `getScheduleStatus()`.
|
|
4023
|
+
*
|
|
4024
|
+
* @returns An object containing:
|
|
4025
|
+
* - `schedules: BusinessSchedule[]` — Array of schedule objects
|
|
4026
|
+
*
|
|
4027
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
4028
|
+
*
|
|
4029
|
+
* @example
|
|
4030
|
+
* ```typescript
|
|
4031
|
+
* const { schedules } = await voip.listSchedules();
|
|
4032
|
+
*
|
|
4033
|
+
* schedules.forEach(s => {
|
|
4034
|
+
* const openDays = s.schedules?.filter(d => d.enabled).map(d => d.day).join(', ') || 'None';
|
|
4035
|
+
* console.log(`${s.name} | ${s.timezone} | Open: ${openDays}`);
|
|
4036
|
+
* });
|
|
4037
|
+
* ```
|
|
4038
|
+
*
|
|
4039
|
+
* @see GET /api/schedules
|
|
4040
|
+
*/
|
|
992
4041
|
listSchedules(): Promise<{
|
|
993
4042
|
schedules: BusinessSchedule[];
|
|
994
4043
|
}>;
|
|
@@ -996,7 +4045,67 @@ export declare class VoIPClient {
|
|
|
996
4045
|
getSchedule(id: string): Promise<{
|
|
997
4046
|
schedule: BusinessSchedule;
|
|
998
4047
|
}>;
|
|
999
|
-
/**
|
|
4048
|
+
/**
|
|
4049
|
+
* Create a business hours schedule.
|
|
4050
|
+
*
|
|
4051
|
+
* @description
|
|
4052
|
+
* Defines when a business is open/closed for call routing purposes. Incoming calls
|
|
4053
|
+
* during open hours follow normal routing; calls outside open hours are routed
|
|
4054
|
+
* according to the `afterHoursAction` setting (voicemail, IVR, another extension,
|
|
4055
|
+
* or an external number).
|
|
4056
|
+
*
|
|
4057
|
+
* **Superadmin mode:** Pass `{ tenantId: 'uuid' }` when using a global superadmin
|
|
4058
|
+
* key to create a schedule for a specific tenant.
|
|
4059
|
+
*
|
|
4060
|
+
* **CRM UI guidance:** Show a form with: Name (required), Timezone (dropdown of
|
|
4061
|
+
* IANA timezones), Day-of-week grid (toggle + start/end time per day), Holidays
|
|
4062
|
+
* (date picker + name), After-Hours Action (select: voicemail/IVR/extension),
|
|
4063
|
+
* After-Hours Target (dynamic based on action type).
|
|
4064
|
+
*
|
|
4065
|
+
* @param params — Schedule creation parameters
|
|
4066
|
+
* @param params.name — Human-readable schedule name. Example: `'Standard Business Hours'`
|
|
4067
|
+
* @param params.timezone — IANA timezone string. Example: `'America/Chicago'`
|
|
4068
|
+
* @param params.schedules — Array of day definitions. Each has:
|
|
4069
|
+
* `day` (`'monday'`-`'sunday'`), `enabled` (boolean), `startTime` (HH:MM), `endTime` (HH:MM)
|
|
4070
|
+
* @param params.holidays — Array of holiday dates with optional names.
|
|
4071
|
+
* Example: `[{ date: '2026-12-25', name: 'Christmas' }]`
|
|
4072
|
+
* @param params.afterHoursAction — Action when closed:
|
|
4073
|
+
* `'voicemail'`, `'ivr'`, `'extension'`, `'hangup'`, `'external'`, `'ai_agent'`
|
|
4074
|
+
* @param params.afterHoursTarget — Target for after-hours routing
|
|
4075
|
+
* (extension number, IVR name, phone number, etc.)
|
|
4076
|
+
* @param options — Optional request overrides
|
|
4077
|
+
* @param options.tenantId — Target tenant UUID (required for superadmin keys only)
|
|
4078
|
+
*
|
|
4079
|
+
* @returns An object containing:
|
|
4080
|
+
* - `schedule: BusinessSchedule` — The created schedule object
|
|
4081
|
+
*
|
|
4082
|
+
* @throws {HttpError} 400 — Invalid timezone, missing required fields, or name already exists
|
|
4083
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
4084
|
+
*
|
|
4085
|
+
* @example
|
|
4086
|
+
* ```typescript
|
|
4087
|
+
* // Create standard 9-to-5 business hours
|
|
4088
|
+
* const { schedule } = await voip.createSchedule({
|
|
4089
|
+
* name: 'Standard Business Hours',
|
|
4090
|
+
* timezone: 'America/Chicago',
|
|
4091
|
+
* schedules: [
|
|
4092
|
+
* { day: 'monday', enabled: true, startTime: '09:00', endTime: '17:00' },
|
|
4093
|
+
* { day: 'tuesday', enabled: true, startTime: '09:00', endTime: '17:00' },
|
|
4094
|
+
* { day: 'wednesday', enabled: true, startTime: '09:00', endTime: '17:00' },
|
|
4095
|
+
* { day: 'thursday', enabled: true, startTime: '09:00', endTime: '17:00' },
|
|
4096
|
+
* { day: 'friday', enabled: true, startTime: '09:00', endTime: '17:00' },
|
|
4097
|
+
* ],
|
|
4098
|
+
* holidays: [
|
|
4099
|
+
* { date: '2026-12-25', name: 'Christmas Day' },
|
|
4100
|
+
* { date: '2027-01-01', name: "New Year's Day" },
|
|
4101
|
+
* ],
|
|
4102
|
+
* afterHoursAction: 'voicemail',
|
|
4103
|
+
* afterHoursTarget: '1001',
|
|
4104
|
+
* });
|
|
4105
|
+
* ```
|
|
4106
|
+
*
|
|
4107
|
+
* @see POST /api/schedules
|
|
4108
|
+
*/
|
|
1000
4109
|
createSchedule(params: CreateScheduleParams, options?: RequestOptions): Promise<{
|
|
1001
4110
|
schedule: BusinessSchedule;
|
|
1002
4111
|
}>;
|
|
@@ -1008,9 +4117,88 @@ export declare class VoIPClient {
|
|
|
1008
4117
|
deleteSchedule(id: string): Promise<{
|
|
1009
4118
|
message: string;
|
|
1010
4119
|
}>;
|
|
1011
|
-
/**
|
|
4120
|
+
/**
|
|
4121
|
+
* Check if a schedule is currently open or closed based on its timezone and current time.
|
|
4122
|
+
*
|
|
4123
|
+
* @description
|
|
4124
|
+
* Evaluates the schedule's day-of-week definitions and holiday list against the
|
|
4125
|
+
* current time in the schedule's timezone. Returns whether the business is
|
|
4126
|
+
* currently "open" (accepting calls normally) or "closed" (after-hours routing active).
|
|
4127
|
+
*
|
|
4128
|
+
* **CRM UI guidance:** Show an "Open" or "Closed" badge on the schedule detail
|
|
4129
|
+
* page and in the schedule list. Use a green badge for "Open", red for "Closed".
|
|
4130
|
+
* Poll this every 60 seconds to keep the status indicator accurate.
|
|
4131
|
+
*
|
|
4132
|
+
* @param id — The UUID of the schedule to check (from `listSchedules()` → `schedules[].id`)
|
|
4133
|
+
*
|
|
4134
|
+
* @returns An object containing:
|
|
4135
|
+
* - `isOpen: boolean` — `true` if current time is within business hours
|
|
4136
|
+
* - `schedule: string` — The schedule name
|
|
4137
|
+
* - `timezone: string` — The schedule's timezone
|
|
4138
|
+
* - `currentTime: string` — Current time in the schedule's timezone (ISO string)
|
|
4139
|
+
*
|
|
4140
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
4141
|
+
* @throws {HttpError} 404 — Schedule not found
|
|
4142
|
+
*
|
|
4143
|
+
* @example
|
|
4144
|
+
* ```typescript
|
|
4145
|
+
* const { isOpen, schedule, currentTime } = await voip.getScheduleStatus('schedule-uuid');
|
|
4146
|
+
*
|
|
4147
|
+
* if (isOpen) {
|
|
4148
|
+
* console.log(`${schedule} is OPEN — ${currentTime}`);
|
|
4149
|
+
* } else {
|
|
4150
|
+
* console.log(`${schedule} is CLOSED — calls go to after-hours routing`);
|
|
4151
|
+
* }
|
|
4152
|
+
*
|
|
4153
|
+
* // CRM UI: show open/closed badge
|
|
4154
|
+
* const { isOpen } = await voip.getScheduleStatus(scheduleId);
|
|
4155
|
+
* if (isOpen) {
|
|
4156
|
+
* showBadge('Open', 'green');
|
|
4157
|
+
* } else {
|
|
4158
|
+
* showBadge('Closed', 'red');
|
|
4159
|
+
* }
|
|
4160
|
+
* ```
|
|
4161
|
+
*
|
|
4162
|
+
* @see GET /api/schedules/{id}/status
|
|
4163
|
+
*/
|
|
1012
4164
|
getScheduleStatus(id: string): Promise<ScheduleStatusResponse>;
|
|
1013
|
-
/**
|
|
4165
|
+
/**
|
|
4166
|
+
* List all SIP trunks (passwords are masked in the response).
|
|
4167
|
+
*
|
|
4168
|
+
* @description
|
|
4169
|
+
* Returns every SIP trunk configured in the system. Trunks can be:
|
|
4170
|
+
* - **Global** (`tenant_id: null`) — Shared across all tenants for outbound calling
|
|
4171
|
+
* - **Tenant-specific** — Assigned to a single tenant
|
|
4172
|
+
*
|
|
4173
|
+
* Each trunk includes gateway configuration (proxy, username, transport, codec prefs),
|
|
4174
|
+
* registration settings, channel limits, and optional live gateway status from FreeSWITCH.
|
|
4175
|
+
*
|
|
4176
|
+
* For security, SIP passwords are ALWAYS masked in list/get responses.
|
|
4177
|
+
* Only `createTrunk()` returns the raw password (and only if you're a superadmin).
|
|
4178
|
+
*
|
|
4179
|
+
* **CRM UI guidance (superadmin only):** Show as a table with columns: Name,
|
|
4180
|
+
* Provider, Gateway, Status (active/inactive), Tenant (or "Global"), and Actions
|
|
4181
|
+
* (Edit, Sync, Delete). Show a live gateway status indicator (UP/DOWN) if available.
|
|
4182
|
+
*
|
|
4183
|
+
* @returns An object containing:
|
|
4184
|
+
* - `trunks: SipTrunk[]` — Array of SIP trunk objects (passwords masked)
|
|
4185
|
+
*
|
|
4186
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
4187
|
+
* @throws {HttpError} 403 — Not a superadmin
|
|
4188
|
+
*
|
|
4189
|
+
* @example
|
|
4190
|
+
* ```typescript
|
|
4191
|
+
* const { trunks } = await voip.listTrunks();
|
|
4192
|
+
*
|
|
4193
|
+
* trunks.forEach(t => {
|
|
4194
|
+
* const scope = t.tenant_id ? `Tenant: ${t.tenants?.company_name}` : 'Global';
|
|
4195
|
+
* const gwStatus = t.gatewayStatus?.status || 'Unknown';
|
|
4196
|
+
* console.log(`${t.name} | ${t.provider} | ${scope} | Gateway: ${gwStatus}`);
|
|
4197
|
+
* });
|
|
4198
|
+
* ```
|
|
4199
|
+
*
|
|
4200
|
+
* @see GET /api/trunks
|
|
4201
|
+
*/
|
|
1014
4202
|
listTrunks(): Promise<{
|
|
1015
4203
|
trunks: SipTrunk[];
|
|
1016
4204
|
}>;
|
|
@@ -1065,7 +4253,51 @@ export declare class VoIPClient {
|
|
|
1065
4253
|
listActiveGateways(): Promise<{
|
|
1066
4254
|
gateways: string[];
|
|
1067
4255
|
}>;
|
|
1068
|
-
/**
|
|
4256
|
+
/**
|
|
4257
|
+
* List all API keys for the current tenant (admin only).
|
|
4258
|
+
*
|
|
4259
|
+
* @description
|
|
4260
|
+
* Returns every API key created for the tenant. For security, the full key value
|
|
4261
|
+
* is NEVER returned — only a masked preview (e.g. `csk_live_••••••••abcd1234`).
|
|
4262
|
+
* The full key is returned only once during `createApiKey()` or `regenerateApiKey()`.
|
|
4263
|
+
*
|
|
4264
|
+
* Each API key includes:
|
|
4265
|
+
* - `name` — Human-readable label
|
|
4266
|
+
* - `keyPreview` — Masked preview of the key (e.g. `csk_live_••••••••abcd1234`)
|
|
4267
|
+
* - `role` — `'admin'`, `'readonly'`, `'user'`, or `'superadmin'`
|
|
4268
|
+
* - `scopes` — Granular permission scopes
|
|
4269
|
+
* - `lastUsedAt` — Last time the key was used (null if never)
|
|
4270
|
+
* - `expiresAt` — Expiration date (null if never expires)
|
|
4271
|
+
* - `status` — `'active'` or `'revoked'`
|
|
4272
|
+
*
|
|
4273
|
+
* **CRM UI guidance:** Show as a table with columns: Name, Key Preview (masked),
|
|
4274
|
+
* Role, Last Used, Expires, Status (active/revoked badge), and Actions
|
|
4275
|
+
* (Regenerate, Revoke). Show a warning banner if any keys are expiring soon.
|
|
4276
|
+
* Never display full keys in the list — only on create/regenerate modals.
|
|
4277
|
+
*
|
|
4278
|
+
* @returns An object containing:
|
|
4279
|
+
* - `apiKeys: ApiKey[]` — Array of API key objects (keys are masked)
|
|
4280
|
+
*
|
|
4281
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
4282
|
+
* @throws {HttpError} 403 — Not an admin
|
|
4283
|
+
*
|
|
4284
|
+
* @example
|
|
4285
|
+
* ```typescript
|
|
4286
|
+
* const { apiKeys } = await voip.listApiKeys();
|
|
4287
|
+
*
|
|
4288
|
+
* // CRM UI: API key table
|
|
4289
|
+
* apiKeys.forEach(key => {
|
|
4290
|
+
* console.log(`${key.name} | ${key.keyPreview} | ${key.role} | ${key.status}`);
|
|
4291
|
+
*
|
|
4292
|
+
* // Warning: key expires soon
|
|
4293
|
+
* if (key.expiresAt && new Date(key.expiresAt).getTime() - Date.now() < 7 * 86400000) {
|
|
4294
|
+
* showWarning(`Key "${key.name}" expires soon`);
|
|
4295
|
+
* }
|
|
4296
|
+
* });
|
|
4297
|
+
* ```
|
|
4298
|
+
*
|
|
4299
|
+
* @see GET /api/api-keys
|
|
4300
|
+
*/
|
|
1069
4301
|
listApiKeys(): Promise<{
|
|
1070
4302
|
apiKeys: ApiKey[];
|
|
1071
4303
|
}>;
|
|
@@ -1073,7 +4305,95 @@ export declare class VoIPClient {
|
|
|
1073
4305
|
getApiKeyDetail(id: string): Promise<{
|
|
1074
4306
|
apiKey: ApiKey;
|
|
1075
4307
|
}>;
|
|
1076
|
-
/**
|
|
4308
|
+
/**
|
|
4309
|
+
* Create a new API key. The full key value is only returned once in this response.
|
|
4310
|
+
*
|
|
4311
|
+
* @description
|
|
4312
|
+
* Generates a new API key for programmatic access to the VoIP API. The full key
|
|
4313
|
+
* (in the format `csk_live_...`) is returned in the `key` field of the response —
|
|
4314
|
+
* **this is the ONLY time you will see the full key**. Store it immediately in a
|
|
4315
|
+
* secure location (password manager, environment variable, or secrets vault).
|
|
4316
|
+
*
|
|
4317
|
+
* API keys support granular role-based access:
|
|
4318
|
+
* - `'admin'` — Full CRUD access within the tenant
|
|
4319
|
+
* - `'readonly'` — Read-only access (GET endpoints only)
|
|
4320
|
+
* - `'user'` — Extension-level access (make/receive calls)
|
|
4321
|
+
* - `'superadmin'` — Cross-tenant access (superadmin only, requires `global: true`)
|
|
4322
|
+
*
|
|
4323
|
+
* **Superadmin mode:** Set `global: true` to create a global key with no tenant
|
|
4324
|
+
* binding (access to ALL tenants). Pass `tenantId` to create a key for a specific
|
|
4325
|
+
* tenant when authenticated as superadmin.
|
|
4326
|
+
*
|
|
4327
|
+
* **CRM UI guidance:** After creation, show a modal with the full key prominently
|
|
4328
|
+
* displayed: "Copy this key now — it will not be shown again." Include a "Copy"
|
|
4329
|
+
* button and a "I have saved this key" confirmation before closing. Never store
|
|
4330
|
+
* or log the full key client-side.
|
|
4331
|
+
*
|
|
4332
|
+
* @param params — API key creation parameters
|
|
4333
|
+
* @param params.name — Human-readable name for this key. Example: `'CRM Integration Key'`
|
|
4334
|
+
* @param params.role — Access role: `'admin'` (default), `'readonly'`, `'user'`, or `'superadmin'`
|
|
4335
|
+
* @param params.scopes — Optional granular permission scopes. Example: `['calls:read', 'extensions:write']`
|
|
4336
|
+
* @param params.expiresAt — Optional ISO date string when the key expires.
|
|
4337
|
+
* Example: `'2027-01-01T00:00:00Z'`. Omit for no expiry
|
|
4338
|
+
* @param params.global — Superadmin only: create a global key with no tenant binding
|
|
4339
|
+
* @param params.tenantId — Superadmin only: specify the tenant this key belongs to
|
|
4340
|
+
* @param options — Optional request overrides
|
|
4341
|
+
* @param options.tenantId — Target tenant UUID (required for superadmin keys only)
|
|
4342
|
+
*
|
|
4343
|
+
* @returns An object containing:
|
|
4344
|
+
* - `apiKey: ApiKey` — The created API key object. The full key is in `apiKey.key`
|
|
4345
|
+
* (only returned once). Also includes `keyPreview` (masked), `role`, `scopes`,
|
|
4346
|
+
* `expiresAt`, `status`, `createdAt`
|
|
4347
|
+
*
|
|
4348
|
+
* @throws {HttpError} 400 — Invalid role, scope, or duplicate name
|
|
4349
|
+
* @throws {HttpError} 401 — Missing or invalid API key
|
|
4350
|
+
* @throws {HttpError} 403 — Not an admin or trying to create a superadmin key without permission
|
|
4351
|
+
*
|
|
4352
|
+
* @example
|
|
4353
|
+
* ```typescript
|
|
4354
|
+
* // Create an admin key for the CRM integration
|
|
4355
|
+
* const { apiKey } = await voip.createApiKey({
|
|
4356
|
+
* name: 'CRM Integration',
|
|
4357
|
+
* role: 'admin',
|
|
4358
|
+
* });
|
|
4359
|
+
*
|
|
4360
|
+
* console.log(`Full key (save this!): ${apiKey.key}`); // csk_live_xxxx...
|
|
4361
|
+
* console.log(`Masked preview: ${apiKey.keyPreview}`); // csk_live_••••••••xxxx
|
|
4362
|
+
*
|
|
4363
|
+
* // Create a readonly key that expires in 90 days
|
|
4364
|
+
* const expiry = new Date(Date.now() + 90 * 86400000).toISOString();
|
|
4365
|
+
* const { apiKey } = await voip.createApiKey({
|
|
4366
|
+
* name: 'Read-Only Report Key',
|
|
4367
|
+
* role: 'readonly',
|
|
4368
|
+
* scopes: ['calls:read', 'recordings:read'],
|
|
4369
|
+
* expiresAt: expiry,
|
|
4370
|
+
* });
|
|
4371
|
+
*
|
|
4372
|
+
* // Superadmin: create a global key
|
|
4373
|
+
* const { apiKey } = await voip.createApiKey({
|
|
4374
|
+
* name: 'Superadmin Global',
|
|
4375
|
+
* role: 'superadmin',
|
|
4376
|
+
* global: true,
|
|
4377
|
+
* });
|
|
4378
|
+
*
|
|
4379
|
+
* // CRM UI: handle key creation with secret modal
|
|
4380
|
+
* async function handleCreateApiKey(formData: CreateApiKeyParams) {
|
|
4381
|
+
* try {
|
|
4382
|
+
* const { apiKey } = await voip.createApiKey(formData);
|
|
4383
|
+
* showSecretModal({
|
|
4384
|
+
* title: 'API Key Created',
|
|
4385
|
+
* secret: apiKey.key,
|
|
4386
|
+
* message: 'Copy this key now — it will not be shown again.',
|
|
4387
|
+
* });
|
|
4388
|
+
* refreshApiKeyList();
|
|
4389
|
+
* } catch (err) {
|
|
4390
|
+
* showError('Failed to create API key');
|
|
4391
|
+
* }
|
|
4392
|
+
* }
|
|
4393
|
+
* ```
|
|
4394
|
+
*
|
|
4395
|
+
* @see POST /api/api-keys
|
|
4396
|
+
*/
|
|
1077
4397
|
createApiKey(params: CreateApiKeyParams, options?: RequestOptions): Promise<{
|
|
1078
4398
|
apiKey: ApiKey;
|
|
1079
4399
|
}>;
|