@spekoai/sdk 0.4.3 → 0.5.2

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +88 -0
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1 -0
  6. package/dist/lib/client.d.ts +13 -1
  7. package/dist/lib/client.d.ts.map +1 -1
  8. package/dist/lib/client.js +22 -2
  9. package/dist/lib/http.d.ts +12 -4
  10. package/dist/lib/http.d.ts.map +1 -1
  11. package/dist/lib/http.js +31 -14
  12. package/dist/lib/resources/agents.d.ts +12 -2
  13. package/dist/lib/resources/agents.d.ts.map +1 -1
  14. package/dist/lib/resources/agents.js +12 -4
  15. package/dist/lib/resources/calls.d.ts +19 -1
  16. package/dist/lib/resources/calls.d.ts.map +1 -1
  17. package/dist/lib/resources/calls.js +22 -0
  18. package/dist/lib/resources/knowledge-bases.d.ts +1 -1
  19. package/dist/lib/resources/knowledge-bases.js +1 -1
  20. package/dist/lib/resources/phone-numbers.d.ts +2 -1
  21. package/dist/lib/resources/phone-numbers.d.ts.map +1 -1
  22. package/dist/lib/resources/phone-numbers.js +2 -1
  23. package/dist/lib/resources/realtime.d.ts +3 -5
  24. package/dist/lib/resources/realtime.d.ts.map +1 -1
  25. package/dist/lib/resources/realtime.js +829 -91
  26. package/dist/lib/resources/sessions.d.ts +53 -0
  27. package/dist/lib/resources/sessions.d.ts.map +1 -0
  28. package/dist/lib/resources/sessions.js +166 -0
  29. package/dist/lib/resources/sms.d.ts +80 -0
  30. package/dist/lib/resources/sms.d.ts.map +1 -0
  31. package/dist/lib/resources/sms.js +152 -0
  32. package/dist/lib/resources/synthesize.d.ts.map +1 -1
  33. package/dist/lib/resources/synthesize.js +12 -6
  34. package/dist/lib/resources/transcribe.d.ts.map +1 -1
  35. package/dist/lib/resources/transcribe.js +9 -2
  36. package/dist/lib/resources/voice.d.ts +283 -1
  37. package/dist/lib/resources/voice.d.ts.map +1 -1
  38. package/dist/lib/resources/voice.js +345 -0
  39. package/dist/lib/resources/webhooks.d.ts +25 -0
  40. package/dist/lib/resources/webhooks.d.ts.map +1 -0
  41. package/dist/lib/resources/webhooks.js +46 -0
  42. package/dist/lib/types/index.d.ts +998 -9
  43. package/dist/lib/types/index.d.ts.map +1 -1
  44. package/dist/lib/voice-contract.d.ts +280 -0
  45. package/dist/lib/voice-contract.d.ts.map +1 -0
  46. package/dist/lib/voice-contract.js +115 -0
  47. package/package.json +2 -1
  48. package/src/index.ts +212 -0
  49. package/src/lib/client.ts +169 -0
  50. package/src/lib/errors.ts +28 -0
  51. package/src/lib/http.ts +442 -0
  52. package/src/lib/resources/agents.ts +211 -0
  53. package/src/lib/resources/callbacks.ts +40 -0
  54. package/src/lib/resources/calls.ts +113 -0
  55. package/src/lib/resources/complete.ts +63 -0
  56. package/src/lib/resources/credits.ts +41 -0
  57. package/src/lib/resources/knowledge-bases.ts +199 -0
  58. package/src/lib/resources/phone-numbers.ts +109 -0
  59. package/src/lib/resources/realtime-globals.d.ts +31 -0
  60. package/src/lib/resources/realtime.spec.ts +565 -0
  61. package/src/lib/resources/realtime.ts +1169 -0
  62. package/src/lib/resources/sessions.ts +191 -0
  63. package/src/lib/resources/sms.ts +214 -0
  64. package/src/lib/resources/synthesize.ts +101 -0
  65. package/src/lib/resources/transcribe.ts +91 -0
  66. package/src/lib/resources/usage.ts +24 -0
  67. package/src/lib/resources/voice.ts +426 -0
  68. package/src/lib/resources/voices.ts +32 -0
  69. package/src/lib/resources/webhooks.ts +67 -0
  70. package/src/lib/types/index.ts +2409 -0
  71. package/src/lib/voice-contract.ts +358 -0
