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.
Files changed (42) hide show
  1. package/README.md +62 -1
  2. package/dist/client.d.ts +24 -0
  3. package/dist/client.js +32 -5
  4. package/dist/client.js.map +1 -1
  5. package/dist/index.d.ts +9 -0
  6. package/dist/index.js +20 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/internal/bytes.d.ts +38 -0
  9. package/dist/internal/bytes.js +84 -0
  10. package/dist/internal/bytes.js.map +1 -0
  11. package/dist/internal/device.d.ts +24 -0
  12. package/dist/internal/device.js +91 -0
  13. package/dist/internal/device.js.map +1 -0
  14. package/dist/internal/md5.d.ts +4 -0
  15. package/dist/internal/md5.js +109 -0
  16. package/dist/internal/md5.js.map +1 -0
  17. package/dist/internal/receiver-file.d.ts +106 -0
  18. package/dist/internal/receiver-file.js +128 -0
  19. package/dist/internal/receiver-file.js.map +1 -0
  20. package/dist/resources/audience/notifications.d.ts +171 -0
  21. package/dist/resources/audience/notifications.js +215 -0
  22. package/dist/resources/audience/notifications.js.map +1 -0
  23. package/dist/resources/campaign-audience.d.ts +132 -0
  24. package/dist/resources/campaign-audience.js +224 -0
  25. package/dist/resources/campaign-audience.js.map +1 -0
  26. package/dist/resources/campaign-messages.d.ts +150 -0
  27. package/dist/resources/campaign-messages.js +172 -0
  28. package/dist/resources/campaign-messages.js.map +1 -0
  29. package/dist/resources/campaigns.d.ts +239 -0
  30. package/dist/resources/campaigns.js +300 -0
  31. package/dist/resources/campaigns.js.map +1 -0
  32. package/dist/types/audience.d.ts +82 -0
  33. package/dist/types/campaign.d.ts +319 -0
  34. package/dist/types/campaign.js +3 -0
  35. package/dist/types/campaign.js.map +1 -0
  36. package/dist/types/index.d.ts +1 -0
  37. package/dist/types/index.js +2 -0
  38. package/dist/types/index.js.map +1 -1
  39. package/docs/audience/notifications.md +263 -0
  40. package/docs/campaign/campaign-messages.md +273 -0
  41. package/docs/campaign/campaigns.md +449 -0
  42. package/package.json +1 -1
