ab-ecosystem-cpass 1.0.11 → 1.0.13
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 +1 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +7 -0
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/embedded-signup.d.ts +46 -0
- package/dist/internal/embedded-signup.js +318 -0
- package/dist/internal/embedded-signup.js.map +1 -0
- package/dist/resources/channels/onboarding.d.ts +89 -0
- package/dist/resources/channels/onboarding.js +96 -0
- package/dist/resources/channels/onboarding.js.map +1 -0
- package/dist/types/campaign.d.ts +8 -0
- package/dist/types/channels.d.ts +129 -0
- package/dist/types/channels.js.map +1 -1
- package/docs/audience/canned-replies.md +3 -0
- package/docs/audience/contact-tags.md +4 -0
- package/docs/audience/contacts.md +30 -1
- package/docs/audience/crm-triggers.md +3 -0
- package/docs/audience/custom-fields.md +23 -0
- package/docs/audience/exports.md +6 -0
- package/docs/audience/imports.md +7 -0
- package/docs/audience/notifications.md +11 -0
- package/docs/audience/scheduled-messages.md +5 -0
- package/docs/audience/segments.md +4 -0
- package/docs/audience/webhooks.md +7 -0
- package/docs/campaign/campaigns.md +27 -0
- package/docs/channels/activity.md +3 -0
- package/docs/channels/billing.md +2 -0
- package/docs/channels/channel-props.md +19 -0
- package/docs/channels/channels.md +35 -2
- package/docs/channels/onboarding.md +530 -0
- package/docs/channels/templates.md +22 -0
- package/docs/channels/wallet.md +2 -0
- package/docs/channels/webhooks.md +6 -0
- package/docs/data-store.md +18 -0
- package/docs/inbox/connection.md +88 -0
- package/package.json +1 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChannelOnboarding = void 0;
|
|
4
|
+
const paths_1 = require("../paths");
|
|
5
|
+
const embedded_signup_1 = require("../../internal/embedded-signup");
|
|
6
|
+
const ONBOARDING = `${paths_1.CHANNELS}/onboarding`;
|
|
7
|
+
/**
|
|
8
|
+
* Connecting a channel. The browser only runs Meta's dialog: Meta returns to
|
|
9
|
+
* the service's callback, which redeems the code and sends the browser back to
|
|
10
|
+
* `redirectUri` with a `status` and a `token` -- no credential reaches this
|
|
11
|
+
* page. `connect()` drives all of it; or call `url()`, open the dialog
|
|
12
|
+
* yourself, and read the return off your own landing page.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* // In the click handler for "Connect WhatsApp" -- not after an await.
|
|
16
|
+
* const candidate = await crm.channelOnboarding.connect({
|
|
17
|
+
* redirectUri: 'https://console.example.com/channels/connected',
|
|
18
|
+
* });
|
|
19
|
+
* // One number, or several to choose between: candidate.numbers
|
|
20
|
+
* const { data: channel } = await crm.channelOnboarding.complete({
|
|
21
|
+
* token: candidate.id,
|
|
22
|
+
* phoneNumberId: chosen.id,
|
|
23
|
+
* name: form.name,
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
class ChannelOnboarding {
|
|
28
|
+
http;
|
|
29
|
+
constructor(http) {
|
|
30
|
+
this.http = http;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* `GET /v1/channels/onboarding/types` -- the families this caller can connect
|
|
34
|
+
* right now. Anything the plan, the provider record or their role rules out is
|
|
35
|
+
* left out, so an empty list means "nothing to offer". Scope: `channels:read`.
|
|
36
|
+
*/
|
|
37
|
+
types() {
|
|
38
|
+
return this.http.request('GET', `${ONBOARDING}/types`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* `POST /v1/channels/onboarding/url` -- the page to open. **Meta refuses to
|
|
42
|
+
* be framed**: use a popup, or navigate. 402 if the plan has no room.
|
|
43
|
+
*
|
|
44
|
+
* `url` is a launcher on the app's own domain that forwards to the dialog
|
|
45
|
+
* (`dialogUrl`); `redirectUri` is where *we* return the browser, not what
|
|
46
|
+
* Meta is given -- that is `callbackUri`.
|
|
47
|
+
*/
|
|
48
|
+
url(input) {
|
|
49
|
+
return this.http.request('POST', `${ONBOARDING}/url`, { body: input });
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* `GET /v1/channels/onboarding/pending/:token` -- what the return URL's
|
|
53
|
+
* `token` refers to: the numbers it covers, and the form's prefill.
|
|
54
|
+
*/
|
|
55
|
+
pending(token) {
|
|
56
|
+
return this.http.request('GET', `${ONBOARDING}/pending/${encodeURIComponent(token)}`);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* `POST /v1/channels/onboarding/exchange` -- redeems a code you hold yourself,
|
|
60
|
+
* for a BSP app whose registered redirect is a page of yours rather than the
|
|
61
|
+
* service's callback. **Creates nothing**: show `phoneNumber` and confirm.
|
|
62
|
+
*/
|
|
63
|
+
exchange(input) {
|
|
64
|
+
return this.http.request('POST', `${ONBOARDING}/exchange`, {
|
|
65
|
+
body: input,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* `POST /v1/channels/onboarding/complete` -- creates the channel, **inactive**:
|
|
70
|
+
* `setProp(id, 'wa_pin_update')` brings it up, or `wa_biz_update` for a WA Biz number.
|
|
71
|
+
*/
|
|
72
|
+
complete(input) {
|
|
73
|
+
return this.http.request('POST', `${ONBOARDING}/complete`, { body: input });
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The URL, the dialog, and the record the callback leaves behind, in one call.
|
|
77
|
+
* **Call it straight from the click handler**, or the popup is blocked.
|
|
78
|
+
* Rejects with `EmbeddedSignupError`; `reason` carries the service's own code.
|
|
79
|
+
*
|
|
80
|
+
* What comes back is a *candidate* -- nothing exists until `complete()`, and
|
|
81
|
+
* if it holds several numbers the person picks one.
|
|
82
|
+
*/
|
|
83
|
+
async connect(options) {
|
|
84
|
+
const { type, slug, provider, redirectUri, ...popup } = options;
|
|
85
|
+
const { data: session } = await this.url({ type, slug, provider, redirectUri });
|
|
86
|
+
const returned = await (0, embedded_signup_1.awaitSignupReturn)(session.url, popup);
|
|
87
|
+
if (!returned.token) {
|
|
88
|
+
throw new embedded_signup_1.EmbeddedSignupError('signup_error', 'The signup came back without an onboarding token');
|
|
89
|
+
}
|
|
90
|
+
// The query string carries a summary; this is the whole record.
|
|
91
|
+
const { data } = await this.pending(returned.token);
|
|
92
|
+
return data;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.ChannelOnboarding = ChannelOnboarding;
|
|
96
|
+
//# sourceMappingURL=onboarding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onboarding.js","sourceRoot":"","sources":["../../../src/resources/channels/onboarding.ts"],"names":[],"mappings":";;;AAYA,oCAAoC;AACpC,oEAAgH;AAEhH,MAAM,UAAU,GAAG,GAAG,gBAAQ,aAAa,CAAC;AAiB5C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAa,iBAAiB;IACC;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD;;;;OAIG;IACH,KAAK;QACH,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAsC,KAAK,EAAE,GAAG,UAAU,QAAQ,CAAC,CAAC;IAC9F,CAAC;IAED;;;;;;;OAOG;IACH,GAAG,CAAC,KAAyB;QAC3B,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAgC,MAAM,EAAE,GAAG,UAAU,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACxG,CAAC;IAED;;;OAGG;IACH,OAAO,CAAC,KAAa;QACnB,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CACtB,KAAK,EACL,GAAG,UAAU,YAAY,kBAAkB,CAAC,KAAK,CAAC,EAAE,CACrD,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACH,QAAQ,CAAC,KAA8B;QACrC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAqC,MAAM,EAAE,GAAG,UAAU,WAAW,EAAE;YAC7F,IAAI,EAAE,KAAK;SACZ,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,QAAQ,CAAC,KAA8B;QACrC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAkB,MAAM,EAAE,GAAG,UAAU,WAAW,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/F,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,OAAO,CAAC,OAAuB;QACnC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,KAAK,EAAE,GAAG,OAAO,CAAC;QAChE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAC;QAEhF,MAAM,QAAQ,GAAG,MAAM,IAAA,mCAAiB,EAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC7D,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACpB,MAAM,IAAI,qCAAmB,CAAC,cAAc,EAAE,kDAAkD,CAAC,CAAC;QACpG,CAAC;QAED,gEAAgE;QAChE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAEpD,OAAO,IAAI,CAAC;IACd,CAAC;CACF;AA5ED,8CA4EC"}
|
package/dist/types/campaign.d.ts
CHANGED
|
@@ -108,6 +108,14 @@ export interface CampaignTriggerInfo {
|
|
|
108
108
|
}
|
|
109
109
|
export interface Campaign {
|
|
110
110
|
id: string;
|
|
111
|
+
/**
|
|
112
|
+
* The team member who created the campaign, by psuid. Absent when the account
|
|
113
|
+
* holder created it, and on campaigns written before this was recorded.
|
|
114
|
+
*
|
|
115
|
+
* Who *wrote* it. Who armed it is `triggerInfo.psuid`, which can be someone
|
|
116
|
+
* else and is set a step later.
|
|
117
|
+
*/
|
|
118
|
+
member?: string;
|
|
111
119
|
version: string;
|
|
112
120
|
crmVersion: string;
|
|
113
121
|
name: string;
|
package/dist/types/channels.d.ts
CHANGED
|
@@ -49,6 +49,12 @@ export interface ChannelWallet {
|
|
|
49
49
|
*/
|
|
50
50
|
export interface Channel {
|
|
51
51
|
id: string;
|
|
52
|
+
/**
|
|
53
|
+
* The team member who connected the channel, by psuid. Absent when the
|
|
54
|
+
* account holder connected it, and on channels created before this was
|
|
55
|
+
* recorded.
|
|
56
|
+
*/
|
|
57
|
+
member?: string;
|
|
52
58
|
/** Enabled/disabled by the owner -- not the connection state. */
|
|
53
59
|
active: boolean;
|
|
54
60
|
ecom?: {
|
|
@@ -381,3 +387,126 @@ export interface ListActivityLogsQuery extends ListQuery {
|
|
|
381
387
|
/** Restrict to a period. */
|
|
382
388
|
range?: UnixRange;
|
|
383
389
|
}
|
|
390
|
+
/** Channel families the onboarding endpoints can connect today. */
|
|
391
|
+
export type OnboardingChannelType = 'whatsapp';
|
|
392
|
+
/** Every family the picker shows, implemented or not. */
|
|
393
|
+
export type OfferableChannelType = 'whatsapp' | 'facebook' | 'instagram';
|
|
394
|
+
/** One row of the "add a channel" picker. Only connectable families are listed. */
|
|
395
|
+
export interface ChannelOnboardingOption {
|
|
396
|
+
type: OfferableChannelType;
|
|
397
|
+
label: string;
|
|
398
|
+
/** The inclusion a channel would be created against. Pass back to `url()`. */
|
|
399
|
+
slug?: string;
|
|
400
|
+
limit?: number;
|
|
401
|
+
used?: number;
|
|
402
|
+
remaining?: number;
|
|
403
|
+
}
|
|
404
|
+
export interface OnboardingUrlInput {
|
|
405
|
+
type?: OnboardingChannelType;
|
|
406
|
+
/** Only needed when the plan carries more than one channel inclusion. */
|
|
407
|
+
slug?: string;
|
|
408
|
+
/**
|
|
409
|
+
* Which BSP to connect through, by provider code. Omit for whichever is
|
|
410
|
+
* active for this family -- pin one only when there is a reason to.
|
|
411
|
+
*/
|
|
412
|
+
provider?: string;
|
|
413
|
+
/**
|
|
414
|
+
* Where the browser is handed back, with the outcome on it. **Required**: the
|
|
415
|
+
* callback has no session, so nothing else says where. Meta never sees it.
|
|
416
|
+
*/
|
|
417
|
+
redirectUri: string;
|
|
418
|
+
}
|
|
419
|
+
export interface OnboardingUrlResult {
|
|
420
|
+
/**
|
|
421
|
+
* Open in a popup, or navigate to it; it cannot be framed. A launcher on the
|
|
422
|
+
* app's own domain that forwards to `dialogUrl`, since Meta ties the dialog
|
|
423
|
+
* to that domain.
|
|
424
|
+
*/
|
|
425
|
+
url: string;
|
|
426
|
+
/** Meta's dialog, which `url` forwards to. Open `url`, not this. */
|
|
427
|
+
dialogUrl: string;
|
|
428
|
+
/** Opaque, and already carrying `redirectUri`. Our callback is what reads it. */
|
|
429
|
+
state: string;
|
|
430
|
+
/** Where the browser will come back to. */
|
|
431
|
+
redirectUri: string;
|
|
432
|
+
/** What Meta was given: the service's own callback, registered on the BSP app. */
|
|
433
|
+
callbackUri: string;
|
|
434
|
+
expiresAt: IsoDate;
|
|
435
|
+
provider: string;
|
|
436
|
+
remaining?: number;
|
|
437
|
+
}
|
|
438
|
+
/** `ready`: one number, bound. `select`: several, so `complete()` names one. */
|
|
439
|
+
export type OnboardingReturnStatus = 'ready' | 'select' | 'error';
|
|
440
|
+
/** The query string the service appends to `redirectUri`. All values are strings. */
|
|
441
|
+
export interface OnboardingReturn {
|
|
442
|
+
status: OnboardingReturnStatus;
|
|
443
|
+
/** Pass to `pending()`, and to `complete()` as `token`. */
|
|
444
|
+
token?: string;
|
|
445
|
+
/** How many numbers the authorisation covered; `"1"` for `ready`. */
|
|
446
|
+
numbers?: string;
|
|
447
|
+
wabaId?: string;
|
|
448
|
+
phoneNumberId?: string;
|
|
449
|
+
displayPhoneNumber?: string;
|
|
450
|
+
verifiedName?: string;
|
|
451
|
+
expiresAt?: string;
|
|
452
|
+
/** `status=error` only. */
|
|
453
|
+
code?: string;
|
|
454
|
+
message?: string;
|
|
455
|
+
}
|
|
456
|
+
/** What Meta reports about the number, for the confirmation card. */
|
|
457
|
+
export interface OnboardingPhoneNumber {
|
|
458
|
+
id: string;
|
|
459
|
+
wabaId: string;
|
|
460
|
+
/** Formatted, e.g. `+91 93116 36081`. */
|
|
461
|
+
displayPhoneNumber: string;
|
|
462
|
+
verifiedName: string;
|
|
463
|
+
status?: string;
|
|
464
|
+
codeVerificationStatus?: string;
|
|
465
|
+
qualityRating?: string;
|
|
466
|
+
}
|
|
467
|
+
export interface OnboardingExchangeInput {
|
|
468
|
+
state: string;
|
|
469
|
+
code: string;
|
|
470
|
+
wabaId: string;
|
|
471
|
+
phoneNumberId: string;
|
|
472
|
+
}
|
|
473
|
+
/** Prefill for the final form. Every field is overridable in `complete()`. */
|
|
474
|
+
export interface OnboardingDefaults {
|
|
475
|
+
name: string;
|
|
476
|
+
color: string;
|
|
477
|
+
currency: ChannelCurrency;
|
|
478
|
+
}
|
|
479
|
+
export interface OnboardingExchangeResult {
|
|
480
|
+
/** The pending record's id. Pass to `complete()` as `token`. */
|
|
481
|
+
id: string;
|
|
482
|
+
expiresAt: IsoDate;
|
|
483
|
+
phoneNumber: OnboardingPhoneNumber;
|
|
484
|
+
defaults: OnboardingDefaults;
|
|
485
|
+
}
|
|
486
|
+
/** A pending record, read back from the `token` on the return URL. */
|
|
487
|
+
export interface OnboardingPendingResult {
|
|
488
|
+
id: string;
|
|
489
|
+
expiresAt: IsoDate;
|
|
490
|
+
type: OnboardingChannelType;
|
|
491
|
+
/** Every number the authorisation covers that is still free to connect. */
|
|
492
|
+
numbers: OnboardingPhoneNumber[];
|
|
493
|
+
/** Set when there was exactly one, and so already bound to the record. */
|
|
494
|
+
phoneNumber?: OnboardingPhoneNumber;
|
|
495
|
+
defaults: OnboardingDefaults;
|
|
496
|
+
}
|
|
497
|
+
export interface CompleteOnboardingInput {
|
|
498
|
+
/** The `id` from `exchange()`, or `token` off the return URL. */
|
|
499
|
+
token: string;
|
|
500
|
+
/** Which number, when the record covers several -- `status=select`. */
|
|
501
|
+
phoneNumberId?: string;
|
|
502
|
+
name: string;
|
|
503
|
+
color?: string;
|
|
504
|
+
currency?: ChannelCurrency;
|
|
505
|
+
}
|
|
506
|
+
/** What the Meta dialog hands back, once both halves of it have arrived. */
|
|
507
|
+
export interface SignupCredentials {
|
|
508
|
+
code: string;
|
|
509
|
+
state: string;
|
|
510
|
+
wabaId: string;
|
|
511
|
+
phoneNumberId: string;
|
|
512
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channels.js","sourceRoot":"","sources":["../../src/types/channels.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"channels.js","sourceRoot":"","sources":["../../src/types/channels.ts"],"names":[],"mappings":";;;AAoLA;;;;GAIG;AACU,QAAA,oBAAoB,GAAG,CAAC,aAAa,EAAE,kBAAkB,EAAE,cAAc,CAAU,CAAC;AAoIpF,QAAA,sBAAsB,GAAmC;IACpE,UAAU;IACV,MAAM;IACN,WAAW;IACX,MAAM;IACN,QAAQ;IACR,SAAS;IACT,UAAU;IACV,SAAS;IACT,cAAc;IACd,OAAO;CACR,CAAC"}
|
|
@@ -22,6 +22,9 @@ Two kinds, and the difference is who gets the last word:
|
|
|
22
22
|
| [`update`](#update) | `PATCH /v1/audience/canned-replies/:reply` | `canned_replies:write` |
|
|
23
23
|
| [`delete`](#delete) | `DELETE /v1/audience/canned-replies/:reply` | `canned_replies:write` |
|
|
24
24
|
|
|
25
|
+
**Permissions** — `CRM.CHAT.CANNED_REPLY`: `LIST`, `READ`, `CREATE`, `UPDATE`,
|
|
26
|
+
`DELETE`. Account-level, so `data_scope` does not narrow it.
|
|
27
|
+
|
|
25
28
|
## Addressing a reply: `:reply` is an id **or** a name
|
|
26
29
|
|
|
27
30
|
A 24-hex-character path segment is read as an **id**; anything else as a
|
|
@@ -14,6 +14,10 @@ tag **ids** on a write, and hands back the full tag documents resolved into
|
|
|
14
14
|
| [`update`](#update) | `PATCH /v1/audience/contact-tags/:id` | `contact_tags:write` |
|
|
15
15
|
| [`delete`](#delete) | `DELETE /v1/audience/contact-tags/:id` | `contact_tags:write` |
|
|
16
16
|
|
|
17
|
+
**Permissions** — `CRM.CONTACT.TAG`: `LIST` to list, `READ` to read one,
|
|
18
|
+
`CREATE`, `UPDATE`, `DELETE`. A tag is account-level, so a role's `data_scope`
|
|
19
|
+
does not narrow this list — a member who holds `LIST` sees the account's tags.
|
|
20
|
+
|
|
17
21
|
## The tag object
|
|
18
22
|
|
|
19
23
|
```ts
|
|
@@ -19,6 +19,35 @@ land on the caller's own.
|
|
|
19
19
|
| [`delete`](#delete) | `DELETE /v1/audience/contacts/:id` | `contacts:write` |
|
|
20
20
|
| [`iterate`](#iterate) | — (walks `list`) | `contacts:read` |
|
|
21
21
|
|
|
22
|
+
**Permissions** — each route also names a permission key, and a team member
|
|
23
|
+
needs it. An owner does not.
|
|
24
|
+
|
|
25
|
+
| Action | Key |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `list` | `CRM.CONTACT.LIST` |
|
|
28
|
+
| `get` | `CRM.CONTACT.READ` |
|
|
29
|
+
| `create` | `CRM.CONTACT.CREATE` |
|
|
30
|
+
| `update` | `CRM.CONTACT.UPDATE` |
|
|
31
|
+
| `delete` | `CRM.CONTACT.DELETE` |
|
|
32
|
+
|
|
33
|
+
A refusal is `403` with code `A403` (the role does not hold the key) or `E403`
|
|
34
|
+
(the account's plan does not cover contacts — `details.feature` names it). An
|
|
35
|
+
owner can meet `E403` but never `A403`.
|
|
36
|
+
|
|
37
|
+
Separately from that, how many contacts a member can see is their role's
|
|
38
|
+
`data_scope`: `own` — the default — returns the contacts assigned to them as
|
|
39
|
+
`agent`, and `all` returns the account's. A contact assigned to nobody is the
|
|
40
|
+
account's and not visible to an `own`-scoped member. Reading, updating or
|
|
41
|
+
deleting a contact outside that scope is `404`, never `403`: a `403` would
|
|
42
|
+
confirm the contact exists, which is what the scope is hiding.
|
|
43
|
+
|
|
44
|
+
A contact a **team member** creates is assigned to that member as its `agent`,
|
|
45
|
+
whatever their `data_scope` and whatever `agent` the body carried: they are the
|
|
46
|
+
one working it. An account holder's create leaves `agent` as given — `null`
|
|
47
|
+
unless they named someone — so a contact stays unassigned until it is handed
|
|
48
|
+
over.
|
|
49
|
+
|
|
50
|
+
|
|
22
51
|
## The contact object
|
|
23
52
|
|
|
24
53
|
```ts
|
|
@@ -145,7 +174,7 @@ blank contact.
|
|
|
145
174
|
| `whatsapp` | string | — | The contact's WhatsApp number, in any shape. Creates the channel identity. Create-only. |
|
|
146
175
|
| `order` | string | — | Free-form external reference. |
|
|
147
176
|
| `tags` | ObjectId[] | `[]` | Contact tag ids. |
|
|
148
|
-
| `agent` | ObjectId \| null | — | Assigned agent. |
|
|
177
|
+
| `agent` | ObjectId \| null | — | Assigned agent. **Ignored for a team member**: the member creating the contact is assigned it. |
|
|
149
178
|
| `team` | ObjectId \| null | — | Assigned team. |
|
|
150
179
|
| `lead_source` | `{ type?, module?, id? }` | — | Where the contact came from. |
|
|
151
180
|
| `status` | `'live' \| 'trashed'` | `'live'` | |
|
|
@@ -25,6 +25,9 @@ mean on the list.
|
|
|
25
25
|
| [`delete`](#delete) | `DELETE /v1/audience/crm-triggers/:id` | `crm_triggers:write` |
|
|
26
26
|
| [`logs`](#logs) | `GET /v1/audience/crm-triggers/logs` | `crm_triggers:read` |
|
|
27
27
|
|
|
28
|
+
**Permissions** — `CRM.TRIGGER`: `LIST`, `READ` (one trigger, and the log
|
|
29
|
+
endpoint), `CREATE`, `UPDATE`, `DELETE`.
|
|
30
|
+
|
|
28
31
|
## The trigger object
|
|
29
32
|
|
|
30
33
|
```ts
|
|
@@ -18,6 +18,29 @@ is looked up as one; anything else is read as a slug.
|
|
|
18
18
|
| [`update`](#update) | `PATCH /v1/audience/custom-fields/:ref` | `custom_fields:write` |
|
|
19
19
|
| [`delete`](#delete) | `DELETE /v1/audience/custom-fields/:ref` | `custom_fields:write` |
|
|
20
20
|
|
|
21
|
+
**Permissions** — the feature is `CRM.CONTACT.FIELD`, which is its own feature
|
|
22
|
+
and not a verb of `CRM.CONTACT`. A team member needs the key; an owner does not.
|
|
23
|
+
|
|
24
|
+
| Action | Key |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `list`, `get` | `CRM.CONTACT.FIELD.LIST` |
|
|
27
|
+
| `create` | `CRM.CONTACT.FIELD.CREATE` |
|
|
28
|
+
| `update` | `CRM.CONTACT.FIELD.UPDATE` |
|
|
29
|
+
| `delete` | `CRM.CONTACT.FIELD.DELETE` |
|
|
30
|
+
|
|
31
|
+
Reading one field uses `LIST`: the feature publishes no `READ` verb.
|
|
32
|
+
|
|
33
|
+
The schema is account-level, so there is nothing for a role's `data_scope` to
|
|
34
|
+
narrow — a field definition is not assigned to anybody, and a member seeing half
|
|
35
|
+
a schema would see half of every contact. A member either reaches custom fields
|
|
36
|
+
or does not.
|
|
37
|
+
|
|
38
|
+
`teamAccess` on a field (`restricted` / `open_to_all`) is **stored and
|
|
39
|
+
filterable, not enforced**: passing `?teamAccess=restricted` filters the list,
|
|
40
|
+
and nothing restricts a member to `open_to_all` fields. If per-field visibility
|
|
41
|
+
is meant to be real, that is a separate change from the key above.
|
|
42
|
+
|
|
43
|
+
|
|
21
44
|
## The field object
|
|
22
45
|
|
|
23
46
|
```ts
|
package/docs/audience/exports.md
CHANGED
|
@@ -23,11 +23,17 @@ The route is `/exports`, not `/contact-exports`: a row's `module` is `contact`
|
|
|
23
23
|
| [`create`](#create) | `POST /v1/audience/exports` | `contacts:read` |
|
|
24
24
|
| [`delete`](#delete) | `DELETE /v1/audience/exports/:id` | `contacts:write` |
|
|
25
25
|
|
|
26
|
+
**Permissions** — every route here needs `CRM.CONTACT.EXPORT`, reads included:
|
|
27
|
+
an export history row *is* the export, so a member who may not export has no
|
|
28
|
+
business reading what one produced. The catalogue notes `EXPORT` implies
|
|
29
|
+
`CRM.CONTACT.LIST`.
|
|
30
|
+
|
|
26
31
|
There is no `update`: a row is moved by the worker, not by a caller.
|
|
27
32
|
|
|
28
33
|
Note the scope on `create` — reading contacts out is a **read**, so an export
|
|
29
34
|
can be started with read-only access.
|
|
30
35
|
|
|
36
|
+
|
|
31
37
|
## The export object
|
|
32
38
|
|
|
33
39
|
```ts
|
package/docs/audience/imports.md
CHANGED
|
@@ -23,8 +23,15 @@ The route is `/imports`, not `/contact-imports`: a row's `module` is `contact`
|
|
|
23
23
|
| [`create`](#create) | `POST /v1/audience/imports` | `contacts:write` |
|
|
24
24
|
| [`delete`](#delete) | `DELETE /v1/audience/imports/:id` | `contacts:write` |
|
|
25
25
|
|
|
26
|
+
**Permissions** — the catalogue publishes `CRM.CONTACT.EXPORT` but **no
|
|
27
|
+
`IMPORT`**, so these map to what an import does: `create` and `delete` need
|
|
28
|
+
`CRM.CONTACT.CREATE` (an import is bulk contact creation) and the reads need
|
|
29
|
+
`CRM.CONTACT.LIST`. If importing should be grantable separately from creating
|
|
30
|
+
one contact at a time, the catalogue needs an `IMPORT` verb.
|
|
31
|
+
|
|
26
32
|
There is no `update`: a row is moved by the worker, not by a caller.
|
|
27
33
|
|
|
34
|
+
|
|
28
35
|
## The import object
|
|
29
36
|
|
|
30
37
|
```ts
|
|
@@ -12,6 +12,10 @@ acting user, and the only party who can see or revoke it is that user.
|
|
|
12
12
|
| [`unsubscribe`](#unsubscribe) | `POST /v1/audience/devices/unregister` | `devices:write` |
|
|
13
13
|
| [`devices`](#devices) | `GET /v1/audience/devices` | `devices:read` |
|
|
14
14
|
|
|
15
|
+
**Permissions** — `CRM.DEVICE`: `LIST`, `CREATE` to register, `DELETE` to
|
|
16
|
+
unregister. Every device row belongs to the acting person rather than the
|
|
17
|
+
account, so a caller only ever sees and changes their own whatever the key says.
|
|
18
|
+
|
|
15
19
|
## What the SDK does, and what you do
|
|
16
20
|
|
|
17
21
|
A push subscription has two halves and only one of them is an API call.
|
|
@@ -342,6 +346,13 @@ The practical consequence: a device registered before this existed is still
|
|
|
342
346
|
reachable by `psuid` exactly as before, and the next `subscribe()` call — which
|
|
343
347
|
the frontend makes on every app load — fills in the second id.
|
|
344
348
|
|
|
349
|
+
A **team member's** device records only their `psuid`. The legacy id on a member
|
|
350
|
+
token belongs to the account holder, not to the member, so filling it in would
|
|
351
|
+
make the member's phone answer notifications addressed to the owner — and the
|
|
352
|
+
owner's answer theirs. The consequence is narrow and worth knowing: a member is
|
|
353
|
+
reachable by `psuid`, and a chat assigned to them notifies them because the chat
|
|
354
|
+
row names them.
|
|
355
|
+
|
|
345
356
|
### Rendering it in a browser
|
|
346
357
|
|
|
347
358
|
If the payload carries a `notification` block, FCM displays it and opens
|
|
@@ -17,6 +17,11 @@ goes out.
|
|
|
17
17
|
| [`cancelSeries`](#cancelseries) | `DELETE /v1/audience/scheduled-messages/series/:series` | `messages:write` |
|
|
18
18
|
| [`sendNow`](#sendnow) | `POST /v1/audience/scheduled-messages/:id/send-now` | `messages:write` |
|
|
19
19
|
|
|
20
|
+
**Permissions** — `CRM.CHAT.SCHEDULED_MESSAGE`: `LIST`, `READ`, `CREATE`
|
|
21
|
+
(a single schedule or a series), `UPDATE` to reschedule, `DELETE` to cancel
|
|
22
|
+
either, and **`SEND` for send-now**, which bypasses the schedule. The rows carry
|
|
23
|
+
the account and no per-member creator, so `data_scope` does not narrow them.
|
|
24
|
+
|
|
20
25
|
---
|
|
21
26
|
|
|
22
27
|
## Why conditions exist
|
|
@@ -17,6 +17,10 @@ campaign. Two runs a day apart can return different people, by design.
|
|
|
17
17
|
| [`update`](#update) | `PATCH /v1/audience/segments/:id` | `segments:write` |
|
|
18
18
|
| [`delete`](#delete) | `DELETE /v1/audience/segments/:id` | `segments:write` |
|
|
19
19
|
|
|
20
|
+
**Permissions** — `CRM.CONTACT.SEGMENT`: `LIST`, `READ`, `CREATE`, `UPDATE`,
|
|
21
|
+
`DELETE`. Reading a segment reveals its filter, not the contacts behind it;
|
|
22
|
+
those still pass `CRM.CONTACT.LIST`.
|
|
23
|
+
|
|
20
24
|
## The segment object
|
|
21
25
|
|
|
22
26
|
```ts
|
|
@@ -12,6 +12,12 @@ Three things in this SDK are called a webhook; they are not the same thing:
|
|
|
12
12
|
| [`crm.crmTriggers`](crm-triggers.md) | One configured request per rule, when a contact matches | no |
|
|
13
13
|
| [`crm.channelWebhooks`](../channels/webhooks.md) | A channel's message traffic | n/a |
|
|
14
14
|
|
|
15
|
+
**Permissions** — `CRM.WEBHOOK`: `LIST`, `READ`, `CREATE`, `UPDATE`, `DELETE`.
|
|
16
|
+
`READ` is a grant rather than a side effect of `LIST` because reading one
|
|
17
|
+
reveals the endpoint URL and its secret header.
|
|
18
|
+
|
|
19
|
+
Distinct from `CRM.CHANNEL.WEBHOOK`, which is where a *provider* delivers
|
|
20
|
+
inbound traffic for one channel.
|
|
15
21
|
**Scoping** — unlike the rest of the audience module, these rows are scoped by
|
|
16
22
|
**tenant** rather than by the calling user.
|
|
17
23
|
|
|
@@ -23,6 +29,7 @@ Three things in this SDK are called a webhook; they are not the same thing:
|
|
|
23
29
|
| [`update`](#update) | `PATCH /v1/audience/webhooks/:id` | `webhooks:write` |
|
|
24
30
|
| [`delete`](#delete) | `DELETE /v1/audience/webhooks/:id` | `webhooks:write` |
|
|
25
31
|
|
|
32
|
+
|
|
26
33
|
## The webhook object
|
|
27
34
|
|
|
28
35
|
```ts
|
|
@@ -18,6 +18,26 @@ One message sent to many people.
|
|
|
18
18
|
| [`cancel`](#cancel) | `POST /v1/campaigns/:id/cancel` | |
|
|
19
19
|
| [`delete`](#delete) | `DELETE /v1/campaigns/:id` | |
|
|
20
20
|
|
|
21
|
+
**Permissions** — the feature is `CRM.CAMPAIGN`. A team member needs the key; an
|
|
22
|
+
owner does not.
|
|
23
|
+
|
|
24
|
+
| Action | Key |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `list` | `CRM.CAMPAIGN.LIST` |
|
|
27
|
+
| `get`, the report endpoints | `CRM.CAMPAIGN.READ` |
|
|
28
|
+
| `create`, `createBulk`, `createGroup`, `uploadReceivers` | `CRM.CAMPAIGN.CREATE` |
|
|
29
|
+
| `update` | `CRM.CAMPAIGN.UPDATE` |
|
|
30
|
+
| `trigger`, `schedule`, `cancel` | `CRM.CAMPAIGN.SEND` |
|
|
31
|
+
| `delete` | `CRM.CAMPAIGN.DELETE` |
|
|
32
|
+
|
|
33
|
+
`cancel` is under `SEND` rather than `UPDATE` on purpose: it is control of a
|
|
34
|
+
dispatch, and somebody who may not launch a campaign should not be able to stop
|
|
35
|
+
one that is going out.
|
|
36
|
+
|
|
37
|
+
A campaign is not assigned to anybody, so a role's `data_scope` does not narrow
|
|
38
|
+
this list — a member who holds `CRM.CAMPAIGN.LIST` sees the account's campaigns.
|
|
39
|
+
|
|
40
|
+
|
|
21
41
|
---
|
|
22
42
|
|
|
23
43
|
## Creating a campaign does not send it
|
|
@@ -47,8 +67,15 @@ named against it.
|
|
|
47
67
|
| --- | --- | --- |
|
|
48
68
|
| `scheduledAt` in the body | **`422`** | that is the point |
|
|
49
69
|
| writes `triggerInfo` | no | yes |
|
|
70
|
+
| writes `member` | on `create`, by a team member | no |
|
|
50
71
|
| campaign becomes sendable | no | yes |
|
|
51
72
|
|
|
73
|
+
`member` is the other half of that record: the team member who **wrote** the
|
|
74
|
+
campaign, stamped at `create` from their token. `triggerInfo.psuid` is who armed
|
|
75
|
+
it, which is a later act and can be somebody else. Both are server-set and
|
|
76
|
+
refused from a body; an account holder's campaign carries no `member`, because
|
|
77
|
+
the campaign is already theirs.
|
|
78
|
+
|
|
52
79
|
**"Now" is not instant.** `trigger` with no time arms the campaign about a minute
|
|
53
80
|
out (`CAMPAIGN_IMMEDIATE_DELAY_SECONDS`), and [`cancel`](#cancel) still stops it
|
|
54
81
|
in that window. A second `trigger` on a campaign that is armed but not yet picked
|
|
@@ -14,6 +14,9 @@ the [wallet ledger](wallet.md) instead.
|
|
|
14
14
|
| --- | --- | --- |
|
|
15
15
|
| [`list`](#list) | `GET /v1/channels/:channelId/logs` | `channels:read` |
|
|
16
16
|
|
|
17
|
+
**Permissions** — `CRM.CHANNEL.LOG.LIST`. Its own feature rather than part of
|
|
18
|
+
reading the channel, because the log exposes message traffic.
|
|
19
|
+
|
|
17
20
|
## The activity log object
|
|
18
21
|
|
|
19
22
|
```ts
|
package/docs/channels/billing.md
CHANGED
|
@@ -17,8 +17,10 @@ message category on a single one.
|
|
|
17
17
|
| [`list`](#list) | `GET /v1/channels/:channelId/conversations` | `channels:read` |
|
|
18
18
|
| [`stats`](#stats) | `GET /v1/channels/:channelId/conversations?stat=…` | `channels:read` |
|
|
19
19
|
|
|
20
|
+
**Permissions** — `CRM.CHANNEL.USAGE.READ`, shared with the channel wallet.
|
|
20
21
|
Read-only: charges are written by the platform as conversations happen.
|
|
21
22
|
|
|
23
|
+
|
|
22
24
|
## The billing log object
|
|
23
25
|
|
|
24
26
|
```ts
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Channel properties
|
|
2
2
|
|
|
3
|
+
|
|
4
|
+
**Permissions** — a prop is not one capability, so the key depends on which prop
|
|
5
|
+
is named. The path carries neither the feature (that is the channel's type) nor
|
|
6
|
+
the verb (that is the prop), so this is checked in the handler.
|
|
7
|
+
|
|
8
|
+
| Prop | Key |
|
|
9
|
+
| --- | --- |
|
|
10
|
+
| `token` | **`CREDENTIAL`** — it returns the provider access token |
|
|
11
|
+
| `wa_pin_update`, `wa_request_otp`, `wa_submit_otp`, `migrate_number`, `wa_deregister`, `whatsapp_business_encryption` | **`REGISTER`** |
|
|
12
|
+
| `message_analytics`, `conversation_analytics` | `ANALYTICS` |
|
|
13
|
+
| `test_message`, `upload_media`, `resumable_upload` | `SEND` |
|
|
14
|
+
| `webhook_status`, `webhook_listener_update` | `CRM.CHANNEL.WEBHOOK.READ` / `.UPDATE` |
|
|
15
|
+
| every other read | `READ` |
|
|
16
|
+
| every other write | `UPDATE` |
|
|
17
|
+
|
|
18
|
+
The first two rows are the reason the split exists. `token` behind `READ` would
|
|
19
|
+
let anybody who can view a channel take its credential, and the number
|
|
20
|
+
lifecycle — retiring a number, moving it to another account, re-keying Flows
|
|
21
|
+
encryption — is not a profile edit.
|
|
3
22
|
`crm.channels.getProp` / `crm.channels.setProp` — `/v1/channels/:id/:prop`
|
|
4
23
|
|
|
5
24
|
Everything about a channel that lives with the **provider** rather than on the
|
|
@@ -6,8 +6,10 @@ The connections an account sends and receives on: a WhatsApp number, an RCS
|
|
|
6
6
|
agent, a webchat widget.
|
|
7
7
|
|
|
8
8
|
A channel is **provisioned by onboarding, not by this API**. There is no
|
|
9
|
-
`create` and no `delete
|
|
10
|
-
billing, wallet and activity history hang off.
|
|
9
|
+
`create` and no `delete` here: `disable` retires one for good, keeping the row
|
|
10
|
+
its billing, wallet and activity history hang off. Connecting a new one is
|
|
11
|
+
`crm.channelOnboarding`, which walks the account holder through Meta's signup
|
|
12
|
+
dialog -- **[connecting a channel](onboarding.md)**.
|
|
11
13
|
|
|
12
14
|
Anything that lives with the provider rather than on the channel document — the
|
|
13
15
|
business profile, the QR code, media uploads, number registration — moves
|
|
@@ -24,6 +26,36 @@ through `getProp` and `setProp`, documented on their own page:
|
|
|
24
26
|
| [`getProp`](channel-props.md) | `GET /v1/channels/:id/:prop` | `channels:read` |
|
|
25
27
|
| [`setProp`](channel-props.md) | `POST /v1/channels/:id/:prop` | `channels:write` |
|
|
26
28
|
|
|
29
|
+
**Permissions** — there is no single `CRM.CHANNEL` key. The catalogue has one
|
|
30
|
+
feature per channel type, because each sits behind its own plan option:
|
|
31
|
+
`CRM.CHANNEL.WHATSAPP`, `CRM.CHANNEL.SMS`, `CRM.CHANNEL.RCS`. So the key for a
|
|
32
|
+
request depends on the **type of the channel being addressed**, which is a field
|
|
33
|
+
on the row — the check runs in the handler, not as route middleware.
|
|
34
|
+
|
|
35
|
+
| Action | Verb |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `list` | `LIST` — see below |
|
|
38
|
+
| `get` | `READ` |
|
|
39
|
+
| `updateDetails` | `UPDATE` |
|
|
40
|
+
| `disable` | `DELETE` (a channel is never deleted; disabling retires it) |
|
|
41
|
+
| `rotateToken` | **`CREDENTIAL`** |
|
|
42
|
+
| `getProp` / `setProp` | depends on the prop — see [channel properties](channel-props.md) |
|
|
43
|
+
|
|
44
|
+
`rotateToken` is `CREDENTIAL` rather than `UPDATE` because it replaces the same
|
|
45
|
+
provider access token the `token` prop reads. Three verbs exist for that reason:
|
|
46
|
+
`CREDENTIAL` for the token, `REGISTER` for the number lifecycle, and `ANALYTICS`
|
|
47
|
+
for the analytics reads — none of which belong beside a business-profile edit.
|
|
48
|
+
|
|
49
|
+
`list` spans every type the account owns, so it cannot be one key: it is
|
|
50
|
+
**filtered** rather than refused. A member holding only
|
|
51
|
+
`CRM.CHANNEL.WHATSAPP.LIST` sees the WhatsApp channels and no others, and asking
|
|
52
|
+
for `?type=sms` without `CRM.CHANNEL.SMS.LIST` returns an empty page rather than
|
|
53
|
+
somebody else's channels.
|
|
54
|
+
|
|
55
|
+
Not every verb exists on every type: SMS reaches no properties at all, so it has
|
|
56
|
+
no `CREDENTIAL`, `REGISTER` or `ANALYTICS`, and asking for one is refused rather
|
|
57
|
+
than ignored.
|
|
58
|
+
|
|
27
59
|
## The channel object
|
|
28
60
|
|
|
29
61
|
Credentials and the private auth block are stripped from every response; what
|
|
@@ -32,6 +64,7 @@ remains is safe to hand a console.
|
|
|
32
64
|
```ts
|
|
33
65
|
interface Channel {
|
|
34
66
|
id: string;
|
|
67
|
+
member?: string; // the team member who connected it; absent when the account holder did
|
|
35
68
|
active: boolean; // enabled by the owner — NOT the connection state
|
|
36
69
|
ecom?: { store_status: boolean; store?: string };
|
|
37
70
|
basic_details: { name: string; color_code: string; channel_identity: string };
|