ab-ecosystem-cpass 1.0.11 → 1.0.12
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 +7 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/embedded-signup.d.ts +29 -0
- package/dist/internal/embedded-signup.js +175 -0
- package/dist/internal/embedded-signup.js.map +1 -0
- package/dist/resources/channels/onboarding.d.ts +62 -0
- package/dist/resources/channels/onboarding.js +76 -0
- package/dist/resources/channels/onboarding.js.map +1 -0
- package/dist/types/channels.d.ts +79 -0
- package/docs/audience/canned-replies.md +3 -0
- package/docs/audience/contact-tags.md +4 -0
- package/docs/audience/contacts.md +23 -0
- 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 +20 -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 +34 -2
- package/docs/channels/onboarding.md +324 -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
|
@@ -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
|
|
@@ -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
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# Connecting a channel
|
|
2
|
+
|
|
3
|
+
`crm.channelOnboarding` — `/v1/channels/onboarding`
|
|
4
|
+
|
|
5
|
+
Creating a channel is the one channel operation that is not a request and a
|
|
6
|
+
response. The credentials belong to Meta, and the only way to get them is to
|
|
7
|
+
walk the account holder through Meta's own **Embedded Signup** — a wizard on
|
|
8
|
+
`facebook.com` where they pick or create a WhatsApp Business Account, add a
|
|
9
|
+
number, and grant our app access to it.
|
|
10
|
+
|
|
11
|
+
So the backend cannot create a channel on its own. It hands out the URL that
|
|
12
|
+
starts the wizard, and redeems what the browser comes back with.
|
|
13
|
+
|
|
14
|
+
| Action | Route | Scope |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| [`types`](#types) | `GET /v1/channels/onboarding/types` | `channels:read` |
|
|
17
|
+
| [`url`](#url) | `POST /v1/channels/onboarding/url` | `channels:write` |
|
|
18
|
+
| [`exchange`](#exchange) | `POST /v1/channels/onboarding/exchange` | `channels:write` |
|
|
19
|
+
| [`complete`](#complete) | `POST /v1/channels/onboarding/complete` | `channels:write` |
|
|
20
|
+
| [`connect`](#connect) | the three above, plus the popup | `channels:write` |
|
|
21
|
+
|
|
22
|
+
**Permissions** — `CRM.CHANNEL.WHATSAPP.CREATE`, checked on `url`, `exchange`
|
|
23
|
+
**and** `complete`. Three checks rather than one because each call is a step
|
|
24
|
+
towards a channel existing, and a role can be edited between them. `types`
|
|
25
|
+
filters instead of refusing, like `GET /v1/channels`: a member who cannot create
|
|
26
|
+
a WhatsApp channel sees the row marked `not_permitted` rather than losing the
|
|
27
|
+
page.
|
|
28
|
+
|
|
29
|
+
Only WhatsApp is onboarded here. Facebook and Instagram come through the same
|
|
30
|
+
Meta dialog with a different signup configuration and a different set of ids
|
|
31
|
+
coming back; they are still created by the legacy stack, and `types` reports them
|
|
32
|
+
as `not_supported` so a picker can render the whole list it already renders.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## The shape of it
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
┌─────────────────────────────── your console ──────────────────────────────┐
|
|
40
|
+
│ │
|
|
41
|
+
│ 1. types() "WhatsApp — 2 of 3 left" │
|
|
42
|
+
│ │
|
|
43
|
+
│ 2. url() ──────────────▶ { url, state } │
|
|
44
|
+
│ │ │
|
|
45
|
+
│ └── open a POPUP on that url ───▶ Meta's wizard │
|
|
46
|
+
│ │ │
|
|
47
|
+
│ code (on the redirect) ◀────────┤ │
|
|
48
|
+
│ waba_id + phone_number_id │ │
|
|
49
|
+
│ (postMessage) ◀────────┘ │
|
|
50
|
+
│ │
|
|
51
|
+
│ 3. exchange() ──────────────▶ { id, phoneNumber, defaults } │
|
|
52
|
+
│ show the number on a card; let them confirm │
|
|
53
|
+
│ │
|
|
54
|
+
│ 4. complete() ──────────────▶ the channel │
|
|
55
|
+
└───────────────────────────────────────────────────────────────────────────┘
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Two things about the middle of that are easy to get wrong, and both cost an
|
|
59
|
+
afternoon:
|
|
60
|
+
|
|
61
|
+
- **The dialog cannot be put in an iframe.** Meta sends `X-Frame-Options` on it.
|
|
62
|
+
A "modal" has to be your own chrome around a popup window, not an embedded
|
|
63
|
+
page. An iframe renders blank, which looks like our bug.
|
|
64
|
+
- **What comes back arrives in two halves.** The authorization `code` comes back
|
|
65
|
+
the OAuth way, on the redirect. The `waba_id` and `phone_number_id` never
|
|
66
|
+
appear in that URL — they arrive as a `postMessage` from `facebook.com`
|
|
67
|
+
carrying `WA_EMBEDDED_SIGNUP`. A code with no ids cannot be verified and ids
|
|
68
|
+
with no code cannot be redeemed, so both are needed.
|
|
69
|
+
|
|
70
|
+
`connect()` handles both halves for you. Use the long way only if you already
|
|
71
|
+
drive `FB.login`, or if your onboarding spans two page loads.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## `types`
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const { data } = await crm.channelOnboarding.types();
|
|
79
|
+
|
|
80
|
+
// [
|
|
81
|
+
// { type: 'whatsapp', label: 'WhatsApp', available: true,
|
|
82
|
+
// slug: 'whatsapp_channels_28335', limit: 3, used: 1, remaining: 2 },
|
|
83
|
+
// { type: 'facebook', label: 'Facebook Messenger', available: false,
|
|
84
|
+
// reason: 'not_supported' },
|
|
85
|
+
// { type: 'instagram', label: 'Instagram', available: false,
|
|
86
|
+
// reason: 'not_supported' },
|
|
87
|
+
// ]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Branch on `available`, and say something different for each `reason`:
|
|
91
|
+
|
|
92
|
+
| `reason` | What happened | What to say |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| `not_included` | The plan has none of this inclusion | Offer an upgrade |
|
|
95
|
+
| `quota_exceeded` | Every unit is spent | "You have used all 3 of your WhatsApp channels" |
|
|
96
|
+
| `not_permitted` | The account can, this person cannot | "Ask the account owner" |
|
|
97
|
+
| `provider_not_configured` | No BSP is set up for the family | Our problem, not theirs — contact support |
|
|
98
|
+
| `not_supported` | Not onboarded by this service yet | Hide the row, or point at the old panel |
|
|
99
|
+
|
|
100
|
+
`limit` and `remaining` are absent when the inclusion is granted without a count
|
|
101
|
+
— some plans grant a channel family as a flag rather than a quantity, which
|
|
102
|
+
means *included, unmetered*. Absent is not zero.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## `url`
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const { data } = await crm.channelOnboarding.url();
|
|
110
|
+
// { url, state, redirectUri, expiresAt, provider, remaining }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| Field | |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| `type` | `whatsapp`, and the default |
|
|
116
|
+
| `slug` | The inclusion to spend. Only needed when the plan carries more than one — each can sit behind a different BSP |
|
|
117
|
+
| `redirectUri` | Must be one of the service's configured redirects. Omit for the default |
|
|
118
|
+
|
|
119
|
+
Checks the plan **before** the account holder is sent anywhere: finding out
|
|
120
|
+
afterwards means telling somebody who has just completed a business verification
|
|
121
|
+
that they cannot have the channel. 402 `not_included` / `quota_exceeded` when
|
|
122
|
+
the plan has no room, 409 `provider_not_configured` when no BSP is set up, 400
|
|
123
|
+
`redirect_uri_not_allowed` for a redirect the service does not know.
|
|
124
|
+
|
|
125
|
+
`state` is ours, not Meta's: signed, and bound to this account, this channel
|
|
126
|
+
family and this provider. **Hand it back to `exchange()` unchanged.** It travels
|
|
127
|
+
through Meta as the OAuth `state` parameter, so a browser that follows `url` gets
|
|
128
|
+
it back for nothing — but `FB.login` will not round-trip it, so keep your copy.
|
|
129
|
+
Good for an hour.
|
|
130
|
+
|
|
131
|
+
`remaining` is what is left *after* this one, which is the number to put next to
|
|
132
|
+
"you can connect 2 more".
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## `exchange`
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const { data } = await crm.channelOnboarding.exchange({
|
|
140
|
+
state, // from url()
|
|
141
|
+
code, // from the dialog
|
|
142
|
+
wabaId, // from the WA_EMBEDDED_SIGNUP message
|
|
143
|
+
phoneNumberId,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// {
|
|
147
|
+
// id: '6721f0...', // pass to complete() as `token`
|
|
148
|
+
// expiresAt: '...',
|
|
149
|
+
// phoneNumber: {
|
|
150
|
+
// id: '...', wabaId: '...', displayPhoneNumber: '+91 93116 36081',
|
|
151
|
+
// verifiedName: 'Acme Support', status: 'CONNECTED',
|
|
152
|
+
// codeVerificationStatus: 'VERIFIED', qualityRating: 'GREEN',
|
|
153
|
+
// },
|
|
154
|
+
// defaults: { name: 'Acme Support', color: '#075E54', currency: 'INR' },
|
|
155
|
+
// }
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Nothing is created yet.** The number is a candidate: show
|
|
159
|
+
`data.phoneNumber` on a card and let the person confirm it before it becomes a
|
|
160
|
+
channel. That is why this is a separate call from `complete` — creating the
|
|
161
|
+
channel the moment the code is redeemed leaves a half-configured channel behind
|
|
162
|
+
every time somebody closes the tab on the confirmation screen.
|
|
163
|
+
|
|
164
|
+
`phoneNumber` is Meta's own description of the number, not an echo of what you
|
|
165
|
+
sent: the service reads the WABA's numbers with the token it just minted, which
|
|
166
|
+
is what proves the token covers that WABA and the number is really on it.
|
|
167
|
+
|
|
168
|
+
`id` stands for the long-lived token, which never leaves the service. The record
|
|
169
|
+
it points at belongs to this account, can be used once, and expires in an hour.
|
|
170
|
+
|
|
171
|
+
| Status | Code | |
|
|
172
|
+
| --- | --- | --- |
|
|
173
|
+
| 400 | `onboarding_code_rejected` | Meta refused the code. It is single use, so the dialog has to be run again |
|
|
174
|
+
| 400 | `onboarding_number_rejected` | The number is not on that WABA, or Meta has banned it |
|
|
175
|
+
| 400 | `onboarding_state_expired` | More than an hour since `url()` |
|
|
176
|
+
| 403 | `onboarding_state_mismatch` | The state was issued to another account |
|
|
177
|
+
| 409 | `phone_number_in_use` | Already connected to a channel |
|
|
178
|
+
| 409 | `phone_number_pending` | **This account's own** unfinished channel — `details.channel` is its id. Resume it rather than making a second one |
|
|
179
|
+
|
|
180
|
+
`phone_number_in_use` is checked across every account, not just this one: a
|
|
181
|
+
number carries one WABA webhook subscription at a time, so a second channel on it
|
|
182
|
+
would quietly take over the first one's message traffic.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## `complete`
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
const { data: channel } = await crm.channelOnboarding.complete({
|
|
190
|
+
token: candidate.id,
|
|
191
|
+
name: 'Acme Support',
|
|
192
|
+
color: '#075E54',
|
|
193
|
+
currency: 'INR',
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Creates the channel and returns it in the same shape `channels.get()` does.
|
|
198
|
+
`name` is required; `color` defaults to WhatsApp's green and `currency` to the
|
|
199
|
+
inclusion's, except that an INR plan falls back to USD for a number outside the
|
|
200
|
+
Indian dial codes — Meta prices those in dollars however the plan is denominated.
|
|
201
|
+
|
|
202
|
+
**The channel is created inactive.** Registering the number with Meta is what
|
|
203
|
+
brings it up, and that is the next step in the console:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// A number new to the Cloud API: set its PIN.
|
|
207
|
+
await crm.channels.setProp(channel.id, 'wa_pin_update', { body: { pin: '123456' } });
|
|
208
|
+
|
|
209
|
+
// A number coming off the WhatsApp Business app: already registered with Meta.
|
|
210
|
+
await crm.channels.setProp(channel.id, 'wa_biz_update');
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`channel.cloud.wa_biz_registration` says which — `pending` until the BSP
|
|
214
|
+
handshake has run. `channel.cloud.is_on_biz_app` is true for the second case.
|
|
215
|
+
|
|
216
|
+
| Status | Code | |
|
|
217
|
+
| --- | --- | --- |
|
|
218
|
+
| 404 | `not_found` | No such session, or it belongs to another account |
|
|
219
|
+
| 409 | `onboarding_already_completed` | A second call with the same token |
|
|
220
|
+
| 410 | `onboarding_expired` | The confirmation screen sat for more than an hour |
|
|
221
|
+
| 409 | `phone_number_in_use` | Somebody connected the number while the confirmation screen was open |
|
|
222
|
+
|
|
223
|
+
A refusal here **releases the session rather than consuming it**, so pressing the
|
|
224
|
+
button again is enough — the Meta dialog does not have to be run a second time.
|
|
225
|
+
Only a create that succeeds spends the token.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## `connect`
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
// In the click handler for "Connect WhatsApp" — see the note below.
|
|
233
|
+
const candidate = await crm.channelOnboarding.connect({
|
|
234
|
+
onProgress: ({ phase, step }) => setStatus(phase === 'step' ? step : phase),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Then show the card and the form, and call complete().
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`url()` → popup → `exchange()`, resolving with exactly what `exchange()` returns.
|
|
241
|
+
It creates nothing, so the person still confirms the number before it becomes a
|
|
242
|
+
channel.
|
|
243
|
+
|
|
244
|
+
**Call it straight from the click handler**, not after an `await` of your own. It
|
|
245
|
+
opens the popup before awaiting anything that would detach it from the gesture,
|
|
246
|
+
because a `window.open` the browser does not attribute to a click is blocked —
|
|
247
|
+
and the block is indistinguishable from the user having a popup blocker.
|
|
248
|
+
|
|
249
|
+
| Option | |
|
|
250
|
+
| --- | --- |
|
|
251
|
+
| `type`, `slug`, `redirectUri` | Passed through to `url()` |
|
|
252
|
+
| `onProgress` | `opened` → `step` (Meta's own step name) → `ids` / `code` |
|
|
253
|
+
| `timeoutMs` | Default 15 minutes. The wizard can include a business verification |
|
|
254
|
+
| `width`, `height`, `name` | The popup window |
|
|
255
|
+
|
|
256
|
+
Rejects with `EmbeddedSignupError`, whose `code` is one of:
|
|
257
|
+
|
|
258
|
+
| `code` | |
|
|
259
|
+
| --- | --- |
|
|
260
|
+
| `popup_blocked` | Allow popups and try again |
|
|
261
|
+
| `popup_closed` | They closed the window |
|
|
262
|
+
| `cancelled` | They backed out inside the wizard; `message` names the step |
|
|
263
|
+
| `signup_error` | Meta reported a problem; `message` is Meta's own text |
|
|
264
|
+
| `timeout` | Still open after `timeoutMs` |
|
|
265
|
+
| `unsupported_environment` | Not a browser — drive the three calls yourself |
|
|
266
|
+
|
|
267
|
+
Everything the service refused arrives as the usual `ApiError`, so handle both:
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
try {
|
|
271
|
+
const candidate = await crm.channelOnboarding.connect();
|
|
272
|
+
} catch (err) {
|
|
273
|
+
if (err instanceof EmbeddedSignupError) {
|
|
274
|
+
if (err.code === 'cancelled') return; // not an error worth showing
|
|
275
|
+
return showNotice(err.message);
|
|
276
|
+
}
|
|
277
|
+
throw err; // a 402, a 409, ...
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Driving the dialog yourself
|
|
282
|
+
|
|
283
|
+
If you serve a page at the redirect, post the code back to the opener and
|
|
284
|
+
`openSignup` will use it instead of reading the popup's URL:
|
|
285
|
+
|
|
286
|
+
```html
|
|
287
|
+
<!-- At your redirectUri -->
|
|
288
|
+
<script>
|
|
289
|
+
const params = new URLSearchParams(location.search);
|
|
290
|
+
window.opener?.postMessage(
|
|
291
|
+
{ type: 'CRM_CHANNEL_ONBOARDING', code: params.get('code'), state: params.get('state') },
|
|
292
|
+
window.origin,
|
|
293
|
+
);
|
|
294
|
+
</script>
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Or skip the popup helper entirely: call `url()`, run `FB.login` with the
|
|
298
|
+
`config_id` from your own app configuration, and call `exchange()` with the
|
|
299
|
+
`state` you kept and the ids from Meta's callback.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## What the service is doing for you
|
|
304
|
+
|
|
305
|
+
Four checks that nothing downstream repeats:
|
|
306
|
+
|
|
307
|
+
1. **The plan has room** — at all three steps, because the last unit can be spent
|
|
308
|
+
in another tab while somebody is in the wizard.
|
|
309
|
+
2. **The number is not already connected** — across every account, for the
|
|
310
|
+
webhook reason above.
|
|
311
|
+
3. **The token really covers the WABA** — both ids arrive in a request body, so
|
|
312
|
+
they are claims until the WABA's numbers are read with the minted token.
|
|
313
|
+
|
|
314
|
+
And two things it deliberately leaves alone:
|
|
315
|
+
|
|
316
|
+
- **The webhook subscription.** Meta is pointed at our listener at another level,
|
|
317
|
+
so onboarding neither registers it nor refuses a channel over it. Check it with
|
|
318
|
+
`channels.getProp(id, 'webhook_status')` and re-point it with
|
|
319
|
+
`channels.setProp(id, 'webhook_listener_update')` if it is ever not in place —
|
|
320
|
+
a channel whose WABA is not subscribed sends perfectly well and reports nothing
|
|
321
|
+
back, which is invisible until somebody replies.
|
|
322
|
+
- **The credit-line handshake** with the BSP. That runs on the first
|
|
323
|
+
`wa_pin_update` or `wa_biz_update`, where it was already implemented and where
|
|
324
|
+
it is idempotent.
|
|
@@ -21,6 +21,28 @@ channel is a `404` rather than a match.
|
|
|
21
21
|
| [`delete`](#delete) | `DELETE /v1/channels/:channelId/templates/:id` | `channels:write` |
|
|
22
22
|
| [`sync`](#sync) | `POST /v1/channels/:channelId/templates/sync` | `channels:write` |
|
|
23
23
|
|
|
24
|
+
**Permissions** — the feature is `CRM.TEMPLATE`, flat and with no plan option of
|
|
25
|
+
its own. A team member needs the key; an owner does not.
|
|
26
|
+
|
|
27
|
+
| Action | Key |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `list` | `CRM.TEMPLATE.LIST` |
|
|
30
|
+
| `get` | `CRM.TEMPLATE.READ` |
|
|
31
|
+
| `create` | `CRM.TEMPLATE.CREATE` |
|
|
32
|
+
| `update`, `sync` | `CRM.TEMPLATE.UPDATE` |
|
|
33
|
+
| `delete` | `CRM.TEMPLATE.DELETE` |
|
|
34
|
+
|
|
35
|
+
`sync` pulls the provider's templates over the stored ones, which is a write to
|
|
36
|
+
every template on the channel — hence `UPDATE`.
|
|
37
|
+
|
|
38
|
+
The **channel** these hang off is gated separately, per type
|
|
39
|
+
(`CRM.CHANNEL.WHATSAPP`, `CRM.CHANNEL.SMS`, `CRM.CHANNEL.RCS`), and that gate is
|
|
40
|
+
not yet applied: the type is only knowable from the channel row, and on
|
|
41
|
+
`/channels/:id/:prop` the verb depends on which prop is named — so both need the
|
|
42
|
+
handler rather than the route. Until it lands, a member holding
|
|
43
|
+
`CRM.TEMPLATE.LIST` can read templates on any channel the account owns.
|
|
44
|
+
|
|
45
|
+
|
|
24
46
|
## The template object
|
|
25
47
|
|
|
26
48
|
```ts
|
package/docs/channels/wallet.md
CHANGED
|
@@ -13,6 +13,8 @@ through this API. To send a user to the top-up page, read
|
|
|
13
13
|
| --- | --- | --- |
|
|
14
14
|
| [`list`](#list) | `GET /v1/channels/:channelId/wallet` | `channels:read` |
|
|
15
15
|
|
|
16
|
+
**Permissions** — `CRM.CHANNEL.USAGE.READ`, shared with the conversation counts.
|
|
17
|
+
|
|
16
18
|
## The wallet log object
|
|
17
19
|
|
|
18
20
|
```ts
|
|
@@ -20,6 +20,12 @@ changes from the audience module.
|
|
|
20
20
|
| [`update`](#update) | `PATCH /v1/channels/:channelId/webhooks/:id` | `channels:write` |
|
|
21
21
|
| [`delete`](#delete) | `DELETE /v1/channels/:channelId/webhooks/:id` | `channels:write` |
|
|
22
22
|
|
|
23
|
+
**Permissions** — `CRM.CHANNEL.WEBHOOK`: `LIST`, `READ`, `CREATE`, `UPDATE`,
|
|
24
|
+
`DELETE`. The two channel props that read and re-point the provider-side
|
|
25
|
+
callback (`webhook_status`, `webhook_listener_update`) belong to this feature
|
|
26
|
+
too, not to the channel: re-pointing a listener diverts every inbound message on
|
|
27
|
+
that channel.
|
|
28
|
+
|
|
23
29
|
## The webhook object
|
|
24
30
|
|
|
25
31
|
```ts
|
package/docs/data-store.md
CHANGED
|
@@ -26,6 +26,24 @@ connections it holds to customer clusters cannot crowd out anything but itself.
|
|
|
26
26
|
| [`dataStoreCollections`](#collections) | `…/:store/collections` | `datastores:read` / `datastores:write` |
|
|
27
27
|
| [`dataStoreRecords`](#records) | `…/:store/collections/:collection/records` | `datastore-records:read` / `datastore-records:write` |
|
|
28
28
|
|
|
29
|
+
**Permissions** — three features, one per level:
|
|
30
|
+
|
|
31
|
+
| | Keys |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| stores | `CRM.DATASTORE`: `LIST`, `READ`, `CREATE`, `UPDATE`, `DELETE`, **`TEST`** |
|
|
34
|
+
| collections | `CRM.DATASTORE.COLLECTION`: `LIST`, `READ`, `CREATE`, `UPDATE`, `DELETE` |
|
|
35
|
+
| records | `CRM.DATASTORE.RECORD`: `LIST`, `READ`, `CREATE`, `UPDATE`, `DELETE` |
|
|
36
|
+
|
|
37
|
+
`TEST` is a verb of its own because testing a connection opens an outbound
|
|
38
|
+
connection to the customer's database, which is more than reading the row that
|
|
39
|
+
describes it. `READ` on a store exposes the connection target; the credential is
|
|
40
|
+
never returned.
|
|
41
|
+
|
|
42
|
+
On records, `search` is `LIST` (a read with a body), and the bulk endpoints are
|
|
43
|
+
the verb they perform — `bulk-update` is `UPDATE`, `bulk-delete` is `DELETE`.
|
|
44
|
+
`CRM.DATASTORE.RECORD` is the most consequential key here: it reads and writes
|
|
45
|
+
rows in the customer's own database.
|
|
46
|
+
|
|
29
47
|
## The model
|
|
30
48
|
|
|
31
49
|
Three things, nested:
|