ab-ecosystem-cpass 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +127 -25
  2. package/dist/client.d.ts +56 -1
  3. package/dist/client.js +76 -30
  4. package/dist/client.js.map +1 -1
  5. package/dist/http.d.ts +8 -0
  6. package/dist/http.js +10 -3
  7. package/dist/http.js.map +1 -1
  8. package/dist/index.d.ts +16 -1
  9. package/dist/index.js +35 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/resources/audience/contact-notes.d.ts +51 -0
  12. package/dist/resources/audience/contact-notes.js +70 -0
  13. package/dist/resources/audience/contact-notes.js.map +1 -0
  14. package/dist/resources/audience/contact-tags.d.ts +43 -0
  15. package/dist/resources/audience/contact-tags.js +60 -0
  16. package/dist/resources/audience/contact-tags.js.map +1 -0
  17. package/dist/resources/audience/contacts.d.ts +76 -0
  18. package/dist/resources/audience/contacts.js +103 -0
  19. package/dist/resources/audience/contacts.js.map +1 -0
  20. package/dist/resources/audience/crm-triggers.d.ts +67 -0
  21. package/dist/resources/audience/crm-triggers.js +86 -0
  22. package/dist/resources/audience/crm-triggers.js.map +1 -0
  23. package/dist/resources/audience/custom-fields.d.ts +54 -0
  24. package/dist/resources/audience/custom-fields.js +71 -0
  25. package/dist/resources/audience/custom-fields.js.map +1 -0
  26. package/dist/resources/audience/export-history.d.ts +52 -0
  27. package/dist/resources/audience/export-history.js +67 -0
  28. package/dist/resources/audience/export-history.js.map +1 -0
  29. package/dist/resources/audience/import-history.d.ts +51 -0
  30. package/dist/resources/audience/import-history.js +66 -0
  31. package/dist/resources/audience/import-history.js.map +1 -0
  32. package/dist/resources/audience/segments.d.ts +49 -0
  33. package/dist/resources/audience/segments.js +66 -0
  34. package/dist/resources/audience/segments.js.map +1 -0
  35. package/dist/resources/audience/webhooks.d.ts +68 -0
  36. package/dist/resources/audience/webhooks.js +83 -0
  37. package/dist/resources/audience/webhooks.js.map +1 -0
  38. package/dist/resources/channels/activity.d.ts +25 -0
  39. package/dist/resources/channels/activity.js +34 -0
  40. package/dist/resources/channels/activity.js.map +1 -0
  41. package/dist/resources/channels/billing.d.ts +46 -0
  42. package/dist/resources/channels/billing.js +59 -0
  43. package/dist/resources/channels/billing.js.map +1 -0
  44. package/dist/resources/channels/channels.d.ts +115 -0
  45. package/dist/resources/channels/channels.js +129 -0
  46. package/dist/resources/channels/channels.js.map +1 -0
  47. package/dist/resources/channels/templates.d.ts +90 -0
  48. package/dist/resources/channels/templates.js +109 -0
  49. package/dist/resources/channels/templates.js.map +1 -0
  50. package/dist/resources/channels/wallet.d.ts +25 -0
  51. package/dist/resources/channels/wallet.js +34 -0
  52. package/dist/resources/channels/wallet.js.map +1 -0
  53. package/dist/resources/channels/webhooks.d.ts +60 -0
  54. package/dist/resources/channels/webhooks.js +79 -0
  55. package/dist/resources/channels/webhooks.js.map +1 -0
  56. package/dist/resources/paths.d.ts +12 -0
  57. package/dist/resources/paths.js +17 -0
  58. package/dist/resources/paths.js.map +1 -0
  59. package/dist/types/audience.d.ts +512 -0
  60. package/dist/types/audience.js +3 -0
  61. package/dist/types/audience.js.map +1 -0
  62. package/dist/types/channels.d.ts +383 -0
  63. package/dist/types/channels.js +22 -0
  64. package/dist/types/channels.js.map +1 -0
  65. package/dist/types/common.d.ts +55 -0
  66. package/dist/types/common.js +8 -0
  67. package/dist/types/common.js.map +1 -0
  68. package/dist/types/contact-filter.d.ts +55 -0
  69. package/dist/types/contact-filter.js +11 -0
  70. package/dist/types/contact-filter.js.map +1 -0
  71. package/dist/types/index.d.ts +4 -0
  72. package/dist/types/index.js +21 -0
  73. package/dist/types/index.js.map +1 -0
  74. package/docs/audience/contact-notes.md +129 -0
  75. package/docs/audience/contact-tags.md +109 -0
  76. package/docs/audience/contacts.md +227 -0
  77. package/docs/audience/crm-triggers.md +240 -0
  78. package/docs/audience/custom-fields.md +174 -0
  79. package/docs/audience/exports.md +163 -0
  80. package/docs/audience/imports.md +161 -0
  81. package/docs/audience/segments.md +120 -0
  82. package/docs/audience/webhooks.md +167 -0
  83. package/docs/channels/activity.md +68 -0
  84. package/docs/channels/billing.md +112 -0
  85. package/docs/channels/channel-props.md +290 -0
  86. package/docs/channels/channels.md +199 -0
  87. package/docs/channels/templates.md +224 -0
  88. package/docs/channels/wallet.md +67 -0
  89. package/docs/channels/webhooks.md +152 -0
  90. package/docs/contact-filter.md +157 -0
  91. package/package.json +7 -3
  92. package/dist/resources/channels.d.ts +0 -10
  93. package/dist/resources/channels.js +0 -19
  94. package/dist/resources/channels.js.map +0 -1
  95. package/dist/types.d.ts +0 -70
  96. package/dist/types.js +0 -7
  97. package/dist/types.js.map +0 -1