@@ -0,0 +1,426 @@
1
+ import type { HttpClient } from '../http.js';
2
+ import type {
3
+ CallControlDialParams,
4
+ CallControlDialResult,
5
+ CallControlListParams,
6
+ VoiceDialParams,
7
+ VoiceDialResult,
8
+ } from '../types/index.js';
9
+ import type {
10
+ BridgeCommandPayload,
11
+ BrokerPresenceResource,
12
+ BrokerPresenceStatus,
13
+ CallCommand,
14
+ CallCommandPayload,
15
+ CallCommandResult,
16
+ CallEventResource,
17
+ CallJoinCredentials,
18
+ CallResource,
19
+ DtmfCommandPayload,
20
+ HangupCommandPayload,
21
+ TransferCommandPayload,
22
+ } from '../voice-contract.js';
23
+
24
+ /**
25
+ * Outbound phone calls via Speko's managed telephony gateway.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const { sessionId, status } = await speko.voice.dial({
30
+ * to: '+12015551234',
31
+ * intent: { language: 'en', optimizeFor: 'latency' },
32
+ * systemPrompt: 'You are a helpful Speko assistant. Greet the caller in English.',
33
+ * });
34
+ * console.log('dialing:', sessionId, status);
35
+ * ```
36
+ */
37
+ export class Voice {
38
+ constructor(private readonly http: HttpClient) {}
39
+
40
+ /**
41
+ * Place an outbound call. The destination's phone rings; once they pick
42
+ * up, audio bridges to a Speko worker running the configured pipeline
43
+ * (STT→LLM→TTS) on the media transport.
44
+ *
45
+ * Lifecycle is reflected in the `voice_session` row's `status`.
46
+ */
47
+ async dial(params: VoiceDialParams): Promise<VoiceDialResult> {
48
+ return this.http.post<VoiceDialResult>('/v1/sessions/phone', params);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Programmable voice — call control for **human** calls.
54
+ *
55
+ * Modeled on Telnyx Call Control, and the mapping is meant to be mechanical:
56
+ * every leg of a call has an opaque `controlId` (Telnyx's `call_control_id`),
57
+ * and every verb is addressed to that handle rather than to a room, a session,
58
+ * or a participant. `speko.callControl.hangup(controlId)` is
59
+ * `POST /calls/{call_control_id}/actions/hangup`, and so on down the list.
60
+ *
61
+ * This is a different product from {@link Voice.dial}, which dials an **AI
62
+ * agent** out over the same telephony gateway. Here the parties are people:
63
+ * brokers on a browser softphone and phones on the PSTN. An AI leg can still be
64
+ * added to a call, which is what makes escalating an agent call to a human an
65
+ * ordinary {@link CallControl.bridge} rather than a special case.
66
+ *
67
+ * ### Porting from Telnyx — the two things that differ
68
+ *
69
+ * 1. **`hold` is silent by default.** There is no hold primitive on the
70
+ * underlying transport; hold is synthesized by isolating subscriptions in
71
+ * both directions. With no music-on-hold source configured the held party
72
+ * hears nothing at all.
73
+ * If your Telnyx flow relied on `playback_start` looping hold music, that
74
+ * has no equivalent yet.
75
+ * 2. **There is no SIP registrar.** A desk phone, a third-party softphone, or
76
+ * another PBX cannot register against Speko and be rung. Human legs are
77
+ * browser legs — `browser` and `pstn` are the only human-reachable leg
78
+ * kinds. Anything in your Telnyx setup that terminated on a SIP connection
79
+ * needs to become a PSTN transfer or a browser client.
80
+ *
81
+ * A handful of Telnyx events also have no faithful equivalent and are absent
82
+ * rather than approximated: no early-media event, and no way to tell a 486 Busy
83
+ * from a generic rejection.
84
+ *
85
+ * ### Broker identity
86
+ *
87
+ * Human calling uses the API key for organization authentication and the
88
+ * `brokerId` passed to `new Speko({ apiKey, brokerId })` for softphone identity.
89
+ * The SDK automatically includes that id on presence, dial, and join requests.
90
+ * Commands and org-wide reads do not need it on the wire.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * const speko = new Speko({ apiKey: process.env.SPEKO_API_KEY, brokerId: 'broker-42' });
95
+ *
96
+ * // Come online, then hold a presence connection open so inbound can ring you.
97
+ * await speko.callControl.register();
98
+ * const presence = await speko.callControl.presenceToken();
99
+ * setInterval(() => speko.callControl.heartbeat(), PRESENCE_STALE_AFTER_MS / 3);
100
+ *
101
+ * // Dial out. The browser leg is yours; the PSTN leg is the customer.
102
+ * const { call, join } = await speko.callControl.dial({ to: '+12015551234' });
103
+ * const mine = call.legs.find((leg) => leg.kind === 'browser');
104
+ *
105
+ * // Park the customer, consult a colleague, then hand the call over.
106
+ * await speko.callControl.hold(mine!.controlId);
107
+ * await speko.callControl.transfer(mine!.controlId, { to: '+12015559876', mode: 'warm' });
108
+ * ```
109
+ */
110
+ export class CallControl {
111
+ constructor(
112
+ private readonly http: HttpClient,
113
+ private readonly configuredBrokerId?: string,
114
+ ) {}
115
+
116
+ // --- Calls ---------------------------------------------------------------
117
+
118
+ /**
119
+ * Place an outbound PSTN call — Telnyx `POST /calls`.
120
+ *
121
+ * Resolves as soon as the call exists, with the PSTN leg in `initiating` /
122
+ * `ringing`; it does **not** wait for anyone to answer. Watch
123
+ * {@link CallControl.events} (or the browser leg's own connection) for
124
+ * `call.answered`.
125
+ *
126
+ * **Resolves to `{ call, join }`, not to a bare call.** The join credentials
127
+ * for the dialing broker's own browser leg come back with the call because the
128
+ * softphone must already be in the room when the far end answers — fetch them
129
+ * in a second round trip and the first moments of the conversation are
130
+ * silence. `call` carries both legs, so the `controlId` every later command
131
+ * needs is in hand without a read: the `browser` leg is the broker's
132
+ * softphone, the `pstn` leg is the far end.
133
+ *
134
+ * The browser leg belongs to the `brokerId` configured on this SDK instance.
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * const { call, join } = await speko.callControl.dial({ to: '+12015551234' });
139
+ * const mine = call.legs.find((leg) => leg.kind === 'browser')!;
140
+ * // In the browser, connect with the credentials you were just handed:
141
+ * // VoiceConversation.create({ transportToken: join.token, transportUrl: join.url })
142
+ * ```
143
+ */
144
+ dial(params: CallControlDialParams): Promise<CallControlDialResult> {
145
+ return this.http.post<CallControlDialResult>('/v1/voice/calls', {
146
+ ...params,
147
+ brokerId: this.brokerId(),
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Mint room credentials for one of **your own** browser legs — how a broker who
153
+ * was rung answers.
154
+ *
155
+ * {@link CallControl.dial} hands you credentials for the leg it created, so
156
+ * this is the inbound counterpart: a `RingOffer` arrives on the presence
157
+ * channel carrying the callee's `controlId`, you `join(controlId)` to get into
158
+ * the room, then {@link CallControl.answer} to connect the audio. Joining
159
+ * first is not optional — answering a leg whose participant is not in the room
160
+ * yet buys you the same silent opening that a post-hoc dial join would.
161
+ *
162
+ * Tokens are minted per join and short-lived by design. A token is a live
163
+ * credential for a room that may carry a customer conversation, so re-join on
164
+ * reconnect instead of caching one; there is no endpoint that hands back a
165
+ * token you already used.
166
+ *
167
+ * Scoped to the configured broker's own legs. A leg belonging to another
168
+ * broker returns `NOT_FOUND` — deliberately indistinguishable from a leg that
169
+ * does not exist, so a `controlId` cannot be probed for existence.
170
+ */
171
+ join(controlId: string): Promise<CallJoinCredentials> {
172
+ return this.http.post<CallJoinCredentials>(
173
+ `/v1/voice/legs/${encodeURIComponent(controlId)}/join`,
174
+ { brokerId: this.brokerId() },
175
+ );
176
+ }
177
+
178
+ /** Retrieve one call and every leg on it. */
179
+ get(callId: string): Promise<CallResource> {
180
+ return this.http.get<CallResource>(`/v1/voice/calls/${encodeURIComponent(callId)}`);
181
+ }
182
+
183
+ /**
184
+ * List the org's calls, newest first. Filters are AND-ed, and `status` is
185
+ * validated against the contract's `CallStatus` — a value outside it is a
186
+ * `VALIDATION_ERROR`, not a silently ignored filter, so this type is worth
187
+ * respecting rather than casting past.
188
+ */
189
+ list(params: CallControlListParams = {}): Promise<{ calls: CallResource[] }> {
190
+ const query = new URLSearchParams();
191
+ if (params.status) query.set('status', params.status);
192
+ if (params.direction) query.set('direction', params.direction);
193
+ if (params.brokerId) query.set('brokerId', params.brokerId);
194
+ if (params.limit !== undefined) query.set('limit', String(params.limit));
195
+ const suffix = query.toString() ? `?${query}` : '';
196
+ return this.http.get<{ calls: CallResource[] }>(`/v1/voice/calls${suffix}`);
197
+ }
198
+
199
+ /**
200
+ * Event history for a call, oldest first — the recorded equivalent of the
201
+ * Telnyx webhook stream, so a flow you drove off webhooks can be reconstructed
202
+ * (or reconciled) after the fact.
203
+ */
204
+ events(callId: string): Promise<{ events: CallEventResource[] }> {
205
+ return this.http.get<{ events: CallEventResource[] }>(
206
+ `/v1/voice/calls/${encodeURIComponent(callId)}/events`,
207
+ );
208
+ }
209
+
210
+ // --- Commands ------------------------------------------------------------
211
+
212
+ /**
213
+ * Answer a ringing leg — Telnyx `actions/answer`.
214
+ *
215
+ * Only meaningful on an inbound leg that is still `ringing`; answering a leg
216
+ * that is already `active` resolves with `noop: true`.
217
+ */
218
+ answer(controlId: string): Promise<CallCommandResult> {
219
+ return this.command(controlId, 'answer');
220
+ }
221
+
222
+ /**
223
+ * Hang a leg up — Telnyx `actions/hangup`.
224
+ *
225
+ * Drops this leg only. The call ends when it runs out of live legs, so
226
+ * hanging up one side of a two-party call ends the call; hanging up one of
227
+ * three legs leaves the other two talking.
228
+ *
229
+ * `reason` is free text and comes back as the leg's `endReason`.
230
+ */
231
+ hangup(controlId: string, params: HangupCommandPayload = {}): Promise<CallCommandResult> {
232
+ return this.command(controlId, 'hangup', params);
233
+ }
234
+
235
+ /**
236
+ * Bridge this leg to another — Telnyx `actions/bridge`.
237
+ *
238
+ * Both legs must be live. `bridgeTo` is the *other* leg's `controlId`, not a
239
+ * call id: bridging is leg-to-leg, which is how an AI leg and a broker leg
240
+ * end up in the same conversation. A target that is not a live leg of a call
241
+ * your org owns fails with `BRIDGE_TARGET_INVALID`.
242
+ */
243
+ bridge(controlId: string, params: BridgeCommandPayload): Promise<CallCommandResult> {
244
+ return this.command(controlId, 'bridge', params);
245
+ }
246
+
247
+ /**
248
+ * Put a leg on hold — the leg stops hearing and being heard, and its status
249
+ * becomes `held`.
250
+ *
251
+ * **Diverges from Telnyx.** There is no hold primitive on the transport, so
252
+ * hold is synthesized by isolating subscriptions in both directions. Without
253
+ * a music-on-hold source configured the held party hears **silence**, not hold
254
+ * music — worth saying out loud in your UI, because callers read silence as a
255
+ * dropped call.
256
+ *
257
+ * Hold is a distinct state from {@link CallControl.mute}: a muted leg still
258
+ * hears the call, a held one does not.
259
+ */
260
+ hold(controlId: string): Promise<CallCommandResult> {
261
+ return this.command(controlId, 'hold');
262
+ }
263
+
264
+ /** Take a leg off hold, restoring audio in both directions. */
265
+ unhold(controlId: string): Promise<CallCommandResult> {
266
+ return this.command(controlId, 'unhold');
267
+ }
268
+
269
+ /**
270
+ * Stop publishing this leg's audio to the call. The leg keeps hearing
271
+ * everyone else — for "they can't hear me but I can hear them", which is what
272
+ * a softphone mute button means. To silence the far end instead, use
273
+ * {@link CallControl.hold}.
274
+ */
275
+ mute(controlId: string): Promise<CallCommandResult> {
276
+ return this.command(controlId, 'mute');
277
+ }
278
+
279
+ /** Resume publishing this leg's audio. */
280
+ unmute(controlId: string): Promise<CallCommandResult> {
281
+ return this.command(controlId, 'unmute');
282
+ }
283
+
284
+ /**
285
+ * Send DTMF digits out of this leg — Telnyx `actions/send_dtmf`.
286
+ *
287
+ * Accepts `0-9`, `*`, `#`, `,`, and `w` for a pause, up to 64 characters;
288
+ * anything else is rejected with `VALIDATION_ERROR`. Pauses matter when
289
+ * you're driving an IVR that swallows digits sent too early — `"w1w2"` beats
290
+ * `"12"` on most menus.
291
+ *
292
+ * **Address it to the `pstn` leg**, the leg the tones are *for* — any other leg
293
+ * kind fails with `UNSUPPORTED_COMMAND`, since DTMF is only meaningful towards
294
+ * a phone. That the server relays the tones out through an in-room browser leg
295
+ * (only a participant inside the room can publish SIP DTMF) is an
296
+ * implementation detail; it does not change which `controlId` you name.
297
+ */
298
+ dtmf(controlId: string, params: DtmfCommandPayload): Promise<CallCommandResult> {
299
+ return this.command(controlId, 'dtmf', params);
300
+ }
301
+
302
+ /**
303
+ * Transfer a leg elsewhere — Telnyx `actions/transfer`.
304
+ *
305
+ * `to` is an E.164 number for a PSTN transfer, or the `controlId` of a broker
306
+ * leg for an internal one.
307
+ *
308
+ * `mode: 'blind'` hands the call off and drops this leg immediately.
309
+ * `mode: 'warm'` moves this leg into a fresh consultation room and dials the
310
+ * destination into it, so the two humans can talk before the handoff
311
+ * completes. Complete it by {@link CallControl.bridge}-ing the consultation
312
+ * leg back into the original call.
313
+ *
314
+ * **The far end is left alone in the original room, not held.** They hear
315
+ * silence because nobody else is in there — but their leg stays `active` with
316
+ * `onHold: false`, and no `call.hold` event fires, so a UI that renders "on
317
+ * hold" from `leg.onHold` will show them as live. Call
318
+ * {@link CallControl.hold} on that leg yourself before transferring if you
319
+ * want the state to say what the caller is experiencing.
320
+ *
321
+ * Resolves once the transfer is *initiated*, and the two modes then report
322
+ * differently — do not wait on the wrong event:
323
+ *
324
+ * - `blind` emits `call.transfer.completed` once the carrier owns the leg (or
325
+ * `call.transfer.failed` if the REFER was rejected).
326
+ * - `warm` emits `call.transfer.initiated` only. There is no
327
+ * `call.transfer.completed` for it, because *you* complete it: bridge the
328
+ * consultation leg back into the original call, which emits `call.bridged`.
329
+ * `call.transfer.failed` still fires if setting the consultation up failed.
330
+ */
331
+ transfer(controlId: string, params: TransferCommandPayload): Promise<CallCommandResult> {
332
+ return this.command(controlId, 'transfer', params);
333
+ }
334
+
335
+ // --- Broker presence -----------------------------------------------------
336
+
337
+ /**
338
+ * Come online: mark the configured broker `available` so inbound calls can
339
+ * be routed to them.
340
+ *
341
+ * **Not a SIP registration** despite the name — there is no registrar here,
342
+ * and no endpoint to bind. This flips a flag; what actually makes a browser
343
+ * reachable is holding the connection minted by
344
+ * {@link CallControl.presenceToken} open and calling
345
+ * {@link CallControl.heartbeat} on a timer.
346
+ *
347
+ * Equivalent to `setStatus('available')`.
348
+ *
349
+ * Requires `brokerId` on the SDK instance.
350
+ */
351
+ register(): Promise<BrokerPresenceResource> {
352
+ return this.setStatus('available');
353
+ }
354
+
355
+ /**
356
+ * Refresh the broker's heartbeat. Presence goes stale after
357
+ * `PRESENCE_STALE_AFTER_MS`, and a stale broker is treated as offline for
358
+ * routing purposes whatever their stored status says — a crashed tab must not
359
+ * black-hole calls. Call this on an interval comfortably inside that window;
360
+ * a third of it is a good default.
361
+ *
362
+ * Refreshes the broker configured on this SDK instance.
363
+ */
364
+ heartbeat(): Promise<BrokerPresenceResource> {
365
+ return this.http.post<BrokerPresenceResource>('/v1/voice/presence/heartbeat', {
366
+ brokerId: this.brokerId(),
367
+ });
368
+ }
369
+
370
+ /**
371
+ * Set the broker's availability. `busy` and `away` both keep the heartbeat
372
+ * alive while diverting inbound elsewhere; `offline` takes them out of
373
+ * routing entirely.
374
+ *
375
+ * Always sets the broker configured on this SDK instance.
376
+ */
377
+ setStatus(status: BrokerPresenceStatus): Promise<BrokerPresenceResource> {
378
+ return this.http.put<BrokerPresenceResource>('/v1/voice/presence', {
379
+ status,
380
+ brokerId: this.brokerId(),
381
+ });
382
+ }
383
+
384
+ /**
385
+ * Mint the credentials a browser needs to hold its presence connection open
386
+ * and be rung. Hand `token` and `url` to `@spekoai/client`.
387
+ *
388
+ * A live credential with a real expiry — mint it when the client connects,
389
+ * re-mint on reconnect, don't stockpile it. A broker with no presence
390
+ * connection open cannot be rung even while `available`: inbound to an
391
+ * unreachable broker fails with `NO_BROKER_AVAILABLE`.
392
+ *
393
+ * What arrives on that connection is a {@link PresenceMessage} — the ring
394
+ * offers and live call events the softphone reacts to.
395
+ *
396
+ * The room minted belongs to the broker configured on this SDK instance.
397
+ */
398
+ presenceToken(): Promise<CallJoinCredentials> {
399
+ return this.http.post<CallJoinCredentials>('/v1/voice/presence/token', {
400
+ brokerId: this.brokerId(),
401
+ });
402
+ }
403
+
404
+ private brokerId(): string {
405
+ if (this.configuredBrokerId) return this.configuredBrokerId;
406
+ throw new Error(
407
+ 'Speko: brokerId is required for human-calling presence, dial, and join methods; pass it to new Speko({ apiKey, brokerId })',
408
+ );
409
+ }
410
+
411
+ /**
412
+ * Every command is one POST to the leg's action endpoint, and every one of
413
+ * them resolves to the leg's post-command state — so a client never has to
414
+ * re-read to find out what it just did.
415
+ */
416
+ private command(
417
+ controlId: string,
418
+ command: CallCommand,
419
+ payload: CallCommandPayload = {},
420
+ ): Promise<CallCommandResult> {
421
+ return this.http.post<CallCommandResult>(
422
+ `/v1/voice/legs/${encodeURIComponent(controlId)}/actions/${command}`,
423
+ payload,
424
+ );
425
+ }
426
+ }
@@ -0,0 +1,32 @@
1
+ import type { HttpClient } from '../http.js';
2
+ import type { VoicesListParams, VoicesListResult } from '../types/index.js';
3
+
4
+ /**
5
+ * Read-only TTS voice catalog. Use this to browse the curated list of
6
+ * voices per provider before calling `speko.synthesize` — handy for
7
+ * picking a voice id without reading each provider's API docs.
8
+ *
9
+ * ElevenLabs voices are account-scoped and are NOT returned by this
10
+ * endpoint; the response's `providers` array sets
11
+ * `voicesFetchedLive: true` on the elevenlabs entry as a signal to
12
+ * fetch them directly from ElevenLabs at runtime.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * // All voices, every provider:
17
+ * const { voices, providers } = await speko.voices.list();
18
+ *
19
+ * // Just Cartesia:
20
+ * const cartesia = await speko.voices.list({ provider: 'cartesia' });
21
+ * ```
22
+ */
23
+ export class Voices {
24
+ constructor(private readonly http: HttpClient) {}
25
+
26
+ async list(params?: VoicesListParams): Promise<VoicesListResult> {
27
+ const query = new URLSearchParams();
28
+ if (params?.provider) query.set('provider', params.provider);
29
+ const qs = query.toString();
30
+ return this.http.get<VoicesListResult>(`/v1/voices${qs ? `?${qs}` : ''}`);
31
+ }
32
+ }
@@ -0,0 +1,67 @@
1
+ import type { HttpClient } from '../http.js';
2
+ import type {
3
+ WebhookDeliveryDetail,
4
+ WebhookDeliveryListParams,
5
+ WebhookDeliveryPage,
6
+ WebhookEndpoint,
7
+ WebhookEndpointInput,
8
+ WebhookEndpointUpdate,
9
+ } from '../types/index.js';
10
+
11
+ /** Organization-owned lifecycle webhook endpoints and their delivery history. */
12
+ export class Webhooks {
13
+ readonly deliveries: WebhookDeliveries;
14
+
15
+ constructor(private readonly http: HttpClient) {
16
+ this.deliveries = new WebhookDeliveries(http);
17
+ }
18
+
19
+ async list(): Promise<WebhookEndpoint[]> {
20
+ const response = await this.http.get<{ data: WebhookEndpoint[] }>('/v1/webhooks');
21
+ return response.data;
22
+ }
23
+
24
+ create(params: WebhookEndpointInput): Promise<WebhookEndpoint> {
25
+ return this.http.post<WebhookEndpoint>('/v1/webhooks', params);
26
+ }
27
+
28
+ get(endpointId: string): Promise<WebhookEndpoint> {
29
+ return this.http.get<WebhookEndpoint>(`/v1/webhooks/${encodeURIComponent(endpointId)}`);
30
+ }
31
+
32
+ update(endpointId: string, params: WebhookEndpointUpdate): Promise<WebhookEndpoint> {
33
+ return this.http.patch<WebhookEndpoint>(
34
+ `/v1/webhooks/${encodeURIComponent(endpointId)}`,
35
+ params,
36
+ );
37
+ }
38
+
39
+ async delete(endpointId: string): Promise<void> {
40
+ await this.http.delete<unknown>(`/v1/webhooks/${encodeURIComponent(endpointId)}`);
41
+ }
42
+ }
43
+
44
+ export class WebhookDeliveries {
45
+ constructor(private readonly http: HttpClient) {}
46
+
47
+ list(params: WebhookDeliveryListParams = {}): Promise<WebhookDeliveryPage> {
48
+ const query = new URLSearchParams();
49
+ for (const [key, value] of Object.entries(params)) {
50
+ if (value !== undefined) query.set(key, String(value));
51
+ }
52
+ const suffix = query.size > 0 ? `?${query.toString()}` : '';
53
+ return this.http.get<WebhookDeliveryPage>(`/v1/webhook-deliveries${suffix}`);
54
+ }
55
+
56
+ get(deliveryId: string): Promise<WebhookDeliveryDetail> {
57
+ return this.http.get<WebhookDeliveryDetail>(
58
+ `/v1/webhook-deliveries/${encodeURIComponent(deliveryId)}`,
59
+ );
60
+ }
61
+
62
+ redeliver(
63
+ deliveryId: string,
64
+ ): Promise<{ delivered: boolean; httpStatus: number | null; error: string | null }> {
65
+ return this.http.post(`/v1/webhook-deliveries/${encodeURIComponent(deliveryId)}/redeliver`, {});
66
+ }
67
+ }