ab-ecosystem-cpass 1.0.5 → 1.0.7
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 +62 -1
- package/dist/client.d.ts +24 -0
- package/dist/client.js +32 -5
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +20 -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/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/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-messages.d.ts +150 -0
- package/dist/resources/campaign-messages.js +172 -0
- package/dist/resources/campaign-messages.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 +82 -0
- package/dist/types/campaign.d.ts +319 -0
- package/dist/types/campaign.js +3 -0
- package/dist/types/campaign.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/docs/audience/notifications.md +263 -0
- package/docs/campaign/campaign-messages.md +273 -0
- package/docs/campaign/campaigns.md +449 -0
- package/package.json +1 -1
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
# Campaigns
|
|
2
|
+
|
|
3
|
+
`crm.campaigns` — `/v1/campaigns`
|
|
4
|
+
|
|
5
|
+
One message sent to many people.
|
|
6
|
+
|
|
7
|
+
| Action | Route | Notes |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| [`list`](#list) | `GET /v1/campaigns` | cursor-paged |
|
|
10
|
+
| [`get`](#get) | `GET /v1/campaigns/:id` | |
|
|
11
|
+
| [`create`](#create) | `POST /v1/campaigns` | you supply `filters` |
|
|
12
|
+
| [`createBulk`](#createbulk) | upload + `POST /v1/campaigns` | **numbers or CSV; the SDK does the rest** |
|
|
13
|
+
| [`createGroup`](#creategroup) | uploads + `POST /v1/campaigns` | **one bucket per channel or template** |
|
|
14
|
+
| [`uploadReceivers`](#uploadreceivers) | upload only | the file half of `createBulk` |
|
|
15
|
+
| [`update`](#update) | `PATCH /v1/campaigns/:id` | before it runs |
|
|
16
|
+
| [`schedule`](#schedule) | `POST /v1/campaigns/:id/schedule` | |
|
|
17
|
+
| [`cancel`](#cancel) | `POST /v1/campaigns/:id/cancel` | |
|
|
18
|
+
| [`delete`](#delete) | `DELETE /v1/campaigns/:id` | |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## The audience is one field with five modes
|
|
23
|
+
|
|
24
|
+
A campaign's recipients live in `filters`, and **the mode is decided by the shape
|
|
25
|
+
of the array**, not by a separate selector:
|
|
26
|
+
|
|
27
|
+
| `filters` | Audience |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `[]` | every contact in the account |
|
|
30
|
+
| `ContactFilterEntry[]` | a segment — see [contact filter](../contact-filter.md) |
|
|
31
|
+
| `[{ numbers, hash }]` | a bulk list from an uploaded file |
|
|
32
|
+
| `[{ type: 'channel' \| 'template', numbers, hash, … }]` | a **group** campaign: one bucket per channel or template |
|
|
33
|
+
| `[{ type: 'status' \| 'selection', … }]` | re-target of an earlier campaign |
|
|
34
|
+
|
|
35
|
+
Modes cannot be mixed. Bulk and re-target take **exactly one entry** —
|
|
36
|
+
everything downstream reads `filters[0]` and ignores the rest, so the server
|
|
37
|
+
rejects a second entry rather than silently dropping it.
|
|
38
|
+
|
|
39
|
+
**Group is the exception**: many entries are the whole point, one per bucket. Its
|
|
40
|
+
buckets must all split the same way, though — a `channel` bucket beside a
|
|
41
|
+
`template` bucket describes two different splits of the same recipients, and is
|
|
42
|
+
refused.
|
|
43
|
+
|
|
44
|
+
### Recipients are counted when the campaign sends, not when you create it
|
|
45
|
+
|
|
46
|
+
`campaign.receivers` is `0` on a campaign you just created. The audience is a
|
|
47
|
+
stored *query* — even a bulk list is a file the sending service reads later — so
|
|
48
|
+
the final count is only known at send time, and a campaign scheduled for next
|
|
49
|
+
week can legitimately reach a different number of people than the preview
|
|
50
|
+
showed.
|
|
51
|
+
|
|
52
|
+
`createBulk` returns its own `receivers` count for exactly this reason: it is the
|
|
53
|
+
number of rows written into the file, known now, and it is what you should show a
|
|
54
|
+
user.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## `createBulk`
|
|
59
|
+
|
|
60
|
+
The bulk path, start to finish. You hand it numbers or a CSV; it hands you a
|
|
61
|
+
created campaign.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const { campaign, receivers, skipped, duplicates } = await crm.campaigns.createBulk({
|
|
65
|
+
name: 'Diwali offer',
|
|
66
|
+
audience: { numbers: '919000000001, 919000000002\n919000000003' },
|
|
67
|
+
messages: [{ channel: { id: channelId }, message: template }],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
console.log(`Sending to ${receivers}`); // 3
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Why this is a method and not a recipe
|
|
74
|
+
|
|
75
|
+
A bulk recipient list does not travel inside the campaign. A million numbers is
|
|
76
|
+
not a request body, and the numbers are personal data that should not sit in an
|
|
77
|
+
API log. So the list takes a different route:
|
|
78
|
+
|
|
79
|
+
1. the recipients are **encrypted** into a file, under a secret generated for
|
|
80
|
+
that one file;
|
|
81
|
+
2. the file is **uploaded** to object storage through a presigned URL, so the
|
|
82
|
+
bytes never pass through the API;
|
|
83
|
+
3. the campaign stores a **pointer** — `{ numbers: <url>, hash: <secret> }` — and
|
|
84
|
+
the sending service fetches and decrypts the file when the campaign runs.
|
|
85
|
+
|
|
86
|
+
Every one of those steps has to be exactly right or the campaign fails at send
|
|
87
|
+
time, hours later, with nothing useful in the error. That is why it is one call
|
|
88
|
+
here rather than a page of instructions.
|
|
89
|
+
|
|
90
|
+
### The four ways to describe an audience
|
|
91
|
+
|
|
92
|
+
#### Numbers
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
audience: { numbers: '919000000001, 919000000002\n919000000003' }
|
|
96
|
+
audience: { numbers: ['+91 90000 00001', '919000000002'] }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
A string is split on **commas, newlines, tabs, semicolons and spaces — all of
|
|
100
|
+
them**, in any mixture. Each entry is reduced to digits, so `+91 90000-00001`
|
|
101
|
+
and `919000000001` are the same number. Entries with no digits are dropped.
|
|
102
|
+
|
|
103
|
+
No per-recipient variables: use one of the forms below if your template has
|
|
104
|
+
placeholders.
|
|
105
|
+
|
|
106
|
+
#### CSV or TSV text
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
audience: { csv: await file.text(), recipientColumn: 'phone' }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The first row is the header. The delimiter is detected (tab, then semicolon, then
|
|
113
|
+
comma) unless you pass `delimiter`. Quoted fields are parsed properly, so
|
|
114
|
+
`"1,299"` is one value and `"He said ""yes"""` is one field containing a quote.
|
|
115
|
+
|
|
116
|
+
**Every column becomes a template variable**, including the recipient column
|
|
117
|
+
itself. Headers are normalised the same way the panel always did them —
|
|
118
|
+
lower-cased, with each non-alphanumeric character replaced by `_` — so
|
|
119
|
+
`First Name` is the variable `first_name`.
|
|
120
|
+
|
|
121
|
+
#### Rows you already parsed
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
audience: { rows: await sheet.getRows(), recipientColumn: 'phone' }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
From your own CSV reader, a spreadsheet API, or a database query. Values are
|
|
128
|
+
stringified; the recipient column is matched by its raw name or its normalised
|
|
129
|
+
one.
|
|
130
|
+
|
|
131
|
+
#### Receivers you built
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
audience: {
|
|
135
|
+
receivers: [
|
|
136
|
+
{ waba: '919000000001', custom_fields: [{ id: 'name', value: 'Priya' }] },
|
|
137
|
+
],
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The escape hatch. Taken exactly as given — nothing is normalised, nothing is
|
|
142
|
+
de-duplicated.
|
|
143
|
+
|
|
144
|
+
### What it drops, and tells you about
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const { receivers, skipped, duplicates } = await crm.campaigns.createBulk({ … });
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- `skipped` — entries with no usable number: blank cells, a `Total` footer row,
|
|
151
|
+
`N/A`.
|
|
152
|
+
- `duplicates` — entries whose number already appeared.
|
|
153
|
+
|
|
154
|
+
Both are **counted and reported** rather than passed over. A row file that is
|
|
155
|
+
one-third blank is something you want to know about before the send, not after.
|
|
156
|
+
|
|
157
|
+
De-duplication is on by default because the cost of a duplicate is not a wasted
|
|
158
|
+
table row — it is the same person getting the same marketing message twice,
|
|
159
|
+
billed twice, and more likely to report the sender. Pass `dedupe: false` if you
|
|
160
|
+
genuinely want two sends to one number.
|
|
161
|
+
|
|
162
|
+
Nothing is uploaded and no campaign is created if the audience resolves to zero
|
|
163
|
+
recipients, or to more than **1,000,000** — both throw `validation_failed`.
|
|
164
|
+
|
|
165
|
+
### Previewing before you send
|
|
166
|
+
|
|
167
|
+
The helpers are exported, so a UI can resolve an audience and show the user what
|
|
168
|
+
it found before anything is uploaded:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { resolveAudience, parseDelimited } from 'ab-ecosystem-cpass';
|
|
172
|
+
|
|
173
|
+
const { headers } = parseDelimited(text); // offer these as the column picker
|
|
174
|
+
const { receivers, skipped, duplicates } = resolveAudience({ csv: text, recipientColumn });
|
|
175
|
+
|
|
176
|
+
// "12,480 recipients · 3 duplicates dropped · 2 rows without a number"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`resolveAudience` does no I/O and no crypto. It is the same function `createBulk`
|
|
180
|
+
calls, so what you preview is what gets sent.
|
|
181
|
+
|
|
182
|
+
### Scheduling one
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
await crm.campaigns.createBulk({
|
|
186
|
+
name: 'Renewal reminder',
|
|
187
|
+
audience: { csv, recipientColumn: 'phone' },
|
|
188
|
+
messages: [{ channel: { id: channelId }, message: template }],
|
|
189
|
+
trigger: 'schedule',
|
|
190
|
+
scheduledAt: '2026-11-01T04:30:00.000Z',
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The file is uploaded immediately; only the send waits.
|
|
195
|
+
|
|
196
|
+
### Two things to be careful about
|
|
197
|
+
|
|
198
|
+
**The `hash` is a credential, not a checksum.** It is the key the file is
|
|
199
|
+
encrypted under, and the file's URL is a public storage link. Anyone holding both
|
|
200
|
+
can read the recipient list. Do not log it, do not put it in an error report, and
|
|
201
|
+
do not copy a `filters` entry into another system.
|
|
202
|
+
|
|
203
|
+
**A campaign is not rolled back if creation fails after upload.** If the upload
|
|
204
|
+
succeeds and the `POST` then fails, you have an uploaded file and no campaign —
|
|
205
|
+
harmless, but the file stays. Retry with [`uploadReceivers`](#uploadreceivers)
|
|
206
|
+
and `create` if you want to avoid re-uploading a large list.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## `createGroup`
|
|
211
|
+
|
|
212
|
+
Splits the recipients into buckets and sends each through a different channel, or
|
|
213
|
+
renders each with a different message.
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
const { campaign, buckets, receivers } = await crm.campaigns.createGroup({
|
|
217
|
+
name: 'Diwali offer',
|
|
218
|
+
distributeBy: 'channel',
|
|
219
|
+
buckets: [
|
|
220
|
+
{ channel: channelA, audience: { rows: north, recipientColumn: 'phone' } },
|
|
221
|
+
{ channel: channelB, audience: { rows: south, recipientColumn: 'phone' } },
|
|
222
|
+
],
|
|
223
|
+
messages: [{ channel: { id: channelA }, message: template }],
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`distributeBy: 'template'` instead splits across the messages, and each bucket
|
|
228
|
+
names its message by `pairIndex`:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
buckets: [
|
|
232
|
+
{ pairIndex: 0, audience: { numbers: variantA } },
|
|
233
|
+
{ pairIndex: 1, audience: { numbers: variantB } },
|
|
234
|
+
]
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Three ways it differs from `createBulk`
|
|
238
|
+
|
|
239
|
+
Each of these is something that would be wrong if you assembled the payload by
|
|
240
|
+
hand:
|
|
241
|
+
|
|
242
|
+
- **One file per bucket.** Each bucket is encrypted under its own secret and
|
|
243
|
+
uploaded separately, so `filters` carries one entry per bucket. This is the
|
|
244
|
+
only audience mode with more than one entry.
|
|
245
|
+
- **A different file format.** A bucket file is a `||`-delimited table with a
|
|
246
|
+
header row — not the JSON array a bulk file holds. The reading side picks by
|
|
247
|
+
campaign type, so the wrong encoding decrypts cleanly and then parses to
|
|
248
|
+
nothing.
|
|
249
|
+
- **Columns are fixed by the first recipient.** A table has one header, so every
|
|
250
|
+
row carries the same variables in the same order. Values are addressed by
|
|
251
|
+
column name, so a row whose variables arrived in a different order is still
|
|
252
|
+
written correctly; a row missing one gets an empty cell rather than a shift.
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
waba||first_name||amount
|
|
256
|
+
919000000001||Priya||1299
|
|
257
|
+
919000000002||Ravi||499
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Newlines and literal `||` inside a value are replaced with a space. The format
|
|
261
|
+
has no escape for either, and one would end the row early and shift every later
|
|
262
|
+
value into the wrong column.
|
|
263
|
+
|
|
264
|
+
### Empty buckets are dropped, not refused
|
|
265
|
+
|
|
266
|
+
A bucket whose audience resolves to nobody is left out of `filters` entirely.
|
|
267
|
+
Distributing a list across six channels routinely leaves one or two with nothing
|
|
268
|
+
in them, and that is not an error. The call fails only when **every** bucket is
|
|
269
|
+
empty — or when the buckets together exceed 1,000,000 recipients.
|
|
270
|
+
|
|
271
|
+
`result.buckets` therefore holds only the buckets that were uploaded, in the
|
|
272
|
+
order given, each with its own `receivers` / `skipped` / `duplicates` and its
|
|
273
|
+
`fileUrl`. The top-level counts are the totals.
|
|
274
|
+
|
|
275
|
+
### De-duplication is per bucket
|
|
276
|
+
|
|
277
|
+
A number appearing in two buckets is sent to twice. That is usually the intent —
|
|
278
|
+
the buckets are different channels — and it is not something this can
|
|
279
|
+
second-guess. Within one bucket, duplicates are dropped as everywhere else.
|
|
280
|
+
|
|
281
|
+
### Extra keys ride along
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
{ channel: channelA, extra: { priority: 2 }, audience: { … } }
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`extra` is merged onto the filter entry beside `numbers` and `hash`. The campaign
|
|
288
|
+
contract passes anything it does not recognise through to the sending service
|
|
289
|
+
untouched, so this is the seam for distribution metadata the SDK does not model.
|
|
290
|
+
It cannot overwrite `numbers`, `hash`, `type` or `count`.
|
|
291
|
+
|
|
292
|
+
### How it is dispatched
|
|
293
|
+
|
|
294
|
+
Stored as `type: 'group_campaign'`, which is what the campaign list and reports
|
|
295
|
+
show. On the hand-off to the sending service it is **relabelled
|
|
296
|
+
`bulk_campaign`**, because the two differ only in how many receiver files there
|
|
297
|
+
are and the bulk handling is what reads a file of numbers.
|
|
298
|
+
|
|
299
|
+
The filters are **not** translated along with the name — every bucket, with its
|
|
300
|
+
channel binding and its count, is forwarded exactly as stored.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## `uploadReceivers`
|
|
305
|
+
|
|
306
|
+
The file half on its own.
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
const filter = await crm.campaigns.uploadReceivers(receivers);
|
|
310
|
+
// { numbers: 'https://…/im_m95ibl56.txt', hash: '081389a62af715e91f92026eac67910a' }
|
|
311
|
+
|
|
312
|
+
// For a group bucket, which uses the || table format:
|
|
313
|
+
const bucket = await crm.campaigns.uploadReceivers(receivers, undefined, 'table');
|
|
314
|
+
|
|
315
|
+
await crm.campaigns.create({
|
|
316
|
+
name: 'Diwali offer',
|
|
317
|
+
type: 'bulk_campaign',
|
|
318
|
+
filters: [filter],
|
|
319
|
+
messages: [{ channel: { id: channelId }, message: template }],
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Useful when the two halves should fail independently: a large upload you do not
|
|
324
|
+
want to repeat if campaign creation is rejected, or several campaigns addressed
|
|
325
|
+
to one uploaded list.
|
|
326
|
+
|
|
327
|
+
Keep the returned `hash` with the URL. There is no copy of it anywhere else —
|
|
328
|
+
lose it and the file cannot be decrypted by anything, including us.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## How the file is encrypted
|
|
333
|
+
|
|
334
|
+
Documented because it is a cross-service contract: the sending service decrypts
|
|
335
|
+
these, so the scheme cannot be changed on one side alone.
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
secret = 16 random bytes, hex -- the campaign's `hash`
|
|
339
|
+
key = md5(secret) -- 32 hex characters
|
|
340
|
+
key bytes = the ASCII bytes of that hex -- 32 bytes, so AES-256
|
|
341
|
+
iv = ASCII bytes of '678025308de70905'
|
|
342
|
+
plaintext = JSON.stringify([{ waba, custom_fields }, …]) -- bulk
|
|
343
|
+
| waba||col||col
|
|
344
|
+
… (a || table with a header row) -- group bucket
|
|
345
|
+
ciphertext = AES-256-CBC, PKCS#7 padded
|
|
346
|
+
file body = base64(ciphertext)
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Two parts are counter-intuitive and both are deliberate, because this has to
|
|
350
|
+
match what `CryptoJS.AES.encrypt(text, keyWordArray, { iv })` produces:
|
|
351
|
+
|
|
352
|
+
- **The key is the hex *text*, not the 16 digest bytes.** `md5()` returns a
|
|
353
|
+
32-character string, parsed as UTF-8 — which is 32 bytes, hence AES-256. Using
|
|
354
|
+
the raw digest would be AES-128 and would not decrypt.
|
|
355
|
+
- **There is no `Salted__` header.** CryptoJS writes one only when deriving a key
|
|
356
|
+
from a passphrase; given a key directly, its default formatter emits bare
|
|
357
|
+
base64.
|
|
358
|
+
|
|
359
|
+
The fixed IV is a known weakness — it means two files encrypted under the same
|
|
360
|
+
secret share leading blocks. What keeps it from mattering is that **the secret is
|
|
361
|
+
fresh for every file**, so the IV is never reused under one key. Revising the
|
|
362
|
+
format means a random IV prepended to the ciphertext, and it has to land on the
|
|
363
|
+
sending service before it lands here.
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## `list`
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
const { data, nextCursor } = await crm.campaigns.list({ limit: 50, status: 'completed' });
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
| Query | Type | Notes |
|
|
374
|
+
| --- | --- | --- |
|
|
375
|
+
| `limit` | `number` | 1–100, default 50 |
|
|
376
|
+
| `cursor` | `string` | from the previous page's `nextCursor` |
|
|
377
|
+
| `status` | `pending` `processing` `completed` `failed` `cancelled` | |
|
|
378
|
+
| `campaignType` | `general` `payment` | |
|
|
379
|
+
| `type` | `string` | dispatch route, e.g. `bulk_campaign` |
|
|
380
|
+
| `hidden` | `boolean` | |
|
|
381
|
+
|
|
382
|
+
## `get`
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
const campaign = await crm.campaigns.get(id);
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## `create`
|
|
389
|
+
|
|
390
|
+
For an audience you have already expressed as `filters` — every contact,
|
|
391
|
+
a segment, or a re-target. For a bulk list use [`createBulk`](#createbulk).
|
|
392
|
+
|
|
393
|
+
| Field | Type | Default |
|
|
394
|
+
| --- | --- | --- |
|
|
395
|
+
| `name` | `string` | required, 1–160 |
|
|
396
|
+
| `messages` | `CampaignMessage[]` | required, at least one |
|
|
397
|
+
| `type` | `string` | `general` |
|
|
398
|
+
| `campaignType` | `general` `payment` | `general` |
|
|
399
|
+
| `trigger` | `immediate` `schedule` | `immediate` |
|
|
400
|
+
| `scheduledAt` | ISO datetime | required when `trigger` is `schedule` |
|
|
401
|
+
| `channelType` | `string` | `whatsapp` |
|
|
402
|
+
| `messagingPattern` | `string` | `A2O` |
|
|
403
|
+
| `filters` | `CampaignFilter[]` | `[]` — every contact |
|
|
404
|
+
| `showMessageInPanel` | `boolean` | `true` |
|
|
405
|
+
| `isRepeating` | `boolean` | `false` |
|
|
406
|
+
| `repeat` | `{ every, unit, until }` | — |
|
|
407
|
+
| `hidden` | `boolean` | `false` |
|
|
408
|
+
|
|
409
|
+
## `update`
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
await crm.campaigns.update(id, { name: 'Diwali offer (revised)' });
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Every `create` field, all optional. Only meaningful before the campaign runs.
|
|
416
|
+
|
|
417
|
+
## `schedule`
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
await crm.campaigns.schedule(id, '2026-11-01T04:30:00.000Z');
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## `cancel`
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
await crm.campaigns.cancel(id);
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
## `delete`
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
await crm.campaigns.delete(id);
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
# Reporting
|
|
438
|
+
|
|
439
|
+
Campaign reporting lives on its own page: **[campaign messages](campaign-messages.md)**
|
|
440
|
+
— `crm.campaignMessages`.
|
|
441
|
+
|
|
442
|
+
There is no report collection and no export job. One row is written per attempted
|
|
443
|
+
send, its status is advanced by the channel's delivery webhooks, and every number
|
|
444
|
+
is counted off those rows when you ask for it:
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
const stats = await crm.campaignMessages.stats({ campaign: id });
|
|
448
|
+
// { total, sent, delivered, read, replied, failed }
|
|
449
|
+
```
|