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