ab-ecosystem-cpass 1.0.0 → 1.0.6
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 +121 -21
- package/dist/client.d.ts +38 -0
- package/dist/client.js +49 -5
- package/dist/client.js.map +1 -1
- package/dist/http.d.ts +15 -0
- package/dist/http.js +21 -1
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.js +24 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/bytes.d.ts +38 -0
- package/dist/internal/bytes.js +84 -0
- package/dist/internal/bytes.js.map +1 -0
- package/dist/internal/device.d.ts +24 -0
- package/dist/internal/device.js +91 -0
- package/dist/internal/device.js.map +1 -0
- package/dist/internal/md5.d.ts +4 -0
- package/dist/internal/md5.js +109 -0
- package/dist/internal/md5.js.map +1 -0
- package/dist/internal/receiver-file.d.ts +106 -0
- package/dist/internal/receiver-file.js +128 -0
- package/dist/internal/receiver-file.js.map +1 -0
- package/dist/realtime/client.d.ts +85 -0
- package/dist/realtime/client.js +126 -0
- package/dist/realtime/client.js.map +1 -0
- package/dist/realtime/events.d.ts +147 -0
- package/dist/realtime/events.js +3 -0
- package/dist/realtime/events.js.map +1 -0
- package/dist/realtime/inbox.d.ts +173 -0
- package/dist/realtime/inbox.js +263 -0
- package/dist/realtime/inbox.js.map +1 -0
- package/dist/realtime/index.d.ts +16 -0
- package/dist/realtime/index.js +37 -0
- package/dist/realtime/index.js.map +1 -0
- package/dist/resources/audience/contacts.d.ts +19 -3
- package/dist/resources/audience/contacts.js +19 -3
- package/dist/resources/audience/contacts.js.map +1 -1
- package/dist/resources/audience/inbox-media.d.ts +92 -0
- package/dist/resources/audience/inbox-media.js +127 -0
- package/dist/resources/audience/inbox-media.js.map +1 -0
- package/dist/resources/audience/notifications.d.ts +171 -0
- package/dist/resources/audience/notifications.js +215 -0
- package/dist/resources/audience/notifications.js.map +1 -0
- package/dist/resources/audience/scheduled-messages.d.ts +234 -0
- package/dist/resources/audience/scheduled-messages.js +259 -0
- package/dist/resources/audience/scheduled-messages.js.map +1 -0
- package/dist/resources/campaign-audience.d.ts +132 -0
- package/dist/resources/campaign-audience.js +224 -0
- package/dist/resources/campaign-audience.js.map +1 -0
- package/dist/resources/campaign-report-exports.d.ts +14 -0
- package/dist/resources/campaign-report-exports.js +28 -0
- package/dist/resources/campaign-report-exports.js.map +1 -0
- package/dist/resources/campaign-reports.d.ts +15 -0
- package/dist/resources/campaign-reports.js +25 -0
- package/dist/resources/campaign-reports.js.map +1 -0
- package/dist/resources/campaigns.d.ts +239 -0
- package/dist/resources/campaigns.js +300 -0
- package/dist/resources/campaigns.js.map +1 -0
- package/dist/types/audience.d.ts +93 -1
- package/dist/types/campaign.d.ts +229 -0
- package/dist/types/campaign.js +3 -0
- package/dist/types/campaign.js.map +1 -0
- package/dist/types/contact-filter.d.ts +3 -2
- package/dist/types/contact-filter.js +3 -2
- package/dist/types/contact-filter.js.map +1 -1
- package/dist/types/inbox.d.ts +391 -0
- package/dist/types/inbox.js +16 -0
- package/dist/types/inbox.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/scheduled-message.d.ts +221 -0
- package/dist/types/scheduled-message.js +16 -0
- package/dist/types/scheduled-message.js.map +1 -0
- package/docs/audience/contacts.md +34 -4
- package/docs/audience/notifications.md +263 -0
- package/docs/audience/scheduled-messages.md +557 -0
- package/docs/campaign/campaigns.md +489 -0
- package/docs/contact-filter.md +2 -1
- package/docs/inbox/chats.md +356 -0
- package/docs/inbox/connection.md +171 -0
- package/docs/inbox/media.md +202 -0
- package/docs/inbox/messages.md +338 -0
- package/docs/inbox/referrals.md +72 -0
- package/docs/inbox/sessions.md +74 -0
- package/package.json +28 -2
|
@@ -142,6 +142,7 @@ blank contact.
|
|
|
142
142
|
| Field | Type | Default | Notes |
|
|
143
143
|
| --- | --- | --- | --- |
|
|
144
144
|
| `name` | `Partial<ContactName>` | — | `salutation` ≤ 20 chars, `first_name` and `last_name` ≤ 120. |
|
|
145
|
+
| `whatsapp` | string | — | The contact's WhatsApp number, in any shape. Creates the channel identity. Create-only. |
|
|
145
146
|
| `order` | string | — | Free-form external reference. |
|
|
146
147
|
| `tags` | ObjectId[] | `[]` | Contact tag ids. |
|
|
147
148
|
| `agent` | ObjectId \| null | — | Assigned agent. |
|
|
@@ -152,17 +153,41 @@ blank contact.
|
|
|
152
153
|
| `custom_field_hash` | object | — | Values keyed by custom field **slug**. |
|
|
153
154
|
| `chat_metadata` | partial | — | Including `blocked`, itself partial. |
|
|
154
155
|
|
|
155
|
-
Channel identities cannot be set here; they are written when a conversation
|
|
156
|
-
starts on a channel.
|
|
157
|
-
|
|
158
156
|
```ts
|
|
159
157
|
const contact = await crm.contacts.create({
|
|
160
158
|
name: { first_name: 'Ada', last_name: 'Lovelace' },
|
|
159
|
+
whatsapp: '+91 98765 43210',
|
|
161
160
|
tags: [vipTagId],
|
|
162
161
|
custom_field_hash: { city: 'Pune', lead_score: 80 },
|
|
163
162
|
});
|
|
163
|
+
|
|
164
|
+
contact.profile.whatsapp.identifier; // '919876543210'
|
|
164
165
|
```
|
|
165
166
|
|
|
167
|
+
### The `whatsapp` field
|
|
168
|
+
|
|
169
|
+
Pass it unless the contact is genuinely a placeholder. A contact with no channel
|
|
170
|
+
identity cannot be messaged, will not be matched to an inbound message, is
|
|
171
|
+
invisible to a number search, and drops out of every segment whose filter reaches
|
|
172
|
+
into channel identities.
|
|
173
|
+
|
|
174
|
+
**Any shape is accepted and stored as digits only.** `+91 98765 43210`,
|
|
175
|
+
`+919876543210` and `919876543210` all store `919876543210`, which is the form an
|
|
176
|
+
inbound message carries — the comparison that matches a reply to this contact is
|
|
177
|
+
made against that string, so the punctuation has to go.
|
|
178
|
+
|
|
179
|
+
| Response | When |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| `409 conflict` | The account already holds that number, on another contact or on this one. Nothing is created; the existing contact is untouched. |
|
|
182
|
+
| `422` | The value contains no digits at all. An empty identifier would be a channel identity that can never be messaged or matched. |
|
|
183
|
+
|
|
184
|
+
A number identifies **one contact per account**, and `profile` holds **one
|
|
185
|
+
identity per channel** — so a contact cannot carry two WhatsApp numbers, and two
|
|
186
|
+
contacts cannot share one.
|
|
187
|
+
|
|
188
|
+
Identities for the other channels are still written by the conversation that
|
|
189
|
+
starts on them.
|
|
190
|
+
|
|
166
191
|
---
|
|
167
192
|
|
|
168
193
|
## update
|
|
@@ -173,11 +198,16 @@ crm.contacts.update(id: string, input: UpdateContactInput): Promise<Contact>
|
|
|
173
198
|
|
|
174
199
|
`PATCH /v1/audience/contacts/:id` · `contacts:write` · **200**
|
|
175
200
|
|
|
176
|
-
Takes the same body as `create`, all optional. Anything left out
|
|
201
|
+
Takes the same body as `create` minus `whatsapp`, all optional. Anything left out
|
|
202
|
+
is untouched.
|
|
177
203
|
|
|
178
204
|
`tags` and `custom_field_hash` **replace** their stored value rather than merging
|
|
179
205
|
into it — read the contact first if you mean to add one tag to the existing set.
|
|
180
206
|
|
|
207
|
+
`whatsapp` is **not** accepted. A channel identity lives in its own collection, so
|
|
208
|
+
changing a number is a write there plus a delete of the identity it replaces —
|
|
209
|
+
not something a patch of the contact can express. It is create-only for now.
|
|
210
|
+
|
|
181
211
|
```ts
|
|
182
212
|
await crm.contacts.update(id, { agent: agentId, chat_metadata: { closed: true } });
|
|
183
213
|
```
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Push notifications
|
|
2
|
+
|
|
3
|
+
`crm.notifications` — `/v1/audience/devices`
|
|
4
|
+
|
|
5
|
+
Which browsers and apps a user wants notifications delivered to. A subscription
|
|
6
|
+
is one device — one browser profile, one app installation — held against the
|
|
7
|
+
acting user, and the only party who can see or revoke it is that user.
|
|
8
|
+
|
|
9
|
+
| Action | Route | Scope |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| [`subscribe`](#subscribe) | `POST /v1/audience/devices` | `devices:write` |
|
|
12
|
+
| [`unsubscribe`](#unsubscribe) | `POST /v1/audience/devices/unregister` | `devices:write` |
|
|
13
|
+
| [`devices`](#devices) | `GET /v1/audience/devices` | `devices:read` |
|
|
14
|
+
|
|
15
|
+
## What the SDK does, and what you do
|
|
16
|
+
|
|
17
|
+
A push subscription has two halves and only one of them is an API call.
|
|
18
|
+
|
|
19
|
+
**In the browser**, which this SDK deliberately does not wrap:
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
const permission = await Notification.requestPermission(); // 'granted' | 'denied' | 'default'
|
|
23
|
+
const token = await getToken(messaging, { vapidKey }); // firebase/messaging
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Wrapping those two would mean a hard dependency on `firebase/*` — in a package
|
|
27
|
+
that also runs on Node — to pass arguments that come from your own Firebase
|
|
28
|
+
project.
|
|
29
|
+
|
|
30
|
+
**Here**, which is the part that is an API call:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
if (permission === 'granted') {
|
|
34
|
+
await crm.notifications.subscribe({ token });
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Where the subscription lives
|
|
39
|
+
|
|
40
|
+
On the platform, against the acting user, **in this product** — not in the core
|
|
41
|
+
platform. Core owns identity and the agency's Firebase credentials; messages
|
|
42
|
+
arrive here, so the tokens and the sending live here too, and core is never on
|
|
43
|
+
the delivery path.
|
|
44
|
+
|
|
45
|
+
Nothing you send names a Firebase project. The platform resolves it from the
|
|
46
|
+
client the call arrives on and stamps it onto the row, because a registration
|
|
47
|
+
token is only sendable through the project that minted it and nothing in the
|
|
48
|
+
token itself says which. That resolution is a cached read, so it costs nothing in
|
|
49
|
+
steady state — but it is why `subscribe` is the one method here that can answer
|
|
50
|
+
`503`.
|
|
51
|
+
|
|
52
|
+
## The device object
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
interface Device {
|
|
56
|
+
id: string;
|
|
57
|
+
platform: 'web' | 'android' | 'ios';
|
|
58
|
+
label: string; // what you sent as device_label; '' when you sent none
|
|
59
|
+
clientId: string | null; // the reseller frontend it registered from
|
|
60
|
+
lastSeenAt: string; // ISO; last successful send, or last subscribe
|
|
61
|
+
createdAt: string;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The **registration token is never returned**. It is a send credential: anything
|
|
66
|
+
holding one can push to that browser through the agency's Firebase project. The
|
|
67
|
+
list exists so a user can recognise and revoke their own devices, which needs the
|
|
68
|
+
label and the last-seen time and nothing more.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## subscribe
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
crm.notifications.subscribe(input: SubscribeNotificationsInput): Promise<Device>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`POST /v1/audience/devices` · `devices:write` · **201**
|
|
79
|
+
|
|
80
|
+
### Payload
|
|
81
|
+
|
|
82
|
+
| Field | Type | Required | Notes |
|
|
83
|
+
| --- | --- | --- | --- |
|
|
84
|
+
| `token` | string | yes | The FCM registration token for this instance, from `getToken()`. 20–4096 characters — a sanity check, not a format; Google publishes no shape for these. Trimmed. |
|
|
85
|
+
| `platform` | `'web' \| 'android' \| 'ios'` | defaulted | `'web'` when the SDK can see a DOM. Required otherwise — see below. |
|
|
86
|
+
| `device_label` | string | no | Up to 200 characters; longer is trimmed, not refused. Defaults to something like `Chrome on Windows`, derived from the user agent. Cosmetic, never matched on. |
|
|
87
|
+
| `tpc_id` | string | no | The reseller client (`tpc_…`) this frontend belongs to. Rarely needed. |
|
|
88
|
+
|
|
89
|
+
What goes on the wire, with everything defaulted:
|
|
90
|
+
|
|
91
|
+
```http
|
|
92
|
+
POST /v1/audience/devices
|
|
93
|
+
authorization: Bearer <ecosystem token>
|
|
94
|
+
client-id: tpc_8Kd3mXp7qR2wL9zB
|
|
95
|
+
content-type: application/json
|
|
96
|
+
|
|
97
|
+
{
|
|
98
|
+
"token": "fNx8…:APA91bH…",
|
|
99
|
+
"platform": "web",
|
|
100
|
+
"device_label": "Chrome on Windows"
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const device = await crm.notifications.subscribe({ token });
|
|
106
|
+
// → { id: '…', platform: 'web', label: 'Chrome on Windows', clientId: 'tpc_…', … }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
There is **no user field**, and that is deliberate: the acting user comes from
|
|
110
|
+
the verified token the client was constructed with. A body that could name its
|
|
111
|
+
own user would let any caller point someone else's notifications at their own
|
|
112
|
+
browser.
|
|
113
|
+
|
|
114
|
+
### Call it on every app load, not once
|
|
115
|
+
|
|
116
|
+
It is an idempotent upsert keyed on the token, so re-sending one the platform
|
|
117
|
+
already holds costs a single indexed update. That is the intended usage, because
|
|
118
|
+
two things change underneath you:
|
|
119
|
+
|
|
120
|
+
- FCM rotates registration tokens on its own schedule, and the old one stops
|
|
121
|
+
being delivered to without anyone being told.
|
|
122
|
+
- A device unseen for **60 days** is pruned, and "seen" means a successful send
|
|
123
|
+
or a call to this method. Someone who receives no notifications for two months
|
|
124
|
+
would otherwise quietly fall off the list.
|
|
125
|
+
|
|
126
|
+
Re-subscribing a token already on file against **another** user moves it rather
|
|
127
|
+
than duplicating it. One browser has one person signed in, and the previous
|
|
128
|
+
owner's notifications have to stop reaching it the moment someone else logs in
|
|
129
|
+
there.
|
|
130
|
+
|
|
131
|
+
### `platform` off-browser
|
|
132
|
+
|
|
133
|
+
The SDK defaults it from `document`, not from `navigator` — which is the
|
|
134
|
+
distinction that matters. React Native defines a `navigator` and no `document`,
|
|
135
|
+
so a navigator-based guess would register every phone as a browser, and the send
|
|
136
|
+
path would fill in the `webpush` block instead of `android`/`apns`: a
|
|
137
|
+
notification FCM accepts and the device never shows. Outside a browser the SDK
|
|
138
|
+
asks instead of guessing:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
await crm.notifications.subscribe({ token, platform: 'android' });
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### `tpc_id`
|
|
145
|
+
|
|
146
|
+
Only ever a confirmation. The `clientId` the `CrmClient` already sends is
|
|
147
|
+
verified by the gateway and wins; this field says which reseller frontend is
|
|
148
|
+
registering, which decides whose Firebase project and which deep-link host apply.
|
|
149
|
+
A value that contradicts the verified client is refused with `403` rather than
|
|
150
|
+
ignored — the two disagreeing means either a frontend bug or an attempt to file a
|
|
151
|
+
device under another tenant, and silently preferring one would hide the first and
|
|
152
|
+
partially succeed at the second. The white-label frontend has no client and sends
|
|
153
|
+
nothing.
|
|
154
|
+
|
|
155
|
+
### Errors
|
|
156
|
+
|
|
157
|
+
| Status | Code | Meaning |
|
|
158
|
+
| --- | --- | --- |
|
|
159
|
+
| 400 | `validation_failed` | Token missing or implausible, or `platform` could not be defaulted. Thrown by the SDK before a request is made. |
|
|
160
|
+
| 403 | `forbidden` | `tpc_id` names a different client than the token was issued for. |
|
|
161
|
+
| 503 | `push_not_configured` | The agency has no Firebase notification resource. Nothing the caller can fix; push is simply off for this account. |
|
|
162
|
+
| 503 | `push_unavailable` | The platform could not read the configuration from core. Transient. |
|
|
163
|
+
|
|
164
|
+
Both `503`s want the same handling as success: try again on the next app load,
|
|
165
|
+
which the advice above means you are doing anyway.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## unsubscribe
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
crm.notifications.unsubscribe(token: string): Promise<void>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`POST /v1/audience/devices/unregister` · `devices:write` · **204**
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
await crm.notifications.unsubscribe(token);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Payload
|
|
182
|
+
|
|
183
|
+
| Field | Type | Required | Notes |
|
|
184
|
+
| --- | --- | --- | --- |
|
|
185
|
+
| `token` | string | yes | The same registration token. Trimmed. |
|
|
186
|
+
|
|
187
|
+
`204` with no body **whether or not a row went** — you are asking for a
|
|
188
|
+
guarantee about the future, and a token that is already gone provides it. So this
|
|
189
|
+
is safe to call unconditionally on logout without first checking whether the
|
|
190
|
+
device was ever subscribed.
|
|
191
|
+
|
|
192
|
+
Scoped to the caller's own devices. A token identifies a browser rather than a
|
|
193
|
+
person, so by the time one user logs out the row may already belong to whoever
|
|
194
|
+
logged in after them; an unscoped delete would cut off the person now holding it.
|
|
195
|
+
|
|
196
|
+
A `POST` with the token in the body rather than `DELETE /devices/:token`: a
|
|
197
|
+
registration token is hundreds of characters and may contain `:` and `/`, and
|
|
198
|
+
putting one in a path invites proxy length limits and writes a live send
|
|
199
|
+
credential into every access log along the way.
|
|
200
|
+
|
|
201
|
+
### Also delete it on the device
|
|
202
|
+
|
|
203
|
+
This withdraws the platform's permission to send to that token. If the user is
|
|
204
|
+
turning notifications **off** — rather than logging out — also drop the token in
|
|
205
|
+
the browser, or the next `getToken()` hands back the same one and the
|
|
206
|
+
subscription can come back by accident:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
await crm.notifications.unsubscribe(token);
|
|
210
|
+
await deleteToken(messaging); // firebase/messaging
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## devices
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
crm.notifications.devices(query?: Partial<ListDevicesQuery>): Promise<Paginated<Device>>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`GET /v1/audience/devices` · `devices:read` · **200**
|
|
222
|
+
|
|
223
|
+
### Query
|
|
224
|
+
|
|
225
|
+
| Field | Type | Default | Notes |
|
|
226
|
+
| --- | --- | --- | --- |
|
|
227
|
+
| `page` | number | `1` | 1-based. |
|
|
228
|
+
| `rows` | number | `25` | 1–100. |
|
|
229
|
+
|
|
230
|
+
Sorted by `lastSeenAt` descending. This is what a "notification settings" screen
|
|
231
|
+
lists.
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
const { data } = await crm.notifications.devices();
|
|
235
|
+
data.map((d) => `${d.label || 'This device'} · last seen ${d.lastSeenAt}`);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
No `search`: the endpoint shares the common list query so the server would accept
|
|
239
|
+
one, but the service does not apply it — a device list is one person's handful of
|
|
240
|
+
rows. It is left out of the SDK type rather than accepted and silently dropped.
|
|
241
|
+
|
|
242
|
+
Only ever the acting user's own devices. There is no account-wide view and no
|
|
243
|
+
device id in the path: a device is one person's browser.
|
|
244
|
+
|
|
245
|
+
Worth knowing: a notification is sent to the **20 most recently seen** devices
|
|
246
|
+
of a user. A longer list is itself the answer to "why did my old laptop stop
|
|
247
|
+
buzzing".
|
|
248
|
+
|
|
249
|
+
### Telling whether *this* browser is subscribed
|
|
250
|
+
|
|
251
|
+
You can't, from here — tokens are not returned, by design. Decide it locally
|
|
252
|
+
instead. The browser knows its own `Notification.permission` and its own token,
|
|
253
|
+
and because `subscribe` is an idempotent upsert, the cheap and correct answer to
|
|
254
|
+
"am I still subscribed?" is to subscribe again:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
if (Notification.permission === 'granted') {
|
|
258
|
+
await crm.notifications.subscribe({ token: await getToken(messaging, { vapidKey }) });
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
That also repairs the two cases a stored flag gets wrong: a rotated token, and a
|
|
263
|
+
row pruned for staleness.
|