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.
Files changed (86) hide show
  1. package/README.md +121 -21
  2. package/dist/client.d.ts +38 -0
  3. package/dist/client.js +49 -5
  4. package/dist/client.js.map +1 -1
  5. package/dist/http.d.ts +15 -0
  6. package/dist/http.js +21 -1
  7. package/dist/http.js.map +1 -1
  8. package/dist/index.d.ts +11 -0
  9. package/dist/index.js +24 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/internal/bytes.d.ts +38 -0
  12. package/dist/internal/bytes.js +84 -0
  13. package/dist/internal/bytes.js.map +1 -0
  14. package/dist/internal/device.d.ts +24 -0
  15. package/dist/internal/device.js +91 -0
  16. package/dist/internal/device.js.map +1 -0
  17. package/dist/internal/md5.d.ts +4 -0
  18. package/dist/internal/md5.js +109 -0
  19. package/dist/internal/md5.js.map +1 -0
  20. package/dist/internal/receiver-file.d.ts +106 -0
  21. package/dist/internal/receiver-file.js +128 -0
  22. package/dist/internal/receiver-file.js.map +1 -0
  23. package/dist/realtime/client.d.ts +85 -0
  24. package/dist/realtime/client.js +126 -0
  25. package/dist/realtime/client.js.map +1 -0
  26. package/dist/realtime/events.d.ts +147 -0
  27. package/dist/realtime/events.js +3 -0
  28. package/dist/realtime/events.js.map +1 -0
  29. package/dist/realtime/inbox.d.ts +173 -0
  30. package/dist/realtime/inbox.js +263 -0
  31. package/dist/realtime/inbox.js.map +1 -0
  32. package/dist/realtime/index.d.ts +16 -0
  33. package/dist/realtime/index.js +37 -0
  34. package/dist/realtime/index.js.map +1 -0
  35. package/dist/resources/audience/contacts.d.ts +19 -3
  36. package/dist/resources/audience/contacts.js +19 -3
  37. package/dist/resources/audience/contacts.js.map +1 -1
  38. package/dist/resources/audience/inbox-media.d.ts +92 -0
  39. package/dist/resources/audience/inbox-media.js +127 -0
  40. package/dist/resources/audience/inbox-media.js.map +1 -0
  41. package/dist/resources/audience/notifications.d.ts +171 -0
  42. package/dist/resources/audience/notifications.js +215 -0
  43. package/dist/resources/audience/notifications.js.map +1 -0
  44. package/dist/resources/audience/scheduled-messages.d.ts +234 -0
  45. package/dist/resources/audience/scheduled-messages.js +259 -0
  46. package/dist/resources/audience/scheduled-messages.js.map +1 -0
  47. package/dist/resources/campaign-audience.d.ts +132 -0
  48. package/dist/resources/campaign-audience.js +224 -0
  49. package/dist/resources/campaign-audience.js.map +1 -0
  50. package/dist/resources/campaign-report-exports.d.ts +14 -0
  51. package/dist/resources/campaign-report-exports.js +28 -0
  52. package/dist/resources/campaign-report-exports.js.map +1 -0
  53. package/dist/resources/campaign-reports.d.ts +15 -0
  54. package/dist/resources/campaign-reports.js +25 -0
  55. package/dist/resources/campaign-reports.js.map +1 -0
  56. package/dist/resources/campaigns.d.ts +239 -0
  57. package/dist/resources/campaigns.js +300 -0
  58. package/dist/resources/campaigns.js.map +1 -0
  59. package/dist/types/audience.d.ts +93 -1
  60. package/dist/types/campaign.d.ts +229 -0
  61. package/dist/types/campaign.js +3 -0
  62. package/dist/types/campaign.js.map +1 -0
  63. package/dist/types/contact-filter.d.ts +3 -2
  64. package/dist/types/contact-filter.js +3 -2
  65. package/dist/types/contact-filter.js.map +1 -1
  66. package/dist/types/inbox.d.ts +391 -0
  67. package/dist/types/inbox.js +16 -0
  68. package/dist/types/inbox.js.map +1 -0
  69. package/dist/types/index.d.ts +3 -0
  70. package/dist/types/index.js +5 -0
  71. package/dist/types/index.js.map +1 -1
  72. package/dist/types/scheduled-message.d.ts +221 -0
  73. package/dist/types/scheduled-message.js +16 -0
  74. package/dist/types/scheduled-message.js.map +1 -0
  75. package/docs/audience/contacts.md +34 -4
  76. package/docs/audience/notifications.md +263 -0
  77. package/docs/audience/scheduled-messages.md +557 -0
  78. package/docs/campaign/campaigns.md +489 -0
  79. package/docs/contact-filter.md +2 -1
  80. package/docs/inbox/chats.md +356 -0
  81. package/docs/inbox/connection.md +171 -0
  82. package/docs/inbox/media.md +202 -0
  83. package/docs/inbox/messages.md +338 -0
  84. package/docs/inbox/referrals.md +72 -0
  85. package/docs/inbox/sessions.md +74 -0
  86. package/package.json +28 -2
