@whooshbang/sdk 5.1.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +232 -83
- package/dist/cli/commands.js +2 -0
- package/dist/cli/commands.js.map +1 -1
- package/dist/client.d.ts +139 -1
- package/dist/client.js +448 -4
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/interactions.js +18 -3
- package/dist/interactions.js.map +1 -1
- package/dist/recipient/element.js +641 -14
- package/dist/recipient/element.js.map +1 -1
- package/dist/recipient/i18n/ar.js +77 -0
- package/dist/recipient/i18n/ar.js.map +1 -1
- package/dist/recipient/i18n/catalog.d.ts +75 -0
- package/dist/recipient/i18n/catalog.js.map +1 -1
- package/dist/recipient/i18n/de.js +82 -0
- package/dist/recipient/i18n/de.js.map +1 -1
- package/dist/recipient/i18n/en.js +82 -0
- package/dist/recipient/i18n/en.js.map +1 -1
- package/dist/recipient/i18n/es.js +82 -0
- package/dist/recipient/i18n/es.js.map +1 -1
- package/dist/recipient/i18n/fa.js +77 -0
- package/dist/recipient/i18n/fa.js.map +1 -1
- package/dist/recipient/i18n/fr.js +82 -0
- package/dist/recipient/i18n/fr.js.map +1 -1
- package/dist/recipient/i18n/hi.js +82 -0
- package/dist/recipient/i18n/hi.js.map +1 -1
- package/dist/recipient/i18n/id.js +82 -0
- package/dist/recipient/i18n/id.js.map +1 -1
- package/dist/recipient/i18n/it.js +82 -0
- package/dist/recipient/i18n/it.js.map +1 -1
- package/dist/recipient/i18n/nl.js +82 -0
- package/dist/recipient/i18n/nl.js.map +1 -1
- package/dist/recipient/i18n/pl.js +82 -0
- package/dist/recipient/i18n/pl.js.map +1 -1
- package/dist/recipient/i18n/pt-BR.js +82 -0
- package/dist/recipient/i18n/pt-BR.js.map +1 -1
- package/dist/recipient/i18n/ru.js +82 -0
- package/dist/recipient/i18n/ru.js.map +1 -1
- package/dist/recipient/i18n/tr.js +82 -0
- package/dist/recipient/i18n/tr.js.map +1 -1
- package/dist/recipient/i18n/uk.js +82 -0
- package/dist/recipient/i18n/uk.js.map +1 -1
- package/dist/recipient/i18n/zh-Hans.js +77 -0
- package/dist/recipient/i18n/zh-Hans.js.map +1 -1
- package/dist/recipient/order-section.d.ts +41 -0
- package/dist/recipient/order-section.js +547 -0
- package/dist/recipient/order-section.js.map +1 -0
- package/dist/recipient/order-view.d.ts +123 -0
- package/dist/recipient/order-view.js +304 -0
- package/dist/recipient/order-view.js.map +1 -0
- package/dist/recipient/platform-mark.d.ts +1 -0
- package/dist/recipient/platform-mark.js +57 -0
- package/dist/recipient/platform-mark.js.map +1 -0
- package/dist/recipient/quiet-section.d.ts +47 -0
- package/dist/recipient/quiet-section.js +490 -0
- package/dist/recipient/quiet-section.js.map +1 -0
- package/dist/recipient/quiet-view.d.ts +84 -0
- package/dist/recipient/quiet-view.js +250 -0
- package/dist/recipient/quiet-view.js.map +1 -0
- package/dist/recipient/view.d.ts +2 -0
- package/dist/recipient/view.js.map +1 -1
- package/dist/webhooks.d.ts +30 -1
- package/dist/webhooks.js +57 -4
- package/dist/webhooks.js.map +1 -1
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -42,17 +42,15 @@ const whooshBang = new WhooshBangClient({
|
|
|
42
42
|
fetch,
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
const sent = await whooshBang.createMessage(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
expires_in_seconds: 900,
|
|
53
|
-
},
|
|
45
|
+
const sent = await whooshBang.createMessage({
|
|
46
|
+
to: "customer_123",
|
|
47
|
+
interaction: {
|
|
48
|
+
prompt: "Approve the synthetic deployment?",
|
|
49
|
+
confirm_label: "Approve",
|
|
50
|
+
deny_label: "Deny",
|
|
51
|
+
expires_in_seconds: 900,
|
|
54
52
|
},
|
|
55
|
-
);
|
|
53
|
+
});
|
|
56
54
|
switch (sent.outcome) {
|
|
57
55
|
case "accepted":
|
|
58
56
|
console.log(sent.message.id);
|
|
@@ -108,7 +106,10 @@ are recognized for at least seven days, and longer when the answer window is lon
|
|
|
108
106
|
same contract (`expires_in_seconds` / `expires_at` on the interaction).
|
|
109
107
|
|
|
110
108
|
```ts
|
|
111
|
-
const question = confirmInteraction({
|
|
109
|
+
const question = confirmInteraction({
|
|
110
|
+
prompt: "Approve this week's plan?",
|
|
111
|
+
expiresInSeconds: 604800,
|
|
112
|
+
});
|
|
112
113
|
```
|
|
113
114
|
|
|
114
115
|
The message's outer `expires_at` is its separate delivery deadline (default 24 hours, extendable through year 9999), not the answer deadline. Delivery acceptance does not end answerability.
|
|
@@ -142,6 +143,127 @@ a correct caller cancels and learns which side won. `cancelled` carries the
|
|
|
142
143
|
cancelled resource; `cancellation_too_late` carries the message's current
|
|
143
144
|
state, or the link's Problem Details, and never claims provider-side deletion.
|
|
144
145
|
|
|
146
|
+
## Escalation
|
|
147
|
+
|
|
148
|
+
A send that carries `escalation` keeps trying the person across their channel
|
|
149
|
+
order, then any further stages, until a genuine human response, your
|
|
150
|
+
cancellation, its expiry, or nobody is left to try. The accepted message is the
|
|
151
|
+
first attempt and carries `message.escalation.id`. Read the whole ladder with
|
|
152
|
+
`getEscalation(escalationId, { wait })`; `wait` holds the read until the
|
|
153
|
+
escalation closes, up to 30 seconds. Its `timeline` says what happened in plain
|
|
154
|
+
sentences, such as "Skipped Slack (step 2): this person hasn't connected Slack."
|
|
155
|
+
or "Acknowledged by email, using the link in the email.", so you can show a
|
|
156
|
+
person why the escalation did what it did. The `escalation.closed` event carries
|
|
157
|
+
the same timeline.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
const sent = await whooshBang.createMessage(request, {
|
|
161
|
+
idempotencyKey: "incident-2041-page",
|
|
162
|
+
});
|
|
163
|
+
// Later, when the matter is resolved some other way:
|
|
164
|
+
const stop = await whooshBang.cancelEscalationByKey({
|
|
165
|
+
idempotency_key: "incident-2041-page",
|
|
166
|
+
reason: "resolved",
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Wait on the escalation, not on that first message. A person may answer on any
|
|
171
|
+
attempt, including an earlier one, so `awaitInteraction` on the first message
|
|
172
|
+
can miss the response that closed the escalation. `escalation.response` says
|
|
173
|
+
which attempt was answered and how: `acknowledged`, `answered`, `declined`
|
|
174
|
+
(a "no" is a reached person refusing, not approval) or `replied`.
|
|
175
|
+
|
|
176
|
+
When nothing can be tried at all, because the person has no connected channel,
|
|
177
|
+
the send is refused with `subscriber_unbound` and no escalation exists. When
|
|
178
|
+
channels exist but every attempt is skipped at acceptance, for example because
|
|
179
|
+
consent was withdrawn, the escalation is created and closes as
|
|
180
|
+
`no_eligible_target`; its first message was never sent and carries
|
|
181
|
+
`escalation.skip_reason`.
|
|
182
|
+
|
|
183
|
+
`cancelEscalation(escalationId, { reason })` stops every future attempt.
|
|
184
|
+
`cancelEscalationByKey` does the same for a caller that kept only the send's
|
|
185
|
+
`Idempotency-Key`; name `send_operation: "sendOperatorMessage"` when the key
|
|
186
|
+
was used on an operator send. Both resolve with `cancelled` when this call
|
|
187
|
+
closed the escalation, or `already_closed` when it had already ended, so
|
|
188
|
+
cancelling after someone answered is safe and keeps their answer.
|
|
189
|
+
|
|
190
|
+
### Escalation defaults
|
|
191
|
+
|
|
192
|
+
Each environment has a default channel order for every urgency tier (`low`,
|
|
193
|
+
`normal`, `high`, `critical`, the message category's level). A person without
|
|
194
|
+
an order of their own gets the tier's default. Until you save one, a tier uses
|
|
195
|
+
the built-in suggested order: the environment's connected channels in the
|
|
196
|
+
order Telegram, Slack, email, 5 minutes each.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
await whooshBang.updateEscalationPolicy(projectId, environmentId, "critical", {
|
|
200
|
+
steps: [
|
|
201
|
+
{ channel: "telegram", wait_minutes: 5 },
|
|
202
|
+
{ channel: "email", wait_minutes: 15 },
|
|
203
|
+
],
|
|
204
|
+
});
|
|
205
|
+
const preview = await whooshBang.previewEscalationPath(
|
|
206
|
+
projectId,
|
|
207
|
+
environmentId,
|
|
208
|
+
"critical",
|
|
209
|
+
"customer-123",
|
|
210
|
+
);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Every step's channel must be connected in that environment, or the save fails
|
|
214
|
+
with `escalation_policy_channel_unconnected`. Each save is a new version;
|
|
215
|
+
running escalations keep the order they started with. `listEscalationPolicies`
|
|
216
|
+
reads all four tiers, and `resetEscalationPolicy` returns a tier to the
|
|
217
|
+
suggested order. The preview shows which of the person's channels each step
|
|
218
|
+
reaches; a step without `binding_id` would be skipped.
|
|
219
|
+
|
|
220
|
+
### A subscriber's own escalation order
|
|
221
|
+
|
|
222
|
+
A subscriber can set their own order for each urgency, naming their exact
|
|
223
|
+
bindings with whole-minute waits. Their order always wins over the project's;
|
|
224
|
+
an empty order means nothing is tried, and an escalation to them closes as
|
|
225
|
+
`no_eligible_target`. Only the subscriber can change it, through their
|
|
226
|
+
recipient session:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const orders = await recipientSession.getEscalationOrders();
|
|
230
|
+
await recipientSession.saveEscalationOrder("critical", {
|
|
231
|
+
steps: [{ binding_id: bindingId, wait_minutes: 2 }],
|
|
232
|
+
version: orders.items[3]?.version,
|
|
233
|
+
});
|
|
234
|
+
await recipientSession.resetEscalationOrder("critical");
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Your application can read it with `getSubscriberEscalationOrders`, but cannot
|
|
238
|
+
change it. That differs on purpose from the preferred binding, which your
|
|
239
|
+
application can set.
|
|
240
|
+
|
|
241
|
+
### Quiet hours
|
|
242
|
+
|
|
243
|
+
A subscriber can set quiet hours: the times they would rather not be
|
|
244
|
+
interrupted, and what each channel does then. A channel can deliver as usual,
|
|
245
|
+
deliver silently (Telegram only) or withhold the message. A withheld message
|
|
246
|
+
waits until quiet hours end, or goes straight to one channel they choose, such
|
|
247
|
+
as their email. Critical messages still arrive unless they turn that off. Only
|
|
248
|
+
the subscriber can change their quiet hours, through their recipient session:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
const quiet = await recipientSession.getQuietHours();
|
|
252
|
+
await recipientSession.saveQuietHours({
|
|
253
|
+
windows: [{ days: [1, 2, 3, 4, 5], start: "22:00", end: "07:00" }],
|
|
254
|
+
channels: { telegram: "silent", slack: "withhold", email: "deliver" },
|
|
255
|
+
critical_breakthrough: true,
|
|
256
|
+
when_withheld: "quiet_channel",
|
|
257
|
+
quiet_channel_binding_id: quiet.quiet_channel_options[0]?.binding_id,
|
|
258
|
+
time_zone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
259
|
+
});
|
|
260
|
+
await recipientSession.resetQuietHours();
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`channel_options` lists the choices each channel offers. Your application can
|
|
264
|
+
read a subscriber's quiet hours with `getSubscriberQuietHours`, but cannot
|
|
265
|
+
change them.
|
|
266
|
+
|
|
145
267
|
## Awaiting an answer
|
|
146
268
|
|
|
147
269
|
Use your configured customer-events webhook to receive answers without keeping
|
|
@@ -248,6 +370,11 @@ both the previous and the current secret so a rotation overlap has no gap. Use
|
|
|
248
370
|
`verifyWhooshBangWebhook` (or `verifyWebhookSignature`) for signature-only
|
|
249
371
|
verification of any signed body.
|
|
250
372
|
|
|
373
|
+
Events newer than the SDK still verify. A known type carrying a new field or
|
|
374
|
+
enum value keeps its type; an unknown type returns `recognized: false` with
|
|
375
|
+
only the shared envelope checked. Answer those with a 2xx: a 4xx stops the
|
|
376
|
+
event and holds the endpoint's later deliveries behind it.
|
|
377
|
+
|
|
251
378
|
## Channel connections
|
|
252
379
|
|
|
253
380
|
`listConnectionProviders` reports what a project can connect to, the setup
|
|
@@ -384,7 +511,8 @@ function onReady(event) {
|
|
|
384
511
|
event.source !== popup ||
|
|
385
512
|
event.origin !== hostedOrigin ||
|
|
386
513
|
event.data?.type !== "whooshbang.recipient-settings.ready"
|
|
387
|
-
)
|
|
514
|
+
)
|
|
515
|
+
return;
|
|
388
516
|
window.removeEventListener("message", onReady);
|
|
389
517
|
popup.postMessage(
|
|
390
518
|
{ type: "whooshbang.recipient-settings.session", session },
|
|
@@ -432,25 +560,27 @@ result type and no global outcome enum. An exhaustive `switch` over `outcome`
|
|
|
432
560
|
compiles with no `default`, and a member added later is a compile error at
|
|
433
561
|
every call site rather than a silent fallthrough.
|
|
434
562
|
|
|
435
|
-
| Method
|
|
436
|
-
|
|
|
437
|
-
| `interactionAnswer`
|
|
438
|
-
|
|
|
439
|
-
|
|
|
440
|
-
|
|
|
441
|
-
|
|
|
442
|
-
|
|
|
443
|
-
| `createMessage`, `createMessageInEnvironment`
|
|
444
|
-
|
|
|
445
|
-
|
|
|
446
|
-
|
|
|
447
|
-
| `awaitInteraction`
|
|
448
|
-
| `ask`
|
|
449
|
-
|
|
|
450
|
-
| `cancelMessage`
|
|
451
|
-
|
|
|
452
|
-
| `cancelSubscriptionLink`
|
|
453
|
-
|
|
|
563
|
+
| Method | `outcome` | Carries | Means |
|
|
564
|
+
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
565
|
+
| `interactionAnswer` | `answered` | `answer`, `summary` | A person answered and the answer is retained. |
|
|
566
|
+
| | `answer_not_retained` | `summary` | Answered; the answer is no longer in message history. `answered_at` survives. |
|
|
567
|
+
| | `open` | `summary` | Nobody has answered yet. |
|
|
568
|
+
| | `expired` | `summary` | The interaction reached its server-side expiry unanswered. |
|
|
569
|
+
| | `cancelled` | `summary` | Cancelled before an answer. |
|
|
570
|
+
| | `retired` | `summary` | The question was retired; later answers are ignored. |
|
|
571
|
+
| `createMessage`, `createMessageInEnvironment` | `accepted` | `message` | The service accepted the send; `message` is its current state. |
|
|
572
|
+
| | `subscriber_unbound` | `problem` | No active binding reaches the subscriber on this notifier. Nothing was sent; usually they have to subscribe again, sometimes your routing changed. |
|
|
573
|
+
| | `preferred_binding_unavailable` | `problem` | The channel the subscriber chose as preferred is paused or revoked, and the service never picks a fallback. Nothing was sent; they have to choose again or subscribe again. |
|
|
574
|
+
| | `funding_required` | `reason`, `problem` | Add credit or wait for a paid allowance to renew, then retry the refused request. |
|
|
575
|
+
| `awaitInteraction` | as `interactionAnswer` | `summary`, `answer` on `answered` | The same members; `open` means the patience bound elapsed and the interaction can be awaited again. |
|
|
576
|
+
| `ask` | as `awaitInteraction` | plus `message` | Every interaction member also carries the accepted message. |
|
|
577
|
+
| | `subscriber_unbound`, `preferred_binding_unavailable`, `funding_required` | `problem` | The send's own refusals, exactly as `createMessage` returns them. Nothing was sent, so there is no message. |
|
|
578
|
+
| `cancelMessage` | `cancelled` | `message` | Cancellation took effect before a worker owned the provider call. |
|
|
579
|
+
| | `cancellation_too_late` | `message` | A worker already owned the provider call. `message` is the current state; no provider-side deletion is claimed. |
|
|
580
|
+
| `cancelSubscriptionLink` | `cancelled` | `link` | The pending link is cancelled and can no longer create a binding. |
|
|
581
|
+
| | `cancellation_too_late` | `problem` | The link had already left `pending` — activated, expired, failed, or revoked. Read the link to learn which. |
|
|
582
|
+
| `cancelEscalation`, `cancelEscalationByKey` and their `…InEnvironment` forms | `cancelled` | `escalation` | This call closed the escalation; no later attempt is sent. An attempt already handed to the provider is recorded, never claimed deleted. |
|
|
583
|
+
| | `already_closed` | `escalation` | The escalation had already ended — answered, expired, exhausted or cancelled earlier — and is returned unchanged; its `outcome` says which. Cancelling twice is safe. |
|
|
454
584
|
|
|
455
585
|
Two outcomes the rule names are already values and need no union. An
|
|
456
586
|
equivalent idempotent replay returns the original resource with the original
|
|
@@ -478,45 +608,52 @@ for a paid allowance to renew, then explicitly retry the same refused request.
|
|
|
478
608
|
Bonus credit funds core sends only; shared email needs purchased or welcome
|
|
479
609
|
credit. These ceilings do not schedule an automatic SDK retry.
|
|
480
610
|
|
|
481
|
-
| Code
|
|
482
|
-
|
|
|
483
|
-
| `cancellation_too_late`
|
|
484
|
-
| `authentication_required`
|
|
485
|
-
| `credential_invalid`
|
|
486
|
-
| `scope_forbidden`
|
|
487
|
-
| `environment_mismatch`
|
|
488
|
-
| `idempotency_key_required`
|
|
489
|
-
| `idempotency_conflict`
|
|
490
|
-
| `project_slug_conflict`
|
|
491
|
-
| `request_invalid`
|
|
492
|
-
| `replay_unavailable`
|
|
493
|
-
| `subscriber_unbound`
|
|
494
|
-
| `subscriber_not_connected`
|
|
495
|
-
| `not_consented_for_marketing`
|
|
496
|
-
| `category_not_found`
|
|
497
|
-
| `category_purpose_mismatch`
|
|
498
|
-
| `category_turned_off`
|
|
499
|
-
| `category_conflict`
|
|
500
|
-
| `
|
|
501
|
-
| `
|
|
502
|
-
| `
|
|
503
|
-
| `
|
|
504
|
-
| `
|
|
505
|
-
| `
|
|
506
|
-
| `
|
|
507
|
-
| `
|
|
508
|
-
| `
|
|
509
|
-
| `
|
|
510
|
-
| `
|
|
511
|
-
| `
|
|
512
|
-
| `
|
|
513
|
-
| `
|
|
514
|
-
| `
|
|
515
|
-
| `
|
|
516
|
-
| `
|
|
517
|
-
| `
|
|
518
|
-
| `
|
|
519
|
-
| `
|
|
611
|
+
| Code | Side | Why |
|
|
612
|
+
| --------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
613
|
+
| `cancellation_too_late` | Outcome | The race is inherent: a correct caller cancels and learns it was too late. `cancelSubscriptionLink` receives it as a problem and returns it; `cancelMessage` receives the same fact as `cancellation_effective: false` on a success, as the contract documents, so the problem code arriving there is outside the documented response and throws. |
|
|
614
|
+
| `authentication_required` | Exception | No credential reached the service. The integration's configuration is wrong. |
|
|
615
|
+
| `credential_invalid` | Exception | The credential is malformed, revoked, or past its rotation overlap. Only a replacement credential fixes it. |
|
|
616
|
+
| `scope_forbidden` | Exception | The credential lacks a scope, or the organization is not yet invited (`pendingInvitation`). No branch at the call site grants either. |
|
|
617
|
+
| `environment_mismatch` | Exception | A credential reached across the test/live boundary. |
|
|
618
|
+
| `idempotency_key_required` | Exception | A supplied key was invalid. Omit it or use 8 to 255 visible ASCII characters. |
|
|
619
|
+
| `idempotency_conflict` | Exception | Same key, different body — including the stale row-version, claimed-identity, revoked-client, and acknowledgement cursor or disposition refusals that reuse the code. The fix is a new request, never a retry. |
|
|
620
|
+
| `project_slug_conflict` | Exception | The slug is already taken in this organization. Refused input, handled as `request_invalid` is: choose another. |
|
|
621
|
+
| `request_invalid` | Exception | The request broke a documented bound. `fieldErrors` names each field. |
|
|
622
|
+
| `replay_unavailable` | Exception | A customer-event replay was refused by the attempt's state. The caller waits for a terminal attempt or accepts that an aged-out payload is gone; there is nothing to branch on. |
|
|
623
|
+
| `subscriber_unbound` | Outcome | On a send, the person's channel state: they revoked their channel or never finished subscribing, so the branch is to invite them again. It is also the answer when a send names a `connection_id` and the subscriber has no connected channel at all. `createMessage` and `createMessageInEnvironment` return it. `createMachineClient` still throws it: a machine client for an unbound subscriber is setup that is wrong. |
|
|
624
|
+
| `subscriber_not_connected` | Exception | A send or a connection test named a connection the subscriber has not connected through, while they are connected through another. `problem.subscriber_id` and `problem.channel` say who and where; nothing was sent. Naming the connection was the caller's choice: omit `connection_id` to let the service choose one of their channels, pick a connection they connected through, or invite them through this one. |
|
|
625
|
+
| `not_consented_for_marketing` | Exception | A `marketing` send or broadcast reached an email recipient enrolled only on a service basis. Nothing was sent to that recipient and nothing was charged. Send it as `service`, or obtain marketing consent first. |
|
|
626
|
+
| `category_not_found` | Exception | The named category is not active in this project. Check its key before sending. |
|
|
627
|
+
| `category_purpose_mismatch` | Exception | The stated purpose disagrees with the category's purpose. Nothing was sent or charged. |
|
|
628
|
+
| `category_turned_off` | Exception | The subscriber switched this category off. Nothing was sent or charged. |
|
|
629
|
+
| `category_conflict` | Exception | A category key is taken, the category is in use, or its settings prevent the requested change. |
|
|
630
|
+
| `escalation_unavailable` | Exception | Escalation is not available in this environment yet. Nothing was sent or charged. Send without `escalation`, or retry once escalation is enabled. |
|
|
631
|
+
| `escalation_shape_unsupported` | Exception | A stage names more than one person. Parallel escalation is reserved and not yet supported; give each person a stage of their own. Nothing was sent. |
|
|
632
|
+
| `escalation_capacity_reached` | Exception | The environment has its maximum number of open escalations. Nothing was sent. Wait until `retryAt`, or for one to close, as for rate limiting. |
|
|
633
|
+
| `escalation_policy_channel_unconnected` | Exception | An escalation default names a channel with no active connection in this environment; `fieldErrors` names each step. Nothing was saved. Connect the channel first or remove the step. |
|
|
634
|
+
| `escalation_order_step_ineligible` | Exception | A subscriber's escalation order names a binding that is not theirs, is paused or revoked, or is on a channel the project turned off; `fieldErrors` names each step. Nothing was saved. |
|
|
635
|
+
| `escalation_limits_conflict` | Exception | The project's escalation limits changed since they were read. Read them again before saving. |
|
|
636
|
+
| `escalation_order_conflict` | Exception | The subscriber's escalation order changed since it was read. Read it again before saving. |
|
|
637
|
+
| `recipient_configuration_invalid` | Exception | The owner's recipient-experience configuration is not usable as submitted or as stored. |
|
|
638
|
+
| `recipient_origin_forbidden` | Exception | The presenting browser origin is not registered for the session. |
|
|
639
|
+
| `return_destination_not_found` | Exception | The return-destination key is not configured. |
|
|
640
|
+
| `recipient_capability_invalid` | Exception | The session's capability is not live. Only the host backend can mint another; this is the browser surface's `credential_invalid`. |
|
|
641
|
+
| `recipient_handoff_invalid` | Exception | The provider handoff is unavailable or belongs to another session. |
|
|
642
|
+
| `preferred_binding_conflict` | Exception | A stale row version. Re-read the session and send a new request. |
|
|
643
|
+
| `preferred_binding_unavailable` | Outcome | On a send, the subscriber's own channel state: the channel they chose as preferred is paused or revoked, and the service never picks a fallback. The same user event as `subscriber_unbound`, so `createMessage` and `createMessageInEnvironment` return it. The recipient-session client's `setPreferredBinding` still throws it: there it is a choice the service refused, and the panel offers the choice again. |
|
|
644
|
+
| `subscriber_conflict` | Exception | A create for a subscriber that exists. Use the update and membership operations. |
|
|
645
|
+
| `subscriber_group_conflict` | Exception | The group's state, revision, or a ceiling refused the operation. |
|
|
646
|
+
| `subscriber_group_archived` | Exception | Deletion is terminal. The caller needs a different group. |
|
|
647
|
+
| `resource_not_found` | Exception | The identifier is wrong or outside the credential's reach; absent and out-of-scope look identical on purpose. |
|
|
648
|
+
| `resource_expired` | Exception | Published but not produced by the service today: a resource's expiry reaches the caller as its own `status` or `state`, which is already a value. The recipient-session element treats it, with `recipient_capability_invalid`, as a session that has ended. |
|
|
649
|
+
| `credential_organization_mismatch` | Exception | An organization credential reached a project outside its organization. |
|
|
650
|
+
| `endpoint_rotation_conflict` | Exception | A prior overlap is still active. The caller waits for `retryAt`, as for rate limiting. |
|
|
651
|
+
| `rate_limited` | Exception | Wait until `retryAt`. The caller's response is to wait, not to branch. |
|
|
652
|
+
| `privacy_operation_in_progress` | Exception | Export, erasure, or organization closure still has in-flight work to drain or a provider cleanup to retry. Wait until `retryAt`, then repeat the same operation. |
|
|
653
|
+
| `provider_retryable` | Exception | The provider failed before acceptance. The service retries where a retry is scheduled; the caller waits. |
|
|
654
|
+
| `provider_terminal` | Exception | The provider refused permanently. Fix the binding or the content and send a new message. |
|
|
655
|
+
| `provider_outcome_unknown` | Exception | The provider may already have acted. Read the resource state before deciding anything. |
|
|
656
|
+
| `internal_error` | Exception | WhooshBang failed on its own side; no provider or customer system was involved. Retried like any retryable problem. If it persists, report `diagnosticId` to support. |
|
|
520
657
|
|
|
521
658
|
## Errors
|
|
522
659
|
|
|
@@ -603,14 +740,12 @@ sole customer-owned email connection supplies its domain; specify `connection_id
|
|
|
603
740
|
when more than one exists. No verification message is sent.
|
|
604
741
|
|
|
605
742
|
```ts
|
|
606
|
-
const binding = await client.assertEmailConsent(
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
},
|
|
613
|
-
);
|
|
743
|
+
const binding = await client.assertEmailConsent({
|
|
744
|
+
subscriber_id: "customer_123",
|
|
745
|
+
address: "Case.Sensitive+tag@example.com",
|
|
746
|
+
consented: true,
|
|
747
|
+
consented_at: "2026-09-10T12:00:00Z",
|
|
748
|
+
});
|
|
614
749
|
```
|
|
615
750
|
|
|
616
751
|
Use a canonical mailbox: preserve the unquoted ASCII local part exactly,
|
|
@@ -660,11 +795,25 @@ Interaction builders now return send-time inputs. Relative durations start when
|
|
|
660
795
|
The production API URL is the default. Override `baseUrl` for a local mock or proxy.
|
|
661
796
|
|
|
662
797
|
```ts
|
|
663
|
-
const wb = new WhooshBangClient({
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
await wb.
|
|
667
|
-
|
|
798
|
+
const wb = new WhooshBangClient({
|
|
799
|
+
credential: process.env.WHOOSHBANG_CREDENTIAL!,
|
|
800
|
+
});
|
|
801
|
+
await wb.createMessage({
|
|
802
|
+
to: "customer_123",
|
|
803
|
+
content: "Order 4821 has shipped.",
|
|
804
|
+
});
|
|
805
|
+
await wb.ask(
|
|
806
|
+
{ to: "customer_123", interaction: { prompt: "Ship it?" } },
|
|
807
|
+
{ patienceMs: 60_000 },
|
|
808
|
+
);
|
|
809
|
+
await wb.ask({
|
|
810
|
+
to: "customer_123",
|
|
811
|
+
interaction: { prompt: "Choose", options: ["A", "B"] },
|
|
812
|
+
});
|
|
813
|
+
await wb.ask({
|
|
814
|
+
to: "customer_123",
|
|
815
|
+
interaction: { type: "input", prompt: "What should change?" },
|
|
816
|
+
});
|
|
668
817
|
```
|
|
669
818
|
|
|
670
819
|
`{ text }` and `{ blocks }` infer their content type. An image needs only its
|
package/dist/cli/commands.js
CHANGED
|
@@ -289,6 +289,8 @@ function answerText(summary) {
|
|
|
289
289
|
if (read.outcome !== "answered")
|
|
290
290
|
return undefined;
|
|
291
291
|
const { answer } = read;
|
|
292
|
+
if (answer.type === "acknowledge")
|
|
293
|
+
return "acknowledged";
|
|
292
294
|
return answer.type === "confirm"
|
|
293
295
|
? answer.value
|
|
294
296
|
? "yes"
|