ab-ecosystem-cpass 1.0.0 → 1.0.6

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 (86) hide show
  1. package/README.md +121 -21
  2. package/dist/client.d.ts +38 -0
  3. package/dist/client.js +49 -5
  4. package/dist/client.js.map +1 -1
  5. package/dist/http.d.ts +15 -0
  6. package/dist/http.js +21 -1
  7. package/dist/http.js.map +1 -1
  8. package/dist/index.d.ts +11 -0
  9. package/dist/index.js +24 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/internal/bytes.d.ts +38 -0
  12. package/dist/internal/bytes.js +84 -0
  13. package/dist/internal/bytes.js.map +1 -0
  14. package/dist/internal/device.d.ts +24 -0
  15. package/dist/internal/device.js +91 -0
  16. package/dist/internal/device.js.map +1 -0
  17. package/dist/internal/md5.d.ts +4 -0
  18. package/dist/internal/md5.js +109 -0
  19. package/dist/internal/md5.js.map +1 -0
  20. package/dist/internal/receiver-file.d.ts +106 -0
  21. package/dist/internal/receiver-file.js +128 -0
  22. package/dist/internal/receiver-file.js.map +1 -0
  23. package/dist/realtime/client.d.ts +85 -0
  24. package/dist/realtime/client.js +126 -0
  25. package/dist/realtime/client.js.map +1 -0
  26. package/dist/realtime/events.d.ts +147 -0
  27. package/dist/realtime/events.js +3 -0
  28. package/dist/realtime/events.js.map +1 -0
  29. package/dist/realtime/inbox.d.ts +173 -0
  30. package/dist/realtime/inbox.js +263 -0
  31. package/dist/realtime/inbox.js.map +1 -0
  32. package/dist/realtime/index.d.ts +16 -0
  33. package/dist/realtime/index.js +37 -0
  34. package/dist/realtime/index.js.map +1 -0
  35. package/dist/resources/audience/contacts.d.ts +19 -3
  36. package/dist/resources/audience/contacts.js +19 -3
  37. package/dist/resources/audience/contacts.js.map +1 -1
  38. package/dist/resources/audience/inbox-media.d.ts +92 -0
  39. package/dist/resources/audience/inbox-media.js +127 -0
  40. package/dist/resources/audience/inbox-media.js.map +1 -0
  41. package/dist/resources/audience/notifications.d.ts +171 -0
  42. package/dist/resources/audience/notifications.js +215 -0
  43. package/dist/resources/audience/notifications.js.map +1 -0
  44. package/dist/resources/audience/scheduled-messages.d.ts +234 -0
  45. package/dist/resources/audience/scheduled-messages.js +259 -0
  46. package/dist/resources/audience/scheduled-messages.js.map +1 -0
  47. package/dist/resources/campaign-audience.d.ts +132 -0
  48. package/dist/resources/campaign-audience.js +224 -0
  49. package/dist/resources/campaign-audience.js.map +1 -0
  50. package/dist/resources/campaign-report-exports.d.ts +14 -0
  51. package/dist/resources/campaign-report-exports.js +28 -0
  52. package/dist/resources/campaign-report-exports.js.map +1 -0
  53. package/dist/resources/campaign-reports.d.ts +15 -0
  54. package/dist/resources/campaign-reports.js +25 -0
  55. package/dist/resources/campaign-reports.js.map +1 -0
  56. package/dist/resources/campaigns.d.ts +239 -0
  57. package/dist/resources/campaigns.js +300 -0
  58. package/dist/resources/campaigns.js.map +1 -0
  59. package/dist/types/audience.d.ts +93 -1
  60. package/dist/types/campaign.d.ts +229 -0
  61. package/dist/types/campaign.js +3 -0
  62. package/dist/types/campaign.js.map +1 -0
  63. package/dist/types/contact-filter.d.ts +3 -2
  64. package/dist/types/contact-filter.js +3 -2
  65. package/dist/types/contact-filter.js.map +1 -1
  66. package/dist/types/inbox.d.ts +391 -0
  67. package/dist/types/inbox.js +16 -0
  68. package/dist/types/inbox.js.map +1 -0
  69. package/dist/types/index.d.ts +3 -0
  70. package/dist/types/index.js +5 -0
  71. package/dist/types/index.js.map +1 -1
  72. package/dist/types/scheduled-message.d.ts +221 -0
  73. package/dist/types/scheduled-message.js +16 -0
  74. package/dist/types/scheduled-message.js.map +1 -0
  75. package/docs/audience/contacts.md +34 -4
  76. package/docs/audience/notifications.md +263 -0
  77. package/docs/audience/scheduled-messages.md +557 -0
  78. package/docs/campaign/campaigns.md +489 -0
  79. package/docs/contact-filter.md +2 -1
  80. package/docs/inbox/chats.md +356 -0
  81. package/docs/inbox/connection.md +171 -0
  82. package/docs/inbox/media.md +202 -0
  83. package/docs/inbox/messages.md +338 -0
  84. package/docs/inbox/referrals.md +72 -0
  85. package/docs/inbox/sessions.md +74 -0
  86. package/package.json +28 -2
