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.
- package/README.md +121 -21
- package/dist/client.d.ts +38 -0
- package/dist/client.js +49 -5
- package/dist/client.js.map +1 -1
- package/dist/http.d.ts +15 -0
- package/dist/http.js +21 -1
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.js +24 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/bytes.d.ts +38 -0
- package/dist/internal/bytes.js +84 -0
- package/dist/internal/bytes.js.map +1 -0
- package/dist/internal/device.d.ts +24 -0
- package/dist/internal/device.js +91 -0
- package/dist/internal/device.js.map +1 -0
- package/dist/internal/md5.d.ts +4 -0
- package/dist/internal/md5.js +109 -0
- package/dist/internal/md5.js.map +1 -0
- package/dist/internal/receiver-file.d.ts +106 -0
- package/dist/internal/receiver-file.js +128 -0
- package/dist/internal/receiver-file.js.map +1 -0
- package/dist/realtime/client.d.ts +85 -0
- package/dist/realtime/client.js +126 -0
- package/dist/realtime/client.js.map +1 -0
- package/dist/realtime/events.d.ts +147 -0
- package/dist/realtime/events.js +3 -0
- package/dist/realtime/events.js.map +1 -0
- package/dist/realtime/inbox.d.ts +173 -0
- package/dist/realtime/inbox.js +263 -0
- package/dist/realtime/inbox.js.map +1 -0
- package/dist/realtime/index.d.ts +16 -0
- package/dist/realtime/index.js +37 -0
- package/dist/realtime/index.js.map +1 -0
- package/dist/resources/audience/contacts.d.ts +19 -3
- package/dist/resources/audience/contacts.js +19 -3
- package/dist/resources/audience/contacts.js.map +1 -1
- package/dist/resources/audience/inbox-media.d.ts +92 -0
- package/dist/resources/audience/inbox-media.js +127 -0
- package/dist/resources/audience/inbox-media.js.map +1 -0
- package/dist/resources/audience/notifications.d.ts +171 -0
- package/dist/resources/audience/notifications.js +215 -0
- package/dist/resources/audience/notifications.js.map +1 -0
- package/dist/resources/audience/scheduled-messages.d.ts +234 -0
- package/dist/resources/audience/scheduled-messages.js +259 -0
- package/dist/resources/audience/scheduled-messages.js.map +1 -0
- package/dist/resources/campaign-audience.d.ts +132 -0
- package/dist/resources/campaign-audience.js +224 -0
- package/dist/resources/campaign-audience.js.map +1 -0
- package/dist/resources/campaign-report-exports.d.ts +14 -0
- package/dist/resources/campaign-report-exports.js +28 -0
- package/dist/resources/campaign-report-exports.js.map +1 -0
- package/dist/resources/campaign-reports.d.ts +15 -0
- package/dist/resources/campaign-reports.js +25 -0
- package/dist/resources/campaign-reports.js.map +1 -0
- package/dist/resources/campaigns.d.ts +239 -0
- package/dist/resources/campaigns.js +300 -0
- package/dist/resources/campaigns.js.map +1 -0
- package/dist/types/audience.d.ts +93 -1
- package/dist/types/campaign.d.ts +229 -0
- package/dist/types/campaign.js +3 -0
- package/dist/types/campaign.js.map +1 -0
- package/dist/types/contact-filter.d.ts +3 -2
- package/dist/types/contact-filter.js +3 -2
- package/dist/types/contact-filter.js.map +1 -1
- package/dist/types/inbox.d.ts +391 -0
- package/dist/types/inbox.js +16 -0
- package/dist/types/inbox.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/scheduled-message.d.ts +221 -0
- package/dist/types/scheduled-message.js +16 -0
- package/dist/types/scheduled-message.js.map +1 -0
- package/docs/audience/contacts.md +34 -4
- package/docs/audience/notifications.md +263 -0
- package/docs/audience/scheduled-messages.md +557 -0
- package/docs/campaign/campaigns.md +489 -0
- package/docs/contact-filter.md +2 -1
- package/docs/inbox/chats.md +356 -0
- package/docs/inbox/connection.md +171 -0
- package/docs/inbox/media.md +202 -0
- package/docs/inbox/messages.md +338 -0
- package/docs/inbox/referrals.md +72 -0
- package/docs/inbox/sessions.md +74 -0
- 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`.
|