ab-ecosystem-cpass 1.0.6 → 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 CHANGED
@@ -275,8 +275,7 @@ to show a user.
275
275
  | Resource | Page | Actions |
276
276
  | --- | --- | --- |
277
277
  | `crm.campaigns` | [campaigns](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/campaign/campaigns.md) | `list` `get` `create` `createBulk` `createGroup` `uploadReceivers` `update` `schedule` `cancel` `delete` |
278
- | `crm.campaignReports` | [campaigns](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/campaign/campaigns.md#campaign-reports) | `list` `getByCampaign` |
279
- | `crm.campaignReportExports` | [campaigns](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/campaign/campaigns.md#campaign-reports) | `list` `get` `create` |
278
+ | `crm.campaignMessages` | [campaign messages](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/campaign/campaign-messages.md) | `stats` `list` `iterate` `replies` — reporting, counted off the per-recipient rows |
280
279
 
281
280
  The audience helpers are exported too, so a UI can show "12,480 recipients, 3
282
281
  duplicates dropped" before anything is uploaded:
package/dist/client.d.ts CHANGED
@@ -18,8 +18,7 @@ import { Billing } from './resources/channels/billing';
18
18
  import { Wallet } from './resources/channels/wallet';
19
19
  import { Activity } from './resources/channels/activity';
20
20
  import { Campaigns } from './resources/campaigns';
21
- import { CampaignReports } from './resources/campaign-reports';
22
- import { CampaignReportExports } from './resources/campaign-report-exports';
21
+ import { CampaignMessages } from './resources/campaign-messages';
23
22
  /**
24
23
  * One client for the whole gateway. Construct it once and share it: it holds no
25
24
  * per-request state, and every resource below runs over the same HTTP client,
@@ -91,9 +90,11 @@ export declare class CrmClient {
91
90
  * has to get exactly right.
92
91
  */
93
92
  readonly campaigns: Campaigns;
94
- /** Per-campaign delivery metrics. */
95
- readonly campaignReports: CampaignReports;
96
- /** Report extracts as CSV or XLSX, generated asynchronously. */
97
- readonly campaignReportExports: CampaignReportExports;
93
+ /**
94
+ * Campaign reporting: the funnel, the per-recipient rows behind it, and what
95
+ * people replied -- all counted off the message rows when asked for, so there
96
+ * is no roll-up to wait for and no export job to poll.
97
+ */
98
+ readonly campaignMessages: CampaignMessages;
98
99
  constructor(options: ClientOptions);
99
100
  }
package/dist/client.js CHANGED
@@ -24,8 +24,7 @@ const wallet_1 = require("./resources/channels/wallet");
24
24
  const activity_1 = require("./resources/channels/activity");
25
25
  // Campaign
26
26
  const campaigns_1 = require("./resources/campaigns");
27
- const campaign_reports_1 = require("./resources/campaign-reports");
28
- const campaign_report_exports_1 = require("./resources/campaign-report-exports");
27
+ const campaign_messages_1 = require("./resources/campaign-messages");
29
28
  /**
30
29
  * One client for the whole gateway. Construct it once and share it: it holds no
31
30
  * per-request state, and every resource below runs over the same HTTP client,
@@ -100,10 +99,12 @@ class CrmClient {
100
99
  * has to get exactly right.
101
100
  */
102
101
  campaigns;
103
- /** Per-campaign delivery metrics. */
104
- campaignReports;
105
- /** Report extracts as CSV or XLSX, generated asynchronously. */
106
- campaignReportExports;
102
+ /**
103
+ * Campaign reporting: the funnel, the per-recipient rows behind it, and what
104
+ * people replied -- all counted off the message rows when asked for, so there
105
+ * is no roll-up to wait for and no export job to poll.
106
+ */
107
+ campaignMessages;
107
108
  constructor(options) {
108
109
  const http = new http_1.HttpClient(options);
109
110
  this.contacts = new contacts_1.Contacts(http);
@@ -128,8 +129,7 @@ class CrmClient {
128
129
  // audience is uploaded through the media endpoint before the campaign is
129
130
  // created.
130
131
  this.campaigns = new campaigns_1.Campaigns(http);
131
- this.campaignReports = new campaign_reports_1.CampaignReports(http);
132
- this.campaignReportExports = new campaign_report_exports_1.CampaignReportExports(http);
132
+ this.campaignMessages = new campaign_messages_1.CampaignMessages(http);
133
133
  }
134
134
  }
135
135
  exports.CrmClient = CrmClient;
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -15,8 +15,7 @@ export { Webhooks } from './resources/audience/webhooks';
15
15
  export { Notifications } from './resources/audience/notifications';
16
16
  export { InboxMedia, type UploadMediaOptions, type UploadMediaSource, } from './resources/audience/inbox-media';
17
17
  export { Campaigns, type CreateBulkCampaignInput, type BulkCampaignResult, type CreateGroupCampaignInput, type GroupBucket, type GroupBucketResult, type GroupCampaignResult, type ReceiverUploader, } from './resources/campaigns';
18
- export { CampaignReports } from './resources/campaign-reports';
19
- export { CampaignReportExports } from './resources/campaign-report-exports';
18
+ export { CampaignMessages } from './resources/campaign-messages';
20
19
  /**
21
20
  * The audience helpers, exported because they are useful before a campaign
22
21
  * exists: a UI can parse a CSV, show "12,480 recipients, 3 duplicates dropped",
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.Activity = exports.Wallet = exports.Billing = exports.ChannelWebhooks = exports.Templates = exports.Channels = exports.resolveAudience = exports.parseDelimited = exports.normaliseNumbers = exports.normaliseColumn = exports.buildReceivers = exports.MAX_BULK_RECEIVERS = exports.CampaignReportExports = exports.CampaignReports = exports.Campaigns = exports.InboxMedia = exports.Notifications = exports.Webhooks = exports.Exports = exports.Imports = exports.CrmTriggers = exports.Segments = exports.CustomFields = exports.ContactNotes = exports.ContactTags = exports.Contacts = exports.CHANNELS = exports.AUDIENCE = void 0;
17
+ exports.Activity = exports.Wallet = exports.Billing = exports.ChannelWebhooks = exports.Templates = exports.Channels = exports.resolveAudience = exports.parseDelimited = exports.normaliseNumbers = exports.normaliseColumn = exports.buildReceivers = exports.MAX_BULK_RECEIVERS = exports.CampaignMessages = exports.Campaigns = exports.InboxMedia = exports.Notifications = exports.Webhooks = exports.Exports = exports.Imports = exports.CrmTriggers = exports.Segments = exports.CustomFields = exports.ContactNotes = exports.ContactTags = exports.Contacts = exports.CHANNELS = exports.AUDIENCE = void 0;
18
18
  __exportStar(require("./client"), exports);
19
19
  __exportStar(require("./http"), exports);
20
20
  __exportStar(require("./errors"), exports);
@@ -48,10 +48,8 @@ Object.defineProperty(exports, "InboxMedia", { enumerable: true, get: function (
48
48
  // Campaign
49
49
  var campaigns_1 = require("./resources/campaigns");
50
50
  Object.defineProperty(exports, "Campaigns", { enumerable: true, get: function () { return campaigns_1.Campaigns; } });
51
- var campaign_reports_1 = require("./resources/campaign-reports");
52
- Object.defineProperty(exports, "CampaignReports", { enumerable: true, get: function () { return campaign_reports_1.CampaignReports; } });
53
- var campaign_report_exports_1 = require("./resources/campaign-report-exports");
54
- Object.defineProperty(exports, "CampaignReportExports", { enumerable: true, get: function () { return campaign_report_exports_1.CampaignReportExports; } });
51
+ var campaign_messages_1 = require("./resources/campaign-messages");
52
+ Object.defineProperty(exports, "CampaignMessages", { enumerable: true, get: function () { return campaign_messages_1.CampaignMessages; } });
55
53
  /**
56
54
  * The audience helpers, exported because they are useful before a campaign
57
55
  * exists: a UI can parse a CSV, show "12,480 recipients, 3 duplicates dropped",
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,2CAAyB;AACzB,yCAAuB;AACvB,2CAAyB;AACzB,0CAAwB;AAExB,2CAAuD;AAA9C,iGAAA,QAAQ,OAAA;AAAE,iGAAA,QAAQ,OAAA;AAE3B,WAAW;AACX,0DAAyD;AAAhD,oGAAA,QAAQ,OAAA;AACjB,kEAAgE;AAAvD,2GAAA,WAAW,OAAA;AACpB,oEAAkE;AAAzD,6GAAA,YAAY,OAAA;AACrB,oEAAkE;AAAzD,6GAAA,YAAY,OAAA;AACrB,0DAAyD;AAAhD,oGAAA,QAAQ,OAAA;AACjB,kEAAgE;AAAvD,2GAAA,WAAW,OAAA;AACpB,sEAA8D;AAArD,yGAAA,OAAO,OAAA;AAChB,sEAA8D;AAArD,yGAAA,OAAO,OAAA;AAChB,0DAAyD;AAAhD,oGAAA,QAAQ,OAAA;AACjB,oEAAmE;AAA1D,8GAAA,aAAa,OAAA;AACtB,gEAI0C;AAHxC,yGAAA,UAAU,OAAA;AAKZ,WAAW;AACX,mDAS+B;AAR7B,sGAAA,SAAS,OAAA;AASX,iEAA+D;AAAtD,mHAAA,eAAe,OAAA;AACxB,+EAA4E;AAAnE,gIAAA,qBAAqB,OAAA;AAC9B;;;;GAIG;AACH,mEAUuC;AATrC,uHAAA,kBAAkB,OAAA;AAClB,mHAAA,cAAc,OAAA;AACd,oHAAA,eAAe,OAAA;AACf,qHAAA,gBAAgB,OAAA;AAChB,mHAAA,cAAc,OAAA;AACd,oHAAA,eAAe,OAAA;AAMjB,WAAW;AACX,0DAA8E;AAArE,oGAAA,QAAQ,OAAA;AACjB,4DAA2D;AAAlD,sGAAA,SAAS,OAAA;AAClB,0DAAgE;AAAvD,2GAAA,eAAe,OAAA;AACxB,wDAAuD;AAA9C,kGAAA,OAAO,OAAA;AAChB,sDAAqD;AAA5C,gGAAA,MAAM,OAAA;AACf,0DAAyD;AAAhD,oGAAA,QAAQ,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,2CAAyB;AACzB,yCAAuB;AACvB,2CAAyB;AACzB,0CAAwB;AAExB,2CAAuD;AAA9C,iGAAA,QAAQ,OAAA;AAAE,iGAAA,QAAQ,OAAA;AAE3B,WAAW;AACX,0DAAyD;AAAhD,oGAAA,QAAQ,OAAA;AACjB,kEAAgE;AAAvD,2GAAA,WAAW,OAAA;AACpB,oEAAkE;AAAzD,6GAAA,YAAY,OAAA;AACrB,oEAAkE;AAAzD,6GAAA,YAAY,OAAA;AACrB,0DAAyD;AAAhD,oGAAA,QAAQ,OAAA;AACjB,kEAAgE;AAAvD,2GAAA,WAAW,OAAA;AACpB,sEAA8D;AAArD,yGAAA,OAAO,OAAA;AAChB,sEAA8D;AAArD,yGAAA,OAAO,OAAA;AAChB,0DAAyD;AAAhD,oGAAA,QAAQ,OAAA;AACjB,oEAAmE;AAA1D,8GAAA,aAAa,OAAA;AACtB,gEAI0C;AAHxC,yGAAA,UAAU,OAAA;AAKZ,WAAW;AACX,mDAS+B;AAR7B,sGAAA,SAAS,OAAA;AASX,mEAAiE;AAAxD,qHAAA,gBAAgB,OAAA;AACzB;;;;GAIG;AACH,mEAUuC;AATrC,uHAAA,kBAAkB,OAAA;AAClB,mHAAA,cAAc,OAAA;AACd,oHAAA,eAAe,OAAA;AACf,qHAAA,gBAAgB,OAAA;AAChB,mHAAA,cAAc,OAAA;AACd,oHAAA,eAAe,OAAA;AAMjB,WAAW;AACX,0DAA8E;AAArE,oGAAA,QAAQ,OAAA;AACjB,4DAA2D;AAAlD,sGAAA,SAAS,OAAA;AAClB,0DAAgE;AAAvD,2GAAA,eAAe,OAAA;AACxB,wDAAuD;AAA9C,kGAAA,OAAO,OAAA;AAChB,sDAAqD;AAA5C,gGAAA,MAAM,OAAA;AACf,0DAAyD;AAAhD,oGAAA,QAAQ,OAAA"}
@@ -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
+ }
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CampaignMessages = void 0;
4
+ const BASE = '/v1/campaign-messages';
5
+ /**
6
+ * Campaign reporting: what actually happened to each message.
7
+ *
8
+ * ```ts
9
+ * const stats = await crm.campaignMessages.stats({ campaign });
10
+ * // { total: 12480, sent: 12410, delivered: 11980, read: 7420, replied: 310, failed: 70 }
11
+ *
12
+ * const { data } = await crm.campaignMessages.list({ campaign, status: 'failed' });
13
+ * data[0].error; // why that one did not go
14
+ * ```
15
+ *
16
+ * ## Where these numbers come from
17
+ *
18
+ * One row per attempted send, written by the campaign manager as it hands each
19
+ * message to a channel, with its `status` moved forward by the channel's
20
+ * delivery webhooks -- which are processed outside the CRM entirely. The report
21
+ * is **counted off those rows when you ask for it**, not rolled up on a
22
+ * schedule, so there is no "generated at" to reason about and no staleness
23
+ * window: the numbers are as current as the webhooks that have arrived.
24
+ *
25
+ * The flip side is that a campaign still going out has moving numbers. Reading
26
+ * `stats` seconds after launching under-counts everything, and that is correct
27
+ * rather than a lag.
28
+ *
29
+ * ## The funnel
30
+ *
31
+ * `status` is a single field holding the furthest stage reached, so the counts
32
+ * are cumulative and nest: `replied ⊆ read ⊆ delivered ⊆ sent`. For a stage's
33
+ * exclusive count, subtract the next one -- `delivered - read` is "delivered but
34
+ * unread". `failed` is outside the funnel and never inside `sent`.
35
+ *
36
+ * ## There is no export endpoint
37
+ *
38
+ * An export is {@link list} written to a file, so {@link iterate} does the
39
+ * paging and the caller formats the rows. Nothing is queued, nothing has to be
40
+ * polled, and no generated file expires before someone downloads it.
41
+ */
42
+ class CampaignMessages {
43
+ http;
44
+ constructor(http) {
45
+ this.http = http;
46
+ }
47
+ /**
48
+ * The campaign's funnel counts.
49
+ *
50
+ * `GET /v1/campaign-messages/stats?campaign=<id>` · `campaigns:read` · **200**
51
+ *
52
+ * ### Query
53
+ *
54
+ * | Field | Type | Required | Notes |
55
+ * | --- | --- | --- | --- |
56
+ * | `campaign` | string | yes | The campaign id. |
57
+ * | `message` | number | no | One message of a multi-message campaign, zero-based. |
58
+ *
59
+ * ```ts
60
+ * const overall = await crm.campaignMessages.stats({ campaign });
61
+ * const second = await crm.campaignMessages.stats({ campaign, message: 1 });
62
+ * ```
63
+ *
64
+ * No date window: a campaign is one send, so its whole life is the only
65
+ * meaningful window. A repeating campaign's runs are separate campaigns with
66
+ * their own ids.
67
+ *
68
+ * `404 not_found` if the campaign is not the caller's -- the same answer as a
69
+ * campaign that does not exist, so the error cannot be used to probe ids.
70
+ */
71
+ stats(query) {
72
+ return this.http.request('GET', `${BASE}/stats`, { query });
73
+ }
74
+ /**
75
+ * The rows behind the funnel. This is the report table.
76
+ *
77
+ * `GET /v1/campaign-messages?campaign=<id>` · `campaigns:read` · **200**
78
+ *
79
+ * ### Query
80
+ *
81
+ * | Field | Type | Default | Notes |
82
+ * | --- | --- | --- | --- |
83
+ * | `campaign` | string | — | Required. |
84
+ * | `message` | number | — | Zero-based index of one campaign message. |
85
+ * | `status` | `sent \| delivered \| read \| replied \| failed` | — | Cumulative; see below. Omit for every row. |
86
+ * | `exact` | boolean | `false` | Only rows sitting at `status` right now. |
87
+ * | `identifier` | string | — | Exact match on one recipient. Not a search. |
88
+ * | `reply_type` | string | — | Replied rows only; refused otherwise. |
89
+ * | `replied_within_hours` | number | — | Replied rows only; refused otherwise. |
90
+ * | `page` | number | `1` | 1-based. |
91
+ * | `rows` | number | `25` | 1–100. |
92
+ *
93
+ * Newest attempt first, ordered by when the row was written -- not by a stage
94
+ * timestamp, which would reorder the list as webhooks arrive and make paging
95
+ * skip and repeat rows.
96
+ *
97
+ * ```ts
98
+ * // the two queries a report screen actually makes
99
+ * const failures = await crm.campaignMessages.list({ campaign, status: 'failed', rows: 100 });
100
+ * const unread = await crm.campaignMessages.list({ campaign, status: 'delivered', exact: true });
101
+ * ```
102
+ *
103
+ * `reply_type` and `replied_within_hours` are **refused** with
104
+ * `400 validation_failed` unless `status` is `replied`, rather than quietly
105
+ * matching nothing.
106
+ */
107
+ list(query) {
108
+ return this.http.request('GET', BASE, { query });
109
+ }
110
+ /**
111
+ * Every matching row, paged through for you. What an export is.
112
+ *
113
+ * ```ts
114
+ * for await (const row of crm.campaignMessages.iterate({ campaign })) {
115
+ * csv.write([row.identifier, row.name, row.status, row.error ?? '']);
116
+ * }
117
+ * ```
118
+ *
119
+ * Pages at 100 rows unless told otherwise -- the server's maximum, and the
120
+ * fewest round trips. A campaign of a million recipients is ten thousand
121
+ * requests, so for exports that size prefer running it where the latency is
122
+ * low rather than from a browser.
123
+ *
124
+ * The collection is live: rows are written while a campaign is going out, and
125
+ * statuses change under an export that takes minutes. Offset paging over
126
+ * moving data can repeat or skip a row -- for a stable snapshot, export after
127
+ * the campaign completes.
128
+ */
129
+ async *iterate(query) {
130
+ const rows = query.rows ?? 100;
131
+ let page = query.page ?? 1;
132
+ for (;;) {
133
+ const result = await this.list({ ...query, page, rows });
134
+ for (const record of result.data)
135
+ yield record;
136
+ if (result.data.length === 0 || page >= result.pagination.pages)
137
+ return;
138
+ page += 1;
139
+ }
140
+ }
141
+ /**
142
+ * What people replied, grouped by the reply itself.
143
+ *
144
+ * `GET /v1/campaign-messages/replies?campaign=<id>` · `campaigns:read` · **200**
145
+ *
146
+ * ### Query
147
+ *
148
+ * | Field | Type | Default | Notes |
149
+ * | --- | --- | --- | --- |
150
+ * | `campaign` | string | — | Required. |
151
+ * | `message` | number | — | Zero-based index of one campaign message. |
152
+ * | `limit` | number | `50` | Distinct replies to return, 1–200. |
153
+ *
154
+ * ```ts
155
+ * const { groups, replied, distinct } = await crm.campaignMessages.replies({ campaign });
156
+ * groups; // [{ reply: 'Yes', replyType: 'button', count: 182 }, …]
157
+ * ```
158
+ *
159
+ * This is what a poll or a button message is sent to find out. Grouped on the
160
+ * reply **and** its type, so a tapped `Yes` and a typed `Yes` stay apart --
161
+ * they say different things about how engaged the recipient was.
162
+ *
163
+ * `distinct` against `groups.length` tells you whether the breakdown is
164
+ * complete. A campaign that asked an open question has as many groups as it
165
+ * has repliers, so a truncated answer there is normal rather than a problem.
166
+ */
167
+ replies(query) {
168
+ return this.http.request('GET', `${BASE}/replies`, { query });
169
+ }
170
+ }
171
+ exports.CampaignMessages = CampaignMessages;
172
+ //# sourceMappingURL=campaign-messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"campaign-messages.js","sourceRoot":"","sources":["../../src/resources/campaign-messages.ts"],"names":[],"mappings":";;;AAWA,MAAM,IAAI,GAAG,uBAAuB,CAAC;AAErC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAa,gBAAgB;IACE;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,KAAK,CAAC,KAAgC;QACpC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAuB,KAAK,EAAE,GAAG,IAAI,QAAQ,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACpF,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACH,IAAI,CAAC,KAAgC;QACnC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAmC,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACrF,CAAC;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACH,KAAK,CAAC,CAAC,OAAO,CAAC,KAAgC;QAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,IAAI,GAAG,CAAC;QAC/B,IAAI,IAAI,GAAG,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;QAE3B,SAAS,CAAC;YACR,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACzD,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,IAAI;gBAAE,MAAM,MAAM,CAAC;YAE/C,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,IAAI,MAAM,CAAC,UAAU,CAAC,KAAK;gBAAE,OAAO;YACxE,IAAI,IAAI,CAAC,CAAC;QACZ,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,OAAO,CAAC,KAA2B;QACjC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAyB,KAAK,EAAE,GAAG,IAAI,UAAU,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACxF,CAAC;CACF;AAjID,4CAiIC"}
@@ -77,8 +77,8 @@ export interface CampaignRetargetSelectionFilter {
77
77
  * rejects a mixture rather than guessing.
78
78
  */
79
79
  export type CampaignFilter = CampaignNumbersFilter | CampaignRetargetStatusFilter | CampaignRetargetSelectionFilter | ContactFilterEntry;
80
- /** Which of the four modes a `filters` array expresses. */
81
- export type CampaignAudienceKind = 'all' | 'segment' | 'numbers' | 'retarget';
80
+ /** Which of the five modes a `filters` array expresses. */
81
+ export type CampaignAudienceKind = 'all' | 'segment' | 'numbers' | 'group' | 'retarget';
82
82
  export interface CampaignMessage {
83
83
  id?: string | number;
84
84
  channel: {
@@ -180,50 +180,140 @@ export type ListCampaignsQuery = {
180
180
  type?: string;
181
181
  hidden?: boolean;
182
182
  };
183
- /** Delivery counts for one campaign. Every field is a running total. */
184
- export interface CampaignMetrics {
185
- queued: number;
186
- sent: number;
187
- delivered: number;
188
- failed: number;
189
- opened: number;
190
- clicked: number;
191
- unsubscribed: number;
192
- }
193
- export interface CampaignReport {
183
+ /**
184
+ * How far one message got.
185
+ *
186
+ * A funnel held in **one field**, not a set of flags: a row carries the furthest
187
+ * state it has reached, so `read` also means delivered and sent. There is no
188
+ * `queued` -- a row only exists once the send has been attempted.
189
+ *
190
+ * `failed` sits off the funnel rather than at the end of it. A failed message
191
+ * was never delivered, so it is counted on its own and never inside `sent`.
192
+ */
193
+ export type CampaignMessageStatus = 'sent' | 'delivered' | 'read' | 'replied' | 'failed';
194
+ /**
195
+ * One attempted send: one campaign message, to one recipient.
196
+ *
197
+ * **Not a {@link CampaignMessage}**, which is the confusingly similar
198
+ * thing: that is one of the messages a campaign is *defined* to send. This is a
199
+ * record of such a message having gone to one person, and there is one per
200
+ * recipient per message.
201
+ *
202
+ * Written by the campaign manager at send time; its `status` is moved forward by
203
+ * the channel's webhooks, which are processed outside the CRM. So a row is
204
+ * append-only from this SDK's side -- there is nothing here that writes one.
205
+ */
206
+ export interface CampaignMessageRecord {
207
+ /**
208
+ * The row's own id.
209
+ *
210
+ * This is the handle a `selection` retarget takes: hand a list of these back
211
+ * and the campaign manager resolves them to recipients, which is how
212
+ * "re-send to everyone who did not read it" is built.
213
+ */
194
214
  id: string;
195
215
  campaignId: string;
196
- metrics: CampaignMetrics;
197
- /** When the metrics were last rolled up, which may lag the campaign. */
198
- generatedAt: IsoDate;
216
+ /** Which message of the campaign this was, zero-based. */
217
+ messageIndex: number;
218
+ /** The provider's id for the message. `''` when the send failed. */
219
+ providerMessageId: string;
220
+ channelId: string;
221
+ /** The recipient's contact id. */
222
+ contactId: string;
223
+ /** The destination itself -- a WhatsApp number, a Telegram id, and so on. */
224
+ identifier: string;
225
+ /** The recipient's name at send time; `''` when the contact had none. */
226
+ name: string;
227
+ status: CampaignMessageStatus;
228
+ /** Set for each stage the row has reached, absent for the ones it has not. */
229
+ sentAt?: IsoDate;
230
+ deliveredAt?: IsoDate;
231
+ readAt?: IsoDate;
232
+ repliedAt?: IsoDate;
233
+ failedAt?: IsoDate;
234
+ /** What the recipient replied; `''` unless `status` is `replied`. */
235
+ reply: string;
236
+ /** The reply's kind -- `text`, `button`, … `''` unless replied. */
237
+ replyType: string;
238
+ /** Seconds between the send and the reply. `0` unless replied. */
239
+ repliedIn: number;
240
+ /** The provider's error, on failed rows only. */
241
+ error?: string;
242
+ /** When the send was attempted. */
199
243
  createdAt: IsoDate;
200
- updatedAt: IsoDate;
201
244
  }
202
- /** A report export shares the job lifecycle used across the platform. */
203
- export type CampaignReportExportStatus = 'queued' | 'processing' | 'completed' | 'failed';
204
- export interface CampaignReportExport {
205
- id: string;
206
- campaignId: string;
207
- format: 'csv' | 'xlsx';
208
- status: CampaignReportExportStatus;
209
- rowCount: number;
210
- /** Present once `status` is `completed`. Expires; see `expiresAt`. */
211
- fileUrl?: string;
212
- expiresAt?: IsoDate;
213
- errorMessage?: string;
214
- createdAt: IsoDate;
215
- updatedAt: IsoDate;
245
+ /**
246
+ * The campaign report: the numbers behind the tiles.
247
+ *
248
+ * Every stage is **cumulative** -- `delivered` counts everything that got at
249
+ * least that far -- so `read` is a subset of `delivered`. Subtract for a stage's
250
+ * exclusive count: `delivered - read` is "delivered but unread".
251
+ *
252
+ * `total` is the only number that counts every row, failures included, and it is
253
+ * the number of attempted sends.
254
+ */
255
+ export interface CampaignMessageStats {
256
+ total: number;
257
+ /** Reached the provider: sent, delivered, read or replied. */
258
+ sent: number;
259
+ /** Delivered, read or replied. */
260
+ delivered: number;
261
+ /** Read or replied. */
262
+ read: number;
263
+ replied: number;
264
+ /** Never part of `sent`. */
265
+ failed: number;
216
266
  }
217
- export interface CreateCampaignReportExportInput {
218
- campaignId: string;
219
- /** Defaults to `csv`. */
220
- format?: 'csv' | 'xlsx';
221
- /** Narrows the rows by the period they fall in. */
222
- from?: IsoDate;
223
- to?: IsoDate;
267
+ /** A type alias, for the same reason as {@link ListCampaignsQuery}. */
268
+ export type CampaignMessageStatsQuery = {
269
+ /** The campaign id. Required. */
270
+ campaign: string;
271
+ /** One message of a multi-message campaign, by zero-based index. */
272
+ message?: number;
273
+ };
274
+ /**
275
+ * `crm.campaignMessages.list()` -- offset paginated.
276
+ *
277
+ * `status` filters **cumulatively** by default, so it agrees with the stats:
278
+ * `status: 'delivered'` returns the same rows the `delivered` number counted.
279
+ * `exact: true` instead returns only rows sitting at that stage -- "delivered
280
+ * but never read" -- which is the drill-down for `delivered - read`.
281
+ */
282
+ export type ListCampaignMessagesQuery = {
283
+ campaign: string;
284
+ message?: number;
285
+ status?: CampaignMessageStatus;
286
+ /** Only rows at `status` right now. No effect without `status`, or on `failed`. */
287
+ exact?: boolean;
288
+ /** Exact match on one recipient, e.g. a single WhatsApp number. Not a search. */
289
+ identifier?: string;
290
+ /** Replied rows only; the API refuses it otherwise. */
291
+ reply_type?: string;
292
+ /** Replied rows only; the API refuses it otherwise. */
293
+ replied_within_hours?: number;
294
+ page?: number;
295
+ /** 1..100, default 25. */
296
+ rows?: number;
297
+ };
298
+ /** One distinct reply, and how many people sent it. */
299
+ export interface CampaignReplyGroup {
300
+ reply: string;
301
+ replyType: string;
302
+ count: number;
303
+ }
304
+ /** What a poll or button message was answered with. */
305
+ export interface CampaignReplyBreakdown {
306
+ /** Most common first, capped by the query's `limit`. */
307
+ groups: CampaignReplyGroup[];
308
+ /** Replied rows in total, whether or not their group is listed. */
309
+ replied: number;
310
+ /** Distinct replies in total -- compare with `groups.length` to detect truncation. */
311
+ distinct: number;
224
312
  }
225
313
  /** A type alias, for the same reason as {@link ListCampaignsQuery}. */
226
- export type GetCampaignReportQuery = {
227
- from?: IsoDate;
228
- to?: IsoDate;
314
+ export type CampaignRepliesQuery = {
315
+ campaign: string;
316
+ message?: number;
317
+ /** Distinct replies to return, 1..200, default 50. */
318
+ limit?: number;
229
319
  };
@@ -0,0 +1,273 @@
1
+ # Campaign messages (reporting)
2
+
3
+ `crm.campaignMessages` — `/v1/campaign-messages`
4
+
5
+ What actually happened to each message a campaign sent. One row per attempted
6
+ send: who it went to, how far it got, what came back, and why it failed.
7
+
8
+ | Action | Route | Scope |
9
+ | --- | --- | --- |
10
+ | [`stats`](#stats) | `GET /v1/campaign-messages/stats` | `campaigns:read` |
11
+ | [`list`](#list) | `GET /v1/campaign-messages` | `campaigns:read` |
12
+ | [`iterate`](#iterate) | the same, paged for you | `campaigns:read` |
13
+ | [`replies`](#replies) | `GET /v1/campaign-messages/replies` | `campaigns:read` |
14
+
15
+ ## Where these numbers come from
16
+
17
+ The campaign manager writes one row as it hands each message to a channel. The
18
+ channel's delivery webhooks — processed **outside the CRM** — then move that
19
+ row's `status` forward as the message is delivered, read and replied to.
20
+
21
+ So reporting is a **read of those rows, counted when you ask**. There is no
22
+ report collection, no roll-up job, and nothing to regenerate: no `generatedAt` to
23
+ reason about, and no window in which a stored number is quietly wrong because a
24
+ webhook has arrived but the roll-up has not run.
25
+
26
+ The flip side is that a campaign still going out has moving numbers. Reading
27
+ `stats` seconds after launch under-counts everything, and that is the truth
28
+ rather than a lag.
29
+
30
+ Nothing in this module writes. There is no endpoint to correct a status: the
31
+ service that owns the lifecycle is the one receiving the webhooks.
32
+
33
+ ## The funnel
34
+
35
+ `status` is a **single field holding the furthest stage reached** — not a set of
36
+ flags. A row at `read` is also delivered and sent; a row at `replied` is all
37
+ three. So the counts nest:
38
+
39
+ ```
40
+ replied ⊆ read ⊆ delivered ⊆ sent failed ∥ (outside the funnel)
41
+ ```
42
+
43
+ Which means:
44
+
45
+ - **Cumulative by default.** `delivered` is everything that got at least as far
46
+ as delivered — the number a "delivered" tile shows.
47
+ - **Subtract for a stage's own count.** `delivered - read` is "delivered but
48
+ unread"; `read - replied` is "read but silent".
49
+ - **`failed` is never inside `sent`.** A failed message was never sent anywhere;
50
+ it is counted on its own.
51
+ - **There is no `queued`.** A row exists only once a send has been attempted, so
52
+ "waiting to go out" is not a state this collection can have. The campaign's own
53
+ `status` is where that lives.
54
+
55
+ `total` counts every row, failures included, and is the number of attempted
56
+ sends.
57
+
58
+ ## The row
59
+
60
+ ```ts
61
+ interface CampaignMessageRecord {
62
+ id: string; // the row's own id — what a `selection` retarget takes
63
+ campaignId: string;
64
+ messageIndex: number; // which message of the campaign, zero-based
65
+ providerMessageId: string; // '' when the send failed
66
+ channelId: string;
67
+ contactId: string; // the recipient's contact id
68
+ identifier: string; // the destination: a WhatsApp number, a Telegram id…
69
+ name: string; // '' when the contact had no name
70
+ status: 'sent' | 'delivered' | 'read' | 'replied' | 'failed';
71
+ sentAt?: string; // set per stage reached, absent for stages not reached
72
+ deliveredAt?: string;
73
+ readAt?: string;
74
+ repliedAt?: string;
75
+ failedAt?: string;
76
+ reply: string; // '' unless replied
77
+ replyType: string; // 'text', 'button', … '' unless replied
78
+ repliedIn: number; // seconds between the send and the reply; 0 unless replied
79
+ error?: string; // failed rows only
80
+ createdAt: string; // when the send was attempted
81
+ }
82
+ ```
83
+
84
+ `id` is worth singling out: it is the handle a **`selection` retarget** campaign
85
+ takes. Collect the ids of the rows you want and the campaign manager resolves
86
+ them back to recipients, which is how "re-send to everyone who did not read it"
87
+ is built.
88
+
89
+ `CampaignMessageRecord` is not `CampaignMessage`, despite the names. A
90
+ `CampaignMessage` is one of the messages a campaign is *defined* to send — a
91
+ channel, a payload, its polls. A `CampaignMessageRecord` is a record of one of
92
+ those having gone to one person, and there is one per recipient per message.
93
+
94
+ ---
95
+
96
+ ## stats
97
+
98
+ ```ts
99
+ crm.campaignMessages.stats(query: CampaignMessageStatsQuery): Promise<CampaignMessageStats>
100
+ ```
101
+
102
+ `GET /v1/campaign-messages/stats` · `campaigns:read` · **200**
103
+
104
+ ### Query
105
+
106
+ | Field | Type | Required | Notes |
107
+ | --- | --- | --- | --- |
108
+ | `campaign` | string | yes | The campaign id, as a 24-character ObjectId. |
109
+ | `message` | number | no | One message of a multi-message campaign, zero-based. |
110
+
111
+ ```ts
112
+ const overall = await crm.campaignMessages.stats({ campaign });
113
+ // { total: 12480, sent: 12410, delivered: 11980, read: 7420, replied: 310, failed: 70 }
114
+
115
+ const second = await crm.campaignMessages.stats({ campaign, message: 1 });
116
+ ```
117
+
118
+ **No date window**, deliberately. A campaign is a single send — its rows are
119
+ written over the minutes it takes to go out and then only change state — so "the
120
+ campaign's whole life" is the only window that means anything. A repeating
121
+ campaign's runs are separate campaigns, each with its own id.
122
+
123
+ One aggregation, not five counts. Legacy issued a `countDocuments` per stage,
124
+ which is five passes for numbers that all come from one grouping, and could
125
+ return a `read` higher than its own `delivered` when a webhook landed between
126
+ two of the passes.
127
+
128
+ `404 not_found` when the campaign is not the caller's — the same answer as a
129
+ campaign that does not exist, so the error cannot be used to probe whether
130
+ someone else's id is real.
131
+
132
+ ---
133
+
134
+ ## list
135
+
136
+ ```ts
137
+ crm.campaignMessages.list(query: ListCampaignMessagesQuery): Promise<Paginated<CampaignMessageRecord>>
138
+ ```
139
+
140
+ `GET /v1/campaign-messages` · `campaigns:read` · **200**
141
+
142
+ ### Query
143
+
144
+ | Field | Type | Default | Notes |
145
+ | --- | --- | --- | --- |
146
+ | `campaign` | string | — | Required. |
147
+ | `message` | number | — | Zero-based index of one campaign message. |
148
+ | `status` | `sent` `delivered` `read` `replied` `failed` | — | Cumulative; see below. Omit for every row. |
149
+ | `exact` | boolean | `false` | Only rows sitting at `status` right now. |
150
+ | `identifier` | string | — | Exact match on one recipient. Not a search. |
151
+ | `reply_type` | string | — | Replied rows only; refused otherwise. |
152
+ | `replied_within_hours` | number | — | Replied rows only; refused otherwise. |
153
+ | `page` | number | `1` | 1-based. |
154
+ | `rows` | number | `25` | 1–100. |
155
+
156
+ ```ts
157
+ // the two queries a report screen actually makes
158
+ const failures = await crm.campaignMessages.list({ campaign, status: 'failed', rows: 100 });
159
+ failures.data[0].error; // why that one did not go
160
+
161
+ const unread = await crm.campaignMessages.list({ campaign, status: 'delivered', exact: true });
162
+ unread.pagination.total; // equals stats.delivered - stats.read
163
+ ```
164
+
165
+ ### `status` and `exact`
166
+
167
+ `status` filters **cumulatively**, so a drill-down returns exactly the rows the
168
+ matching stat counted: `status: 'delivered'` gives you the `delivered` number's
169
+ rows. `exact: true` switches to the single stage — "delivered but never read" —
170
+ which is the drill-down for `delivered - read`.
171
+
172
+ `exact` has no effect without `status`, and none on `failed`, which has nothing
173
+ beyond it.
174
+
175
+ ### Reply filters
176
+
177
+ `reply_type` and `replied_within_hours` are **refused** with
178
+ `400 validation_failed` unless `status` is `replied`. They cannot match anything
179
+ on a non-replied row, and a filter that cannot match is a caller mistake worth
180
+ reporting rather than an empty page.
181
+
182
+ `replied_within_hours: 2` means the reply arrived within two hours of the send —
183
+ useful for separating people who answered immediately from people who came back
184
+ the next day.
185
+
186
+ ### Ordering
187
+
188
+ Newest attempt first, by when the row was **written**, not by a stage timestamp.
189
+ Sorting on `deliveredAt` would reorder the list as webhooks arrive, and offset
190
+ paging over a list that reorders itself skips and repeats rows.
191
+
192
+ ---
193
+
194
+ ## iterate
195
+
196
+ ```ts
197
+ crm.campaignMessages.iterate(query: ListCampaignMessagesQuery): AsyncGenerator<CampaignMessageRecord>
198
+ ```
199
+
200
+ Every matching row, paged through for you. **This is what an export is** — there
201
+ is no export endpoint, nothing to queue, nothing to poll, and no generated file
202
+ that expires before someone downloads it.
203
+
204
+ ```ts
205
+ for await (const row of crm.campaignMessages.iterate({ campaign })) {
206
+ csv.write([row.identifier, row.name, row.status, row.error ?? '']);
207
+ }
208
+ ```
209
+
210
+ Pages at 100 rows unless you pass `rows` — the server's maximum, and so the
211
+ fewest round trips. A million recipients is ten thousand requests, so run an
212
+ export that size somewhere with low latency to the gateway rather than from a
213
+ browser tab.
214
+
215
+ The collection is live. Rows are written while a campaign goes out and statuses
216
+ change under an export that takes minutes, and offset paging over moving data can
217
+ repeat or miss a row. For a stable snapshot, export after the campaign completes.
218
+
219
+ ---
220
+
221
+ ## replies
222
+
223
+ ```ts
224
+ crm.campaignMessages.replies(query: CampaignRepliesQuery): Promise<CampaignReplyBreakdown>
225
+ ```
226
+
227
+ `GET /v1/campaign-messages/replies` · `campaigns:read` · **200**
228
+
229
+ ### Query
230
+
231
+ | Field | Type | Default | Notes |
232
+ | --- | --- | --- | --- |
233
+ | `campaign` | string | — | Required. |
234
+ | `message` | number | — | Zero-based index of one campaign message. |
235
+ | `limit` | number | `50` | Distinct replies to return, 1–200. |
236
+
237
+ ```ts
238
+ const { groups, replied, distinct } = await crm.campaignMessages.replies({ campaign });
239
+
240
+ groups; // [{ reply: 'Yes', replyType: 'button', count: 182 }, { reply: 'No', … }]
241
+ replied; // 310 — every replied row, whether or not its group is listed
242
+ distinct; // 4 — distinct replies in total
243
+ ```
244
+
245
+ What a poll or a button message is sent to find out. Grouped on the reply **and**
246
+ its type, so a tapped `Yes` and a typed `Yes` stay apart — they say different
247
+ things about how engaged the recipient was. Most common first, ties broken by the
248
+ reply text so the order is stable between reads.
249
+
250
+ `distinct` against `groups.length` tells you whether the breakdown is complete.
251
+ A campaign that asked an open question has as many groups as it has repliers, so
252
+ a truncated answer there is normal rather than a problem — raise `limit`, or
253
+ treat the tail as "other".
254
+
255
+ ---
256
+
257
+ ## Operational note: the index this needs
258
+
259
+ Every read here matches on the campaign first, and the collection — which belongs
260
+ to the campaign manager — declares no index on it. Without one, each report is a
261
+ scan of every campaign message ever sent:
262
+
263
+ ```js
264
+ db.newcampaignmessages.createIndex(
265
+ { cid: 1, cmid: 1, status: 1 },
266
+ { name: 'Campaign Report', background: true },
267
+ );
268
+ ```
269
+
270
+ One compound index serves all three reads. This service deliberately does **not**
271
+ create it: building an index on a multi-million row collection owned by another
272
+ service, as a side effect of a process starting, is not something that should
273
+ happen by accident.
@@ -434,56 +434,16 @@ await crm.campaigns.delete(id);
434
434
 
435
435
  ---
436
436
 
437
- # Campaign reports
437
+ # Reporting
438
438
 
439
- `crm.campaignReports` — `/v1/campaign-reports`
439
+ Campaign reporting lives on its own page: **[campaign messages](campaign-messages.md)**
440
+ — `crm.campaignMessages`.
440
441
 
441
- Delivery metrics for a campaign, rolled up by the platform.
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:
442
445
 
443
446
  ```ts
444
- const report = await crm.campaignReports.getByCampaign(campaignId);
445
- report.metrics; // { queued, sent, delivered, failed, opened, clicked, unsubscribed }
447
+ const stats = await crm.campaignMessages.stats({ campaign: id });
448
+ // { total, sent, delivered, read, replied, failed }
446
449
  ```
447
-
448
- | Action | Route |
449
- | --- | --- |
450
- | `list` | `GET /v1/campaign-reports` — cursor-paged, `campaignId` optional |
451
- | `getByCampaign` | `GET /v1/campaign-reports/:campaignId` |
452
-
453
- `getByCampaign` takes an optional window; without it the report covers the
454
- campaign's whole life:
455
-
456
- ```ts
457
- await crm.campaignReports.getByCampaign(campaignId, {
458
- from: '2026-10-01T00:00:00.000Z',
459
- to: '2026-10-31T23:59:59.000Z',
460
- });
461
- ```
462
-
463
- `generatedAt` is when the metrics were last rolled up, which may lag the
464
- campaign — a report read seconds after a send will under-count.
465
-
466
- ## Report exports
467
-
468
- `crm.campaignReportExports` — `/v1/campaign-report-exports`
469
-
470
- A per-recipient extract, generated asynchronously because it is a file rather
471
- than a few counters.
472
-
473
- ```ts
474
- const job = await crm.campaignReportExports.create({ campaignId, format: 'xlsx' });
475
-
476
- // Poll until it finishes, then download.
477
- const done = await crm.campaignReportExports.get(job.id);
478
- if (done.status === 'completed') window.location.href = done.fileUrl!;
479
- ```
480
-
481
- | Field | Type | Notes |
482
- | --- | --- | --- |
483
- | `campaignId` | `string` | required |
484
- | `format` | `csv` `xlsx` | default `csv` |
485
- | `from` / `to` | ISO datetime | narrows the rows by period |
486
-
487
- `status` moves `queued` → `processing` → `completed` or `failed`; `rowCount` is
488
- filled in as it goes, and `errorMessage` explains a failure. **`fileUrl` expires**
489
- — download it rather than storing the link.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ab-ecosystem-cpass",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "JavaScript/TypeScript SDK for the CPaaS CRM gateway: audience and channels",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -1,14 +0,0 @@
1
- import type { CampaignReportExport, CreateCampaignReportExportInput } from '../types/campaign';
2
- import type { Page, PageQuery } from '../types/common';
3
- import type { HttpClient } from '../http';
4
- export declare class CampaignReportExports {
5
- private readonly http;
6
- constructor(http: HttpClient);
7
- list(query?: Partial<PageQuery>): Promise<Page<CampaignReportExport>>;
8
- /**
9
- * One export. Poll this until `status` is `completed`, then read `fileUrl` --
10
- * which expires, so download rather than storing the link.
11
- */
12
- get(id: string): Promise<CampaignReportExport>;
13
- create(input: CreateCampaignReportExportInput): Promise<CampaignReportExport>;
14
- }
@@ -1,28 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CampaignReportExports = void 0;
4
- class CampaignReportExports {
5
- http;
6
- constructor(http) {
7
- this.http = http;
8
- }
9
- list(query = {}) {
10
- return this.http.request('GET', '/v1/campaign-report-exports', {
11
- query,
12
- });
13
- }
14
- /**
15
- * One export. Poll this until `status` is `completed`, then read `fileUrl` --
16
- * which expires, so download rather than storing the link.
17
- */
18
- get(id) {
19
- return this.http.request('GET', `/v1/campaign-report-exports/${encodeURIComponent(id)}`);
20
- }
21
- create(input) {
22
- return this.http.request('POST', '/v1/campaign-report-exports', {
23
- body: input,
24
- });
25
- }
26
- }
27
- exports.CampaignReportExports = CampaignReportExports;
28
- //# sourceMappingURL=campaign-report-exports.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"campaign-report-exports.js","sourceRoot":"","sources":["../../src/resources/campaign-report-exports.ts"],"names":[],"mappings":";;;AAIA,MAAa,qBAAqB;IACH;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD,IAAI,CAAC,QAA4B,EAAE;QACjC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAA6B,KAAK,EAAE,6BAA6B,EAAE;YACzF,KAAK;SACN,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,GAAG,CAAC,EAAU;QACZ,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CACtB,KAAK,EACL,+BAA+B,kBAAkB,CAAC,EAAE,CAAC,EAAE,CACxD,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,KAAsC;QAC3C,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAuB,MAAM,EAAE,6BAA6B,EAAE;YACpF,IAAI,EAAE,KAAK;SACZ,CAAC,CAAC;IACL,CAAC;CACF;AAzBD,sDAyBC"}
@@ -1,15 +0,0 @@
1
- import type { CampaignReport, GetCampaignReportQuery } from '../types/campaign';
2
- import type { Page, PageQuery } from '../types/common';
3
- import type { HttpClient } from '../http';
4
- export declare class CampaignReports {
5
- private readonly http;
6
- constructor(http: HttpClient);
7
- list(query?: Partial<PageQuery>): Promise<Page<CampaignReport>>;
8
- /**
9
- * The report for one campaign.
10
- *
11
- * `from`/`to` narrow the window the metrics are rolled up over; without them
12
- * the report covers the campaign's whole life.
13
- */
14
- getByCampaign(campaignId: string, query?: GetCampaignReportQuery): Promise<CampaignReport>;
15
- }
@@ -1,25 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CampaignReports = void 0;
4
- class CampaignReports {
5
- http;
6
- constructor(http) {
7
- this.http = http;
8
- }
9
- list(query = {}) {
10
- return this.http.request('GET', '/v1/campaign-reports', { query });
11
- }
12
- /**
13
- * The report for one campaign.
14
- *
15
- * `from`/`to` narrow the window the metrics are rolled up over; without them
16
- * the report covers the campaign's whole life.
17
- */
18
- getByCampaign(campaignId, query = {}) {
19
- return this.http.request('GET', `/v1/campaign-reports/${encodeURIComponent(campaignId)}`, {
20
- query,
21
- });
22
- }
23
- }
24
- exports.CampaignReports = CampaignReports;
25
- //# sourceMappingURL=campaign-reports.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"campaign-reports.js","sourceRoot":"","sources":["../../src/resources/campaign-reports.ts"],"names":[],"mappings":";;;AAIA,MAAa,eAAe;IACG;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD,IAAI,CAAC,QAA4B,EAAE;QACjC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAuB,KAAK,EAAE,sBAAsB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3F,CAAC;IAED;;;;;OAKG;IACH,aAAa,CAAC,UAAkB,EAAE,QAAgC,EAAE;QAClE,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAiB,KAAK,EAAE,wBAAwB,kBAAkB,CAAC,UAAU,CAAC,EAAE,EAAE;YACxG,KAAK;SACN,CAAC,CAAC;IACL,CAAC;CACF;AAlBD,0CAkBC"}