@@ -0,0 +1,557 @@
1
+ # Scheduled messages
2
+
3
+ `crm.scheduledMessages` — `/v1/audience/scheduled-messages`
4
+
5
+ A send with a time, and optionally a **condition** checked immediately before it
6
+ goes out.
7
+
8
+ | Action | Route | Scope |
9
+ | --- | --- | --- |
10
+ | [`list`](#list) | `GET /v1/audience/scheduled-messages` | `messages:read` |
11
+ | [`get`](#get) | `GET /v1/audience/scheduled-messages/:id` | `messages:read` |
12
+ | [`create`](#create) | `POST /v1/audience/scheduled-messages` | `messages:write` |
13
+ | [`createSeries`](#createseries) | `POST /v1/audience/scheduled-messages/series` | `messages:write` |
14
+ | [`createRepeating`](#createrepeating) | `POST /v1/audience/scheduled-messages/series` | `messages:write` |
15
+ | [`reschedule`](#reschedule) | `PATCH /v1/audience/scheduled-messages/:id` | `messages:write` |
16
+ | [`cancel`](#cancel) | `DELETE /v1/audience/scheduled-messages/:id` | `messages:write` |
17
+ | [`cancelSeries`](#cancelseries) | `DELETE /v1/audience/scheduled-messages/series/:series` | `messages:write` |
18
+ | [`sendNow`](#sendnow) | `POST /v1/audience/scheduled-messages/:id/send-now` | `messages:write` |
19
+
20
+ ---
21
+
22
+ ## Why conditions exist
23
+
24
+ A drip is easy to start and awkward to stop.
25
+
26
+ You schedule five messages over two weeks for a lead. They convert after the
27
+ third. The last two still go out — now addressed to a paying customer, as a
28
+ pitch. Avoiding that means somebody noticing in time and cancelling by hand.
29
+
30
+ So each occurrence **re-reads the contact a moment before sending** and stands
31
+ down if they no longer match the condition you set. Put a `stage` custom field on
32
+ your contacts, condition the series on `stage equal to Lead`, and the only thing
33
+ you have to do when a lead converts is what you were going to do anyway:
34
+
35
+ ```ts
36
+ await crm.contacts.update(contactId, { custom_field_hash: { stage: 'Customer' } });
37
+ ```
38
+
39
+ The remaining messages cancel themselves. No scheduled-message ids to track, no
40
+ cancellation call, and the record says exactly why each one did not send.
41
+
42
+ The condition is evaluated **after** the scheduler claims the occurrence and
43
+ **immediately before** the message is handed to the channel — not at schedule
44
+ time, not a pass earlier. A check run even a minute early is a check that can be
45
+ wrong.
46
+
47
+ ---
48
+
49
+ ## Two things that will catch you out
50
+
51
+ ### Templates, not free text
52
+
53
+ WhatsApp only allows free-form messages within **24 hours** of the contact's last
54
+ inbound message. A drip spaced in days is, by construction, outside that window
55
+ from the second message onward — so every step but the first has to be an
56
+ approved template.
57
+
58
+ `create` and `createSeries` reject free text that falls outside the window with a
59
+ `422`, rather than accepting it and letting the provider refuse it on day four.
60
+ That failure mode is the reason to check at schedule time: a drip that silently
61
+ dies from message 2 looks like a scheduling bug and isn't one.
62
+
63
+ ### A series is expanded, not recurring
64
+
65
+ `createSeries` writes **one row per message** immediately. So you can list them,
66
+ move one, or cancel one without touching the rest, and "what is queued for this
67
+ contact" is a plain list rather than a computation.
68
+
69
+ The consequence: `until` has to be a real end. There is no rule stored for later
70
+ evaluation, so an open-ended cadence has nothing to expand into. Expansions are
71
+ capped at 365 occurrences.
72
+
73
+ ---
74
+
75
+ ## The scheduled message object
76
+
77
+ ```ts
78
+ interface ScheduledMessage {
79
+ id: string;
80
+ /** The series this belongs to, or null for a one-off. */
81
+ series: string | null;
82
+ /** 1-based position in the series. 1 for a one-off. */
83
+ occurrence: number;
84
+ title: string;
85
+ contact: string;
86
+ channel: string;
87
+ content: MessageContent;
88
+ context: JsonObject;
89
+ /** IANA zone the schedule was expressed in. */
90
+ timezone: string;
91
+ /** The resolved instant, always UTC. What the scheduler fires on. */
92
+ scheduled_at: string;
93
+ /** The same moment as wall-clock in `timezone` — what the user chose. */
94
+ scheduled_at_local: string;
95
+ /** The series condition. null means send unconditionally. */
96
+ conditions: ContactFilter | null;
97
+ /** This occurrence's own extra narrowing, from its step. Both must pass. */
98
+ step_conditions: ContactFilter | null;
99
+ on_condition_fail: 'skip' | 'cancel_series';
100
+ status: 'pending' | 'processing' | 'sent' | 'skipped' | 'failed' | 'cancelled';
101
+ result: ScheduledMessageResult | null;
102
+ processed_at: string | null;
103
+ completed_at: string | null;
104
+ createdAt: string;
105
+ updatedAt: string;
106
+ }
107
+ ```
108
+
109
+ ### Status
110
+
111
+ | Status | Meaning |
112
+ | --- | --- |
113
+ | `pending` | Waiting for its moment. The only status that can be rescheduled or cancelled. |
114
+ | `processing` | Claimed by the scheduler, mid-send. |
115
+ | `sent` | Handed to the channel. What happened next is a **message** status, not this one. |
116
+ | `skipped` | Its condition did not match. `result` says what was expected and what was found. |
117
+ | `cancelled` | Stood down — by a caller, or by an earlier occurrence failing under `cancel_series`. |
118
+ | `failed` | Could not be sent. `result.reason` is one of `contact_missing`, `contact_trashed`, `outgoing_blocked`, `no_channel_identity`. |
119
+
120
+ `sent` means *accepted by the channel*. Delivery, read receipts and provider
121
+ failures live on the message row the send produces, reachable through the inbox —
122
+ a scheduled message's job ends when it hands over.
123
+
124
+ ### Result
125
+
126
+ ```ts
127
+ interface ScheduledMessageResult {
128
+ reason?: string; // condition_not_met | series_cancelled_by_condition |
129
+ // cancelled_by_user | queued | contact_missing |
130
+ // contact_trashed | outgoing_blocked | no_channel_identity
131
+ scope?: 'series' | 'step'; // condition_not_met: which condition refused
132
+ field_id?: string; // condition_not_met: the first condition that failed
133
+ condition?: string; // its comparison, e.g. "equal to"
134
+ expected?: unknown; // the value it was looking for
135
+ at_occurrence?: number; // series_cancelled_by_condition: whose condition failed
136
+ }
137
+ ```
138
+
139
+ Only the **first** failing condition is named. Listing all of them would be noise
140
+ when the first already explains the skip.
141
+
142
+ This is what makes "why didn't my lead get message 4" answerable:
143
+
144
+ ```ts
145
+ const { data } = await crm.scheduledMessages.list({ series });
146
+ ```
147
+
148
+ ```json
149
+ [
150
+ { "occurrence": 1, "status": "sent" },
151
+ { "occurrence": 2, "status": "sent" },
152
+ { "occurrence": 3, "status": "sent" },
153
+ { "occurrence": 4, "status": "skipped",
154
+ "result": { "reason": "condition_not_met", "scope": "series",
155
+ "field_id": "stage", "condition": "equal to", "expected": "Lead" } },
156
+ { "occurrence": 5, "status": "cancelled",
157
+ "result": { "reason": "series_cancelled_by_condition", "at_occurrence": 4 } }
158
+ ]
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Time and timezone
164
+
165
+ `timezone` is **required** on every create, and must be an IANA zone name —
166
+ `Asia/Kolkata`, `Europe/London`, `America/New_York`. Abbreviations and offsets
167
+ (`IST`, `+05:30`, `GMT+5:30`) are rejected with a `422`.
168
+
169
+ How it combines with the time you send:
170
+
171
+ | `scheduled_at` / `starts_at` | Effect of `timezone` |
172
+ | --- | --- |
173
+ | No offset — `"2026-10-15T10:00:00"` | **Resolves it.** The value is wall-clock in that zone. |
174
+ | Carries `Z` or an offset — `"2026-10-15T04:30:00Z"` | **Advisory.** The instant is already pinned; the zone is recorded for display and for later edits. |
175
+
176
+ Both are accepted because clients differ — an app has a local time and a zone, a
177
+ server integration has a UTC instant. The second row is worth reading twice:
178
+ passing a zone alongside a `Z` time does not move the send.
179
+
180
+ Why a zone is stored even when the instant is pinned:
181
+
182
+ - **Display.** Keep only `04:30Z` and the intent — "ten in the morning" — is
183
+ gone. Every client then re-derives it from the viewer's browser, so an agent in
184
+ Dubai sees a different time from the person who scheduled it.
185
+ - **Rescheduling.** `reschedule(id, { scheduled_at: '2026-10-16T10:00:00' })`
186
+ needs a zone to mean anything, and uses the stored one unless you override it.
187
+ - **Spacing.** A series spaced in days is expanded in its zone, so a drip that
188
+ crosses a DST boundary keeps landing at 10:00 local rather than walking by an
189
+ hour.
190
+
191
+ **DST edges.** A wall-clock time can be nonexistent or doubled at a transition —
192
+ never in `Asia/Kolkata`, which has no DST, but `2026-03-08T02:30` does not exist
193
+ in `America/New_York` and `2026-11-01T01:30` happens twice. The rules:
194
+
195
+ - **Nonexistent** — shifted **forward by the length of the gap**. `02:30`
196
+ becomes `03:30` local.
197
+ - **Ambiguous** — the **earlier** of the two instants. `01:30` resolves to
198
+ `05:30Z`, the one before the clocks go back.
199
+
200
+ Series spacing in `days`, `weeks` and `months` is **calendar** arithmetic in the
201
+ schedule's zone, so a 10:00 drip keeps landing at 10:00 local across a
202
+ transition rather than walking by an hour. `minutes` and `hours` are real
203
+ elapsed time, not calendar — two hours is two hours even where the clocks
204
+ change.
205
+
206
+ ---
207
+
208
+ ## Conditions
209
+
210
+ The condition is a [contact filter](../contact-filter.md) — the same DSL as
211
+ `contacts.list({ filter })`, a saved `Segment`, and a CRM trigger's `filters`.
212
+ Nothing new to learn, and a condition that selects a contact in a segment selects
213
+ them here too.
214
+
215
+ ```ts
216
+ conditions: {
217
+ conditions: [
218
+ { field_id: 'stage', field_type: 'dropdown', condition: 'equal to', value: 'Lead' },
219
+ ],
220
+ }
221
+ ```
222
+
223
+ | Field | Notes |
224
+ | --- | --- |
225
+ | `field_id` | The custom field's **slug**, not its id. Also accepts `tags`, `SYSTEM_LAST_SEEN`, `SYSTEM_CREATED_ON`, `SYSTEM_IDENTIFIER`, `SYSTEM_PROFILE_NAME`. |
226
+ | `field_type` | Must match the field's real data type. A pairing with no meaning — `greater than` on a dropdown — is a `422`. Use `string` for an untyped value. |
227
+ | `condition` | One of the 23 comparisons. |
228
+ | `value` | Scalar for text and number; a JSON array string for date comparisons. |
229
+ | `operator` | `null` on the first entry, `and` / `or` after. **`and` binds tighter than `or`.** |
230
+
231
+ Several conditions — still a lead, and not tagged do-not-contact:
232
+
233
+ ```ts
234
+ conditions: {
235
+ conditions: [
236
+ { field_id: 'stage', field_type: 'dropdown', condition: 'equal to', value: 'Lead' },
237
+ { operator: 'and', field_id: 'tags', field_type: 'string',
238
+ condition: 'not equal to', value: [doNotContactTagId] },
239
+ ],
240
+ }
241
+ ```
242
+
243
+ Omit `conditions` and the message always sends.
244
+
245
+ `REFERRAL_ID` is **not** available here — it resolves against a collection the
246
+ scheduler does not read. Using it is a `422` at schedule time rather than a
247
+ silent pass.
248
+
249
+ ### `on_condition_fail`
250
+
251
+ | Value | Effect | Default for |
252
+ | --- | --- | --- |
253
+ | `skip` | This occurrence only, marked `skipped`. Later ones are still checked, so a contact who leaves and re-enters the condition resumes receiving. | `create` |
254
+ | `cancel_series` | This occurrence and every later one. | `createSeries`, `createRepeating` |
255
+
256
+ `skip` suits "don't message them on a day they already replied". `cancel_series`
257
+ suits "they converted, we're done" — and is the default for a series precisely
258
+ because a converted lead should drop out for good, rather than re-entering if
259
+ someone edits their stage back later.
260
+
261
+ ### Guards you don't have to write
262
+
263
+ Independently of your condition, a send is refused when the contact is trashed,
264
+ `chat_metadata.blocked.outgoing` is set, or the channel has been disabled. Those
265
+ end `failed` with a `result.error`, not `skipped` — they are platform state, not
266
+ a condition you chose.
267
+
268
+ ---
269
+
270
+ ## list
271
+
272
+ ```ts
273
+ crm.scheduledMessages.list(query?: Partial<ListScheduledMessagesQuery>): Promise<Paginated<ScheduledMessage>>
274
+ ```
275
+
276
+ `GET /v1/audience/scheduled-messages` · `messages:read` · **200**
277
+
278
+ | Field | Type | Default | Notes |
279
+ | --- | --- | --- | --- |
280
+ | `page` | number | `1` | 1-based. |
281
+ | `rows` | number | `25` | 1–100. |
282
+ | `search` | string | — | Matches the title. |
283
+ | `contact` | ObjectId | — | Everything queued for one person. |
284
+ | `series` | ObjectId | — | One drip's occurrences. |
285
+ | `status` | status | — | See the table above. |
286
+ | `channel` | ObjectId | — | |
287
+
288
+ Soonest first among `pending`; newest first among the rest.
289
+
290
+ ```ts
291
+ // What is still waiting to go out for this contact
292
+ const { data } = await crm.scheduledMessages.list({ contact: contactId, status: 'pending' });
293
+
294
+ // What stood down, and why
295
+ const skipped = await crm.scheduledMessages.list({ status: 'skipped' });
296
+ ```
297
+
298
+ ---
299
+
300
+ ## get
301
+
302
+ ```ts
303
+ crm.scheduledMessages.get(id: string): Promise<ScheduledMessage>
304
+ ```
305
+
306
+ `GET /v1/audience/scheduled-messages/:id` · `messages:read` · **200**
307
+
308
+ One occurrence, with its `result` if it has reached a terminal status. `404
309
+ not_found` if it does not belong to the caller.
310
+
311
+ ---
312
+
313
+ ## create
314
+
315
+ ```ts
316
+ crm.scheduledMessages.create(input: CreateScheduledMessageInput): Promise<ScheduledMessage>
317
+ ```
318
+
319
+ `POST /v1/audience/scheduled-messages` · `messages:write` · **201**
320
+
321
+ One message, one time. `series` comes back `null` and `occurrence` is `1`.
322
+
323
+ ### Body
324
+
325
+ | Field | Type | Required | Notes |
326
+ | --- | --- | --- | --- |
327
+ | `channel` | ObjectId | yes | Channel to send on. |
328
+ | `contact` | `{ id }` | yes | The identifier is resolved from the contact's `profile` at send time, so a number corrected in between is the one used. |
329
+ | `timezone` | string | yes | IANA zone. See [Time and timezone](#time-and-timezone). |
330
+ | `scheduled_at` | ISO string | yes | Must be in the future. |
331
+ | `content` | `MessageContent` | yes | The same open body `inbox.messages.send` takes — `{ type }` pinned, the body under a key of that name. |
332
+ | `context` | object | no | Provider context: a replied-to id, forwarding flags. |
333
+ | `title` | string | no | Shown in the list. Defaults to `'Untitled'`. |
334
+ | `conditions` | `ContactFilter` | no | Omit to always send. |
335
+ | `on_condition_fail` | `'skip' \| 'cancel_series'` | no | Defaults to `skip`. |
336
+
337
+ ```ts
338
+ await crm.scheduledMessages.create({
339
+ title: 'Pricing follow-up',
340
+ channel: channelId,
341
+ contact: { id: contactId },
342
+ timezone: 'Asia/Kolkata',
343
+ scheduled_at: '2026-10-15T10:00:00', // 10:00 IST
344
+ content: { type: 'text', text: { body: 'Following up on the pricing.' } },
345
+ conditions: {
346
+ conditions: [
347
+ { field_id: 'stage', field_type: 'dropdown', condition: 'equal to', value: 'Lead' },
348
+ ],
349
+ },
350
+ });
351
+ ```
352
+
353
+ `422` when `scheduled_at` is in the past, `timezone` is not an IANA zone, a
354
+ condition names a field type its comparison cannot apply to, or the body is free
355
+ text and the send falls outside the channel's reply window.
356
+
357
+ ---
358
+
359
+ ## createSeries
360
+
361
+ ```ts
362
+ crm.scheduledMessages.createSeries(input: CreateScheduledSeriesInput): Promise<ScheduledMessageSeries>
363
+ ```
364
+
365
+ `POST /v1/audience/scheduled-messages/series` · `messages:write` · **201**
366
+
367
+ A sequence whose steps carry different copy.
368
+
369
+ ### Body
370
+
371
+ Everything `create` takes except `scheduled_at`, `content` and `context`, plus:
372
+
373
+ | Field | Type | Required | Notes |
374
+ | --- | --- | --- | --- |
375
+ | `starts_at` | ISO string | yes | The first step's moment. Every `after` is measured from here. |
376
+ | `steps` | `ScheduledMessageStep[]` | yes | 1–365. Sorted by `after` before expansion, so declaration order does not matter. |
377
+
378
+ ```ts
379
+ interface ScheduledMessageStep {
380
+ /** Cumulative from `starts_at`, never relative to the previous step. */
381
+ after: { value: number; unit: 'minutes' | 'hours' | 'days' | 'weeks' | 'months' };
382
+ content: MessageContent;
383
+ context?: JsonObject;
384
+ /** Checked in addition to the series condition — both must pass. */
385
+ conditions?: ContactFilter | null;
386
+ }
387
+ ```
388
+
389
+ ```ts
390
+ const series = await crm.scheduledMessages.createSeries({
391
+ title: 'Lead nurture — 2 weeks',
392
+ channel: channelId,
393
+ contact: { id: contactId },
394
+ timezone: 'Asia/Kolkata',
395
+ starts_at: '2026-10-13T10:00:00',
396
+ conditions: {
397
+ conditions: [
398
+ { field_id: 'stage', field_type: 'dropdown', condition: 'equal to', value: 'Lead' },
399
+ ],
400
+ },
401
+ on_condition_fail: 'cancel_series',
402
+ steps: [
403
+ { after: { value: 0, unit: 'days' }, content: introTemplate },
404
+ { after: { value: 3, unit: 'days' }, content: caseStudyTemplate },
405
+ { after: { value: 6, unit: 'days' }, content: pricingTemplate },
406
+ { after: { value: 9, unit: 'days' }, content: demoTemplate },
407
+ { after: { value: 12, unit: 'days' }, content: lastCallTemplate },
408
+ ],
409
+ });
410
+
411
+ series.occurrences.length; // 5
412
+ series.occurrences[1].scheduled_at_local; // '2026-10-16T10:00:00'
413
+ ```
414
+
415
+ **`after` is cumulative from `starts_at`.** Changing step 2's delay therefore
416
+ leaves steps 3–5 exactly where they were; relative offsets would shift all of
417
+ them, which is almost never what an edit means.
418
+
419
+ ### Response
420
+
421
+ ```ts
422
+ interface ScheduledMessageSeries {
423
+ series: string;
424
+ title: string;
425
+ contact: string;
426
+ channel: string;
427
+ timezone: string;
428
+ conditions: ContactFilter | null;
429
+ on_condition_fail: 'skip' | 'cancel_series';
430
+ /** In send order. One row per message, each independently cancellable. */
431
+ occurrences: ScheduledMessage[];
432
+ }
433
+ ```
434
+
435
+ ---
436
+
437
+ ## createRepeating
438
+
439
+ ```ts
440
+ crm.scheduledMessages.createRepeating(input: CreateRepeatingScheduleInput): Promise<ScheduledMessageSeries>
441
+ ```
442
+
443
+ `POST /v1/audience/scheduled-messages/series` · `messages:write` · **201**
444
+
445
+ The same route, for when every occurrence sends the same message.
446
+
447
+ | Field | Type | Required | Notes |
448
+ | --- | --- | --- | --- |
449
+ | `starts_at` | ISO string | yes | First occurrence. |
450
+ | `repeat.every` | number | yes | At least 1. |
451
+ | `repeat.unit` | unit | yes | `minutes` … `months`. |
452
+ | `repeat.until` | ISO string | yes | **Inclusive** end. |
453
+ | `content` | `MessageContent` | yes | Sent by every occurrence. |
454
+
455
+ ```ts
456
+ await crm.scheduledMessages.createRepeating({
457
+ title: 'Weekly check-in',
458
+ channel: channelId,
459
+ contact: { id: contactId },
460
+ timezone: 'Asia/Kolkata',
461
+ starts_at: '2026-10-13T10:00:00',
462
+ repeat: { every: 3, unit: 'days', until: '2026-10-27T10:00:00' },
463
+ content: nudgeTemplate,
464
+ conditions: { conditions: [stillALead] },
465
+ });
466
+ // → 5 occurrences: 13th, 16th, 19th, 22nd, 25th
467
+ ```
468
+
469
+ `until` is inclusive, so the last occurrence is the final one at or before it.
470
+ `422` if the expansion would exceed 365 occurrences.
471
+
472
+ Once created, a repeating schedule and a stepped one are indistinguishable — both
473
+ are just rows.
474
+
475
+ ---
476
+
477
+ ## reschedule
478
+
479
+ ```ts
480
+ crm.scheduledMessages.reschedule(id: string, input: RescheduleMessageInput): Promise<ScheduledMessage>
481
+ ```
482
+
483
+ `PATCH /v1/audience/scheduled-messages/:id` · `messages:write` · **200**
484
+
485
+ | Field | Type | Required | Notes |
486
+ | --- | --- | --- | --- |
487
+ | `scheduled_at` | ISO string | yes | Must be in the future. |
488
+ | `timezone` | string | no | Re-reads it in a different zone. Defaults to the stored one. |
489
+ | `title` | string | no | |
490
+
491
+ Only a `pending` occurrence. Rescheduling one that is already `sent`, `skipped`
492
+ or `cancelled` is a `409 conflict` — not a silent no-op.
493
+
494
+ Moving one occurrence does **not** move the rest of its series.
495
+
496
+ ```ts
497
+ await crm.scheduledMessages.reschedule(id, { scheduled_at: '2026-10-16T14:00:00' });
498
+ ```
499
+
500
+ ---
501
+
502
+ ## cancel
503
+
504
+ ```ts
505
+ crm.scheduledMessages.cancel(id: string): Promise<ScheduledMessage>
506
+ ```
507
+
508
+ `DELETE /v1/audience/scheduled-messages/:id` · `messages:write` · **200**
509
+
510
+ Stands down one occurrence and answers with the row at `status: 'cancelled'`. The
511
+ row is kept rather than deleted, so a message that did not send still shows why.
512
+
513
+ The other occurrences in its series stay queued — use
514
+ [`cancelSeries`](#cancelseries) to stop the whole drip.
515
+
516
+ ---
517
+
518
+ ## cancelSeries
519
+
520
+ ```ts
521
+ crm.scheduledMessages.cancelSeries(series: string): Promise<CancelSeriesResult>
522
+ ```
523
+
524
+ `DELETE /v1/audience/scheduled-messages/series/:series` · `messages:write` · **200**
525
+
526
+ ```ts
527
+ { series: string, cancelled: number }
528
+ ```
529
+
530
+ Cancels every `pending` occurrence. Anything already sent is untouched and not
531
+ counted. Idempotent — a second call cancels nothing and answers `0`.
532
+
533
+ **Usually you do not need this.** If the series carries a condition, changing the
534
+ contact so it no longer matches is the better way to stop a drip: the reason ends
535
+ up on the record, and it works for every contact in the same sequence rather than
536
+ one series at a time.
537
+
538
+ ---
539
+
540
+ ## sendNow
541
+
542
+ ```ts
543
+ crm.scheduledMessages.sendNow(id: string): Promise<ScheduledMessage>
544
+ ```
545
+
546
+ `POST /v1/audience/scheduled-messages/:id/send-now` · `messages:write` · **200**
547
+
548
+ Brings a `pending` occurrence forward to immediately.
549
+
550
+ **The condition is still checked** — that is the point of it. A send-now on a
551
+ contact who no longer matches comes back `skipped`, not `sent`. Cancel the
552
+ occurrence if you want it gone regardless.
553
+
554
+ It moves the occurrence rather than copying it, so the original time no longer
555
+ fires and the contact receives one message.
556
+
557
+ `409 conflict` if the occurrence is not `pending`.