@unboundcx/sdk 4.8.9 → 4.8.10

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/base.js CHANGED
@@ -410,37 +410,36 @@ export class BaseSDK {
410
410
  'application/json';
411
411
 
412
412
  if (!response.ok) {
413
+ // A fetch Response's `.body` is a ReadableStream, not the parsed
414
+ // payload — it must go through json()/text() or the API's
415
+ // `{ message }` envelope is lost and every error surfaces as the
416
+ // generic "API Error". Only NATS-transport responses carry a
417
+ // pre-parsed `.body`.
413
418
  let errorBody;
414
- if (response?.body) {
415
- errorBody = response.body;
416
- } else if (response?.headers?.['content-type']) {
417
- try {
418
- if (
419
- typeof response?.json === 'function' ||
420
- typeof response?.text === 'function'
421
- ) {
422
- if (contentType.includes('application/json')) {
423
- errorBody = await response.json();
424
- } else if (contentType.includes('text/')) {
425
- errorBody = await response.text();
426
- }
427
- } else {
428
- if (contentType.includes('application/json')) {
429
- errorBody = this._getJsonSafely(
430
- response?.body,
431
- response?.body || {},
432
- );
433
- } else if (contentType.includes('text/')) {
434
- errorBody = response?.body || '';
435
- }
419
+ try {
420
+ if (
421
+ typeof response?.json === 'function' ||
422
+ typeof response?.text === 'function'
423
+ ) {
424
+ if (contentType.includes('application/json')) {
425
+ errorBody = await response.json();
426
+ } else if (contentType.includes('text/')) {
427
+ errorBody = await response.text();
436
428
  }
437
- if (!errorBody) {
438
- errorBody = `HTTP ${response.status} ${response.statusText}`;
429
+ } else if (response?.body) {
430
+ if (contentType.includes('application/json')) {
431
+ errorBody = this._getJsonSafely(
432
+ response?.body,
433
+ response?.body || {},
434
+ );
435
+ } else {
436
+ errorBody = response.body;
439
437
  }
440
- } catch (parseError) {
441
- errorBody = `HTTP ${response.status} ${response.statusText}`;
442
438
  }
443
- } else {
439
+ } catch (parseError) {
440
+ // fall through to the status-line fallback below
441
+ }
442
+ if (!errorBody) {
444
443
  errorBody = `HTTP ${response.status} ${response.statusText}`;
445
444
  }
446
445
 
@@ -455,7 +454,10 @@ export class BaseSDK {
455
454
  httpError.method = method;
456
455
  httpError.endpoint = endpoint;
457
456
  httpError.body = errorBody;
458
- httpError.message = errorBody?.error || errorBody?.message || 'API Error';
457
+ httpError.message =
458
+ errorBody?.error ||
459
+ errorBody?.message ||
460
+ (typeof errorBody === 'string' ? errorBody : 'API Error');
459
461
 
460
462
  // Debug logging for successful HTTP requests
461
463
  if (this.debugMode) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unboundcx/sdk",
3
- "version": "4.8.9",
3
+ "version": "4.8.10",
4
4
  "description": "Official JavaScript SDK for the Unbound API - A comprehensive toolkit for integrating with Unbound's communication, AI, and data management services",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -15,27 +15,33 @@ export class DirectoryService {
15
15
  }
16
16
 
17
17
  /**
18
- * Add a person or company to directory favorites. Reactivates a
19
- * soft-deleted favorite on unique conflict.
18
+ * Add a person, company, user, or queue to directory favorites.
19
+ * Reactivates a soft-deleted favorite on unique conflict.
20
20
  *
21
21
  * @param {Object} params
22
- * @param {string} params.objectType - 'person' or 'company' (required).
22
+ * @param {string} params.objectType - 'person', 'company', 'user', or 'queue' (required).
23
23
  * @param {string} params.objectId - Id of the favorited record (required).
24
24
  * @param {string[]} [params.channelIds] - Channel ids to pin for this favorite.
25
+ * @param {string} [params.channelId] - Channel id to pin for this favorite.
26
+ * @param {string} [params.numberField] - Number field to associate with this favorite.
25
27
  * @returns {Promise<Object>} The created (or reactivated) favorite.
26
28
  */
27
- async addFavorite({ objectType, objectId, channelIds }) {
29
+ async addFavorite({ objectType, objectId, channelIds, channelId, numberField }) {
28
30
  this.sdk.validateParams(
29
- { objectType, objectId, channelIds },
31
+ { objectType, objectId, channelIds, channelId, numberField },
30
32
  {
31
33
  objectType: { type: 'string', required: true },
32
34
  objectId: { type: 'string', required: true },
33
35
  channelIds: { type: 'array', required: false },
36
+ channelId: { type: 'string', required: false },
37
+ numberField: { type: 'string', required: false },
34
38
  },
35
39
  );
36
40
 
37
41
  const body = { objectType, objectId };
38
42
  if (channelIds !== undefined) body.channelIds = channelIds;
43
+ if (channelId !== undefined) body.channelId = channelId;
44
+ if (numberField !== undefined) body.numberField = numberField;
39
45
 
40
46
  const params = { body };
41
47
 
@@ -49,21 +55,27 @@ export class DirectoryService {
49
55
  * @param {string} favoriteId - Id of the favorite to update (required).
50
56
  * @param {Object} params
51
57
  * @param {string[]} [params.channelIds]
58
+ * @param {string} [params.channelId]
59
+ * @param {string} [params.numberField]
52
60
  * @param {number} [params.sortOrder]
53
61
  * @returns {Promise<Object>} The updated favorite.
54
62
  */
55
- async updateFavorite(favoriteId, { channelIds, sortOrder } = {}) {
63
+ async updateFavorite(favoriteId, { channelIds, channelId, numberField, sortOrder } = {}) {
56
64
  this.sdk.validateParams(
57
- { favoriteId, channelIds, sortOrder },
65
+ { favoriteId, channelIds, channelId, numberField, sortOrder },
58
66
  {
59
67
  favoriteId: { type: 'string', required: true },
60
68
  channelIds: { type: 'array', required: false },
69
+ channelId: { type: 'string', required: false },
70
+ numberField: { type: 'string', required: false },
61
71
  sortOrder: { type: 'number', required: false },
62
72
  },
63
73
  );
64
74
 
65
75
  const body = {};
66
76
  if (channelIds !== undefined) body.channelIds = channelIds;
77
+ if (channelId !== undefined) body.channelId = channelId;
78
+ if (numberField !== undefined) body.numberField = numberField;
67
79
  if (sortOrder !== undefined) body.sortOrder = sortOrder;
68
80
 
69
81
  const params = { body };
@@ -121,4 +133,58 @@ export class DirectoryService {
121
133
  );
122
134
  return result;
123
135
  }
136
+
137
+ /**
138
+ * List the current user's directory contacts.
139
+ *
140
+ * @returns {Promise<{contacts: Object[]}>}
141
+ */
142
+ async listContacts() {
143
+ const result = await this.sdk._fetch('/directory/contacts', 'GET');
144
+ return result;
145
+ }
146
+
147
+ /**
148
+ * Add a person or company to directory contacts.
149
+ *
150
+ * @param {Object} params
151
+ * @param {string} params.objectType - 'person' or 'company' (required).
152
+ * @param {string} params.objectId - Id of the contacted record (required).
153
+ * @returns {Promise<Object>} The created contact.
154
+ */
155
+ async addContact({ objectType, objectId }) {
156
+ this.sdk.validateParams(
157
+ { objectType, objectId },
158
+ {
159
+ objectType: { type: 'string', required: true },
160
+ objectId: { type: 'string', required: true },
161
+ },
162
+ );
163
+
164
+ const params = { body: { objectType, objectId } };
165
+
166
+ const result = await this.sdk._fetch('/directory/contacts', 'POST', params);
167
+ return result;
168
+ }
169
+
170
+ /**
171
+ * Remove a directory contact.
172
+ *
173
+ * @param {string} contactId - Id of the contact to remove (required).
174
+ * @returns {Promise<Object>}
175
+ */
176
+ async removeContact(contactId) {
177
+ this.sdk.validateParams(
178
+ { contactId },
179
+ {
180
+ contactId: { type: 'string', required: true },
181
+ },
182
+ );
183
+
184
+ const result = await this.sdk._fetch(
185
+ `/directory/contacts/${contactId}`,
186
+ 'DELETE',
187
+ );
188
+ return result;
189
+ }
124
190
  }
package/services/inbox.js CHANGED
@@ -9,21 +9,59 @@ export class InboxService {
9
9
  *
10
10
  * @param {Object} params
11
11
  * @param {string|string[]} [params.types] - Source kinds to include
12
- * ('call','voicemail','fax','sms'). Pass an array or a comma-joined
13
- * string; omit to include all kinds.
12
+ * ('call','voicemail','fax','sms','meeting'). Pass an array or a
13
+ * comma-joined string; omit to include all kinds.
14
14
  * @param {number} [params.limit] - Max items to return.
15
15
  * @param {string} [params.before] - UTC cursor 'YYYY-MM-DD HH:mm:ss';
16
16
  * only items strictly older than this are returned.
17
17
  * @param {boolean} [params.unreadOnly] - When true, only unread items.
18
+ * @param {string} [params.startDate] - Inclusive range start `YYYY-MM-DD`.
19
+ * @param {string} [params.endDate] - Inclusive range end `YYYY-MM-DD`.
20
+ * @param {string} [params.q] - Search numbers, names, previews.
21
+ * @param {string} [params.direction] - `inbound` or `outbound`.
22
+ * @param {boolean} [params.missed] - Missed inbound calls only.
23
+ * @param {boolean} [params.hasRecording] - Calls/meetings with a recording.
24
+ * @param {boolean} [params.hasTranscription] - Calls/meetings/voicemail
25
+ * with a transcript.
18
26
  * @returns {Promise<{items: Object[], nextCursor: string|null}>}
19
27
  */
20
- async list({ types, limit, before, unreadOnly } = {}) {
28
+ async list({
29
+ types,
30
+ limit,
31
+ before,
32
+ unreadOnly,
33
+ startDate,
34
+ endDate,
35
+ q,
36
+ direction,
37
+ missed,
38
+ hasRecording,
39
+ hasTranscription,
40
+ } = {}) {
21
41
  this.sdk.validateParams(
22
- { limit, before, unreadOnly },
42
+ {
43
+ limit,
44
+ before,
45
+ unreadOnly,
46
+ startDate,
47
+ endDate,
48
+ q,
49
+ direction,
50
+ missed,
51
+ hasRecording,
52
+ hasTranscription,
53
+ },
23
54
  {
24
55
  limit: { type: 'number', required: false },
25
56
  before: { type: 'string', required: false },
26
57
  unreadOnly: { type: 'boolean', required: false },
58
+ startDate: { type: 'string', required: false },
59
+ endDate: { type: 'string', required: false },
60
+ q: { type: 'string', required: false },
61
+ direction: { type: 'string', required: false },
62
+ missed: { type: 'boolean', required: false },
63
+ hasRecording: { type: 'boolean', required: false },
64
+ hasTranscription: { type: 'boolean', required: false },
27
65
  },
28
66
  );
29
67
 
@@ -42,6 +80,15 @@ export class InboxService {
42
80
  if (limit !== undefined) query.limit = limit;
43
81
  if (before !== undefined) query.before = before;
44
82
  if (unreadOnly !== undefined) query.unreadOnly = unreadOnly;
83
+ if (startDate !== undefined) query.startDate = startDate;
84
+ if (endDate !== undefined) query.endDate = endDate;
85
+ if (q !== undefined) query.q = q;
86
+ if (direction !== undefined) query.direction = direction;
87
+ if (missed !== undefined) query.missed = missed;
88
+ if (hasRecording !== undefined) query.hasRecording = hasRecording;
89
+ if (hasTranscription !== undefined) {
90
+ query.hasTranscription = hasTranscription;
91
+ }
45
92
 
46
93
  const params = { query };
47
94
 
@@ -82,10 +129,10 @@ export class InboxService {
82
129
  }
83
130
 
84
131
  /**
85
- * Fetch inbox stats (calls/talk time/missed/unread voicemail) for the
86
- * current user.
132
+ * Fetch inbox stats (calls/talk time/missed/unread voicemail/meetings) for
133
+ * the current user.
87
134
  *
88
- * @returns {Promise<{callsToday: number, talkTimeSeconds: number, missedToday: number, unreadVoicemail: number, oldestUnreadVoicemailAt: string|null}>}
135
+ * @returns {Promise<{callsToday: number, talkTimeSeconds: number, missedToday: number, unreadVoicemail: number, oldestUnreadVoicemailAt: string|null, meetingsToday: number, meetingMinutesToday: number}>}
89
136
  */
90
137
  async stats() {
91
138
  const result = await this.sdk._fetch('/inbox/stats', 'GET');
@@ -455,4 +455,53 @@ export class EmailMailboxesService {
455
455
  );
456
456
  return result;
457
457
  }
458
+
459
+ async createFolder(mailboxId, { name, parent } = {}) {
460
+ this.sdk.validateParams(
461
+ { mailboxId, name, parent },
462
+ {
463
+ mailboxId: { type: 'string', required: true },
464
+ name: { type: 'string', required: true },
465
+ parent: { type: 'string', required: false },
466
+ },
467
+ );
468
+ const body = { name };
469
+ if (parent) body.parent = parent;
470
+ return this.sdk._fetch(
471
+ `/messaging/email/mailbox/${mailboxId}/folders`,
472
+ 'POST',
473
+ { body },
474
+ );
475
+ }
476
+
477
+ async renameFolder(mailboxId, { from, to } = {}) {
478
+ this.sdk.validateParams(
479
+ { mailboxId, from, to },
480
+ {
481
+ mailboxId: { type: 'string', required: true },
482
+ from: { type: 'string', required: true },
483
+ to: { type: 'string', required: true },
484
+ },
485
+ );
486
+ return this.sdk._fetch(
487
+ `/messaging/email/mailbox/${mailboxId}/folders`,
488
+ 'PUT',
489
+ { body: { from, to } },
490
+ );
491
+ }
492
+
493
+ async deleteFolder(mailboxId, { name } = {}) {
494
+ this.sdk.validateParams(
495
+ { mailboxId, name },
496
+ {
497
+ mailboxId: { type: 'string', required: true },
498
+ name: { type: 'string', required: true },
499
+ },
500
+ );
501
+ return this.sdk._fetch(
502
+ `/messaging/email/mailbox/${mailboxId}/folders`,
503
+ 'DELETE',
504
+ { query: { name }, body: { name } },
505
+ );
506
+ }
458
507
  }
@@ -0,0 +1,302 @@
1
+ export class CCService {
2
+ constructor(sdk) {
3
+ this.sdk = sdk;
4
+ }
5
+
6
+ /**
7
+ * Resolve the caller's Contact Center queue scope + role.
8
+ * Managers see every non-deleted queue; agents see only the queues they
9
+ * belong to (queueUsers).
10
+ *
11
+ * @returns {Promise<Object>} result
12
+ * @returns {boolean} result.isManager - Whether the caller has queue-manager scope
13
+ * @returns {string|null} result.workerId - The caller's own worker id, if any
14
+ * @returns {Array<Object>} result.queues - Accessible queues [{id, name, slaThreshold, slaTargetPct, timezone}]
15
+ *
16
+ * @example
17
+ * const scope = await sdk.taskRouter.cc.getScope();
18
+ * console.log(scope.isManager, scope.queues.length);
19
+ */
20
+ async getScope() {
21
+ const result = await this.sdk._fetch('/taskRouter/cc/scope', 'GET', {});
22
+ return result;
23
+ }
24
+
25
+ /**
26
+ * Get a live Contact Center snapshot (KPIs, per-queue summaries, team roster
27
+ * with active tasks) scoped to a set of queues.
28
+ *
29
+ * @param {Object} [options] - Parameters
30
+ * @param {string[]} [options.queueIds] - Queue ids to scope to (must be a subset of the caller's accessible queues). Omit/empty for full scope.
31
+ * @returns {Promise<Object>} result
32
+ * @returns {Object} result.kpis - Aggregate KPIs (inQueueNow, longestWaitSec, myHandledToday, myAvgHandleSecToday, slaTodayPct, slaTargetPct)
33
+ * @returns {Array<Object>} result.queues - Per-queue summaries (id, name, waiting, longestWaitSec, agentsAvailable, agentsTotal, slaPct, slaThreshold, slaTargetPct, timezone, health)
34
+ * @returns {Array<Object>} result.team - Team roster with active tasks
35
+ *
36
+ * @example
37
+ * const snapshot = await sdk.taskRouter.cc.getSnapshot({ queueIds: ['q1', 'q2'] });
38
+ * console.log(snapshot.kpis.inQueueNow);
39
+ */
40
+ async getSnapshot(options = {}) {
41
+ const { queueIds } = options;
42
+
43
+ this.sdk.validateParams(
44
+ { queueIds },
45
+ {
46
+ queueIds: { type: 'array', required: false },
47
+ },
48
+ );
49
+
50
+ const params = {};
51
+ if (queueIds && queueIds.length) {
52
+ params.query = { queueIds: queueIds.join(',') };
53
+ }
54
+
55
+ const result = await this.sdk._fetch(
56
+ '/taskRouter/cc/snapshot',
57
+ 'GET',
58
+ params,
59
+ );
60
+ return result;
61
+ }
62
+
63
+ /**
64
+ * Get on-queue session history for a worker (self allowed; another
65
+ * worker's sessions require the queue-manager scope).
66
+ *
67
+ * @param {Object} [options] - Parameters
68
+ * @param {string} [options.workerId] - Worker id to fetch sessions for (defaults to the caller's own worker)
69
+ * @param {string} [options.from] - ISO-8601 range start
70
+ * @param {string} [options.to] - ISO-8601 range end
71
+ * @returns {Promise<Object>} result
72
+ * @returns {Array<Object>} result.sessions - [{id, startedAt, endedAt, onQueueSec, breakSec, doneCount}]
73
+ *
74
+ * @example
75
+ * const { sessions } = await sdk.taskRouter.cc.getSessions({ workerId: 'w1', from: '2026-08-12T00:00:00Z', to: '2026-08-19T00:00:00Z' });
76
+ */
77
+ async getSessions(options = {}) {
78
+ const { workerId, from, to } = options;
79
+
80
+ this.sdk.validateParams(
81
+ { workerId, from, to },
82
+ {
83
+ workerId: { type: 'string', required: false },
84
+ from: { type: 'string', required: false },
85
+ to: { type: 'string', required: false },
86
+ },
87
+ );
88
+
89
+ const query = {};
90
+ if (workerId) query.workerId = workerId;
91
+ if (from) query.from = from;
92
+ if (to) query.to = to;
93
+
94
+ const result = await this.sdk._fetch('/taskRouter/cc/sessions', 'GET', {
95
+ query,
96
+ });
97
+ return result;
98
+ }
99
+
100
+ /**
101
+ * Get windowed composite agent rankings (top/struggling) per queue + an
102
+ * 'all' rollup, scored from sentiment/SLA/volume/acceptance. `struggling`
103
+ * arrays are only present when the caller has the queue-manager scope
104
+ * (see `strugglingIncluded` on the result).
105
+ *
106
+ * @param {Object} [options] - Parameters
107
+ * @param {string[]} [options.queueIds] - Queue ids to scope to (must be a subset of the caller's accessible queues). Omit/empty for full scope.
108
+ * @param {string} options.from - ISO-8601 window start
109
+ * @param {string} options.to - ISO-8601 window end
110
+ * @returns {Promise<Object>} result
111
+ * @returns {Object} result.queues - Per-queue rankings, keyed by queueId: `{ top: [{workerId,name,score}], struggling?: [...] }`
112
+ * @returns {Object} result.all - Same shape, aggregated across every scoped queue
113
+ * @returns {boolean} result.strugglingIncluded - Whether `struggling` arrays were included
114
+ *
115
+ * @example
116
+ * const { queues, all } = await sdk.taskRouter.cc.getRankings({
117
+ * queueIds: ['q1'],
118
+ * from: '2026-08-12T00:00:00Z',
119
+ * to: '2026-08-19T00:00:00Z',
120
+ * });
121
+ * console.log(all.top[0].name, all.top[0].score);
122
+ */
123
+ async getRankings(options = {}) {
124
+ const { queueIds, from, to } = options;
125
+
126
+ this.sdk.validateParams(
127
+ { queueIds, from, to },
128
+ {
129
+ queueIds: { type: 'array', required: false },
130
+ from: { type: 'string', required: true },
131
+ to: { type: 'string', required: true },
132
+ },
133
+ );
134
+
135
+ const query = { from, to };
136
+ if (queueIds && queueIds.length) {
137
+ query.queueIds = queueIds.join(',');
138
+ }
139
+
140
+ const result = await this.sdk._fetch('/taskRouter/cc/rankings', 'GET', {
141
+ query,
142
+ });
143
+ return result;
144
+ }
145
+
146
+ /**
147
+ * Set a queue's composite ranking-weights override. Manager-only
148
+ * (`taskrouter:queue:manage`). Weights must be integers 0-100 that sum to
149
+ * exactly 100.
150
+ *
151
+ * @param {Object} options - Parameters
152
+ * @param {string} options.queueId - Queue id to update
153
+ * @param {Object} options.weights - `{sentiment, aht, volume, acceptance}` ints summing to 100
154
+ * @returns {Promise<Object>} result
155
+ * @returns {string} result.queueId
156
+ * @returns {Object} result.rankingWeights - The stored weights
157
+ *
158
+ * @example
159
+ * await sdk.taskRouter.cc.setQueueRankingWeights({
160
+ * queueId: 'q1',
161
+ * weights: { sentiment: 40, aht: 30, volume: 5, acceptance: 25 },
162
+ * });
163
+ */
164
+ async setQueueRankingWeights(options = {}) {
165
+ const { queueId, weights } = options;
166
+
167
+ this.sdk.validateParams(
168
+ { queueId, weights },
169
+ {
170
+ queueId: { type: 'string', required: true },
171
+ weights: { type: 'object', required: true },
172
+ },
173
+ );
174
+
175
+ const result = await this.sdk._fetch(
176
+ `/taskRouter/cc/queues/${queueId}/rankingWeights`,
177
+ 'PUT',
178
+ { body: weights },
179
+ );
180
+ return result;
181
+ }
182
+
183
+ /**
184
+ * Get one agent's Contact Center performance summary (KPIs, per-day
185
+ * charts, live tasks, on-queue sessions) over a date/date-range, with a
186
+ * comparison window for deltas. Self allowed; another worker's summary
187
+ * requires the queue-manager scope.
188
+ *
189
+ * @param {Object} options - Parameters
190
+ * @param {string} options.workerId - Worker id to summarize
191
+ * @param {string} options.from - ISO-8601 window start
192
+ * @param {string} options.to - ISO-8601 window end
193
+ * @param {string} [options.compareFrom] - ISO-8601 comparison window start (defaults to the preceding equal-length period)
194
+ * @param {string} [options.compareTo] - ISO-8601 comparison window end
195
+ * @returns {Promise<Object>} result
196
+ * @returns {Object} result.kpis - tasksHandled, offersAccepted, avgHandleSec, slaMet, avgSentiment, composite, onQueue
197
+ * @returns {Array<Object>} result.tasksPerDay - [{date, count}]
198
+ * @returns {Array<Object>} result.trend - [{date, sentiment, slaPct}]
199
+ * @returns {Array<Object>} result.liveNow - Current tasks (only when the window includes today)
200
+ * @returns {Array<Object>} result.sessions - On-queue session log
201
+ *
202
+ * @example
203
+ * const summary = await sdk.taskRouter.cc.getAgentSummary({
204
+ * workerId: 'w1',
205
+ * from: '2026-08-12T00:00:00Z',
206
+ * to: '2026-08-19T00:00:00Z',
207
+ * });
208
+ * console.log(summary.kpis.tasksHandled.value);
209
+ */
210
+ async getAgentSummary(options = {}) {
211
+ const { workerId, from, to, compareFrom, compareTo } = options;
212
+
213
+ this.sdk.validateParams(
214
+ { workerId, from, to, compareFrom, compareTo },
215
+ {
216
+ workerId: { type: 'string', required: true },
217
+ from: { type: 'string', required: true },
218
+ to: { type: 'string', required: true },
219
+ compareFrom: { type: 'string', required: false },
220
+ compareTo: { type: 'string', required: false },
221
+ },
222
+ );
223
+
224
+ const query = { from, to };
225
+ if (compareFrom) query.compareFrom = compareFrom;
226
+ if (compareTo) query.compareTo = compareTo;
227
+
228
+ const result = await this.sdk._fetch(
229
+ `/taskRouter/cc/agents/${workerId}/summary`,
230
+ 'GET',
231
+ { query },
232
+ );
233
+ return result;
234
+ }
235
+
236
+ /**
237
+ * Log a worker into or out of a queue. Manager-only
238
+ * (`taskrouter:queue:manage`). Mutates the worker's queue membership
239
+ * through task-router (audited).
240
+ *
241
+ * @param {Object} options - Parameters
242
+ * @param {string} options.workerId - Worker id to update
243
+ * @param {string} options.queueId - Queue id to log in/out of
244
+ * @param {'login'|'logout'} options.action - Whether to add or remove the queue
245
+ * @returns {Promise<Object>} result
246
+ * @returns {string} result.workerId
247
+ * @returns {string} result.status
248
+ * @returns {Array<string>} result.queues - The worker's updated queue list
249
+ *
250
+ * @example
251
+ * await sdk.taskRouter.cc.setWorkerQueue({ workerId: 'w1', queueId: 'q1', action: 'login' });
252
+ */
253
+ async setWorkerQueue(options = {}) {
254
+ const { workerId, queueId, action } = options;
255
+
256
+ this.sdk.validateParams(
257
+ { workerId, queueId, action },
258
+ {
259
+ workerId: { type: 'string', required: true },
260
+ queueId: { type: 'string', required: true },
261
+ action: { type: 'string', required: true },
262
+ },
263
+ );
264
+
265
+ const result = await this.sdk._fetch(
266
+ `/taskRouter/cc/workers/${workerId}/queues`,
267
+ 'PUT',
268
+ { body: { queueId, action } },
269
+ );
270
+ return result;
271
+ }
272
+
273
+ /**
274
+ * Force a worker offline (out of every queue), closing their on-queue
275
+ * session. Manager-only (`taskrouter:queue:manage`, audited).
276
+ *
277
+ * @param {Object} options - Parameters
278
+ * @param {string} options.workerId - Worker id to log out
279
+ * @returns {Promise<Object>} result
280
+ * @returns {string} result.workerId
281
+ * @returns {string} result.status
282
+ * @returns {Array<string>} result.queues
283
+ *
284
+ * @example
285
+ * await sdk.taskRouter.cc.forceLogoutWorker({ workerId: 'w1' });
286
+ */
287
+ async forceLogoutWorker(options = {}) {
288
+ const { workerId } = options;
289
+
290
+ this.sdk.validateParams(
291
+ { workerId },
292
+ { workerId: { type: 'string', required: true } },
293
+ );
294
+
295
+ const result = await this.sdk._fetch(
296
+ `/taskRouter/cc/workers/${workerId}/forceLogout`,
297
+ 'POST',
298
+ {},
299
+ );
300
+ return result;
301
+ }
302
+ }
@@ -108,4 +108,58 @@ export class MetricsService {
108
108
  );
109
109
  return result;
110
110
  }
111
+
112
+ /**
113
+ * Get windowed queue/company metrics with a compare-window delta, per
114
+ * queue + an 'all' rollup (avg wait, avg handle, service level, longest
115
+ * wait, live depth/workers, and a waiting-count sparkline).
116
+ *
117
+ * @param {Object} params - Parameters
118
+ * @param {string[]} [params.queueIds] - Queue ids to scope to. Omit/empty for all queues.
119
+ * @param {string} params.from - ISO-8601 window start
120
+ * @param {string} params.to - ISO-8601 window end
121
+ * @param {string} params.compareFrom - ISO-8601 compare-window start
122
+ * @param {string} params.compareTo - ISO-8601 compare-window end
123
+ * @returns {Promise<Object>} result
124
+ * @returns {Object} result.window - `{from, to}`
125
+ * @returns {Object} result.compare - `{from, to}`
126
+ * @returns {Object} result.all - Rollup across every scoped queue (avgWaitSec, avgHandleSec, serviceLevelPct, longestWaitSec, depth, workersAvailable, workersTotal, waitingSparkline, deltas)
127
+ * @returns {Object} result.queues - Same shape as `result.all`, keyed by queueId
128
+ *
129
+ * @example
130
+ * const { all, queues } = await sdk.taskRouter.metrics.getWindow({
131
+ * queueIds: ['q1', 'q2'],
132
+ * from: '2026-08-19T22:00:00Z',
133
+ * to: '2026-08-19T23:00:00Z',
134
+ * compareFrom: '2026-08-19T21:00:00Z',
135
+ * compareTo: '2026-08-19T22:00:00Z',
136
+ * });
137
+ * console.log(all.serviceLevelPct, all.deltas.serviceLevelPct);
138
+ */
139
+ async getWindow(params = {}) {
140
+ const { queueIds, from, to, compareFrom, compareTo } = params;
141
+
142
+ this.sdk.validateParams(
143
+ { queueIds, from, to, compareFrom, compareTo },
144
+ {
145
+ queueIds: { type: 'array', required: false },
146
+ from: { type: 'string', required: true },
147
+ to: { type: 'string', required: true },
148
+ compareFrom: { type: 'string', required: false },
149
+ compareTo: { type: 'string', required: false },
150
+ },
151
+ );
152
+
153
+ const query = { from, to };
154
+ if (queueIds && queueIds.length) query.queueIds = queueIds.join(',');
155
+ if (compareFrom) query.compareFrom = compareFrom;
156
+ if (compareTo) query.compareTo = compareTo;
157
+
158
+ const result = await this.sdk._fetch(
159
+ '/taskRouter/metrics/window',
160
+ 'GET',
161
+ { query },
162
+ );
163
+ return result;
164
+ }
111
165
  }
@@ -1,6 +1,7 @@
1
1
  import { WorkerService } from './WorkerService.js';
2
2
  import { TaskService } from './TaskService.js';
3
3
  import { MetricsService } from './MetricsService.js';
4
+ import { CCService } from './CCService.js';
4
5
 
5
6
  export class TaskRouterService {
6
7
  constructor(sdk) {
@@ -8,5 +9,6 @@ export class TaskRouterService {
8
9
  this.worker = new WorkerService(sdk);
9
10
  this.task = new TaskService(sdk);
10
11
  this.metrics = new MetricsService(sdk);
12
+ this.cc = new CCService(sdk);
11
13
  }
12
14
  }
package/services/video.js CHANGED
@@ -230,6 +230,7 @@ export class VideoService {
230
230
  calendarProvider,
231
231
  vocabularyTerms,
232
232
  shareOcrEnabled,
233
+ earlyJoinMinutes,
233
234
  }) {
234
235
  this.sdk.validateParams(
235
236
  {
@@ -259,6 +260,7 @@ export class VideoService {
259
260
  calendarProvider,
260
261
  vocabularyTerms,
261
262
  shareOcrEnabled,
263
+ earlyJoinMinutes,
262
264
  },
263
265
  {
264
266
  name: { type: 'string', required: false },
@@ -287,6 +289,9 @@ export class VideoService {
287
289
  calendarProvider: { type: 'string', required: false },
288
290
  vocabularyTerms: { type: 'array', required: false },
289
291
  shareOcrEnabled: { type: 'boolean', required: false },
292
+ // 0 = strict at startTime, 5 = 5-min early window (api rejects
293
+ // other values); omit for no schedule-window enforcement.
294
+ earlyJoinMinutes: { type: 'number', required: false },
290
295
  },
291
296
  );
292
297
  const params = {
@@ -317,6 +322,7 @@ export class VideoService {
317
322
  calendarProvider,
318
323
  vocabularyTerms,
319
324
  shareOcrEnabled,
325
+ earlyJoinMinutes,
320
326
  },
321
327
  };
322
328
  const result = await this.sdk._fetch(`/video`, 'POST', params);
@@ -362,6 +368,8 @@ export class VideoService {
362
368
  validationSchema.vocabularyTerms = { type: 'array' };
363
369
  if ('shareOcrEnabled' in update)
364
370
  validationSchema.shareOcrEnabled = { type: 'boolean' };
371
+ if ('earlyJoinMinutes' in update)
372
+ validationSchema.earlyJoinMinutes = { type: 'number' };
365
373
 
366
374
  if (Object.keys(validationSchema).length > 0) {
367
375
  this.sdk.validateParams(update, validationSchema);
@@ -1057,7 +1065,10 @@ export class VideoService {
1057
1065
  },
1058
1066
  );
1059
1067
 
1060
- const result = await this.sdk._fetch(`/video/${roomId}/livePresence`, 'GET');
1068
+ const result = await this.sdk._fetch(
1069
+ `/video/${roomId}/livePresence`,
1070
+ 'GET',
1071
+ );
1061
1072
  return result;
1062
1073
  }
1063
1074
 
@@ -1274,7 +1285,11 @@ export class VideoService {
1274
1285
  },
1275
1286
  );
1276
1287
 
1277
- const result = await this.sdk._fetch(`/video/${roomId}/auto-name`, 'POST', {});
1288
+ const result = await this.sdk._fetch(
1289
+ `/video/${roomId}/auto-name`,
1290
+ 'POST',
1291
+ {},
1292
+ );
1278
1293
  return result;
1279
1294
  }
1280
1295
 
@@ -1411,4 +1426,173 @@ export class VideoService {
1411
1426
  );
1412
1427
  return result;
1413
1428
  }
1429
+
1430
+ /**
1431
+ * Get (or lazily create) the calling user's personal meeting room for this
1432
+ * account. Every account user has exactly one; the first call provisions
1433
+ * it server-side.
1434
+ *
1435
+ * NOTE: implemented flat (`getPersonalRoom`/`updatePersonalRoom`/
1436
+ * `regeneratePersonalRoomPin`/`resolvePersonalRoom`) rather than the
1437
+ * `sdk.video.personalRoom.{get,update,regeneratePin}` namespace named in
1438
+ * the meet-hub plan — this file has no existing nested-namespace
1439
+ * precedent, so flat matches every other method here.
1440
+ *
1441
+ * @param {string} [userId] - Optional target userId (admin viewing another user's room, e.g. Setup -> Users -> Meet). Defaults to the calling user.
1442
+ * @returns {Promise<{personalRoom: {id: string, slug: string, url: string|null, dialInPin: string, guestsCanStart: boolean}}>}
1443
+ */
1444
+ async getPersonalRoom(userId = null) {
1445
+ const params = { query: {} };
1446
+ if (userId) {
1447
+ this.sdk.validateParams(
1448
+ { userId },
1449
+ { userId: { type: 'string', required: true } },
1450
+ );
1451
+ params.query.userId = userId;
1452
+ }
1453
+ const result = await this.sdk._fetch('/video/personal-room', 'GET', params);
1454
+ return result;
1455
+ }
1456
+
1457
+ /**
1458
+ * Update the calling user's personal meeting room (slug and/or
1459
+ * guests-can-start). Omit a field to leave it unchanged.
1460
+ *
1461
+ * @param {Object} [update]
1462
+ * @param {string} [update.slug] - New slug (3-32 chars, lowercase/numbers/hyphens, not reserved). 409-equivalent BadRequestError on collision.
1463
+ * @param {boolean} [update.guestsCanStart] - Whether guests can start the room without the host present.
1464
+ * @param {string} [update.password] - New static room passcode.
1465
+ * @param {string} [update.userId] - Optional target userId (admin editing another user's room). Defaults to the calling user.
1466
+ * @returns {Promise<{personalRoom: {id: string, slug: string, url: string|null, dialInPin: string, guestsCanStart: boolean}}>}
1467
+ */
1468
+ async updatePersonalRoom({ slug, guestsCanStart, password, userId } = {}) {
1469
+ const validationSchema = {};
1470
+ if (slug !== undefined) validationSchema.slug = { type: 'string' };
1471
+ if (guestsCanStart !== undefined)
1472
+ validationSchema.guestsCanStart = { type: 'boolean' };
1473
+ // Static room passcode — 4-6 digits (api-enforced); applied to every
1474
+ // session the room link mints.
1475
+ if (password !== undefined) validationSchema.password = { type: 'string' };
1476
+ if (userId !== undefined) validationSchema.userId = { type: 'string' };
1477
+
1478
+ if (Object.keys(validationSchema).length > 0) {
1479
+ this.sdk.validateParams(
1480
+ { slug, guestsCanStart, password, userId },
1481
+ validationSchema,
1482
+ );
1483
+ }
1484
+
1485
+ const body = {};
1486
+ if (slug !== undefined) body.slug = slug;
1487
+ if (guestsCanStart !== undefined) body.guestsCanStart = guestsCanStart;
1488
+ if (password !== undefined) body.password = password;
1489
+ if (userId !== undefined) body.userId = userId;
1490
+
1491
+ const params = { body };
1492
+ const result = await this.sdk._fetch('/video/personal-room', 'PUT', params);
1493
+ return result;
1494
+ }
1495
+
1496
+ /**
1497
+ * Regenerate the dial-in PIN for the calling user's personal meeting room.
1498
+ *
1499
+ * @param {string} [userId] - Optional target userId (admin action). Defaults to the calling user.
1500
+ * @returns {Promise<{personalRoom: {id: string, dialInPin: string}}>}
1501
+ */
1502
+ async regeneratePersonalRoomPin(userId = null) {
1503
+ const body = {};
1504
+ if (userId) {
1505
+ this.sdk.validateParams(
1506
+ { userId },
1507
+ { userId: { type: 'string', required: true } },
1508
+ );
1509
+ body.userId = userId;
1510
+ }
1511
+ const result = await this.sdk._fetch(
1512
+ '/video/personal-room/regenerate-pin',
1513
+ 'POST',
1514
+ { body },
1515
+ );
1516
+ return result;
1517
+ }
1518
+
1519
+ /**
1520
+ * Live availability dry-run for a personal-room slug (settings editor).
1521
+ *
1522
+ * @param {string} slug - Candidate slug.
1523
+ * @param {string} [userId] - Optional target userId (admin action). Defaults to the calling user.
1524
+ * @returns {Promise<{slug: string, available: boolean, reason?: 'invalid'|'taken'}>}
1525
+ */
1526
+ async checkPersonalRoomSlug(slug, userId = null) {
1527
+ this.sdk.validateParams(
1528
+ { slug, userId },
1529
+ {
1530
+ slug: { type: 'string', required: true },
1531
+ userId: { type: 'string', required: false },
1532
+ },
1533
+ );
1534
+ const query = { slug };
1535
+ if (userId) query.userId = userId;
1536
+ const result = await this.sdk._fetch(
1537
+ '/video/personal-room/slug-available',
1538
+ 'GET',
1539
+ { query },
1540
+ );
1541
+ return result;
1542
+ }
1543
+
1544
+ /**
1545
+ * Regenerate the static web passcode for the calling user's personal
1546
+ * meeting room (a separate secret from the dial-in PIN). Sessions minted
1547
+ * after this use the new value.
1548
+ *
1549
+ * @param {string} [userId] - Optional target userId (admin action). Defaults to the calling user.
1550
+ * @returns {Promise<{personalRoom: {id: string, password: string}}>}
1551
+ */
1552
+ async regeneratePersonalRoomPassword(userId = null) {
1553
+ const body = {};
1554
+ if (userId) {
1555
+ this.sdk.validateParams(
1556
+ { userId },
1557
+ { userId: { type: 'string', required: true } },
1558
+ );
1559
+ body.userId = userId;
1560
+ }
1561
+ const result = await this.sdk._fetch(
1562
+ '/video/personal-room/regenerate-password',
1563
+ 'POST',
1564
+ { body },
1565
+ );
1566
+ return result;
1567
+ }
1568
+
1569
+ /**
1570
+ * Resolve a personal-room slug to a live session (join page). Guest-capable
1571
+ * — mints/claims a fresh meeting if none is live, or returns the already
1572
+ * -claimed session; if the caller is an unauthenticated guest and the room
1573
+ * doesn't allow guests to start, returns `waitingForHost: true` instead.
1574
+ * Guests only receive `password` when they supply the room's static
1575
+ * passcode; otherwise `passwordRequired: true` is returned and the join
1576
+ * page should prompt.
1577
+ *
1578
+ * @param {string} slug - The personal room's slug.
1579
+ * @param {{password?: string}} [options] - The room's static passcode, when known.
1580
+ * @returns {Promise<{claimed: boolean, meetingId?: string, friendlyName?: string, password?: string, passwordRequired?: boolean, waitingForHost?: boolean}>}
1581
+ */
1582
+ async resolvePersonalRoom(slug, { password } = {}) {
1583
+ this.sdk.validateParams(
1584
+ { slug, password },
1585
+ {
1586
+ slug: { type: 'string', required: true },
1587
+ password: { type: 'string', required: false },
1588
+ },
1589
+ );
1590
+
1591
+ const result = await this.sdk._fetch(
1592
+ `/video/personal-room/resolve/${slug}`,
1593
+ 'POST',
1594
+ { body: { password } },
1595
+ );
1596
+ return result;
1597
+ }
1414
1598
  }
package/services/voice.js CHANGED
@@ -22,6 +22,34 @@ export class VoiceService {
22
22
  return result;
23
23
  }
24
24
 
25
+ async transcription({
26
+ cdrId,
27
+ callId,
28
+ action = 'start',
29
+ direction = 'sendrecv',
30
+ }) {
31
+ this.sdk.validateParams(
32
+ { callId, cdrId, action, direction },
33
+ {
34
+ cdrId: { type: 'string', required: false },
35
+ callId: { type: 'string', required: false },
36
+ action: { type: 'string', required: false },
37
+ direction: { type: 'string', required: false },
38
+ },
39
+ );
40
+
41
+ const params = {
42
+ body: { callId, cdrId, action, direction },
43
+ };
44
+
45
+ const result = await this.sdk._fetch(
46
+ `/voice/transcription/`,
47
+ 'POST',
48
+ params,
49
+ );
50
+ return result;
51
+ }
52
+
25
53
  async call({ to, from, destination, app, timeout, customHeaders, statusWebhook }) {
26
54
  this.sdk.validateParams(
27
55
  { to, from, destination, app, timeout, customHeaders, statusWebhook },