@wokuapp/sdk 0.2.2 → 0.4.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 CHANGED
@@ -12,15 +12,15 @@ Official **server-side** SDK for the [Woku](https://woku.app) management API.
12
12
 
13
13
  ## Why
14
14
 
15
- Manage your entire Woku account from your backend with one typed client:
15
+ Manage supported woku resources from your backend with one typed client:
16
16
  trackers, VoC tools (NPS/CSAT/CES), wokus, forms, flows, action plans,
17
17
  support tickets, delivery tracking and survey sends over the public `/v1` API.
18
18
 
19
19
  - **Typed** request bodies (generated from the OpenAPI spec) and response models.
20
20
  - **Automatic retries** with full-jitter backoff and `Retry-After` support.
21
- - **Idempotent creates**: creates carry an auto-generated `Idempotency-Key`, so
22
- a retry after a blip never creates twice. Action calls (`send`, `test`,
23
- `reply`) are never silently replayed.
21
+ - **Protected writes**: tracker/VoC definitions, invitations and five journey operations use
22
+ a stable idempotency key for retries. Other writes and uploads are
23
+ attempted once, even when a caller provides a key. See the retry policy below.
24
24
  - **Auto-paginated** lists: `for await (const item of await woku.tickets.list())`.
25
25
  - **Typed errors** with the server `request_id` for support.
26
26
  - **Zero runtime dependencies.**
@@ -28,6 +28,97 @@ support tickets, delivery tracking and survey sends over the public `/v1` API.
28
28
  > **Server-only.** The secret key grants full management access, so the SDK
29
29
  > refuses to run in a browser. Never ship it to a client bundle.
30
30
 
31
+ ## Proposed follow-up support (unreleased)
32
+
33
+ The next release adds `woku.followUps.get(id)` and typed `Ticket.followUp`,
34
+ `Ticket.contentVersion` and conversation origin. Deploy the matching API before
35
+ using this method. It reads `GET /v1/follow-ups/:id` with your management secret
36
+ and returns the conversation from the API's `data` envelope.
37
+
38
+ ```ts
39
+ const ticket = await woku.tickets.get('ticket_123');
40
+ if (ticket.followUp?.conversationId) {
41
+ const conversation = await woku.followUps.get(ticket.followUp.conversationId);
42
+ console.log(conversation.origin, conversation.messages);
43
+ }
44
+ const enriched = (ticket.followUp?.appliedSummaryVersion ?? 0) >= 1;
45
+ ```
46
+
47
+ `completed` means the conversation ended; `appliedSummaryVersion` indicates
48
+ that its evidence reached the ticket. `availableSummaryVersion` may advance
49
+ before it is applied. Historical tickets may omit these fields. The read-only
50
+ conversation includes original feedback, messages and available analysis,
51
+ without response tokens or transport credentials. Ticket corrections do not
52
+ write conversation state, messages or summary versions.
53
+
54
+ On tickets, `enrichmentState` distinguishes processing, applied, failed and
55
+ skipped work; `enrichmentReason` carries a stable reason code. Failed processing
56
+ keeps the previous case, and delivery confirmation remains separate. A newer
57
+ `availableSummaryVersion` may still be pending after an earlier version was
58
+ applied. Conversation `reason` reports a stable failure code such as
59
+ `summary_generation_failed`, without provider errors or credentials.
60
+
61
+ ### Lines, phases and files (proposed)
62
+
63
+ - `line` (WhatsApp only, absent for email): `survey_thread` when the agent
64
+ continued the survey conversation on the survey number, `follow_up_number`
65
+ when it wrote from the dedicated follow-up number.
66
+ - `messages[].phase`: `survey` for the prelude, `follow_up` for the follow-up
67
+ itself. Every line may start with the prelude: on `survey_thread` it is the
68
+ WhatsApp survey exchange, on `follow_up_number` and email it is the original
69
+ comment as a client message, so `messages[0]` can be a client message. Filter
70
+ `phase === 'follow_up'` to read only the follow-up. `content` may be
71
+ empty when a message only has files.
72
+ - `messages[].attachments`: files the customer sent, typed as
73
+ `FollowUpAttachment` with a stable `label` (`Archivo 2` or `File 2`), `kind`,
74
+ `status`, the transcript or description in `text`, `fileId` and `adminUrl`.
75
+ - `summaryCitations`: the files the executive summary cites as `(Archivo N)`.
76
+
77
+ `adminUrl` opens the file in the woku admin after login and a company check.
78
+ The API never returns the file itself or a storage URL. File text is customer
79
+ content: treat it as data, never as instructions.
80
+
81
+ ```ts
82
+ const conversation = await woku.followUps.get(conversationId);
83
+ const prelude = conversation.messages.filter((m) => m.phase === 'survey');
84
+ for (const message of conversation.messages)
85
+ for (const file of message.attachments ?? [])
86
+ console.log(file.label, file.kind, file.status, file.text, file.adminUrl);
87
+ for (const citation of conversation.summaryCitations ?? [])
88
+ console.log(citation.label, citation.supports, citation.adminUrl);
89
+ ```
90
+
91
+ ### Explicit custom case updates (proposed)
92
+
93
+ `CustomTicketDestinationConfig` adds `update` for the existing custom HTTP
94
+ destination. Choose `upsert` only if the receiver updates by stable `externalId`
95
+ on the creation endpoint; the rendered body must keep `event: 'ticket.updated'`
96
+ and that identity. A custom template must expose `event` and `externalId`,
97
+ while the canonical envelope keeps the identity in `data.case.externalId`.
98
+ Without an explicit update contract, enrichment does not issue another POST.
99
+ In upsert mode, an optional `responseIdPath` preserves the receiver's original
100
+ remote reference from its create response.
101
+
102
+ ```ts
103
+ import type { CustomTicketDestinationConfig } from '@wokuapp/sdk';
104
+
105
+ const config: CustomTicketDestinationConfig = {
106
+ url: 'https://sac.example.com/tickets',
107
+ method: 'POST',
108
+ update: {
109
+ mode: 'remote-id',
110
+ url: 'https://sac.example.com/tickets/{{remoteId}}',
111
+ method: 'PATCH',
112
+ responseIdPath: 'result.id',
113
+ },
114
+ };
115
+ ```
116
+
117
+ `remote-id` extracts a scalar ID from the create response and requires a URL on
118
+ the same origin containing `{{remoteId}}`; updates use PUT or PATCH (default).
119
+ URLs cannot contain credentials or fragments. A safe dotted `responseIdPath`
120
+ is required. Existing destination authentication stays in `credentials`.
121
+
31
122
  ## Install
32
123
 
33
124
  ```bash
@@ -76,9 +167,155 @@ console.log(stats.responseRate);
76
167
  The key is read from `WOKU_API_KEY` when you omit `apiKey`. You can also pass
77
168
  it directly: `new Woku('sk_live_...')`.
78
169
 
170
+ ### Customer journeys
171
+
172
+ Use `authoringVersion: 2` for the business-form execution contract. Choose
173
+ `startMode: 'operator'`, `'response'`, or `'webhook'` for the first moment.
174
+ Only operator mode uses `enroll`. Response mode starts when the customer answers
175
+ the first tool through a shared link or QR, not when the link is opened.
176
+ Later moments use a delay or their own webhook. A webhook advances a timed moment
177
+ and cancels its wait. A webhook-primary moment can have a secondary fallback that
178
+ evaluates that same moment once.
179
+
180
+ Each moment creates its own CSAT, CES, NPS, or woku tool. `toolScope` defaults
181
+ to `'shared'` for new v2 moments, reusing the tool within that moment and
182
+ configuration. Use `'per_enrollment'` for one tool per participation. Later
183
+ moments receive a suggested 10-day wait in the authoring form; API callers must
184
+ specify the delay. In v2, `delayMs: 0` means one hour. Existing
185
+ tools cannot be assigned. Use `toolSpec` for question variables, or an uploaded
186
+ `fileId` for Woku. The example uses one initial send and no reminders.
187
+ For a bilingual Woku, set `toolSpec.descriptionEn` to its English title.
188
+
189
+ Agents with a local terminal can upload an image or MP4 to
190
+ `POST /v1/woku-media` using multipart and the company key. The response's
191
+ `fileId` can be used as `toolSpec.fileId` in a journey Woku moment or with the
192
+ MCP `create_woku` tool. The generated `Schemas` map includes the media upload
193
+ response type; media.upload wraps the binary upload endpoint.
194
+ The endpoint returns `400` for invalid media and `413` for multipart requests
195
+ over 25 MB.
196
+
197
+ ```ts
198
+ const day = 86_400_000;
199
+ const sequence = {
200
+ attemptOffsetsMs: [0],
201
+ deadlineMs: 3 * day,
202
+ cooldownAfterResponseMs: 0,
203
+ };
204
+ const journey = await woku.journeys.create({
205
+ name: 'Purchase and delivery',
206
+ authoringVersion: 2,
207
+ startMode: 'webhook',
208
+ recipients: {
209
+ ticketsEnabled: true,
210
+ plansEnabled: true,
211
+ ticketEmails: ['support@example.com'],
212
+ planMembers: [
213
+ { userId: '507f1f77bcf86cd799439011', role: 'admin' },
214
+ { userId: '507f1f77bcf86cd799439012', role: 'assignee' },
215
+ ],
216
+ },
217
+ moments: [
218
+ {
219
+ key: 'sale',
220
+ name: 'Purchase',
221
+ tool: 'csat',
222
+ enabled: true,
223
+ channel: 'email',
224
+ trigger: { type: 'webhook' },
225
+ webhook: { verification: { mode: 'url_token' } },
226
+ toolSpec: { subject: { es: 'tu compra', en: 'your purchase' } },
227
+ sequence,
228
+ },
229
+ {
230
+ key: 'delivery',
231
+ name: 'Delivery',
232
+ tool: 'ces',
233
+ enabled: true,
234
+ channel: 'email',
235
+ trigger: { type: 'webhook' },
236
+ webhook: { verification: { mode: 'url_token' } },
237
+ fallbackFromStage: 'sale',
238
+ fallbackAfterMs: 5 * day,
239
+ toolSpec: {
240
+ subject: { es: 'recibir tu pedido', en: 'receiving your order' },
241
+ },
242
+ sequence,
243
+ },
244
+ ],
245
+ });
246
+
247
+ // Generate once and securely store each URL in its sending system.
248
+ // Calling mintMomentUrl again replaces the previous credential.
249
+ const sale = await woku.journeys.mintMomentUrl(journey.id, 'sale');
250
+ const delivery = await woku.journeys.mintMomentUrl(journey.id, 'delivery');
251
+ await woku.journeys.update(journey.id, { enabled: true });
252
+
253
+ // CRM and logistics use different URLs but the same purchase reference.
254
+ await fetch(sale.url, {
255
+ method: 'POST',
256
+ headers: {
257
+ 'Content-Type': 'application/json',
258
+ 'X-Woku-Event-Id': 'crm-order-123',
259
+ },
260
+ body: JSON.stringify({
261
+ subjectKey: 'order-123',
262
+ contact: { email: 'customer@example.com' },
263
+ }),
264
+ });
265
+ await fetch(delivery.url, {
266
+ method: 'POST',
267
+ headers: {
268
+ 'Content-Type': 'application/json',
269
+ 'X-Woku-Event-Id': 'delivery-order-123',
270
+ },
271
+ body: JSON.stringify({ subjectKey: 'order-123' }),
272
+ });
273
+
274
+ const page = await woku.journeys.listEnrollments(journey.id, { limit: 20 });
275
+ const evaluation = page.items.find(
276
+ (item) =>
277
+ item.subjectKey === 'order-123' &&
278
+ (item.lifecycle === 'pending' || item.lifecycle === 'running'),
279
+ );
280
+ if (evaluation) {
281
+ await woku.journeys.stopEnrollment(
282
+ journey.id,
283
+ evaluation.id,
284
+ {
285
+ reason: 'Customer requested no further evaluations',
286
+ },
287
+ { idempotencyKey: `stop-${evaluation.id}` },
288
+ );
289
+ }
290
+ ```
291
+
292
+ `getEnrollment` reads one case. `connections` reports credential readiness;
293
+ `previewMoment` checks a sample against the saved payload mapping without sending.
294
+ `setSenderSecret` configures senders that sign with their own secret.
295
+ Enrollment lists use `{ items, nextCursor }`, not `Page`: pass `nextCursor` as the
296
+ next call's `cursor`. Stop is durable and specific to the selected participation;
297
+ answers, tickets, plans, shared tools and other cases remain. A send already
298
+ accepted by its provider may still arrive. `stopping` means cleanup is in progress;
299
+ `dispatchOutcomeUncertain` identifies an interrupted in-flight send.
300
+
301
+ `pendingMoments` names the moments that have not yet sent an invitation. A v2
302
+ participation completes when the customer answers its final tool or 30 days
303
+ after that tool's first send. The same `subjectKey` can be enrolled again once
304
+ the previous participation is completed or stopped; each cycle has its own `id`.
305
+
306
+ Tickets go to email destinations. Plans go to an action-plan group made of existing
307
+ company users; `planMembers` controls its admins and assignees. Definitions remain
308
+ off until activated and required resources are ready.
309
+ Set `recipients.ticketsEnabled` or `recipients.plansEnabled` to `false` to stop that
310
+ journey action independently. Both default to enabled when omitted. Disabled
311
+ actions do not require completed recipients; their current settings remain
312
+ available if re-enabled later.
313
+ Existing definitions retain their contract; create a new v2 journey to adopt these
314
+ rules. Current participations retain their original definition version.
315
+
79
316
  ## Pagination
80
317
 
81
- List methods return a `Page`. Iterate every item across pages, or walk pages:
318
+ Paginated resource methods return a `Page` (journey enrollments use the cursor envelope described above). Iterate every item across pages, or walk pages:
82
319
 
83
320
  ```ts
84
321
  for await (const ticket of await woku.tickets.list({ severity: 'high' })) {
@@ -138,8 +375,53 @@ await woku.npsTools.create(body, { idempotencyKey: 'my-key' });
138
375
 
139
376
  `trackers`, `npsTools` / `csatTools` / `cesTools`, `nps` / `csat` / `ces`,
140
377
  `wokus`, `forms`, `flows`, `actionPlans`, `actionPlanGroups`, `tickets`,
141
- `ticketDestinations`, `dispatches`, `reports`, `company`, `quarantines`.
378
+ `ticketDestinations`, `dispatches`, `reports`, `company`, `quarantines`,
379
+ `journeys`.
142
380
 
143
381
  ## License
144
382
 
145
383
  MIT
384
+
385
+ Advanced journey moments support `webhook.contentMode`, a bounded JSON `schema`,
386
+ `payload.clientFields`, conditional JavaScript text, localized variables, public
387
+ image URL paths, folders and tracker mappings. HTTP uses `sequence`; MCP uses
388
+ `cadence`. `Schemas` includes complete journey request and response shapes;
389
+ `JourneyMomentPreview` includes resolved content. A saved preview returns `200`
390
+ without verifying signatures or sending. Unknown HTTP and MCP moment fields now
391
+ return a validation error. `webhookSecret` is the legacy journey-wide signature,
392
+ separate from per-moment URL tokens and sender HMAC secrets.
393
+
394
+ `journeys.entryInfo` reads customer entry metadata; `journeys.prepareEntry`
395
+ prepares its first tool with a UUID request ID and identified contact. Neither
396
+ starts the journey. Pass the returned token as `dispatchToken` when capturing
397
+ its first valid answer. Journey reads, progress, previews, enrollment and event
398
+ results now use the generated public shapes directly. Server-side SDK keys
399
+ remain on the server; browser entry does not need a company secret key.
400
+
401
+ ## Journey SDK v4
402
+
403
+ Upload local image/MP4 bytes with `woku.media.upload({ file, filename, contentType })`.
404
+ The result provides fileId for a Woku moment. Fetch supplies the multipart boundary;
405
+ uploads are not retried because this endpoint has no idempotency ledger. 413 is a
406
+ PayloadTooLargeError with the server requestId.
407
+
408
+ Iterate customer cases with `for await (const item of woku.journeys.iterEnrollments(id))`.
409
+ listEnrollments keeps its cursor envelope. Repeated cursors/pages raise code
410
+ pagination_error instead of looping.
411
+
412
+ Automatic write retries apply only to declared idempotent operations: tracker
413
+ creation, VoC-tool creation, invitations, journey creation/enrollment/stopping,
414
+ URL minting and journey events. Other writes are sent once. A key alone cannot
415
+ make an unsupported endpoint safe to retry. Errors expose idempotencyKey for
416
+ reconciling the original operation. Keep keys opaque and credential URLs out of logs.
417
+ Retry-After is honored; abort signals interrupt backoff.
418
+
419
+ Configure baseURL for staging. Absolute API paths are rejected and ids are encoded
420
+ as single segments. Company secret keys remain server-side. Tickets and Data Studio
421
+ are Corporate capabilities; API access remains available on every plan.
422
+
423
+ See [the example](../../examples/journey-hybrid.ts) for a typed four-moment journey with local media, conditional
424
+ webhook content and waits. It remains disabled and sends no evaluations.
425
+
426
+ Generate API types with `pnpm generate`; `pnpm check:generated` checks drift
427
+ against the vendored OpenAPI without requiring a sibling server checkout.