ab-ecosystem-cpass 0.1.0 → 1.0.0

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 (97) hide show
  1. package/README.md +127 -25
  2. package/dist/client.d.ts +56 -1
  3. package/dist/client.js +76 -30
  4. package/dist/client.js.map +1 -1
  5. package/dist/http.d.ts +8 -0
  6. package/dist/http.js +10 -3
  7. package/dist/http.js.map +1 -1
  8. package/dist/index.d.ts +16 -1
  9. package/dist/index.js +35 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/resources/audience/contact-notes.d.ts +51 -0
  12. package/dist/resources/audience/contact-notes.js +70 -0
  13. package/dist/resources/audience/contact-notes.js.map +1 -0
  14. package/dist/resources/audience/contact-tags.d.ts +43 -0
  15. package/dist/resources/audience/contact-tags.js +60 -0
  16. package/dist/resources/audience/contact-tags.js.map +1 -0
  17. package/dist/resources/audience/contacts.d.ts +76 -0
  18. package/dist/resources/audience/contacts.js +103 -0
  19. package/dist/resources/audience/contacts.js.map +1 -0
  20. package/dist/resources/audience/crm-triggers.d.ts +67 -0
  21. package/dist/resources/audience/crm-triggers.js +86 -0
  22. package/dist/resources/audience/crm-triggers.js.map +1 -0
  23. package/dist/resources/audience/custom-fields.d.ts +54 -0
  24. package/dist/resources/audience/custom-fields.js +71 -0
  25. package/dist/resources/audience/custom-fields.js.map +1 -0
  26. package/dist/resources/audience/export-history.d.ts +52 -0
  27. package/dist/resources/audience/export-history.js +67 -0
  28. package/dist/resources/audience/export-history.js.map +1 -0
  29. package/dist/resources/audience/import-history.d.ts +51 -0
  30. package/dist/resources/audience/import-history.js +66 -0
  31. package/dist/resources/audience/import-history.js.map +1 -0
  32. package/dist/resources/audience/segments.d.ts +49 -0
  33. package/dist/resources/audience/segments.js +66 -0
  34. package/dist/resources/audience/segments.js.map +1 -0
  35. package/dist/resources/audience/webhooks.d.ts +68 -0
  36. package/dist/resources/audience/webhooks.js +83 -0
  37. package/dist/resources/audience/webhooks.js.map +1 -0
  38. package/dist/resources/channels/activity.d.ts +25 -0
  39. package/dist/resources/channels/activity.js +34 -0
  40. package/dist/resources/channels/activity.js.map +1 -0
  41. package/dist/resources/channels/billing.d.ts +46 -0
  42. package/dist/resources/channels/billing.js +59 -0
  43. package/dist/resources/channels/billing.js.map +1 -0
  44. package/dist/resources/channels/channels.d.ts +115 -0
  45. package/dist/resources/channels/channels.js +129 -0
  46. package/dist/resources/channels/channels.js.map +1 -0
  47. package/dist/resources/channels/templates.d.ts +90 -0
  48. package/dist/resources/channels/templates.js +109 -0
  49. package/dist/resources/channels/templates.js.map +1 -0
  50. package/dist/resources/channels/wallet.d.ts +25 -0
  51. package/dist/resources/channels/wallet.js +34 -0
  52. package/dist/resources/channels/wallet.js.map +1 -0
  53. package/dist/resources/channels/webhooks.d.ts +60 -0
  54. package/dist/resources/channels/webhooks.js +79 -0
  55. package/dist/resources/channels/webhooks.js.map +1 -0
  56. package/dist/resources/paths.d.ts +12 -0
  57. package/dist/resources/paths.js +17 -0
  58. package/dist/resources/paths.js.map +1 -0
  59. package/dist/types/audience.d.ts +512 -0
  60. package/dist/types/audience.js +3 -0
  61. package/dist/types/audience.js.map +1 -0
  62. package/dist/types/channels.d.ts +383 -0
  63. package/dist/types/channels.js +22 -0
  64. package/dist/types/channels.js.map +1 -0
  65. package/dist/types/common.d.ts +55 -0
  66. package/dist/types/common.js +8 -0
  67. package/dist/types/common.js.map +1 -0
  68. package/dist/types/contact-filter.d.ts +55 -0
  69. package/dist/types/contact-filter.js +11 -0
  70. package/dist/types/contact-filter.js.map +1 -0
  71. package/dist/types/index.d.ts +4 -0
  72. package/dist/types/index.js +21 -0
  73. package/dist/types/index.js.map +1 -0
  74. package/docs/audience/contact-notes.md +129 -0
  75. package/docs/audience/contact-tags.md +109 -0
  76. package/docs/audience/contacts.md +227 -0
  77. package/docs/audience/crm-triggers.md +240 -0
  78. package/docs/audience/custom-fields.md +174 -0
  79. package/docs/audience/exports.md +163 -0
  80. package/docs/audience/imports.md +161 -0
  81. package/docs/audience/segments.md +120 -0
  82. package/docs/audience/webhooks.md +167 -0
  83. package/docs/channels/activity.md +68 -0
  84. package/docs/channels/billing.md +112 -0
  85. package/docs/channels/channel-props.md +290 -0
  86. package/docs/channels/channels.md +199 -0
  87. package/docs/channels/templates.md +224 -0
  88. package/docs/channels/wallet.md +67 -0
  89. package/docs/channels/webhooks.md +152 -0
  90. package/docs/contact-filter.md +157 -0
  91. package/package.json +7 -3
  92. package/dist/resources/channels.d.ts +0 -10
  93. package/dist/resources/channels.js +0 -19
  94. package/dist/resources/channels.js.map +0 -1
  95. package/dist/types.d.ts +0 -70
  96. package/dist/types.js +0 -7
  97. package/dist/types.js.map +0 -1