@@ -0,0 +1,132 @@
1
+ import type { CampaignReceiver } from '../internal/receiver-file';
2
+ /**
3
+ * Most recipients one bulk campaign may carry.
4
+ *
5
+ * The same ceiling the panel has always enforced. It is not arbitrary: the list
6
+ * is encrypted into a single file and read back into memory by the campaign
7
+ * manager, so this is the point at which "one campaign" stops being a sensible
8
+ * unit of work.
9
+ */
10
+ export declare const MAX_BULK_RECEIVERS = 1000000;
11
+ /**
12
+ * How a bulk audience is described.
13
+ *
14
+ * Four shapes, because there are four ways a person actually has this data to
15
+ * hand. All of them end as the same encrypted file; the difference is only how
16
+ * much parsing the SDK does on the way.
17
+ */
18
+ export type BulkAudienceInput =
19
+ /**
20
+ * Just numbers, with no per-recipient variables.
21
+ *
22
+ * Accepts an array, or one string holding many -- a textarea's contents,
23
+ * separated by commas, newlines, semicolons or spaces, in any combination.
24
+ */
25
+ {
26
+ numbers: string | readonly string[];
27
+ dedupe?: boolean;
28
+ }
29
+ /**
30
+ * Raw CSV or TSV text whose first row is the header.
31
+ *
32
+ * `recipientColumn` names the header holding the number; every column,
33
+ * including that one, also becomes a template variable. The delimiter is
34
+ * detected from the header row unless you pass one.
35
+ */
36
+ | {
37
+ csv: string;
38
+ recipientColumn: string;
39
+ delimiter?: string;
40
+ dedupe?: boolean;
41
+ }
42
+ /**
43
+ * Rows you have already parsed -- from your own CSV reader, a spreadsheet API,
44
+ * or a database query.
45
+ */
46
+ | {
47
+ rows: readonly Record<string, unknown>[];
48
+ recipientColumn: string;
49
+ dedupe?: boolean;
50
+ }
51
+ /** Receivers you have already built. The escape hatch; nothing is inferred. */
52
+ | {
53
+ receivers: readonly CampaignReceiver[];
54
+ };
55
+ /** What resolving an audience produced, including what it discarded. */
56
+ export interface ResolvedAudience {
57
+ receivers: CampaignReceiver[];
58
+ /** Entries that carried no usable digits and were skipped. */
59
+ skipped: number;
60
+ /** Entries dropped because an earlier entry had the same number. */
61
+ duplicates: number;
62
+ }
63
+ /**
64
+ * Normalises a column name into a template variable id.
65
+ *
66
+ * Lower-cased with every non-alphanumeric character replaced by `_`, which is
67
+ * what the panel's CSV reader and its paste-a-table path both did -- so a header
68
+ * of `First Name` is the variable `first_name` here exactly as it was there, and
69
+ * templates built against the old behaviour keep working.
70
+ *
71
+ * A blank header becomes `empty_<index>` rather than `_`, so two unnamed columns
72
+ * do not collapse into one variable.
73
+ */
74
+ export declare const normaliseColumn: (header: unknown, index: number) => string;
75
+ /**
76
+ * Pulls the numbers out of one string, or tidies an array of them.
77
+ *
78
+ * Splits on commas, newlines, carriage returns, tabs, semicolons and spaces --
79
+ * all of them, in any mixture. The panel chose a single separator per paste
80
+ * (commas if the text contained any, otherwise newlines), which quietly mangled
81
+ * the common case of pasting a column from a spreadsheet into a field that
82
+ * already had comma-separated numbers: the newlines were never split on, so two
83
+ * numbers merged into one 24-digit number and the send went nowhere. Splitting on
84
+ * every separator cannot produce that, and for input with one separator it
85
+ * behaves identically.
86
+ */
87
+ export declare const normaliseNumbers: (input: string | readonly string[]) => string[];
88
+ /**
89
+ * Splits delimited text into a header and rows, RFC 4180 style.
90
+ *
91
+ * A proper parser rather than `split(',')`, because a quoted field may contain
92
+ * the delimiter, a newline, or an escaped quote -- and a currency-formatted
93
+ * variable (`"1,299"`) is exactly the sort of value that appears in these files.
94
+ * Splitting naively turns one such row into two bad ones.
95
+ *
96
+ * Values stay strings. The panel parsed with `dynamicTyping`, which coerced
97
+ * numeric-looking cells to JavaScript numbers -- and that silently corrupted the
98
+ * one column that matters: a 16-digit number exceeds `Number.MAX_SAFE_INTEGER`
99
+ * and comes back with its last digits changed, while a number written with a
100
+ * leading zero loses it. Keeping text as text avoids both.
101
+ */
102
+ export declare const parseDelimited: (text: string, delimiter?: string) => {
103
+ headers: string[];
104
+ rows: Record<string, string>[];
105
+ };
106
+ /**
107
+ * Turns parsed rows into receivers.
108
+ *
109
+ * Every column becomes a variable, including the recipient column itself -- which
110
+ * is intentional and matches the panel: a template that greets someone by the
111
+ * number they were messaged on still works.
112
+ *
113
+ * A row whose recipient column holds no digits is skipped rather than failing the
114
+ * whole batch. In a file of ten thousand rows, a handful of blanks and a trailing
115
+ * footer line are normal, and refusing the campaign over them would mean the
116
+ * caller has to clean a file they may not control. The count comes back in
117
+ * {@link ResolvedAudience.skipped} so the omission is visible rather than silent.
118
+ */
119
+ export declare const buildReceivers: (rows: readonly Record<string, unknown>[], recipientColumn: string, options?: {
120
+ dedupe?: boolean;
121
+ }) => ResolvedAudience;
122
+ /**
123
+ * Resolves any of the four audience shapes into one receiver list.
124
+ *
125
+ * De-duplicates by number unless `dedupe: false`. On by default because the cost
126
+ * of a duplicate here is not a wasted row in a table -- it is the same person
127
+ * receiving the same marketing message twice, billed twice, and more likely to
128
+ * report the sender. A caller who genuinely wants two sends to one number can
129
+ * say so.
130
+ */
131
+ export declare const resolveAudience: (input: BulkAudienceInput) => ResolvedAudience;
132
+ export type { CampaignReceiver };
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveAudience = exports.buildReceivers = exports.parseDelimited = exports.normaliseNumbers = exports.normaliseColumn = exports.MAX_BULK_RECEIVERS = void 0;
4
+ /**
5
+ * Most recipients one bulk campaign may carry.
6
+ *
7
+ * The same ceiling the panel has always enforced. It is not arbitrary: the list
8
+ * is encrypted into a single file and read back into memory by the campaign
9
+ * manager, so this is the point at which "one campaign" stops being a sensible
10
+ * unit of work.
11
+ */
12
+ exports.MAX_BULK_RECEIVERS = 1_000_000;
13
+ /** Digits only: no `+`, no spaces, no dashes, no parentheses. */
14
+ const digitsOf = (value) => (value === null || value === undefined ? '' : String(value).replace(/\D/g, ''));
15
+ /**
16
+ * Normalises a column name into a template variable id.
17
+ *
18
+ * Lower-cased with every non-alphanumeric character replaced by `_`, which is
19
+ * what the panel's CSV reader and its paste-a-table path both did -- so a header
20
+ * of `First Name` is the variable `first_name` here exactly as it was there, and
21
+ * templates built against the old behaviour keep working.
22
+ *
23
+ * A blank header becomes `empty_<index>` rather than `_`, so two unnamed columns
24
+ * do not collapse into one variable.
25
+ */
26
+ const normaliseColumn = (header, index) => {
27
+ const name = String(header ?? '').trim();
28
+ if (!name)
29
+ return `empty_${index}`;
30
+ return name.toLowerCase().replace(/[^a-z0-9]/g, '_');
31
+ };
32
+ exports.normaliseColumn = normaliseColumn;
33
+ /**
34
+ * Pulls the numbers out of one string, or tidies an array of them.
35
+ *
36
+ * Splits on commas, newlines, carriage returns, tabs, semicolons and spaces --
37
+ * all of them, in any mixture. The panel chose a single separator per paste
38
+ * (commas if the text contained any, otherwise newlines), which quietly mangled
39
+ * the common case of pasting a column from a spreadsheet into a field that
40
+ * already had comma-separated numbers: the newlines were never split on, so two
41
+ * numbers merged into one 24-digit number and the send went nowhere. Splitting on
42
+ * every separator cannot produce that, and for input with one separator it
43
+ * behaves identically.
44
+ */
45
+ const normaliseNumbers = (input) => {
46
+ const parts = typeof input === 'string' ? input.split(/[\s,;]+/) : input;
47
+ return parts.map(digitsOf).filter((value) => value.length > 0);
48
+ };
49
+ exports.normaliseNumbers = normaliseNumbers;
50
+ /**
51
+ * Splits delimited text into a header and rows, RFC 4180 style.
52
+ *
53
+ * A proper parser rather than `split(',')`, because a quoted field may contain
54
+ * the delimiter, a newline, or an escaped quote -- and a currency-formatted
55
+ * variable (`"1,299"`) is exactly the sort of value that appears in these files.
56
+ * Splitting naively turns one such row into two bad ones.
57
+ *
58
+ * Values stay strings. The panel parsed with `dynamicTyping`, which coerced
59
+ * numeric-looking cells to JavaScript numbers -- and that silently corrupted the
60
+ * one column that matters: a 16-digit number exceeds `Number.MAX_SAFE_INTEGER`
61
+ * and comes back with its last digits changed, while a number written with a
62
+ * leading zero loses it. Keeping text as text avoids both.
63
+ */
64
+ const parseDelimited = (text, delimiter) => {
65
+ const rows = splitRows(text, delimiter ?? detectDelimiter(text));
66
+ const headerRow = rows.shift();
67
+ if (!headerRow)
68
+ return { headers: [], rows: [] };
69
+ const headers = headerRow.map(exports.normaliseColumn);
70
+ const parsed = rows
71
+ // A trailing newline, or a blank line in the middle, is not a record.
72
+ .filter((cells) => cells.some((cell) => cell.trim() !== ''))
73
+ .map((cells) => {
74
+ const row = {};
75
+ headers.forEach((header, index) => {
76
+ row[header] = cells[index] ?? '';
77
+ });
78
+ return row;
79
+ });
80
+ return { headers, rows: parsed };
81
+ };
82
+ exports.parseDelimited = parseDelimited;
83
+ /** Tab if the header row has one, else semicolon, else comma. */
84
+ const detectDelimiter = (text) => {
85
+ const firstLine = text.slice(0, text.search(/\r?\n/) === -1 ? undefined : text.search(/\r?\n/));
86
+ if (firstLine.includes('\t'))
87
+ return '\t';
88
+ if (firstLine.includes(';') && !firstLine.includes(','))
89
+ return ';';
90
+ return ',';
91
+ };
92
+ /** The parser proper: quotes, doubled quotes inside them, and newlines within a field. */
93
+ const splitRows = (text, delimiter) => {
94
+ const rows = [];
95
+ let row = [];
96
+ let field = '';
97
+ let quoted = false;
98
+ for (let i = 0; i < text.length; i += 1) {
99
+ const char = text[i];
100
+ if (quoted) {
101
+ if (char !== '"') {
102
+ field += char;
103
+ }
104
+ else if (text[i + 1] === '"') {
105
+ // A doubled quote inside a quoted field is one literal quote.
106
+ field += '"';
107
+ i += 1;
108
+ }
109
+ else {
110
+ quoted = false;
111
+ }
112
+ continue;
113
+ }
114
+ if (char === '"' && field === '') {
115
+ quoted = true;
116
+ }
117
+ else if (char === delimiter) {
118
+ row.push(field);
119
+ field = '';
120
+ }
121
+ else if (char === '\n' || char === '\r') {
122
+ // Either line ending, and CRLF counts once.
123
+ if (char === '\r' && text[i + 1] === '\n')
124
+ i += 1;
125
+ row.push(field);
126
+ rows.push(row);
127
+ row = [];
128
+ field = '';
129
+ }
130
+ else {
131
+ field += char;
132
+ }
133
+ }
134
+ if (field !== '' || row.length) {
135
+ row.push(field);
136
+ rows.push(row);
137
+ }
138
+ return rows;
139
+ };
140
+ /**
141
+ * Turns parsed rows into receivers.
142
+ *
143
+ * Every column becomes a variable, including the recipient column itself -- which
144
+ * is intentional and matches the panel: a template that greets someone by the
145
+ * number they were messaged on still works.
146
+ *
147
+ * A row whose recipient column holds no digits is skipped rather than failing the
148
+ * whole batch. In a file of ten thousand rows, a handful of blanks and a trailing
149
+ * footer line are normal, and refusing the campaign over them would mean the
150
+ * caller has to clean a file they may not control. The count comes back in
151
+ * {@link ResolvedAudience.skipped} so the omission is visible rather than silent.
152
+ */
153
+ const buildReceivers = (rows, recipientColumn, options = {}) => {
154
+ const column = (0, exports.normaliseColumn)(recipientColumn, 0);
155
+ const receivers = [];
156
+ const seen = new Set();
157
+ let skipped = 0;
158
+ let duplicates = 0;
159
+ for (const row of rows) {
160
+ // Accept the column as written or as normalised, so a caller passing
161
+ // `'First Name'` against already-normalised rows still resolves.
162
+ const raw = row[recipientColumn] ?? row[column];
163
+ const waba = digitsOf(raw);
164
+ if (!waba) {
165
+ skipped += 1;
166
+ continue;
167
+ }
168
+ if (options.dedupe !== false) {
169
+ if (seen.has(waba)) {
170
+ duplicates += 1;
171
+ continue;
172
+ }
173
+ seen.add(waba);
174
+ }
175
+ receivers.push({
176
+ waba,
177
+ custom_fields: Object.keys(row).map((key, index) => ({
178
+ id: (0, exports.normaliseColumn)(key, index),
179
+ value: row[key] === null || row[key] === undefined ? '' : String(row[key]),
180
+ })),
181
+ });
182
+ }
183
+ return { receivers, skipped, duplicates };
184
+ };
185
+ exports.buildReceivers = buildReceivers;
186
+ /**
187
+ * Resolves any of the four audience shapes into one receiver list.
188
+ *
189
+ * De-duplicates by number unless `dedupe: false`. On by default because the cost
190
+ * of a duplicate here is not a wasted row in a table -- it is the same person
191
+ * receiving the same marketing message twice, billed twice, and more likely to
192
+ * report the sender. A caller who genuinely wants two sends to one number can
193
+ * say so.
194
+ */
195
+ const resolveAudience = (input) => {
196
+ if ('receivers' in input) {
197
+ // Taken as given: a caller who built these chose the shape deliberately.
198
+ return { receivers: [...input.receivers], skipped: 0, duplicates: 0 };
199
+ }
200
+ if ('numbers' in input) {
201
+ const numbers = (0, exports.normaliseNumbers)(input.numbers);
202
+ const receivers = [];
203
+ const seen = new Set();
204
+ let duplicates = 0;
205
+ for (const waba of numbers) {
206
+ if (input.dedupe !== false) {
207
+ if (seen.has(waba)) {
208
+ duplicates += 1;
209
+ continue;
210
+ }
211
+ seen.add(waba);
212
+ }
213
+ receivers.push({ waba, custom_fields: [] });
214
+ }
215
+ const supplied = typeof input.numbers === 'string' ? numbers.length + countBlank(input.numbers) : input.numbers.length;
216
+ return { receivers, skipped: Math.max(0, supplied - numbers.length - duplicates), duplicates };
217
+ }
218
+ const rows = 'csv' in input ? (0, exports.parseDelimited)(input.csv, input.delimiter).rows : input.rows;
219
+ return (0, exports.buildReceivers)(rows, input.recipientColumn, { dedupe: input.dedupe });
220
+ };
221
+ exports.resolveAudience = resolveAudience;
222
+ /** Entries in a pasted string that held no digits at all, for the skipped count. */
223
+ const countBlank = (text) => text.split(/[\s,;]+/).filter((part) => part !== '' && digitsOf(part) === '').length;
224
+ //# sourceMappingURL=campaign-audience.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"campaign-audience.js","sourceRoot":"","sources":["../../src/resources/campaign-audience.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,150 @@
1
+ import type { CampaignMessageRecord, CampaignMessageStats, CampaignMessageStatsQuery, CampaignRepliesQuery, CampaignReplyBreakdown, ListCampaignMessagesQuery } from '../types/campaign';
2
+ import type { Paginated } from '../types/common';
3
+ import type { HttpClient } from '../http';
4
+ /**
5
+ * Campaign reporting: what actually happened to each message.
6
+ *
7
+ * ```ts
8
+ * const stats = await crm.campaignMessages.stats({ campaign });
9
+ * // { total: 12480, sent: 12410, delivered: 11980, read: 7420, replied: 310, failed: 70 }
10
+ *
11
+ * const { data } = await crm.campaignMessages.list({ campaign, status: 'failed' });
12
+ * data[0].error; // why that one did not go
13
+ * ```
14
+ *
15
+ * ## Where these numbers come from
16
+ *
17
+ * One row per attempted send, written by the campaign manager as it hands each
18
+ * message to a channel, with its `status` moved forward by the channel's
19
+ * delivery webhooks -- which are processed outside the CRM entirely. The report
20
+ * is **counted off those rows when you ask for it**, not rolled up on a
21
+ * schedule, so there is no "generated at" to reason about and no staleness
22
+ * window: the numbers are as current as the webhooks that have arrived.
23
+ *
24
+ * The flip side is that a campaign still going out has moving numbers. Reading
25
+ * `stats` seconds after launching under-counts everything, and that is correct
26
+ * rather than a lag.
27
+ *
28
+ * ## The funnel
29
+ *
30
+ * `status` is a single field holding the furthest stage reached, so the counts
31
+ * are cumulative and nest: `replied ⊆ read ⊆ delivered ⊆ sent`. For a stage's
32
+ * exclusive count, subtract the next one -- `delivered - read` is "delivered but
33
+ * unread". `failed` is outside the funnel and never inside `sent`.
34
+ *
35
+ * ## There is no export endpoint
36
+ *
37
+ * An export is {@link list} written to a file, so {@link iterate} does the
38
+ * paging and the caller formats the rows. Nothing is queued, nothing has to be
39
+ * polled, and no generated file expires before someone downloads it.
40
+ */
41
+ export declare class CampaignMessages {
42
+ private readonly http;
43
+ constructor(http: HttpClient);
44
+ /**
45
+ * The campaign's funnel counts.
46
+ *
47
+ * `GET /v1/campaign-messages/stats?campaign=<id>` · `campaigns:read` · **200**
48
+ *
49
+ * ### Query
50
+ *
51
+ * | Field | Type | Required | Notes |
52
+ * | --- | --- | --- | --- |
53
+ * | `campaign` | string | yes | The campaign id. |
54
+ * | `message` | number | no | One message of a multi-message campaign, zero-based. |
55
+ *
56
+ * ```ts
57
+ * const overall = await crm.campaignMessages.stats({ campaign });
58
+ * const second = await crm.campaignMessages.stats({ campaign, message: 1 });
59
+ * ```
60
+ *
61
+ * No date window: a campaign is one send, so its whole life is the only
62
+ * meaningful window. A repeating campaign's runs are separate campaigns with
63
+ * their own ids.
64
+ *
65
+ * `404 not_found` if the campaign is not the caller's -- the same answer as a
66
+ * campaign that does not exist, so the error cannot be used to probe ids.
67
+ */
68
+ stats(query: CampaignMessageStatsQuery): Promise<CampaignMessageStats>;
69
+ /**
70
+ * The rows behind the funnel. This is the report table.
71
+ *
72
+ * `GET /v1/campaign-messages?campaign=<id>` · `campaigns:read` · **200**
73
+ *
74
+ * ### Query
75
+ *
76
+ * | Field | Type | Default | Notes |
77
+ * | --- | --- | --- | --- |
78
+ * | `campaign` | string | — | Required. |
79
+ * | `message` | number | — | Zero-based index of one campaign message. |
80
+ * | `status` | `sent \| delivered \| read \| replied \| failed` | — | Cumulative; see below. Omit for every row. |
81
+ * | `exact` | boolean | `false` | Only rows sitting at `status` right now. |
82
+ * | `identifier` | string | — | Exact match on one recipient. Not a search. |
83
+ * | `reply_type` | string | — | Replied rows only; refused otherwise. |
84
+ * | `replied_within_hours` | number | — | Replied rows only; refused otherwise. |
85
+ * | `page` | number | `1` | 1-based. |
86
+ * | `rows` | number | `25` | 1–100. |
87
+ *
88
+ * Newest attempt first, ordered by when the row was written -- not by a stage
89
+ * timestamp, which would reorder the list as webhooks arrive and make paging
90
+ * skip and repeat rows.
91
+ *
92
+ * ```ts
93
+ * // the two queries a report screen actually makes
94
+ * const failures = await crm.campaignMessages.list({ campaign, status: 'failed', rows: 100 });
95
+ * const unread = await crm.campaignMessages.list({ campaign, status: 'delivered', exact: true });
96
+ * ```
97
+ *
98
+ * `reply_type` and `replied_within_hours` are **refused** with
99
+ * `400 validation_failed` unless `status` is `replied`, rather than quietly
100
+ * matching nothing.
101
+ */
102
+ list(query: ListCampaignMessagesQuery): Promise<Paginated<CampaignMessageRecord>>;
103
+ /**
104
+ * Every matching row, paged through for you. What an export is.
105
+ *
106
+ * ```ts
107
+ * for await (const row of crm.campaignMessages.iterate({ campaign })) {
108
+ * csv.write([row.identifier, row.name, row.status, row.error ?? '']);
109
+ * }
110
+ * ```
111
+ *
112
+ * Pages at 100 rows unless told otherwise -- the server's maximum, and the
113
+ * fewest round trips. A campaign of a million recipients is ten thousand
114
+ * requests, so for exports that size prefer running it where the latency is
115
+ * low rather than from a browser.
116
+ *
117
+ * The collection is live: rows are written while a campaign is going out, and
118
+ * statuses change under an export that takes minutes. Offset paging over
119
+ * moving data can repeat or skip a row -- for a stable snapshot, export after
120
+ * the campaign completes.
121
+ */
122
+ iterate(query: ListCampaignMessagesQuery): AsyncGenerator<CampaignMessageRecord>;
123
+ /**
124
+ * What people replied, grouped by the reply itself.
125
+ *
126
+ * `GET /v1/campaign-messages/replies?campaign=<id>` · `campaigns:read` · **200**
127
+ *
128
+ * ### Query
129
+ *
130
+ * | Field | Type | Default | Notes |
131
+ * | --- | --- | --- | --- |
132
+ * | `campaign` | string | — | Required. |
133
+ * | `message` | number | — | Zero-based index of one campaign message. |
134
+ * | `limit` | number | `50` | Distinct replies to return, 1–200. |
135
+ *
136
+ * ```ts
137
+ * const { groups, replied, distinct } = await crm.campaignMessages.replies({ campaign });
138
+ * groups; // [{ reply: 'Yes', replyType: 'button', count: 182 }, …]
139
+ * ```
140
+ *
141
+ * This is what a poll or a button message is sent to find out. Grouped on the
142
+ * reply **and** its type, so a tapped `Yes` and a typed `Yes` stay apart --
143
+ * they say different things about how engaged the recipient was.
144
+ *
145
+ * `distinct` against `groups.length` tells you whether the breakdown is
146
+ * complete. A campaign that asked an open question has as many groups as it
147
+ * has repliers, so a truncated answer there is normal rather than a problem.
148
+ */
149
+ replies(query: CampaignRepliesQuery): Promise<CampaignReplyBreakdown>;
150
+ }