@@ -0,0 +1,356 @@
1
+ # Inbox — chats
2
+
3
+ A chat *is* a contact. There is no separate conversation record: the inbox state
4
+ lives on the contact document, which is why archiving a chat and editing a
5
+ contact through the REST API touch the same row and why both show up in each
6
+ other's views.
7
+
8
+ `inbox.chats.*` · see [connection.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/inbox/connection.md) first
9
+
10
+ | Action | Event | Scope |
11
+ |---|---|---|
12
+ | `list` | `contact.list` | `contacts:read` |
13
+ | `get` | `contact.fetch` | `contacts:read` |
14
+ | `delete` `transfer` `bulkTransfer` `archive` `unarchive` `block` `markAsRead` `markAsUnread` `pauseAutomation` `resumeAutomation` `close` `done` `pauseTimer` `updateTags` | `chat.action` | `contacts:write` |
15
+ | `update` | `contact.update` | `contacts:write` |
16
+ | `assignBot` | `contact.bot_assign` | `contacts:write` |
17
+
18
+ ## The chat shape
19
+
20
+ ```ts
21
+ interface Chat {
22
+ id: string;
23
+ name: { salutation: string; first_name: string; last_name: string };
24
+ /** One entry per channel the contact is reachable on, keyed by channel kind. */
25
+ profile: Record<string, ContactProfile>;
26
+ chat_metadata: {
27
+ new: boolean; // nobody has answered yet
28
+ archived: boolean;
29
+ unread: number; // account-wide, not per agent
30
+ pause_duration: number; // seconds automation stays paused; default 3600
31
+ marked_as_unread: boolean; // flagged by hand
32
+ closed: boolean;
33
+ marked_as_done: boolean;
34
+ blocked: { incoming: boolean; outgoing: boolean; bot: boolean; api: boolean };
35
+ };
36
+ last_seen: number; // unix seconds of the last inbound message
37
+ bot: JsonObject; // flow state: id, node, attempt, pause, pause_timestamp
38
+ last_interaction_props: JsonObject; // last-message preview
39
+ agent: string | null;
40
+ team: string | null;
41
+ tags: ContactTag[];
42
+ custom_field_hash?: JsonObject; // list rows only
43
+ }
44
+ ```
45
+
46
+ `unread` is one counter on the chat, not one per agent. In a shared inbox, one
47
+ agent opening a thread clears it for everyone.
48
+
49
+ `last_interaction_props` is a denormalised preview of the most recent message, so
50
+ rendering a list of chats needs no read of the message history at all.
51
+
52
+ ## list
53
+
54
+ ```ts
55
+ const { contacts, filter, nextPage } = await inbox.chats.list({
56
+ filter: { assignment: 'me', unread: 'unread', closed: 'open' },
57
+ limit: 20,
58
+ });
59
+ ```
60
+
61
+ `contact.list` · scope `contacts:read`
62
+
63
+ | Field | Type | Required | Default | Notes |
64
+ |---|---|---|---|---|
65
+ | `filter` | `ChatListFilter` | no | `{}` | The view. Every key independent; see below. |
66
+ | `conditions` | `ContactFilterEntry[]` | no | `[]` | Up to 50. The same DSL the contact list and segments use. |
67
+ | `search` | `string` | no | — | Max 120 chars. Matches first name, last name and every channel identity. |
68
+ | `nextPage` | `number` | no | `0` | Cursor: the `last_seen` of the oldest row you hold. |
69
+ | `limit` | `number` | no | `20` | Max 100. |
70
+ | `department` | `string` | no | — | Narrow to one team. |
71
+ | `team_member` | `string` | no | — | Narrow to one member's chats. Intersects whichever view is active. |
72
+
73
+ ### The filter
74
+
75
+ Each key is independent, and omitting one means "don't care". An empty filter is
76
+ every live chat. Trashed chats never appear, with or without a search term.
77
+
78
+ | Key | Values | Matches |
79
+ |---|---|---|
80
+ | `assignment` | `all` | Every chat, assigned to anyone or no one. |
81
+ | | `me` | Chats assigned to the calling member. |
82
+ | | `none` | The unassigned queue. |
83
+ | `unread` | `unread` | Counter above zero **or** flagged unread by hand. |
84
+ | | `replied` | Caught up: counter at zero and no hand flag. |
85
+ | `new` | `new` | Nobody has answered yet. |
86
+ | | `old` | Everything else. |
87
+ | `closed` | `closed` | Closed **or** marked done. |
88
+ | | `open` | Neither. |
89
+ | `archived` | `archived` / `unarchived` | The archive flag. |
90
+ | `automation` | `active` | Automation running. |
91
+ | | `paused` | Automation held. |
92
+ | | `active_no_bot` | Running, with no flow attached. |
93
+ | `block` | `ic` / `og` / `api` / `bot` | That one block flag is set. |
94
+ | | `unblocked` | None of the four set. |
95
+ | `session` | `active` | Seen inside the last 24 hours — the provider reply window. |
96
+ | | `expired` | Outside it, or never seen. |
97
+
98
+ ### Paging
99
+
100
+ Keyset, not offset. Rows are ordered by `last_seen` descending; `nextPage` is the
101
+ `last_seen` of the last row returned, and `null` once the list is exhausted.
102
+
103
+ ```ts
104
+ let cursor: number | undefined;
105
+ const all: Chat[] = [];
106
+
107
+ do {
108
+ const page = await inbox.chats.list({ nextPage: cursor, limit: 50 });
109
+ all.push(...page.contacts);
110
+ cursor = page.nextPage ?? undefined;
111
+ } while (cursor);
112
+ ```
113
+
114
+ This holds a moving list stable in a way offset paging cannot: a chat that gets a
115
+ new message while you are paging jumps to the top, and offsets would then shift
116
+ every row after it.
117
+
118
+ ### The applied filter
119
+
120
+ The reply's `filter` is the filter that was **applied**, which is not always the
121
+ one you sent. A team member without `live_chat.unassigned.ALL` asking for `all`
122
+ is narrowed to `me`:
123
+
124
+ ```ts
125
+ const page = await inbox.chats.list({ filter: { assignment: 'all' } });
126
+ page.filter.assignment; // 'me' for a restricted team member
127
+ ```
128
+
129
+ Render what came back, not what you asked for — asking for `all` and silently
130
+ showing a shorter list looks like missing data.
131
+
132
+ ### conditions
133
+
134
+ The same condition DSL as the contact list and saved segments. `and` binds
135
+ tighter than `or`; the list is cut at every `or`. See
136
+ [contact-filter.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/contact-filter.md).
137
+
138
+ ```ts
139
+ await inbox.chats.list({
140
+ conditions: [
141
+ { field_id: 'city', field_type: 'string', condition: 'equal to', value: 'Pune', operator: null },
142
+ { field_id: 'REFERRAL_ID', field_type: 'string', condition: 'equal to', value: '120210000000', operator: 'and' },
143
+ ],
144
+ });
145
+ ```
146
+
147
+ `REFERRAL_ID` is available here and **not** on the REST contact list: it resolves
148
+ through the click-to-WhatsApp referrals, which only the inbox reads. See
149
+ [referrals.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/inbox/referrals.md).
150
+
151
+ ## get
152
+
153
+ ```ts
154
+ const chat = await inbox.chats.get(chatId);
155
+ ```
156
+
157
+ `contact.fetch` · scope `contacts:read` · `404` if no such chat for this account
158
+
159
+ Also **subscribes to the thread**: messages and state changes for this
160
+ conversation reach the socket from here on without asking again.
161
+
162
+ ## Actions
163
+
164
+ Every action answers the caller and broadcasts the same thing to the account — an
165
+ inbox is shared, so one agent archiving a chat has to make it leave everyone
166
+ else's list too. Both arrive as `chat.action`:
167
+
168
+ ```ts
169
+ interface ChatActionResult {
170
+ action: ChatActionName;
171
+ id: string;
172
+ chat?: Chat | null; // null for delete: the row has left the list
173
+ }
174
+ ```
175
+
176
+ Every action takes the chat id as `chat`. `id` is accepted as an alias on all of
177
+ them, because the original clients use one or the other depending on the action.
178
+
179
+ ### delete
180
+
181
+ ```ts
182
+ await inbox.chats.delete(chatId);
183
+ ```
184
+
185
+ Trashes the contact. The chat leaves every inbox view; the row and its history
186
+ stay. `chat` comes back `null` — there is nothing to draw.
187
+
188
+ ### transfer
189
+
190
+ ```ts
191
+ await inbox.chats.transfer(chatId, memberId);
192
+ ```
193
+
194
+ | Field | Type | Required | Notes |
195
+ |---|---|---|---|
196
+ | `chat` | `string` | yes | |
197
+ | `member` | `string` | yes | Team member to assign to. |
198
+
199
+ Also flags the chat unread for the receiving member, so it surfaces in their
200
+ `unread` view.
201
+
202
+ ### bulkTransfer
203
+
204
+ ```ts
205
+ const { count } = await inbox.chats.bulkTransfer({ source: 'UNASSIGN', target: memberId });
206
+ ```
207
+
208
+ | Field | Type | Required | Default | Notes |
209
+ |---|---|---|---|---|
210
+ | `source` | `string \| 'UNASSIGN'` | yes | — | Member to take chats from, or the unassigned queue. |
211
+ | `target` | `string \| 'UNASSIGN'` | yes | — | Member or team to hand them to, or clear the assignment. |
212
+ | `target_type` | `'member' \| 'department'` | no | `'member'` | What `target` names. |
213
+
214
+ Capped at **500 chats per call**. Each moved chat is broadcast individually under
215
+ `action: 'transfer'`, so clients need no batch shape. `target: 'UNASSIGN'` clears
216
+ the team as well as the agent.
217
+
218
+ ### archive / unarchive
219
+
220
+ ```ts
221
+ await inbox.chats.archive(chatId);
222
+ await inbox.chats.unarchive(chatId);
223
+ ```
224
+
225
+ ### block
226
+
227
+ ```ts
228
+ await inbox.chats.block(chatId, { incoming: true, bot: true });
229
+ ```
230
+
231
+ | Field | Type | Required | Notes |
232
+ |---|---|---|---|
233
+ | `update.incoming` | `boolean` | no | Drop inbound messages. |
234
+ | `update.outgoing` | `boolean` | no | Refuse outbound sends. |
235
+ | `update.bot` | `boolean` | no | Keep automation off this chat. |
236
+ | `update.api` | `boolean` | no | Refuse API sends. |
237
+
238
+ **Only the flags you send change.** Flags left out keep their current value.
239
+
240
+ ### markAsRead / markAsUnread
241
+
242
+ ```ts
243
+ await inbox.chats.markAsRead(chatId);
244
+ await inbox.chats.markAsUnread(chatId);
245
+ ```
246
+
247
+ `markAsRead` clears both the counter and the hand flag. `markAsUnread` sets the
248
+ hand flag only, leaving the counter alone.
249
+
250
+ ### pauseAutomation / resumeAutomation
251
+
252
+ ```ts
253
+ await inbox.chats.pauseAutomation(chatId);
254
+ await inbox.chats.resumeAutomation(chatId);
255
+ ```
256
+
257
+ `pauseAutomation` detaches the flow entirely — id, node, attempt counter and
258
+ waiting state — and holds automation for `chat_metadata.pause_duration` seconds
259
+ (default 3600). The pause is stamped with a token, so re-pausing a chat
260
+ invalidates the earlier resume rather than letting it fire early.
261
+
262
+ Sending a message by hand pauses automation on its own; see
263
+ [messages.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/inbox/messages.md#send).
264
+
265
+ ### pauseTimer
266
+
267
+ ```ts
268
+ await inbox.chats.pauseTimer(chatId, 7200);
269
+ ```
270
+
271
+ | Field | Type | Required | Notes |
272
+ |---|---|---|---|
273
+ | `duration` | `number` | yes | Seconds, `0`–`2592000` (30 days). |
274
+
275
+ Sets a new `pause_duration` and re-stamps the pause from now.
276
+
277
+ ### close / done
278
+
279
+ ```ts
280
+ await inbox.chats.close(chatId);
281
+ await inbox.chats.done(chatId, true);
282
+ ```
283
+
284
+ Both detach the flow. They differ in what else they touch:
285
+
286
+ | | `close` | `done` |
287
+ |---|---|---|
288
+ | `chat_metadata.closed` | `true` | unchanged |
289
+ | `chat_metadata.marked_as_done` | unchanged | `true` |
290
+ | `chat_metadata.new` | unchanged | `false` |
291
+ | `agent` | unchanged | `null` |
292
+
293
+ So `close` ends the conversation but keeps the agent on it; `done` hands it back.
294
+ `done` takes an optional `isEnded` flag, which travels to assignment analytics.
295
+
296
+ The `closed` filter matches either.
297
+
298
+ ### updateTags
299
+
300
+ ```ts
301
+ await inbox.chats.updateTags(chatId, [tagId1, tagId2]);
302
+ ```
303
+
304
+ | Field | Type | Required | Notes |
305
+ |---|---|---|---|
306
+ | `tags` | `string[]` | yes | Up to 100 tag ids. **Replaces** the set outright. |
307
+
308
+ Send the full list, not a delta. Passing `[]` clears every tag.
309
+
310
+ ## update
311
+
312
+ ```ts
313
+ const { chat_metadata } = await inbox.chats.update({ id: chatId, action: 'done' });
314
+ ```
315
+
316
+ `contact.update` · scope `contacts:write`
317
+
318
+ | Field | Type | Required | Notes |
319
+ |---|---|---|---|
320
+ | `id` | `string` | yes | |
321
+ | `action` | `'done' \| 'archive'` | yes | |
322
+
323
+ A narrower action set whose reply carries only the metadata. It does the same
324
+ thing as `chats.done` / `chats.archive` and exists because the original clients
325
+ hold this contract — prefer the action methods in new code.
326
+
327
+ ## assignBot
328
+
329
+ ```ts
330
+ await inbox.chats.assignBot({ chat: chatId, channel: channelId, bot: botId, node: 'welcome' });
331
+ ```
332
+
333
+ `contact.bot_assign` · scope `contacts:write`
334
+
335
+ | Field | Type | Required | Default | Notes |
336
+ |---|---|---|---|---|
337
+ | `chat` | `string` | yes | — | |
338
+ | `channel` | `string` | yes | — | Channel the flow runs on. |
339
+ | `bot` | `string` | yes | — | Flow id. |
340
+ | `node` | `string` | no | `''` | Where in the flow to start. |
341
+ | `action` | `'assign' \| 'trigger'` | no | `'assign'` | |
342
+
343
+ `assign` writes the flow pointer directly and resumes automation, which arrives
344
+ as a `chat.action` with `action: 'resume_automation'`.
345
+
346
+ `trigger` publishes instead and answers immediately: where in a flow a chat
347
+ belongs is the bot server's decision, so the chat's new state arrives as a push
348
+ once it has decided.
349
+
350
+ ## Service-side notes
351
+
352
+ Not SDK behaviour, but worth knowing:
353
+
354
+ - **Deleting a contact tag does not pull it off the chats carrying it.** A tag id
355
+ can outlive its tag; filter defensively.
356
+ - **`unread` is account-wide.** There is no per-agent read state.
@@ -0,0 +1,171 @@
1
+ # Inbox — connecting
2
+
3
+ The live-chat inbox is a Socket.IO connection, not REST. It has its own origin,
4
+ separate from the REST gateway: the gateway proxies HTTP only and forwards no
5
+ WebSocket upgrade, so a handshake routed through it never completes.
6
+
7
+ ```bash
8
+ npm install ab-ecosystem-cpass socket.io-client
9
+ ```
10
+
11
+ `socket.io-client` is an optional peer dependency. The REST client never resolves
12
+ it — only the `/realtime` subpath does.
13
+
14
+ ```ts
15
+ import { Inbox } from 'ab-ecosystem-cpass/realtime';
16
+
17
+ const inbox = new Inbox({
18
+ url: 'https://inbox.example.com',
19
+ clientId: process.env.CRM_CLIENT_ID!,
20
+ token: () => session.accessToken, // called on every connect
21
+ });
22
+
23
+ await inbox.ready();
24
+ ```
25
+
26
+ ## Client options
27
+
28
+ | Option | Type | Default | Notes |
29
+ |---|---|---|---|
30
+ | `url` | `string` | — | Required. The inbox socket's origin. |
31
+ | `clientId` | `string` | — | Required. Your ecosystem client id, as on every REST call. |
32
+ | `token` | `string \| () => string \| Promise<string>` | — | Required. Bearer token. |
33
+ | `path` | `string` | `/socket.io` | Only if the deployment moved it. |
34
+ | `timeout` | `number` | `20000` | How long a request waits for its acknowledgement, in ms. |
35
+ | `autoConnect` | `boolean` | `true` | Connect on construction. |
36
+ | `socketOptions` | `object` | — | Merged into the socket.io options last. |
37
+
38
+ **Pass `token` as a function.** It is resolved on every connect, so a reconnect
39
+ after an expiry picks up a fresh token. A string is captured once, and a socket
40
+ that drops after the token expires will retry forever with the dead one.
41
+
42
+ ## How credentials travel
43
+
44
+ In `auth`, not in a header:
45
+
46
+ ```ts
47
+ { token: '<bearer token>', client_id: '<client id>' }
48
+ ```
49
+
50
+ A browser cannot set headers on a WebSocket upgrade at all. A header-based
51
+ handshake only works while the connection is still on HTTP long-polling, and
52
+ breaks the moment a client sets `transports: ['websocket']`. The server still
53
+ accepts `authorization` and `client-id` headers for existing clients.
54
+
55
+ The handshake checks two things: the token verifies against the ecosystem JWKS
56
+ for the `client_id` sent, and the verified claims name that same client. There is
57
+ **no origin allowlist** — the credential is the boundary, not the page it came
58
+ from, which is also why a server-to-server client with no `Origin` at all
59
+ connects fine. A failure arrives as `connect_error` with the reason:
60
+
61
+ ```ts
62
+ inbox.raw.on('connect_error', (err) => console.error(err.message));
63
+ ```
64
+
65
+ ## Requests and pushes
66
+
67
+ Two kinds of traffic, and the same event name can be both.
68
+
69
+ **Requests** are promises. Every emit carries an acknowledgement callback, so two
70
+ concurrent reads of different threads cannot be confused for one another:
71
+
72
+ ```ts
73
+ const { contacts, nextPage } = await inbox.chats.list({ filter: { assignment: 'me' } });
74
+ ```
75
+
76
+ **Pushes** are things that happened elsewhere — another agent archiving a chat, a
77
+ contact replying:
78
+
79
+ ```ts
80
+ const off = inbox.on('chat.action', ({ action, id, chat }) => applyToList(action, id, chat));
81
+ off(); // unsubscribe
82
+ ```
83
+
84
+ An archive you performed and an archive another agent performed arrive under the
85
+ same name with the same shape, so one handler per kind of change is enough.
86
+
87
+ | Push | Payload | When |
88
+ |---|---|---|
89
+ | `message.received` | `Message` | A message arrived, or one you sent now exists as a stored row. |
90
+ | `message.status` | `MessageStatusUpdate` | A delivery state moved: sent, delivered, read, failed. |
91
+ | `chat.action` | `ChatActionResult` | A chat changed: unread, assignment, automation, tags. |
92
+ | `contact.received` | stored document | A contact wrote in for the first time, so a chat row appeared. |
93
+ | `message.delete` | `MessageDeleteResult` | A message was removed from an open thread. |
94
+ | `message.retry_download` | `Message` | Media was re-fetched and its link changed. |
95
+ | `contact.done` | `ChatActionResult` | A chat was marked done. |
96
+ | `message.send` | stored document | Legacy. The same row as `message.received`, unmapped. |
97
+ | `error` | `InboxError` | An event was refused. Carries the failing event name. |
98
+
99
+ `message.send` as a *push* exists only because the inbox this replaced used one
100
+ name for both its own reply and the row that followed. Prefer
101
+ `message.received`: it is mapped (`id`, ISO `createdAt`), where `message.send`
102
+ carries `_id` and `user` as the database holds them.
103
+
104
+ Each push is delivered **once** per socket even when you are in two rooms it
105
+ targets — your account's room and the thread you have open.
106
+
107
+ ## Subscriptions
108
+
109
+ A socket starts subscribed to the account, so every account-wide change reaches
110
+ it. Thread-level pushes need the thread: calling `chats.get(id)` or
111
+ `messages.list(id)` subscribes to that conversation, and typing notices and new
112
+ messages for it arrive from then on without asking again.
113
+
114
+ ## Errors
115
+
116
+ A refused request rejects with `CrmApiError`, carrying the same `status` and
117
+ `code` the REST API would have returned — one error path covers both transports.
118
+
119
+ ```ts
120
+ import { CrmApiError } from 'ab-ecosystem-cpass';
121
+
122
+ try {
123
+ await inbox.chats.transfer(chatId, memberId);
124
+ } catch (err) {
125
+ if (err instanceof CrmApiError && err.status === 403) {
126
+ // not allowed to reassign this chat
127
+ }
128
+ }
129
+ ```
130
+
131
+ | `status` | `code` | Means |
132
+ |---|---|---|
133
+ | 401 | `unauthorized` | The handshake's token did not verify. |
134
+ | 403 | `forbidden` | The action is beyond this member's permissions. |
135
+ | 404 | `not_found` | No such chat or message **for this account**. |
136
+ | 422 | `validation_failed` | Payload rejected. `details` names the fields. |
137
+ | 500 | `internal_error` | Server fault. Nothing further is disclosed. |
138
+ | 504 | `timeout` | No acknowledgement inside `timeout` ms. |
139
+
140
+ An unknown event name is ignored rather than refused, so a client emitting its
141
+ own lifecycle events gets no spurious errors.
142
+
143
+ ## Scoping
144
+
145
+ Every read and write is scoped to the account the token identifies, the same way
146
+ the REST contact endpoints are. Quoting another account's chat or message id
147
+ produces `404`, not that account's data.
148
+
149
+ ## Permissions
150
+
151
+ Visibility inside an account is gated on inbox permissions, not API scopes. An
152
+ owner or a manager is never gated; a team member needs the permission named.
153
+
154
+ | Permission | Grants |
155
+ |---|---|
156
+ | `live_chat.unassigned.ALL` | See every chat in the account, assigned or not. |
157
+ | `live_chat.unassigned.NEW` | See chats nobody has answered yet. |
158
+ | `live_chat.send.AS_OTHER` | Attribute an outgoing message to another member. |
159
+
160
+ A team member without these is narrowed to their own chats rather than refused —
161
+ and the reply says so. See [chats.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/inbox/chats.md#the-applied-filter).
162
+
163
+ ## Modules
164
+
165
+ | Page | Actions |
166
+ |---|---|
167
+ | [chats.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/inbox/chats.md) | `list` `get` `delete` `transfer` `bulkTransfer` `archive` `unarchive` `block` `markAsRead` `markAsUnread` `pauseAutomation` `resumeAutomation` `close` `done` `pauseTimer` `updateTags` `update` `assignBot` |
168
+ | [messages.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/inbox/messages.md) | `list` `get` `send` `sendMedia` `delete` `retryDownload` |
169
+ | [media.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/inbox/media.md) | `sendMedia` · `crm.inboxMedia.upload` `createUpload` |
170
+ | [referrals.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/inbox/referrals.md) | `referrals` |
171
+ | [sessions.md](https://github.com/udghatan-dev/crm-sdk/blob/main/packages/sdk-js/docs/inbox/sessions.md) | `sessions` |