@@ -0,0 +1,167 @@
1
+ # Audience webhooks
2
+
3
+ `crm.webhooks` — `/v1/audience/webhooks`
4
+
5
+ HTTPS endpoints the platform POSTs contact events to.
6
+
7
+ Three things in this SDK are called a webhook; they are not the same thing:
8
+
9
+ | | Carries | Signed |
10
+ | --- | --- | --- |
11
+ | `crm.webhooks` | Contact events: created, updated, deleted | **yes** |
12
+ | [`crm.crmTriggers`](crm-triggers.md) | One configured request per rule, when a contact matches | no |
13
+ | [`crm.channelWebhooks`](../channels/webhooks.md) | A channel's message traffic | n/a |
14
+
15
+ **Scoping** — unlike the rest of the audience module, these rows are scoped by
16
+ **tenant** rather than by the calling user.
17
+
18
+ | Action | Route | Scope |
19
+ | --- | --- | --- |
20
+ | [`list`](#list) | `GET /v1/audience/webhooks` | `webhooks:read` |
21
+ | [`get`](#get) | `GET /v1/audience/webhooks/:id` | `webhooks:read` |
22
+ | [`create`](#create) | `POST /v1/audience/webhooks` | `webhooks:write` |
23
+ | [`update`](#update) | `PATCH /v1/audience/webhooks/:id` | `webhooks:write` |
24
+ | [`delete`](#delete) | `DELETE /v1/audience/webhooks/:id` | `webhooks:write` |
25
+
26
+ ## The webhook object
27
+
28
+ ```ts
29
+ interface Webhook {
30
+ id: string;
31
+ url: string;
32
+ events: ('contact.created' | 'contact.updated' | 'contact.deleted')[];
33
+ enabled: boolean;
34
+ failureCount: number; // consecutive delivery failures
35
+ lastDeliveredAt?: string;
36
+ createdAt: string;
37
+ updatedAt: string;
38
+ }
39
+ ```
40
+
41
+ ## The delivery
42
+
43
+ Your endpoint receives:
44
+
45
+ ```ts
46
+ interface WebhookEnvelope {
47
+ id: string; // delivery id, stable across retries of the same event
48
+ type: string; // e.g. 'contact.created'
49
+ tenantId: string;
50
+ createdAt: string;
51
+ data: unknown; // the contact, or { id } for a deletion
52
+ }
53
+ ```
54
+
55
+ with two headers:
56
+
57
+ ```
58
+ x-crm-timestamp: 1774512000
59
+ x-crm-signature: v1=<hex>
60
+ ```
61
+
62
+ where the digest is `HMAC-SHA256(secret, "<timestamp>.<raw body>")`. Verify
63
+ against the **raw bytes**, before any JSON parsing:
64
+
65
+ ```ts
66
+ import { createHmac, timingSafeEqual } from 'node:crypto';
67
+
68
+ const verify = (secret: string, timestamp: string, rawBody: string, header: string) => {
69
+ const digest = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
70
+ const expected = Buffer.from(`v1=${digest}`);
71
+ const actual = Buffer.from(header);
72
+ return expected.length === actual.length && timingSafeEqual(expected, actual);
73
+ };
74
+ ```
75
+
76
+ Reject a timestamp too far from now to stop replays.
77
+
78
+ ---
79
+
80
+ ## list
81
+
82
+ ```ts
83
+ crm.webhooks.list(query?: Partial<ListWebhooksQuery>): Promise<Page<Webhook>>
84
+ ```
85
+
86
+ `GET /v1/audience/webhooks` · `webhooks:read` · **200**
87
+
88
+ **Cursor paginated**, not offset.
89
+
90
+ | Field | Type | Default | Notes |
91
+ | --- | --- | --- | --- |
92
+ | `limit` | number | `50` | 1–100. |
93
+ | `cursor` | string | — | From the previous response's `nextCursor`. |
94
+ | `enabled` | boolean | — | Accepted by the contract, but **not yet applied** by the service — the list comes back unfiltered. Filter client-side until that lands. |
95
+
96
+ ---
97
+
98
+ ## get
99
+
100
+ ```ts
101
+ crm.webhooks.get(id: string): Promise<Webhook>
102
+ ```
103
+
104
+ `GET /v1/audience/webhooks/:id` · `webhooks:read` · **200**
105
+
106
+ The signing secret is **never** returned here. It is shown once, by `create`.
107
+
108
+ ---
109
+
110
+ ## create
111
+
112
+ ```ts
113
+ crm.webhooks.create(input: CreateWebhookInput): Promise<Webhook & { secret: string }>
114
+ ```
115
+
116
+ `POST /v1/audience/webhooks` · `webhooks:write` · **201**
117
+
118
+ ### Body
119
+
120
+ | Field | Type | Required | Default | Notes |
121
+ | --- | --- | --- | --- | --- |
122
+ | `url` | string | yes | — | Must be a valid absolute URL. |
123
+ | `events` | `WebhookEventType[]` | yes | — | At least one of `contact.created`, `contact.updated`, `contact.deleted`. |
124
+ | `enabled` | boolean | no | `true` | |
125
+
126
+ The URL has to be **public https**. A non-https URL, or a hostname resolving to
127
+ a loopback, link-local or private address, is rejected with
128
+ `422 invalid_webhook_url` — the DNS lookup happens at write time, so a name that
129
+ resolves privately is caught here rather than at delivery.
130
+
131
+ ```ts
132
+ const hook = await crm.webhooks.create({
133
+ url: 'https://ops.example.com/hooks/crm',
134
+ events: ['contact.created', 'contact.updated'],
135
+ });
136
+
137
+ await vault.store(hook.secret); // the only response that carries it
138
+ ```
139
+
140
+ ---
141
+
142
+ ## update
143
+
144
+ ```ts
145
+ crm.webhooks.update(id: string, input: UpdateWebhookInput): Promise<Webhook>
146
+ ```
147
+
148
+ `PATCH /v1/audience/webhooks/:id` · `webhooks:write` · **200**
149
+
150
+ Same fields as `create`, all optional. A new `url` is checked the same way.
151
+ `events` **replaces** the subscription list rather than adding to it.
152
+
153
+ The secret does not change and cannot be rotated through this route — create a
154
+ second webhook, move over, and delete the first.
155
+
156
+ ---
157
+
158
+ ## delete
159
+
160
+ ```ts
161
+ crm.webhooks.delete(id: string): Promise<void>
162
+ ```
163
+
164
+ `DELETE /v1/audience/webhooks/:id` · `webhooks:write` · **204**, no body.
165
+
166
+ Delivery stops immediately; queued attempts are dropped. To pause instead, send
167
+ `update(id, { enabled: false })`.
@@ -0,0 +1,68 @@
1
+ # Activity log
2
+
3
+ `crm.activity` — `/v1/channels/:channelId/logs`
4
+
5
+ What happened to a channel, and who did it — the owner, the agency, or the
6
+ platform itself.
7
+
8
+ **Read-only**, and written by the services as they work. Wallet movements share
9
+ the feed with ordinary activity, so `type: 'activity'` is the filter for
10
+ "everything except money"; for the money itself with its running balance, read
11
+ the [wallet ledger](wallet.md) instead.
12
+
13
+ | Action | Route | Scope |
14
+ | --- | --- | --- |
15
+ | [`list`](#list) | `GET /v1/channels/:channelId/logs` | `channels:read` |
16
+
17
+ ## The activity log object
18
+
19
+ ```ts
20
+ interface ActivityLog {
21
+ id: string;
22
+ source: {
23
+ type?: 'user' | 'agency' | 'system';
24
+ id?: string;
25
+ };
26
+ log: {
27
+ type?: 'wallet' | 'activity';
28
+ event: string; // machine name, e.g. 'template.approved'
29
+ message: string; // human-readable line for the feed
30
+ };
31
+ createdAt: string;
32
+ updatedAt: string;
33
+ }
34
+ ```
35
+
36
+ ---
37
+
38
+ ## list
39
+
40
+ ```ts
41
+ crm.activity.list(channelId: string, query?: Partial<ListActivityLogsQuery>): Promise<Paginated<ActivityLog>>
42
+ ```
43
+
44
+ `GET /v1/channels/:channelId/logs` · `channels:read` · **200**
45
+
46
+ Newest first.
47
+
48
+ | Field | Type | Default | Notes |
49
+ | --- | --- | --- | --- |
50
+ | `page` | number | `1` | 1-based. |
51
+ | `rows` | number | `25` | 1–100. |
52
+ | `search` | string | — | 1–120 chars. Matches the **event name and the message**. |
53
+ | `type` | `'wallet' \| 'activity'` | — | Quick filter. |
54
+ | `source` | `'user' \| 'agency' \| 'system'` | — | Quick filter: who caused the entry. |
55
+ | `event` | string | — | Quick filter: one exact event name, 1–120 chars. |
56
+ | `range` | `{ start, end }` | — | Unix seconds. `0` on either end means no bound. Serialised to JSON by the SDK. |
57
+
58
+ ```ts
59
+ // Everything the platform did to this channel this week
60
+ const feed = await crm.activity.list(channelId, {
61
+ source: 'system',
62
+ type: 'activity',
63
+ range: { start: weekStart, end: 0 },
64
+ });
65
+
66
+ // One event's history
67
+ const approvals = await crm.activity.list(channelId, { event: 'template.approved' });
68
+ ```
@@ -0,0 +1,112 @@
1
+ # Conversation billing
2
+
3
+ `crm.billing` — `/v1/channels/:channelId/conversations`
4
+
5
+ What each conversation on a channel cost.
6
+
7
+ One route answers in two shapes. Without `stat` it pages through individual
8
+ charges; with `stat` it returns aggregates instead — which is why this SDK
9
+ splits it into two methods.
10
+
11
+ **`channelId` may be `'all'`** to read across every channel the caller owns.
12
+ That also changes what `summary` groups by: currency across all channels,
13
+ message category on a single one.
14
+
15
+ | Action | Route | Scope |
16
+ | --- | --- | --- |
17
+ | [`list`](#list) | `GET /v1/channels/:channelId/conversations` | `channels:read` |
18
+ | [`stats`](#stats) | `GET /v1/channels/:channelId/conversations?stat=…` | `channels:read` |
19
+
20
+ Read-only: charges are written by the platform as conversations happen.
21
+
22
+ ## The billing log object
23
+
24
+ ```ts
25
+ interface BillingLog {
26
+ id: string;
27
+ chatId?: string; // the conversation the charge belongs to
28
+ identifier: string; // the recipient, as the channel addresses them
29
+ deduction: {
30
+ billable: boolean; // false for free-tier conversations — logged, not charged
31
+ amount: number;
32
+ currency: string;
33
+ };
34
+ meta?: Record<string, unknown>; // recipient country, category, template, campaign, message id
35
+ createdAt: string;
36
+ updatedAt: string;
37
+ }
38
+ ```
39
+
40
+ ---
41
+
42
+ ## list
43
+
44
+ ```ts
45
+ crm.billing.list(channelId: string, query?): Promise<Paginated<BillingLog>>
46
+ ```
47
+
48
+ `GET /v1/channels/:channelId/conversations` · `channels:read` · **200**
49
+
50
+ | Field | Type | Default | Notes |
51
+ | --- | --- | --- | --- |
52
+ | `page` | number | `1` | 1-based. |
53
+ | `rows` | number | `25` | 1–100. |
54
+ | `search` | string | — | 1–120 chars. Matches the recipient `identifier` and the `meta` fields: recipient, category, country and message id. |
55
+ | `range` | `{ start, end }` | — | Unix seconds. `0` on either end means **no bound**. Serialised to JSON by the SDK. |
56
+ | `template` | string[] | — | Quick filter: templates the conversation was billed against. |
57
+ | `country` | string[] | — | Quick filter: recipient country codes. |
58
+ | `campaign` | string[] | — | Quick filter: campaigns that produced the conversation. |
59
+
60
+ The three array filters are sent as JSON; the server also accepts a
61
+ comma-separated string if you build the query yourself.
62
+
63
+ ```ts
64
+ const charges = await crm.billing.list('all', {
65
+ range: { start: 1767225600, end: 1769817600 },
66
+ country: ['IN', 'US'],
67
+ rows: 100,
68
+ });
69
+ ```
70
+
71
+ ---
72
+
73
+ ## stats
74
+
75
+ ```ts
76
+ crm.billing.stats(channelId: string, stat?: BillingStat, query?): Promise<BillingStats>
77
+ ```
78
+
79
+ `GET /v1/channels/:channelId/conversations?stat=…` · `channels:read` · **200**
80
+
81
+ The same filters, aggregated instead of paged. `stat` defaults to `'summary'`.
82
+
83
+ ```ts
84
+ interface BillingStats {
85
+ data: { group: string; totalRecords: number; totalDeductionAmount: number }[];
86
+ stat: 'summary' | 'day_wise';
87
+ }
88
+ ```
89
+
90
+ `group` is what the totals are cut by:
91
+
92
+ | `stat` | `channelId` | `group` is |
93
+ | --- | --- | --- |
94
+ | `summary` | `'all'` | the currency |
95
+ | `summary` | one channel | the message category |
96
+ | `day_wise` | either | the calendar date |
97
+
98
+ **Pagination does not apply** — every group is returned, and `page`/`rows` are
99
+ ignored.
100
+
101
+ ```ts
102
+ const monthly = await crm.billing.stats('all', 'summary', {
103
+ range: { start: 1767225600, end: 1769817600 },
104
+ });
105
+
106
+ for (const row of monthly.data) {
107
+ console.log(row.group, row.totalDeductionAmount, `(${row.totalRecords} conversations)`);
108
+ }
109
+
110
+ // Per day, one channel
111
+ const daily = await crm.billing.stats(channelId, 'day_wise', { range: { start: 0, end: 0 } });
112
+ ```
@@ -0,0 +1,290 @@
1
+ # Channel properties
2
+
3
+ `crm.channels.getProp` / `crm.channels.setProp` — `/v1/channels/:id/:prop`
4
+
5
+ Everything about a channel that lives with the **provider** rather than on the
6
+ channel document: the business profile, QR codes, media uploads, number
7
+ registration, the message queue.
8
+
9
+ ```ts
10
+ crm.channels.getProp<T>(id, prop, query?): Promise<ChannelPropResponse<T>>
11
+ crm.channels.setProp<T>(id, prop, options?): Promise<ChannelPropResponse<T>>
12
+ ```
13
+
14
+ Both answer the same envelope:
15
+
16
+ ```ts
17
+ interface ChannelPropResponse<T> {
18
+ data: T; // whatever the provider returned — shape follows `prop`
19
+ prop: string; // echo
20
+ channel: string; // echo
21
+ }
22
+ ```
23
+
24
+ `data` is deliberately untyped: each property returns whatever Meta (or the
25
+ local config) hands back. Pass a type argument when you know the shape.
26
+
27
+ ## Which channels support which properties
28
+
29
+ | Category | Readable | Writable |
30
+ | --- | --- | --- |
31
+ | `whatsapp` | all | all |
32
+ | `rcs` | `fetch_channel_queue` `recharge_url` `token` | `test_message` |
33
+ | anything else | none | none |
34
+
35
+ Asking for an unsupported property is `400 unsupported_channel_prop`, with the
36
+ supported list in `details`.
37
+
38
+ ## `setProp` options
39
+
40
+ ```ts
41
+ interface SetPropOptions {
42
+ body?: Record<string, unknown>; // JSON body — ignored when `raw` is set
43
+ query?: Record<string, unknown>; // query string parameters
44
+ raw?: BodyInit; // file bytes, for the three binary properties
45
+ contentType?: string; // required whenever `raw` is set
46
+ }
47
+ ```
48
+
49
+ **Three properties take file bytes instead of JSON**: `profile_pic`,
50
+ `resumable_upload` and `upload_media`. Pass them as `raw` with a `contentType`.
51
+
52
+ Several write properties wrap their payload in an `update` key — that is Meta's
53
+ own envelope, passed straight through. The table below says which.
54
+
55
+ ---
56
+
57
+ ## Readable properties
58
+
59
+ ### `general`
60
+
61
+ Meta's general details for the phone number: verified name, quality rating,
62
+ display number status.
63
+
64
+ ```ts
65
+ const { data } = await crm.channels.getProp(id, 'general');
66
+ ```
67
+
68
+ ### `business_profile`
69
+
70
+ The WhatsApp business profile: about text, address, description, email,
71
+ websites, vertical, profile picture URL.
72
+
73
+ ### `business_compliance`
74
+
75
+ Compliance information registered for the business (India's customer-care
76
+ details, and similar).
77
+
78
+ ### `business_commerce`
79
+
80
+ Commerce settings: whether the catalog and cart are on.
81
+
82
+ ### `conversation_automation`
83
+
84
+ The configured welcome message, ice-breaker prompts and commands.
85
+
86
+ ### `qr_code`
87
+
88
+ The channel's WhatsApp QR codes and their short links.
89
+
90
+ ### `message_analytics`
91
+
92
+ Meta's message analytics for the number.
93
+
94
+ | Query | Required | Default | Notes |
95
+ | --- | --- | --- | --- |
96
+ | `start` | yes | — | Unix seconds. |
97
+ | `end` | yes | — | Unix seconds. |
98
+ | `breakdown` | no | `'DAY'` | Meta's breakdown granularity. |
99
+ | `number` | no | the channel's own identity | Phone number to report on. |
100
+
101
+ ```ts
102
+ const { data } = await crm.channels.getProp(id, 'message_analytics', {
103
+ start: 1767225600,
104
+ end: 1769817600,
105
+ breakdown: 'DAY',
106
+ });
107
+ ```
108
+
109
+ A missing `start` or `end` is a `422`.
110
+
111
+ ### `redirect_url`
112
+
113
+ A deep link into Meta Business Suite, prefilled for this channel.
114
+
115
+ | Query | Required | Values |
116
+ | --- | --- | --- |
117
+ | `target` | yes | `meta_catalog_manager` · `link_whatsApp_catalog` · `wa_manager` · `business` |
118
+
119
+ An unknown target is a `422` listing the valid ones. `data` is the URL string.
120
+
121
+ ### `token`
122
+
123
+ The channel's inbound API token, followed by an encrypted copy: the prefix
124
+ identifies the channel, the remainder proves the platform issued it. `data` is
125
+ the concatenated string. This is how you read a token back after
126
+ [`rotateToken`](channels.md#rotatetoken).
127
+
128
+ ### `channel_config`
129
+
130
+ What the agency permits on this channel. Agencies that have never saved a config
131
+ get permissive defaults.
132
+
133
+ ```ts
134
+ { about: boolean } // may the about text be edited
135
+ ```
136
+
137
+ ### `fetch_channel_queue`
138
+
139
+ How many messages are waiting in the channel's outbound queue.
140
+
141
+ ### `recharge_url`
142
+
143
+ The agency's hosted top-up page for the channel's currency, prefilled with the
144
+ caller's details. `404 recharge_link_unavailable` when no link is configured for
145
+ that currency.
146
+
147
+ ---
148
+
149
+ ## Writable properties
150
+
151
+ ### Profile and compliance
152
+
153
+ | `prop` | Body | Notes |
154
+ | --- | --- | --- |
155
+ | `business_profile` | `{ update: {...} }` | Meta's business profile fields. |
156
+ | `business_compliance` | `{ update: {...} }` | Compliance details. |
157
+ | `business_commerce` | `{ update: {...} }` | Catalog and cart settings. |
158
+
159
+ `update` is required on all three; leaving it out is a `422`.
160
+
161
+ ```ts
162
+ await crm.channels.setProp(id, 'business_profile', {
163
+ body: { update: { about: 'We reply in minutes', email: 'hi@example.com' } },
164
+ });
165
+ ```
166
+
167
+ ### `qr_code`
168
+
169
+ `{ update: {...} }`. Pass `update.edit: true` to change an existing code;
170
+ otherwise a new one is created.
171
+
172
+ ### `conversation_automation`
173
+
174
+ Reads the current automation, changes one part of it, and writes it back — so
175
+ the rest survives.
176
+
177
+ | Body key | Effect |
178
+ | --- | --- |
179
+ | `welcome_message` | Sets `enable_welcome_message`. |
180
+ | `prop: 'prompts'` + `prompts` | Replaces the ice-breaker prompts. |
181
+ | `prop: 'commands'` + `commands` | Replaces the commands. |
182
+
183
+ ```ts
184
+ await crm.channels.setProp(id, 'conversation_automation', {
185
+ body: { prop: 'prompts', prompts: [{ prompt_text: 'Track my order' }] },
186
+ });
187
+ ```
188
+
189
+ ### `conversation_analytics`
190
+
191
+ | Body | Required | Default |
192
+ | --- | --- | --- |
193
+ | `range` | yes | — |
194
+ | `granularity` | no | `'DAILY'` |
195
+ | `breakdown` | no | `''` |
196
+
197
+ Meta's conversation statistics for the channel's own number.
198
+
199
+ ### `whatsapp_business_encryption`
200
+
201
+ `{ business_public_key }` — required. Registers the key used for encrypted flow
202
+ responses.
203
+
204
+ ### Number registration and migration
205
+
206
+ | `prop` | Body | What it does |
207
+ | --- | --- | --- |
208
+ | `wa_request_otp` | `{ method?: 'SMS' \| 'VOICE' }` | Asks Meta to send a verification code. Anything but `'VOICE'` is treated as `'SMS'`. |
209
+ | `wa_submit_otp` | `{ code }` | Verifies the code. Required. |
210
+ | `wa_pin_update` | `{ pin, localization? }` | Registers the number with Meta, which is what enables messaging on it. Runs the credit-line handshake and provisions the BSP account first. Already registered? The call is a no-op returning `{ pin: 'unchanged' }`. |
211
+ | `wa_biz_update` | — | For WA Biz numbers, already registered with Meta: does the billing handshake and activates the channel. `400 not_biz_app_channel` on any other channel. |
212
+ | `wa_deregister` | — | Deregisters the number at Meta. |
213
+ | `migrate_number` | `{ cc, phone_number }` | Both required. Moves the channel to a new number and rewrites its `phone_number_id` and identity. |
214
+
215
+ `wa_pin_update` and `wa_biz_update` both clear the channel's cache: registration
216
+ changes how the number behaves, and anything cached describes the state before
217
+ it.
218
+
219
+ ### File uploads — **binary body**
220
+
221
+ | `prop` | Sends | Returns |
222
+ | --- | --- | --- |
223
+ | `profile_pic` | The image bytes | Uploads to Meta, then sets it as the profile picture. |
224
+ | `resumable_upload` | The file bytes | `{ h: '<handle>' }` — Meta's upload handle, for use in a template. |
225
+ | `upload_media` | The file bytes | Meta's media id, for sending. |
226
+
227
+ ```ts
228
+ await crm.channels.setProp(id, 'profile_pic', {
229
+ raw: await file.arrayBuffer(),
230
+ contentType: 'image/jpeg',
231
+ });
232
+
233
+ const { data } = await crm.channels.setProp<{ h: string }>(id, 'resumable_upload', {
234
+ raw: bytes,
235
+ contentType: 'image/png',
236
+ });
237
+ data.h; // pass this handle to a template component
238
+ ```
239
+
240
+ Up to 25 MB per request. A missing `contentType` will not be inferred.
241
+
242
+ ### `test_message`
243
+
244
+ Sends one message on the channel, to check it end to end.
245
+
246
+ `409 channel_inactive` if the channel is not active.
247
+
248
+ For WhatsApp the body is an outbound message. `{ type: 'flow', flow_id, to, ... }`
249
+ sends a flow instead, and the flow must belong to this channel and be sendable.
250
+ `wait_for_flow_response` waits for the reply.
251
+
252
+ RCS speaks its own content schema: the whole body is passed through as the
253
+ message, with `queue_id` read off it.
254
+
255
+ ```ts
256
+ await crm.channels.setProp(id, 'test_message', {
257
+ body: { to: '919876543210', type: 'text', text: { body: 'ping' } },
258
+ });
259
+ ```
260
+
261
+ ### `webhook_listener_update`
262
+
263
+ Points both Meta subscriptions — the WABA (account-level events) and the phone
264
+ number (messages) — at the platform's listener, and marks the channel as
265
+ upgraded. Both have to point there for the listener to be whole.
266
+
267
+ Returns `{ webhook: '<listener url>' }`.
268
+
269
+ ### `delete_channel_queue`
270
+
271
+ Purges the channel's outbound queue. Returns `{ deleted: true }`. Anything
272
+ waiting is dropped, not sent.
273
+
274
+ ### `enable_whatsapp_direct` / `disable_whatsapp_direct`
275
+
276
+ Flips `whatsapp_direct_enabled` on the channel. Takes no body; returns
277
+ `{ whatsapp_direct_enabled: boolean }`.
278
+
279
+ ---
280
+
281
+ ## Errors
282
+
283
+ | Status | Code | When |
284
+ | --- | --- | --- |
285
+ | 400 | `unsupported_channel_prop` | The channel's category does not support this property. `details.supported` lists what it does. |
286
+ | 400 | `provider_request_failed` | Meta rejected the call (a 4xx from them). `details` carries their reason. |
287
+ | 502 | `provider_request_failed` | Meta failed or was unreachable (a 5xx). |
288
+ | 502 | `partner_request_failed` | The BSP partner call failed. |
289
+ | 422 | — | A required body or query key was missing. |
290
+ | 404 | `not_found` | No such channel for this caller. |