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.
- package/README.md +127 -25
- package/dist/client.d.ts +56 -1
- package/dist/client.js +76 -30
- package/dist/client.js.map +1 -1
- package/dist/http.d.ts +8 -0
- package/dist/http.js +10 -3
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +35 -2
- package/dist/index.js.map +1 -1
- package/dist/resources/audience/contact-notes.d.ts +51 -0
- package/dist/resources/audience/contact-notes.js +70 -0
- package/dist/resources/audience/contact-notes.js.map +1 -0
- package/dist/resources/audience/contact-tags.d.ts +43 -0
- package/dist/resources/audience/contact-tags.js +60 -0
- package/dist/resources/audience/contact-tags.js.map +1 -0
- package/dist/resources/audience/contacts.d.ts +76 -0
- package/dist/resources/audience/contacts.js +103 -0
- package/dist/resources/audience/contacts.js.map +1 -0
- package/dist/resources/audience/crm-triggers.d.ts +67 -0
- package/dist/resources/audience/crm-triggers.js +86 -0
- package/dist/resources/audience/crm-triggers.js.map +1 -0
- package/dist/resources/audience/custom-fields.d.ts +54 -0
- package/dist/resources/audience/custom-fields.js +71 -0
- package/dist/resources/audience/custom-fields.js.map +1 -0
- package/dist/resources/audience/export-history.d.ts +52 -0
- package/dist/resources/audience/export-history.js +67 -0
- package/dist/resources/audience/export-history.js.map +1 -0
- package/dist/resources/audience/import-history.d.ts +51 -0
- package/dist/resources/audience/import-history.js +66 -0
- package/dist/resources/audience/import-history.js.map +1 -0
- package/dist/resources/audience/segments.d.ts +49 -0
- package/dist/resources/audience/segments.js +66 -0
- package/dist/resources/audience/segments.js.map +1 -0
- package/dist/resources/audience/webhooks.d.ts +68 -0
- package/dist/resources/audience/webhooks.js +83 -0
- package/dist/resources/audience/webhooks.js.map +1 -0
- package/dist/resources/channels/activity.d.ts +25 -0
- package/dist/resources/channels/activity.js +34 -0
- package/dist/resources/channels/activity.js.map +1 -0
- package/dist/resources/channels/billing.d.ts +46 -0
- package/dist/resources/channels/billing.js +59 -0
- package/dist/resources/channels/billing.js.map +1 -0
- package/dist/resources/channels/channels.d.ts +115 -0
- package/dist/resources/channels/channels.js +129 -0
- package/dist/resources/channels/channels.js.map +1 -0
- package/dist/resources/channels/templates.d.ts +90 -0
- package/dist/resources/channels/templates.js +109 -0
- package/dist/resources/channels/templates.js.map +1 -0
- package/dist/resources/channels/wallet.d.ts +25 -0
- package/dist/resources/channels/wallet.js +34 -0
- package/dist/resources/channels/wallet.js.map +1 -0
- package/dist/resources/channels/webhooks.d.ts +60 -0
- package/dist/resources/channels/webhooks.js +79 -0
- package/dist/resources/channels/webhooks.js.map +1 -0
- package/dist/resources/paths.d.ts +12 -0
- package/dist/resources/paths.js +17 -0
- package/dist/resources/paths.js.map +1 -0
- package/dist/types/audience.d.ts +512 -0
- package/dist/types/audience.js +3 -0
- package/dist/types/audience.js.map +1 -0
- package/dist/types/channels.d.ts +383 -0
- package/dist/types/channels.js +22 -0
- package/dist/types/channels.js.map +1 -0
- package/dist/types/common.d.ts +55 -0
- package/dist/types/common.js +8 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/contact-filter.d.ts +55 -0
- package/dist/types/contact-filter.js +11 -0
- package/dist/types/contact-filter.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.js +21 -0
- package/dist/types/index.js.map +1 -0
- package/docs/audience/contact-notes.md +129 -0
- package/docs/audience/contact-tags.md +109 -0
- package/docs/audience/contacts.md +227 -0
- package/docs/audience/crm-triggers.md +240 -0
- package/docs/audience/custom-fields.md +174 -0
- package/docs/audience/exports.md +163 -0
- package/docs/audience/imports.md +161 -0
- package/docs/audience/segments.md +120 -0
- package/docs/audience/webhooks.md +167 -0
- package/docs/channels/activity.md +68 -0
- package/docs/channels/billing.md +112 -0
- package/docs/channels/channel-props.md +290 -0
- package/docs/channels/channels.md +199 -0
- package/docs/channels/templates.md +224 -0
- package/docs/channels/wallet.md +67 -0
- package/docs/channels/webhooks.md +152 -0
- package/docs/contact-filter.md +157 -0
- package/package.json +7 -3
- package/dist/resources/channels.d.ts +0 -10
- package/dist/resources/channels.js +0 -19
- package/dist/resources/channels.js.map +0 -1
- package/dist/types.d.ts +0 -70
- package/dist/types.js +0 -7
- 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
|
+
```
|