@@ -0,0 +1,129 @@
1
+ # Contact notes
2
+
3
+ `crm.contactNotes` — `/v1/audience/contacts/:contactId/notes`
4
+
5
+ Internal notes on a contact: free text, attachments, and team members mentioned
6
+ in them. Notes are never shown to the contact.
7
+
8
+ > **Not mounted yet.** These routes are commented out in the audience service's
9
+ > `routes.ts`, so they answer `404` until that line is restored. The contract,
10
+ > the service and this client are complete; only the mount is missing.
11
+
12
+ Every method takes the contact id first — notes are addressed under their
13
+ contact, and a note id belonging to another contact is a `404` rather than a
14
+ match.
15
+
16
+ | Action | Route | Scope |
17
+ | --- | --- | --- |
18
+ | [`list`](#list) | `GET /v1/audience/contacts/:contactId/notes` | `contacts:read` |
19
+ | [`get`](#get) | `GET /v1/audience/contacts/:contactId/notes/:noteId` | `contacts:read` |
20
+ | [`create`](#create) | `POST /v1/audience/contacts/:contactId/notes` | `contacts:write` |
21
+ | [`update`](#update) | `PATCH /v1/audience/contacts/:contactId/notes/:noteId` | `contacts:write` |
22
+ | [`delete`](#delete) | `DELETE /v1/audience/contacts/:contactId/notes/:noteId` | `contacts:write` |
23
+
24
+ ## The note object
25
+
26
+ ```ts
27
+ interface ContactNote {
28
+ id: string;
29
+ contact: string; // the contact it hangs off
30
+ note: string;
31
+ attachments: ContactNoteAttachment[];
32
+ mentions: string[]; // team member ids
33
+ createdAt: string;
34
+ updatedAt: string;
35
+ }
36
+
37
+ interface ContactNoteAttachment {
38
+ url: string; // required
39
+ type?: string;
40
+ size?: string;
41
+ mime?: string;
42
+ file_name?: string;
43
+ }
44
+ ```
45
+
46
+ ---
47
+
48
+ ## list
49
+
50
+ ```ts
51
+ crm.contactNotes.list(contactId: string, query?: Partial<ListContactNotesQuery>): Promise<Page<ContactNote>>
52
+ ```
53
+
54
+ `GET /v1/audience/contacts/:contactId/notes` · `contacts:read` · **200**
55
+
56
+ **Cursor paginated**, not offset — notes are appended constantly, and an offset
57
+ would shift under a caller mid-walk.
58
+
59
+ | Field | Type | Default | Notes |
60
+ | --- | --- | --- | --- |
61
+ | `limit` | number | `50` | 1–100. |
62
+ | `cursor` | string | — | From the previous response's `nextCursor`. |
63
+
64
+ ```ts
65
+ let cursor: string | null = null;
66
+ do {
67
+ const page = await crm.contactNotes.list(contactId, { limit: 25, cursor: cursor ?? undefined });
68
+ for (const note of page.data) render(note);
69
+ cursor = page.nextCursor;
70
+ } while (cursor);
71
+ ```
72
+
73
+ ---
74
+
75
+ ## get
76
+
77
+ ```ts
78
+ crm.contactNotes.get(contactId: string, noteId: string): Promise<ContactNote>
79
+ ```
80
+
81
+ `GET /v1/audience/contacts/:contactId/notes/:noteId` · `contacts:read` · **200**
82
+
83
+ ---
84
+
85
+ ## create
86
+
87
+ ```ts
88
+ crm.contactNotes.create(contactId: string, input: CreateContactNoteInput): Promise<ContactNote>
89
+ ```
90
+
91
+ `POST /v1/audience/contacts/:contactId/notes` · `contacts:write` · **201**
92
+
93
+ | Field | Type | Required | Default | Notes |
94
+ | --- | --- | --- | --- | --- |
95
+ | `note` | string | yes | — | 1–5000 chars. |
96
+ | `attachments` | `ContactNoteAttachment[]` | no | `[]` | `url` is the only required key on each. |
97
+ | `mentions` | ObjectId[] | no | `[]` | Team member ids. |
98
+
99
+ ```ts
100
+ await crm.contactNotes.create(contactId, {
101
+ note: 'Called back — wants a demo on Friday.',
102
+ mentions: [teamMemberId],
103
+ });
104
+ ```
105
+
106
+ ---
107
+
108
+ ## update
109
+
110
+ ```ts
111
+ crm.contactNotes.update(contactId: string, noteId: string, input: UpdateContactNoteInput): Promise<ContactNote>
112
+ ```
113
+
114
+ `PATCH /v1/audience/contacts/:contactId/notes/:noteId` · `contacts:write` · **200**
115
+
116
+ Same fields as `create`, all optional. `attachments` and `mentions` **replace**
117
+ their stored value rather than appending to it.
118
+
119
+ ---
120
+
121
+ ## delete
122
+
123
+ ```ts
124
+ crm.contactNotes.delete(contactId: string, noteId: string): Promise<void>
125
+ ```
126
+
127
+ `DELETE /v1/audience/contacts/:contactId/notes/:noteId` · `contacts:write` · **204**, no body.
128
+
129
+ Destroying a contact (the second `crm.contacts.delete`) removes its notes too.
@@ -0,0 +1,109 @@
1
+ # Contact tags
2
+
3
+ `crm.contactTags` — `/v1/audience/contact-tags`
4
+
5
+ The labels a contact can carry. A tag is its own resource: `crm.contacts` takes
6
+ tag **ids** on a write, and hands back the full tag documents resolved into
7
+ `contact.tags` on a read.
8
+
9
+ | Action | Route | Scope |
10
+ | --- | --- | --- |
11
+ | [`list`](#list) | `GET /v1/audience/contact-tags` | `contact_tags:read` |
12
+ | [`get`](#get) | `GET /v1/audience/contact-tags/:id` | `contact_tags:read` |
13
+ | [`create`](#create) | `POST /v1/audience/contact-tags` | `contact_tags:write` |
14
+ | [`update`](#update) | `PATCH /v1/audience/contact-tags/:id` | `contact_tags:write` |
15
+ | [`delete`](#delete) | `DELETE /v1/audience/contact-tags/:id` | `contact_tags:write` |
16
+
17
+ ## The tag object
18
+
19
+ ```ts
20
+ interface ContactTag {
21
+ id: string;
22
+ name: string;
23
+ icon: string; // icon name or emoji; '' when unset
24
+ color: string; // free-form; '' when unset
25
+ }
26
+ ```
27
+
28
+ ---
29
+
30
+ ## list
31
+
32
+ ```ts
33
+ crm.contactTags.list(query?: Partial<ListContactTagsQuery>): Promise<Paginated<ContactTag>>
34
+ ```
35
+
36
+ `GET /v1/audience/contact-tags` · `contact_tags:read` · **200**
37
+
38
+ ### Query
39
+
40
+ | Field | Type | Default | Notes |
41
+ | --- | --- | --- | --- |
42
+ | `page` | number | `1` | 1-based. |
43
+ | `rows` | number | `25` | 1–100. |
44
+ | `search` | string | — | 1–120 chars. Matches the tag **name**. |
45
+
46
+ ```ts
47
+ const { data, pagination } = await crm.contactTags.list({ search: 'vip' });
48
+ ```
49
+
50
+ ---
51
+
52
+ ## get
53
+
54
+ ```ts
55
+ crm.contactTags.get(id: string): Promise<ContactTag>
56
+ ```
57
+
58
+ `GET /v1/audience/contact-tags/:id` · `contact_tags:read` · **200**
59
+
60
+ `404 not_found` if the tag is not the caller's.
61
+
62
+ ---
63
+
64
+ ## create
65
+
66
+ ```ts
67
+ crm.contactTags.create(input: CreateContactTagInput): Promise<ContactTag>
68
+ ```
69
+
70
+ `POST /v1/audience/contact-tags` · `contact_tags:write` · **201**
71
+
72
+ ### Body
73
+
74
+ | Field | Type | Required | Default | Notes |
75
+ | --- | --- | --- | --- | --- |
76
+ | `name` | string | yes | — | Trimmed, 1–120 chars. |
77
+ | `icon` | string | no | `''` | Up to 120 chars. |
78
+ | `color` | string | no | `''` | Up to 32 chars. Free-form, so the palette can evolve; only length is checked. |
79
+
80
+ ```ts
81
+ const tag = await crm.contactTags.create({ name: 'VIP', icon: 'star', color: '#f59e0b' });
82
+ ```
83
+
84
+ ---
85
+
86
+ ## update
87
+
88
+ ```ts
89
+ crm.contactTags.update(id: string, input: UpdateContactTagInput): Promise<ContactTag>
90
+ ```
91
+
92
+ `PATCH /v1/audience/contact-tags/:id` · `contact_tags:write` · **200**
93
+
94
+ Same fields as `create`, all optional. Renaming a tag changes it everywhere it
95
+ is shown — contacts hold the id, not a copy of the name.
96
+
97
+ ---
98
+
99
+ ## delete
100
+
101
+ ```ts
102
+ crm.contactTags.delete(id: string): Promise<void>
103
+ ```
104
+
105
+ `DELETE /v1/audience/contact-tags/:id` · `contact_tags:write` · **204**, no body.
106
+
107
+ > **Known gap.** Deleting a tag does not currently remove its id from the
108
+ > contacts that carry it, so those contacts keep a reference that resolves to
109
+ > nothing. Clear the tag off its contacts first if that matters to you.
@@ -0,0 +1,227 @@
1
+ # Contacts
2
+
3
+ `crm.contacts` — `/v1/audience/contacts`
4
+
5
+ The people an account talks to, and the resource everything else in the audience
6
+ module hangs off. A contact carries a name, a set of channel identities, the
7
+ values of every custom field, its tags, and the state of its conversation.
8
+
9
+ **Scoping** — contacts belong to an account, and a request carries two
10
+ identities: the caller and the account they act for. Reads match either; writes
11
+ land on the caller's own.
12
+
13
+ | Action | Route | Scope |
14
+ | --- | --- | --- |
15
+ | [`list`](#list) | `GET /v1/audience/contacts` | `contacts:read` |
16
+ | [`get`](#get) | `GET /v1/audience/contacts/:id` | `contacts:read` |
17
+ | [`create`](#create) | `POST /v1/audience/contacts` | `contacts:write` |
18
+ | [`update`](#update) | `PATCH /v1/audience/contacts/:id` | `contacts:write` |
19
+ | [`delete`](#delete) | `DELETE /v1/audience/contacts/:id` | `contacts:write` |
20
+ | [`iterate`](#iterate) | — (walks `list`) | `contacts:read` |
21
+
22
+ ## The contact object
23
+
24
+ ```ts
25
+ interface Contact {
26
+ id: string;
27
+ name: { salutation: string; first_name: string; last_name: string };
28
+ profile: Record<string, ContactProfile>; // channel identities, keyed by channel
29
+ custom_field_hash: Record<string, unknown>; // values keyed by custom field slug
30
+ chat_metadata: ContactChatMetadata;
31
+ order?: string;
32
+ last_seen: number; // unix seconds
33
+ last_icm_timestamp: number; // unix seconds, last inbound message
34
+ last_interaction_props: Record<string, unknown>;
35
+ agent: string | null;
36
+ team: string | null;
37
+ lead_source: { type?: string; module?: string; id: string | null };
38
+ status: 'live' | 'trashed';
39
+ privacy_status: 'open' | 'locked';
40
+ tags: ContactTag[]; // resolved documents, not ids
41
+ createdAt: string;
42
+ updatedAt: string;
43
+ }
44
+
45
+ interface ContactProfile {
46
+ id: string;
47
+ channel: 'whatsapp' | 'facebook' | 'instagram' | 'telegram' | 'email' | 'rcs' | 'webchat';
48
+ identifier: string; // number, handle or address
49
+ name: string;
50
+ profile_pic: string;
51
+ extra: Record<string, unknown>;
52
+ }
53
+
54
+ interface ContactChatMetadata {
55
+ new: boolean;
56
+ archived: boolean;
57
+ unread: number;
58
+ pause_duration: number; // seconds to pause automation for
59
+ marked_as_unread: boolean;
60
+ closed: boolean;
61
+ marked_as_done: boolean;
62
+ blocked: { incoming: boolean; outgoing: boolean; bot: boolean; api: boolean };
63
+ }
64
+ ```
65
+
66
+ `profile`, `last_seen`, `last_icm_timestamp` and `last_interaction_props` are
67
+ written by the platform as conversations happen. They are not settable.
68
+
69
+ ---
70
+
71
+ ## list
72
+
73
+ ```ts
74
+ crm.contacts.list(query?: Partial<ListContactsQuery>): Promise<Paginated<Contact>>
75
+ ```
76
+
77
+ `GET /v1/audience/contacts` · `contacts:read` · **200**
78
+
79
+ ### Query
80
+
81
+ | Field | Type | Default | Notes |
82
+ | --- | --- | --- | --- |
83
+ | `page` | number | `1` | 1-based. |
84
+ | `rows` | number | `25` | 1–100. |
85
+ | `search` | string | — | 1–120 chars. Matches the first and last name, plus the name and identifier of every channel identity. |
86
+ | `tag` | ObjectId | — | Contacts carrying this tag. |
87
+ | `agent` | ObjectId | — | Contacts assigned to this agent. |
88
+ | `status` | `'live' \| 'trashed'` | `'live'` | The trash needs an explicit `'trashed'`. |
89
+ | `segment` | string | — | A segment's id **or** its name. A reference matching neither is a `404` — listing everyone because the segment went missing would be the wrong answer. |
90
+ | `filter` | `ContactFilter` | — | Ad-hoc conditions. Serialised to JSON by the SDK. See [contact filter](../contact-filter.md). |
91
+
92
+ `segment` and `filter` can be sent together: the ad-hoc conditions then **narrow**
93
+ the saved ones rather than replacing them. `tag`, `agent` and `status` narrow
94
+ whatever those produce.
95
+
96
+ ### Example
97
+
98
+ ```ts
99
+ const page = await crm.contacts.list({
100
+ search: 'acme',
101
+ status: 'live',
102
+ segment: 'High budget',
103
+ filter: {
104
+ conditions: [
105
+ { operator: null, field_id: 'city', field_type: 'text', condition: 'equal to', value: 'Pune' },
106
+ ],
107
+ },
108
+ rows: 50,
109
+ });
110
+
111
+ console.log(page.pagination); // { page: 1, rows: 50, total: 128, pages: 3 }
112
+ ```
113
+
114
+ ---
115
+
116
+ ## get
117
+
118
+ ```ts
119
+ crm.contacts.get(id: string): Promise<Contact>
120
+ ```
121
+
122
+ `GET /v1/audience/contacts/:id` · `contacts:read` · **200**
123
+
124
+ One contact with its tags resolved. `404 not_found` if no contact with that id
125
+ belongs to the caller — including a contact in the trash of another account.
126
+
127
+ ---
128
+
129
+ ## create
130
+
131
+ ```ts
132
+ crm.contacts.create(input?: CreateContactInput): Promise<Contact>
133
+ ```
134
+
135
+ `POST /v1/audience/contacts` · `contacts:write` · **201**
136
+
137
+ ### Body
138
+
139
+ Every field is optional; `crm.contacts.create()` with no argument creates a
140
+ blank contact.
141
+
142
+ | Field | Type | Default | Notes |
143
+ | --- | --- | --- | --- |
144
+ | `name` | `Partial<ContactName>` | — | `salutation` ≤ 20 chars, `first_name` and `last_name` ≤ 120. |
145
+ | `order` | string | — | Free-form external reference. |
146
+ | `tags` | ObjectId[] | `[]` | Contact tag ids. |
147
+ | `agent` | ObjectId \| null | — | Assigned agent. |
148
+ | `team` | ObjectId \| null | — | Assigned team. |
149
+ | `lead_source` | `{ type?, module?, id? }` | — | Where the contact came from. |
150
+ | `status` | `'live' \| 'trashed'` | `'live'` | |
151
+ | `privacy_status` | `'open' \| 'locked'` | `'open'` | |
152
+ | `custom_field_hash` | object | — | Values keyed by custom field **slug**. |
153
+ | `chat_metadata` | partial | — | Including `blocked`, itself partial. |
154
+
155
+ Channel identities cannot be set here; they are written when a conversation
156
+ starts on a channel.
157
+
158
+ ```ts
159
+ const contact = await crm.contacts.create({
160
+ name: { first_name: 'Ada', last_name: 'Lovelace' },
161
+ tags: [vipTagId],
162
+ custom_field_hash: { city: 'Pune', lead_score: 80 },
163
+ });
164
+ ```
165
+
166
+ ---
167
+
168
+ ## update
169
+
170
+ ```ts
171
+ crm.contacts.update(id: string, input: UpdateContactInput): Promise<Contact>
172
+ ```
173
+
174
+ `PATCH /v1/audience/contacts/:id` · `contacts:write` · **200**
175
+
176
+ Takes the same body as `create`, all optional. Anything left out is untouched.
177
+
178
+ `tags` and `custom_field_hash` **replace** their stored value rather than merging
179
+ into it — read the contact first if you mean to add one tag to the existing set.
180
+
181
+ ```ts
182
+ await crm.contacts.update(id, { agent: agentId, chat_metadata: { closed: true } });
183
+ ```
184
+
185
+ ---
186
+
187
+ ## delete
188
+
189
+ ```ts
190
+ crm.contacts.delete(id: string): Promise<ContactDeleteResult>
191
+ ```
192
+
193
+ `DELETE /v1/audience/contacts/:id` · `contacts:write` · **200**
194
+
195
+ Two-stage, and **200 rather than 204 both times**: the body is what says which
196
+ stage ran.
197
+
198
+ | Call | Contact was | Result |
199
+ | --- | --- | --- |
200
+ | first | `live` | `{ id, status: 'trashed' }` — recoverable with `update(id, { status: 'live' })` |
201
+ | second | `trashed` | `{ id, status: 'deleted' }` — the contact, its notes and its channel identities are gone |
202
+
203
+ ```ts
204
+ const first = await crm.contacts.delete(id); // { status: 'trashed' }
205
+ const second = await crm.contacts.delete(id); // { status: 'deleted' }
206
+ ```
207
+
208
+ ---
209
+
210
+ ## iterate
211
+
212
+ ```ts
213
+ crm.contacts.iterate(query?: Partial<ListContactsQuery>): AsyncGenerator<Contact>
214
+ ```
215
+
216
+ Walks every page of `list`, yielding one contact at a time. Takes the same query;
217
+ `page` is managed for you and is ignored if passed.
218
+
219
+ ```ts
220
+ for await (const contact of crm.contacts.iterate({ tag: vipTagId })) {
221
+ await sync(contact);
222
+ }
223
+ ```
224
+
225
+ The walk reads live data. A contact created while you are part-way through may
226
+ be missed or seen twice — take a `filter` on `SYSTEM_CREATED_ON` if you need a
227
+ stable set.
@@ -0,0 +1,240 @@
1
+ # CRM triggers
2
+
3
+ `crm.crmTriggers` — `/v1/audience/crm-triggers`
4
+
5
+ Fire a webhook when a contact changes.
6
+
7
+ A trigger asks three questions, and all three have to pass before its action
8
+ runs:
9
+
10
+ 1. **`trigger`** — which contact event happened.
11
+ 2. **`when`** — did the right *fields* move?
12
+ 3. **`filters`** — does the contact itself match these conditions?
13
+
14
+ `filters` speak the same [contact filter](../contact-filter.md) DSL segments do,
15
+ flat rather than wrapped. They are checked by running the contact list query
16
+ pinned to that one contact, so a trigger's conditions mean exactly what they
17
+ mean on the list.
18
+
19
+ | Action | Route | Scope |
20
+ | --- | --- | --- |
21
+ | [`list`](#list) | `GET /v1/audience/crm-triggers` | `crm_triggers:read` |
22
+ | [`get`](#get) | `GET /v1/audience/crm-triggers/:id` | `crm_triggers:read` |
23
+ | [`create`](#create) | `POST /v1/audience/crm-triggers` | `crm_triggers:write` |
24
+ | [`update`](#update) | `PATCH /v1/audience/crm-triggers/:id` | `crm_triggers:write` |
25
+ | [`delete`](#delete) | `DELETE /v1/audience/crm-triggers/:id` | `crm_triggers:write` |
26
+ | [`logs`](#logs) | `GET /v1/audience/crm-triggers/logs` | `crm_triggers:read` |
27
+
28
+ ## The trigger object
29
+
30
+ ```ts
31
+ interface CrmTrigger {
32
+ id: string;
33
+ name: string;
34
+ active: boolean;
35
+ module: 'contact';
36
+ type: 'event' | 'time';
37
+ trigger: CrmTriggerEvent;
38
+ when: { condition: 'any' | 'any_of_selected' | 'all'; fields: string[] };
39
+ filters: ContactFilterEntry[];
40
+ action: {
41
+ type: 'webhook';
42
+ config: { url: string; method: 'POST' | 'PUT' | 'PATCH' | 'GET'; headers: Record<string, string> };
43
+ };
44
+ createdAt: string;
45
+ updatedAt: string;
46
+ }
47
+ ```
48
+
49
+ ### Events
50
+
51
+ | `trigger` | Fires when |
52
+ | --- | --- |
53
+ | `created` | A contact is created. |
54
+ | `edited`, `modified` | Any change — the two are equivalent. |
55
+ | `deleted` | A contact is destroyed. |
56
+ | `agent_assigned`, `agent_deassigned` | The assigned agent changed. |
57
+ | `marked_as_done`, `marked_as_closed`, `marked_as_archived` | The matching `chat_metadata` flag moved. |
58
+ | `blocked` | A `chat_metadata.blocked` flag moved. |
59
+
60
+ All of these except `created` and `deleted` are narrower readings of the same
61
+ update, decided by which paths moved.
62
+
63
+ ### `when`
64
+
65
+ | `condition` | Passes when |
66
+ | --- | --- |
67
+ | `any` | Every change — `fields` is ignored. |
68
+ | `any_of_selected` | At least one path in `fields` moved. |
69
+ | `all` | Every path in `fields` moved. |
70
+
71
+ `fields` are dotted contact paths: `name.first_name`, `chat_metadata.closed`,
72
+ `custom_field_hash.lead_score`.
73
+
74
+ Events that carry no change set — a creation, a deletion — pass regardless.
75
+
76
+ > **Known gap.** `filters` are skipped for `deleted` events: the contact is
77
+ > already gone, so there is nothing left to match against.
78
+
79
+ ### Time triggers
80
+
81
+ `type: 'time'` is stored and served, but **nothing dispatches it yet**. Creating
82
+ one is not an error; it simply never fires.
83
+
84
+ ---
85
+
86
+ ## list
87
+
88
+ ```ts
89
+ crm.crmTriggers.list(query?: Partial<ListCrmTriggersQuery>): Promise<Paginated<CrmTrigger>>
90
+ ```
91
+
92
+ `GET /v1/audience/crm-triggers` · `crm_triggers:read` · **200**
93
+
94
+ | Field | Type | Default | Notes |
95
+ | --- | --- | --- | --- |
96
+ | `page` | number | `1` | 1-based. |
97
+ | `rows` | number | `25` | 1–100. |
98
+ | `search` | string | — | 1–120 chars. Matches the trigger **name**. |
99
+ | `trigger` | `CrmTriggerEvent` | — | Quick filter. |
100
+ | `type` | `'event' \| 'time'` | — | Quick filter. |
101
+ | `active` | boolean | — | Quick filter. |
102
+
103
+ ---
104
+
105
+ ## get
106
+
107
+ ```ts
108
+ crm.crmTriggers.get(id: string): Promise<CrmTrigger>
109
+ ```
110
+
111
+ `GET /v1/audience/crm-triggers/:id` · `crm_triggers:read` · **200**
112
+
113
+ ---
114
+
115
+ ## create
116
+
117
+ ```ts
118
+ crm.crmTriggers.create(input: CreateCrmTriggerInput): Promise<CrmTrigger>
119
+ ```
120
+
121
+ `POST /v1/audience/crm-triggers` · `crm_triggers:write` · **201**
122
+
123
+ ### Body
124
+
125
+ | Field | Type | Required | Default | Notes |
126
+ | --- | --- | --- | --- | --- |
127
+ | `name` | string | yes | — | Trimmed, 1–120 chars. |
128
+ | `action` | `CrmTriggerAction` | yes | — | See below. |
129
+ | `active` | boolean | no | `true` | |
130
+ | `module` | `'contact'` | no | `'contact'` | |
131
+ | `type` | `'event' \| 'time'` | no | `'event'` | |
132
+ | `trigger` | `CrmTriggerEvent` | no | `'edited'` | |
133
+ | `when` | `CrmTriggerWhen` | no | `{ condition: 'any', fields: [] }` | |
134
+ | `filters` | `ContactFilterEntry[]` | no | `[]` | At most 50. |
135
+
136
+ With only `name` and `action`, you get an active `event` trigger on `edited`
137
+ with no narrowing — it fires on every change to every contact.
138
+
139
+ `action.config`:
140
+
141
+ | Field | Type | Default | Notes |
142
+ | --- | --- | --- | --- |
143
+ | `url` | string | — | Required, and must be a valid URL. |
144
+ | `method` | `'POST' \| 'PUT' \| 'PATCH' \| 'GET'` | `'POST'` | |
145
+ | `headers` | `Record<string, string>` | `{}` | Sent alongside the contact payload. |
146
+
147
+ ```ts
148
+ await crm.crmTriggers.create({
149
+ name: 'Notify ops when a VIP conversation closes',
150
+ trigger: 'edited',
151
+ when: { condition: 'any_of_selected', fields: ['chat_metadata.closed'] },
152
+ filters: [
153
+ { operator: null, field_id: 'tags', field_type: 'string', condition: 'equal to', value: [vipTagId] },
154
+ ],
155
+ action: {
156
+ config: {
157
+ url: 'https://ops.example.com/hooks/vip',
158
+ method: 'POST',
159
+ headers: { 'x-source': 'crm' },
160
+ },
161
+ },
162
+ });
163
+ ```
164
+
165
+ > **Not signed.** Trigger deliveries carry no signature — the schema has no
166
+ > per-trigger secret. Put a shared token in `action.config.headers` and check it
167
+ > at your endpoint. (`crm.webhooks` deliveries *are* signed.)
168
+
169
+ ---
170
+
171
+ ## update
172
+
173
+ ```ts
174
+ crm.crmTriggers.update(id: string, input: UpdateCrmTriggerInput): Promise<CrmTrigger>
175
+ ```
176
+
177
+ `PATCH /v1/audience/crm-triggers/:id` · `crm_triggers:write` · **200**
178
+
179
+ Same fields as `create`, all optional. `when`, `filters` and `action`
180
+ **replace** their stored value rather than merging into it.
181
+
182
+ Pause a trigger with `{ active: false }` rather than deleting it — the
183
+ configuration comes back, and the delivery history stays readable either way.
184
+
185
+ ---
186
+
187
+ ## delete
188
+
189
+ ```ts
190
+ crm.crmTriggers.delete(id: string): Promise<void>
191
+ ```
192
+
193
+ `DELETE /v1/audience/crm-triggers/:id` · `crm_triggers:write` · **204**, no body.
194
+
195
+ ---
196
+
197
+ ## logs
198
+
199
+ ```ts
200
+ crm.crmTriggers.logs(query?: Partial<ListCrmTriggerLogsQuery>): Promise<Paginated<CrmTriggerLog>>
201
+ ```
202
+
203
+ `GET /v1/audience/crm-triggers/logs` · `crm_triggers:read` · **200**
204
+
205
+ Delivery history across every trigger, or one trigger's with `{ trigger: id }`.
206
+ Newest first.
207
+
208
+ Note the path: **`logs` is a sibling of `:id`, not a child of it** — there is no
209
+ `/crm-triggers/:id/logs`.
210
+
211
+ Logs are written by the dispatcher, never by a caller, and the collection
212
+ expires them after 30 days. There is no way to write or delete one.
213
+
214
+ | Field | Type | Default | Notes |
215
+ | --- | --- | --- | --- |
216
+ | `page` | number | `1` | 1-based. |
217
+ | `rows` | number | `25` | 1–100. |
218
+ | `search` | string | — | Accepted by the route, but ignored: a log row has no searchable text. |
219
+ | `trigger` | ObjectId | — | One trigger's history. |
220
+ | `event` | `CrmTriggerEvent` | — | Quick filter. |
221
+ | `status` | `'success' \| 'fail'` | — | Quick filter. |
222
+
223
+ ```ts
224
+ interface CrmTriggerLog {
225
+ id: string;
226
+ trigger: string; // the trigger that fired
227
+ event: CrmTriggerEvent;
228
+ status: 'success' | 'fail';
229
+ payload: { url?: string; body?: unknown };
230
+ response?: unknown; // status and body, or the error that stopped the call
231
+ expiryDate: string; // when the row is removed
232
+ createdAt: string;
233
+ updatedAt: string;
234
+ }
235
+ ```
236
+
237
+ ```ts
238
+ const failures = await crm.crmTriggers.logs({ trigger: triggerId, status: 'fail' });
239
+ for (const log of failures.data) console.error(log.payload.url, log.response);
240
+ ```