@cemscale-voip/voip-sdk 2.0.10 → 2.0.12

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/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
- /** Set JWT token (only for browser login flows — most apps should use apiKey) */
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
- /** Login as an extension user */
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
- /** Login as a tenant admin */
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
- /** Login as platform superadmin */
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
- /** Get TURN credentials for WebRTC */
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 filters.
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.
73
302
  *
74
- * Each call with a recording includes `cdn_url` (CloudFront, instant playback)
75
- * and `audio_url` (API fallback). Always prefer `cdn_url` when non-null.
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.
305
+ *
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-04-01',
83
- * limit: 50,
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
- * console.log(`${call.caller_id_number} -> ${call.destination}`);
88
- * if (call.cdn_url) {
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,27 +381,487 @@ export declare class VoIPClient {
112
381
  getCall(id: string): Promise<{
113
382
  call: CallRecord;
114
383
  }>;
115
- /** Originate a call via FreeSWITCH. Pass `options.tenantId` when using a global superadmin key. */
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
- /** Hang up an active call */
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
- /** Transfer an active call (blind or attended) */
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
- /** List active calls from FreeSWITCH */
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
- /** Get call statistics for a period */
639
+ /**
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
+ */
130
747
  getCallStats(period?: 'today' | '7d' | '30d' | '90d'): Promise<CallStats>;
131
- /** Place a call on hold or resume */
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
+ */
132
795
  holdCall(uuid: string, hold?: boolean): Promise<{
133
796
  message: string;
134
797
  }>;
135
- /** Park an active call */
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
+ */
136
865
  parkCall(uuid: string, slot?: number): Promise<{
137
866
  message: string;
138
867
  slot: string;
@@ -142,8 +871,69 @@ export declare class VoIPClient {
142
871
  * an incoming call. When answered, audio from the target call is bridged
143
872
  * to the monitoring extension.
144
873
  *
145
- * DTMF controls after answering:
146
- * 1 = whisper, 2 = barge, 3 = mute, 0 = toggle mute
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)
147
937
  */
148
938
  eavesdropCall(callUuid: string, extension: string, mode?: 'listen' | 'whisper' | 'barge'): Promise<EavesdropResponse>;
149
939
  /** Switch eavesdrop mode (server-side DTMF). */
@@ -156,58 +946,722 @@ export declare class VoIPClient {
156
946
  getParkedCalls(): Promise<{
157
947
  parkedCalls: ParkedCall[];
158
948
  }>;
159
- /** Export CDR as CSV. Returns raw CSV text. */
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
+ */
160
1009
  exportCalls(params?: ExportCallsParams): Promise<string>;
161
1010
  /**
162
- * Step 1 Hold the current call and originate a new leg to a third party.
163
- * The caller should watch for a `call.answered` WebSocket event on `newCallUuid`,
164
- * then call `mergeCalls()` to bring all three into a conference.
1011
+ * Add a third participant to an existing two-party call (Step 1 of three-way calling).
165
1012
  *
166
- * @param callUuid UUID of the user's active WebRTC session
167
- * @param params `{ toNumber }` for PSTN or `{ toExtension }` for internal
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)
168
1096
  */
169
1097
  addCallParticipant(callUuid: string, params: AddParticipantParams, options?: RequestOptions): Promise<AddParticipantResponse>;
170
1098
  /**
171
- * Step 2 — Merge two active call legs into a FreeSWITCH conference room.
172
- * All three parties (user + A + B) will hear each other.
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
173
1400
  *
174
- * @param params `{ callUuidA, callUuidB, conferenceName? }`
175
- */
176
- mergeCalls(params: MergeCallsParams, options?: RequestOptions): Promise<MergeCallsResponse>;
177
- /**
178
- * Swap — go "private" with one participant by removing the other from the
179
- * conference and placing them on hold. Call `mergeCalls()` again to restore.
1401
+ * @example
1402
+ * ```typescript
1403
+ * const { extensions } = await voip.listExtensions();
180
1404
  *
181
- * @param params `{ conferenceName, keepUuid, holdUuid }`
182
- */
183
- swapCallParticipant(params: SwapParticipantParams, options?: RequestOptions): Promise<SwapParticipantResponse>;
184
- /**
185
- * Merge two independently-created calls directly into a conference.
186
- * Does NOT require `addCallParticipant()` first — works with any two active
187
- * call UUIDs (e.g. from a desk phone where the user held call A and dialed
188
- * call B manually).
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
+ * ```
189
1420
  *
190
- * @param params `{ callUuidA, callUuidB, conferenceName? }`
1421
+ * @see GET /api/extensions
191
1422
  */
192
- mergeDirectCalls(params: MergeDirectParams, options?: RequestOptions): Promise<MergeDirectResponse>;
193
- /** List extensions for the current tenant */
194
1423
  listExtensions(): Promise<{
195
1424
  extensions: Extension[];
196
1425
  }>;
197
- /** Get a single extension */
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
+ */
198
1459
  getExtension(id: string): Promise<{
199
1460
  extension: Extension;
200
1461
  }>;
201
- /** Create an extension (also creates Kamailio subscriber). Pass `options.tenantId` when using a global superadmin key. */
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
+ */
202
1539
  createExtension(params: CreateExtensionParams, options?: RequestOptions): Promise<{
203
1540
  extension: Extension;
204
1541
  sip: SipProvisioningInfo;
205
1542
  }>;
206
- /** Update an extension */
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
1555
+ *
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.
1559
+ *
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)
1572
+ *
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
1578
+ *
1579
+ * @example
1580
+ * ```typescript
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
+ * }
1605
+ * }
1606
+ * ```
1607
+ *
1608
+ * @see PUT /api/extensions/{id}
1609
+ */
207
1610
  updateExtension(id: string, params: UpdateExtensionParams, options?: RequestOptions): Promise<{
208
1611
  message: string;
209
1612
  }>;
210
- /** Delete an extension */
1613
+ /**
1614
+ * Delete an extension permanently.
1615
+ *
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
1623
+ *
1624
+ * This operation is irreversible. Call records (CDRs) belonging to this extension
1625
+ * are preserved for audit purposes.
1626
+ *
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
+ * ```
1662
+ *
1663
+ * @see DELETE /api/extensions/{id}
1664
+ */
211
1665
  deleteExtension(id: string): Promise<{
212
1666
  message: string;
213
1667
  }>;
@@ -229,26 +1683,122 @@ export declare class VoIPClient {
229
1683
  crmMetadata: any;
230
1684
  }>;
231
1685
  /**
232
- * Get SIP credentials for connecting a WebRTC softphone.
233
- * Returns everything needed to register: extension, password, SIP domain, WebSocket URI.
234
- * Use this so your app never needs to store SIP passwords.
1686
+ * Get SIP credentials for connecting a WebRTC softphone or SIP device.
235
1687
  *
236
- * @param target - Extension UUID, or extension number when `byNumber: true`
237
- * @param options - Optional `{ tenantId }` for superadmin context, plus `{ byNumber: true }` to look up by extension number
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
238
1715
  *
239
1716
  * @example
240
- * // By UUID (default)
1717
+ * ```typescript
1718
+ * // By UUID (default) — pass id from listExtensions()
241
1719
  * const { sipCredentials } = await voip.getSipCredentials('extension-uuid');
242
1720
  *
243
- * // By extension number (e.g. "1001")
244
- * const { sipCredentials } = await voip.getSipCredentials('1001', { byNumber: true, tenantId: 'tenant-uuid' });
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
245
1757
  */
246
1758
  getSipCredentials(target: string, options?: RequestOptions & {
247
1759
  byNumber?: boolean;
248
1760
  }): Promise<{
249
1761
  sipCredentials: SipCredentials;
250
1762
  }>;
251
- /** List all tenants (superadmin only) */
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
+ */
252
1802
  listTenants(): Promise<{
253
1803
  tenants: Tenant[];
254
1804
  }>;
@@ -306,7 +1856,53 @@ export declare class VoIPClient {
306
1856
  totalCalls: number;
307
1857
  };
308
1858
  }>;
309
- /** List DID numbers */
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
+ */
310
1906
  listDids(): Promise<{
311
1907
  dids: DidNumber[];
312
1908
  }>;
@@ -639,89 +2235,467 @@ export declare class VoIPClient {
639
2235
  */
640
2236
  generateIvrTts(id: string, params: IvrTtsParams): Promise<{
641
2237
  message: string;
642
- menu: IvrMenu;
643
- }>;
644
- /** List available TTS voices for IVR greetings */
645
- listIvrVoices(): Promise<{
646
- 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;
647
2547
  }>;
648
2548
  /**
649
- * List recordings with pagination.
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'`
650
2569
  *
651
- * Each recording includes `cdn_url` (CloudFront CDN, instant) and `audio_url` (API fallback).
652
- * Always prefer `cdn_url` when non-null for the fastest playback.
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
653
2576
  *
654
2577
  * @example
655
2578
  * ```typescript
656
- * const { recordings, pagination } = await voip.listRecordings({
657
- * page: 1,
658
- * limit: 20,
659
- * direction: 'inbound',
2579
+ * // After mergeCalls, kick the third party
2580
+ * const { conferenceName, participants } = await voip.mergeCalls({
2581
+ * callUuidA: selfUuid,
2582
+ * callUuidB: newCallUuid,
660
2583
  * });
661
2584
  *
662
- * recordings.forEach(rec => {
663
- * // Always prefer CDN URL — instant playback from nearest edge
664
- * const src = rec.cdn_url ?? voip.buildUrl(`/api/recordings/${rec.id}/audio`);
665
- * render(`<audio src="${src}" preload="none" controls />`);
666
- * });
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));
667
2608
  * ```
2609
+ *
2610
+ * @see POST /api/conferences/{conferenceName}/kick
2611
+ * @see mergeCalls (get conferenceName and memberId)
668
2612
  */
669
- listRecordings(params?: RecordingListParams): Promise<RecordingListResponse>;
2613
+ kickFromConference(conferenceName: string, memberId: string): Promise<{
2614
+ message: string;
2615
+ }>;
670
2616
  /**
671
- * Get the best available URL to play a recording.
2617
+ * Mute or unmute a participant in a conference room.
672
2618
  *
673
- * Returns a response with:
674
- * - `url` CloudFront CDN URL (if in S3) or API URL (local fallback)
675
- * - `source` `'cdn'` (fast, ~5-20ms) or `'local'` (slower, disk stream)
676
- * - `format` `'mp3'` or `'wav'`
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.
677
2623
  *
678
- * **For most CRM use cases**, you don't need this method. Just read `cdn_url` from
679
- * `listCalls()` or `getCall()` directly. Use this only when you need to resolve the
680
- * URL programmatically before rendering.
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
681
2652
  *
682
2653
  * @example
683
2654
  * ```typescript
684
- * const { url, source, format } = await voip.getRecordingUrl(callId);
685
- * console.log(`Playing from ${source}: ${url} (${format})`);
686
- * audioElement.src = url;
687
- * ```
688
- */
689
- getRecordingUrl(id: string): Promise<RecordingUrlResponse>;
690
- /**
691
- * Delete a recording permanently.
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);
692
2668
  *
693
- * Removes the audio file from:
694
- * - S3 cloud storage (if uploaded)
695
- * - Local server disk
696
- * - Redis cache
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
+ * ```
697
2695
  *
698
- * The `recording_url`, `s3_key`, `cdn_url`, and `audio_url` fields for this call
699
- * will become null after deletion.
2696
+ * @see POST /api/conferences/{conferenceName}/mute
2697
+ * @see mergeCalls (get conferenceName and memberId)
700
2698
  */
701
- deleteRecording(id: string): Promise<{
702
- message: string;
703
- }>;
704
- /** List active conferences */
705
- listConferences(): Promise<{
706
- conferences: Conference[];
707
- }>;
708
- /** Get conference details */
709
- getConference(name: string): Promise<{
710
- conference: Conference;
711
- }>;
712
- /** Join a call to a conference */
713
- joinConference(conferenceName: string, callUuid: string): Promise<{
714
- message: string;
715
- }>;
716
- /** Transfer a call to conference */
717
- transferToConference(uuid: string, conferenceName?: string): Promise<{
718
- message: string;
719
- }>;
720
- /** Kick a member from conference */
721
- kickFromConference(conferenceName: string, memberId: string): Promise<{
722
- message: string;
723
- }>;
724
- /** Mute/unmute a conference member */
725
2699
  muteConferenceMember(conferenceName: string, memberId: string, mute?: boolean): Promise<{
726
2700
  message: string;
727
2701
  }>;
@@ -738,7 +2712,44 @@ export declare class VoIPClient {
738
2712
  message: string;
739
2713
  filePath?: string;
740
2714
  }>;
741
- /** List ring groups */
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
+ */
742
2753
  listRingGroups(): Promise<{
743
2754
  ringGroups: RingGroup[];
744
2755
  }>;
@@ -746,7 +2757,84 @@ export declare class VoIPClient {
746
2757
  getRingGroup(id: string): Promise<{
747
2758
  ringGroup: RingGroup;
748
2759
  }>;
749
- /** Create a ring group. Pass `options.tenantId` when using a global superadmin key. */
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
+ */
750
2838
  createRingGroup(params: CreateRingGroupParams, options?: RequestOptions): Promise<{
751
2839
  ringGroup: RingGroup;
752
2840
  }>;
@@ -758,7 +2846,46 @@ export declare class VoIPClient {
758
2846
  deleteRingGroup(id: string): Promise<{
759
2847
  message: string;
760
2848
  }>;
761
- /** List paging groups */
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
+ */
762
2889
  listPagingGroups(): Promise<{
763
2890
  pagingGroups: PagingGroup[];
764
2891
  }>;
@@ -766,7 +2893,76 @@ export declare class VoIPClient {
766
2893
  getPagingGroup(id: string): Promise<{
767
2894
  pagingGroup: PagingGroup;
768
2895
  }>;
769
- /** Create a paging group */
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
+ */
770
2966
  createPagingGroup(params: CreatePagingGroupParams, options?: RequestOptions): Promise<{
771
2967
  pagingGroup: PagingGroup;
772
2968
  }>;
@@ -778,9 +2974,126 @@ export declare class VoIPClient {
778
2974
  deletePagingGroup(id: string): Promise<{
779
2975
  message: string;
780
2976
  }>;
781
- /** Broadcast a paging announcement to all online members */
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
+ */
782
3052
  broadcastPaging(id: string, callerExtension: string): Promise<BroadcastResult>;
783
- /** List call queues */
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
+ */
784
3097
  listQueues(): Promise<{
785
3098
  queues: CallQueue[];
786
3099
  }>;
@@ -812,7 +3125,50 @@ export declare class VoIPClient {
812
3125
  getQueueStats(queueId: string): Promise<{
813
3126
  stats: QueueStats;
814
3127
  }>;
815
- /** List webhooks */
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
+ */
816
3172
  listWebhooks(): Promise<{
817
3173
  webhooks: Webhook[];
818
3174
  supportedEvents: string[];
@@ -822,7 +3178,76 @@ export declare class VoIPClient {
822
3178
  webhook: Webhook;
823
3179
  recentDeliveries: WebhookDelivery[];
824
3180
  }>;
825
- /** Create a webhook. Pass `options.tenantId` when using a global superadmin key. */
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
+ */
826
3251
  createWebhook(params: CreateWebhookParams, options?: RequestOptions): Promise<{
827
3252
  webhook: Webhook & {
828
3253
  secret: string;
@@ -847,17 +3272,245 @@ export declare class VoIPClient {
847
3272
  deliveries: WebhookDelivery[];
848
3273
  pagination: PaginationInfo;
849
3274
  }>;
850
- /** List voicemail messages with pagination */
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
+ */
851
3336
  listVoicemails(params?: VoicemailListParams): Promise<VoicemailListResponse>;
852
- /** Get voicemail count (total and unread) */
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
+ */
853
3378
  getVoicemailCount(extension?: string): Promise<VoicemailCountResponse>;
854
- /** Get a single voicemail */
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
+ */
855
3420
  getVoicemail(id: string): Promise<VoicemailMessage>;
856
- /** Mark voicemail as read */
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
+ */
857
3469
  markVoicemailRead(id: string): Promise<{
858
3470
  message: string;
859
3471
  }>;
860
- /** Delete a voicemail (hard delete — removes audio file from disk) */
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
+ */
861
3514
  deleteVoicemail(id: string): Promise<{
862
3515
  message: string;
863
3516
  }>;
@@ -920,40 +3573,367 @@ export declare class VoIPClient {
920
3573
  message: string;
921
3574
  }>;
922
3575
  /**
923
- * Get presence for all extensions.
924
- * Pass `{ detailed: true }` to include call info (callUuid, caller, direction, etc.).
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
925
3606
  *
926
3607
  * @example
927
- * // Simple presence
3608
+ * ```typescript
3609
+ * // Simple presence — extension list with colored dots
928
3610
  * const { presence } = await voip.getPresence();
929
- * // { '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
+ * });
930
3617
  *
931
3618
  * // Detailed presence with call info
932
3619
  * const { presence } = await voip.getPresence({ detailed: true });
933
- * // { '1001': { status: 'on_call', callUuid: '...', caller: '+1555...' } }
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
934
3629
  */
935
3630
  getPresence(options?: {
936
3631
  detailed?: boolean;
937
3632
  }): Promise<{
938
3633
  presence: PresenceMap | PresenceDetailMap;
939
3634
  }>;
940
- /** Set manual presence status for an extension (busy / available / offline) */
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
+ */
941
3691
  setPresence(extensionId: string, status: 'busy' | 'available' | 'offline'): Promise<{
942
3692
  ok: boolean;
943
3693
  extension: string;
944
3694
  status: string;
945
3695
  }>;
946
- /** Get dashboard summary stats */
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
+ */
947
3739
  getDashboardStats(): Promise<DashboardStats>;
948
- /** Get calls grouped by day */
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
+ */
949
3789
  getCallsByDay(days?: number): Promise<CallsByDayResponse>;
950
- /** Get top extensions by call volume */
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
+ */
951
3836
  getTopExtensions(days?: number, limit?: number): Promise<TopExtensionsResponse>;
952
- /** List all blocked numbers for the tenant */
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
+ */
953
3873
  listBlockedNumbers(): Promise<{
954
3874
  blocked: BlockedNumber[];
955
3875
  }>;
956
- /** Block a phone number. Pass `options.tenantId` when using a global superadmin key. */
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
+ */
957
3937
  blockNumber(params: CreateBlockedNumberParams, options?: RequestOptions): Promise<{
958
3938
  blocked: BlockedNumber;
959
3939
  }>;
@@ -961,9 +3941,103 @@ export declare class VoIPClient {
961
3941
  unblockNumber(id: string): Promise<{
962
3942
  message: string;
963
3943
  }>;
964
- /** Check if a specific number is blocked */
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
+ */
965
4003
  checkBlocked(number: string): Promise<BlocklistCheckResponse>;
966
- /** List business hour schedules */
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
+ */
967
4041
  listSchedules(): Promise<{
968
4042
  schedules: BusinessSchedule[];
969
4043
  }>;
@@ -971,7 +4045,67 @@ export declare class VoIPClient {
971
4045
  getSchedule(id: string): Promise<{
972
4046
  schedule: BusinessSchedule;
973
4047
  }>;
974
- /** Create a business hours schedule. Pass `options.tenantId` when using a global superadmin key. */
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
+ */
975
4109
  createSchedule(params: CreateScheduleParams, options?: RequestOptions): Promise<{
976
4110
  schedule: BusinessSchedule;
977
4111
  }>;
@@ -983,9 +4117,88 @@ export declare class VoIPClient {
983
4117
  deleteSchedule(id: string): Promise<{
984
4118
  message: string;
985
4119
  }>;
986
- /** Check if a schedule is currently open or closed */
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
+ */
987
4164
  getScheduleStatus(id: string): Promise<ScheduleStatusResponse>;
988
- /** List SIP trunks (passwords masked) */
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
+ */
989
4202
  listTrunks(): Promise<{
990
4203
  trunks: SipTrunk[];
991
4204
  }>;
@@ -1040,7 +4253,51 @@ export declare class VoIPClient {
1040
4253
  listActiveGateways(): Promise<{
1041
4254
  gateways: string[];
1042
4255
  }>;
1043
- /** List API keys for the current tenant (admin only) */
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
+ */
1044
4301
  listApiKeys(): Promise<{
1045
4302
  apiKeys: ApiKey[];
1046
4303
  }>;
@@ -1048,7 +4305,95 @@ export declare class VoIPClient {
1048
4305
  getApiKeyDetail(id: string): Promise<{
1049
4306
  apiKey: ApiKey;
1050
4307
  }>;
1051
- /** Create a new API key. The full key is only returned once in the response. Pass `options.tenantId` when using a global superadmin key. */
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
+ */
1052
4397
  createApiKey(params: CreateApiKeyParams, options?: RequestOptions): Promise<{
1053
4398
  apiKey: ApiKey;
1054
4399
  }>;