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,174 @@
|
|
|
1
|
+
# Custom fields
|
|
2
|
+
|
|
3
|
+
`crm.customFields` — `/v1/audience/custom-fields`
|
|
4
|
+
|
|
5
|
+
The per-account schema for contacts. A field's `slug` is the key its values are
|
|
6
|
+
stored under in `contact.custom_field_hash`, and the `field_id` the
|
|
7
|
+
[contact filter](../contact-filter.md) addresses it by.
|
|
8
|
+
|
|
9
|
+
**Every route takes either the field's id or its slug**, so a caller that knows
|
|
10
|
+
the schema never has to look an id up first. A value that reads as a 24-hex id
|
|
11
|
+
is looked up as one; anything else is read as a slug.
|
|
12
|
+
|
|
13
|
+
| Action | Route | Scope |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| [`list`](#list) | `GET /v1/audience/custom-fields` | `custom_fields:read` |
|
|
16
|
+
| [`get`](#get) | `GET /v1/audience/custom-fields/:ref` | `custom_fields:read` |
|
|
17
|
+
| [`create`](#create) | `POST /v1/audience/custom-fields` | `custom_fields:write` |
|
|
18
|
+
| [`update`](#update) | `PATCH /v1/audience/custom-fields/:ref` | `custom_fields:write` |
|
|
19
|
+
| [`delete`](#delete) | `DELETE /v1/audience/custom-fields/:ref` | `custom_fields:write` |
|
|
20
|
+
|
|
21
|
+
## The field object
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
interface CustomField {
|
|
25
|
+
id: string;
|
|
26
|
+
slug: string; // machine name, unique per account
|
|
27
|
+
name?: string; // label
|
|
28
|
+
caption?: string; // helper text
|
|
29
|
+
type: 'system' | 'user'; // who owns it — NOT the data type
|
|
30
|
+
dataType?: CustomFieldType; // the data type
|
|
31
|
+
default: Record<string, unknown>;
|
|
32
|
+
props: Record<string, unknown>; // type-specific config: dropdown options, precision, …
|
|
33
|
+
required: boolean;
|
|
34
|
+
displayInPanel: boolean;
|
|
35
|
+
showInSidebar: boolean;
|
|
36
|
+
showAsLabel: boolean;
|
|
37
|
+
isBoard: boolean;
|
|
38
|
+
teamAccess: 'restricted' | 'open_to_all';
|
|
39
|
+
createdAt: string;
|
|
40
|
+
updatedAt: string;
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`type` and `dataType` are easy to confuse. `type` says **who owns the field** —
|
|
45
|
+
`system` for platform-provisioned, `user` for the account's own. `dataType` is
|
|
46
|
+
what it holds.
|
|
47
|
+
|
|
48
|
+
### Data types
|
|
49
|
+
|
|
50
|
+
`text` `textarea` `url` `email` `phone` `dial_code` `country` `language`
|
|
51
|
+
`timezone` `date` `timestamp` `periodic_time` `number` `decimal` `amount`
|
|
52
|
+
`percent` `address` `location` `attachment` `checkbox` `dropdown`
|
|
53
|
+
`multi_select`
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## list
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
crm.customFields.list(query?: Partial<ListCustomFieldsQuery>): Promise<Paginated<CustomField>>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`GET /v1/audience/custom-fields` · `custom_fields:read` · **200**
|
|
64
|
+
|
|
65
|
+
### Query
|
|
66
|
+
|
|
67
|
+
| Field | Type | Default | Notes |
|
|
68
|
+
| --- | --- | --- | --- |
|
|
69
|
+
| `page` | number | `1` | 1-based. |
|
|
70
|
+
| `rows` | number | `25` | 1–100. |
|
|
71
|
+
| `search` | string | — | 1–120 chars. Matches the **slug, the label and the caption**. |
|
|
72
|
+
| `dataType` | `CustomFieldType` | — | Quick filter. |
|
|
73
|
+
| `type` | `'system' \| 'user'` | — | Quick filter: platform-provisioned or the account's own. |
|
|
74
|
+
| `teamAccess` | `'restricted' \| 'open_to_all'` | — | Quick filter. |
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const fields = await crm.customFields.list({ type: 'user', dataType: 'dropdown' });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## get
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
crm.customFields.get(ref: string): Promise<CustomField>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`GET /v1/audience/custom-fields/:ref` · `custom_fields:read` · **200**
|
|
89
|
+
|
|
90
|
+
`ref` is the field's id **or** its slug.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
await crm.customFields.get('lead_score'); // by slug
|
|
94
|
+
await crm.customFields.get('65f0a1b2c3d4e5f6a7b8c9d0'); // by id
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## create
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
crm.customFields.create(input: CreateCustomFieldInput): Promise<CustomField>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`POST /v1/audience/custom-fields` · `custom_fields:write` · **201**
|
|
106
|
+
|
|
107
|
+
### Body
|
|
108
|
+
|
|
109
|
+
| Field | Type | Required | Default | Notes |
|
|
110
|
+
| --- | --- | --- | --- | --- |
|
|
111
|
+
| `slug` | string | yes | — | `^[a-z][a-z0-9_]{0,63}$`, and not reserved. Immutable once created. |
|
|
112
|
+
| `name` | string | yes | — | 1–120 chars. The label. |
|
|
113
|
+
| `caption` | string | no | — | Up to 240 chars. |
|
|
114
|
+
| `dataType` | `CustomFieldType` | yes | — | See the list above. |
|
|
115
|
+
| `default` | object | no | `{}` | Default value, shaped by `dataType`. |
|
|
116
|
+
| `props` | object | no | `{}` | Type-specific config — dropdown options, number precision, and so on. |
|
|
117
|
+
| `required` | boolean | no | `false` | |
|
|
118
|
+
| `displayInPanel` | boolean | no | `false` | |
|
|
119
|
+
| `showInSidebar` | boolean | no | `false` | |
|
|
120
|
+
| `showAsLabel` | boolean | no | `false` | |
|
|
121
|
+
| `isBoard` | boolean | no | `false` | |
|
|
122
|
+
| `teamAccess` | `'restricted' \| 'open_to_all'` | no | `'open_to_all'` | |
|
|
123
|
+
|
|
124
|
+
### Reserved slugs
|
|
125
|
+
|
|
126
|
+
A slug that would shadow something else is rejected with `422`. Three groups:
|
|
127
|
+
|
|
128
|
+
- **Contact document paths** — `id` `_id` `__v` `user` `business` `name`
|
|
129
|
+
`profile` `custom_field_hash` `custom_fields` `chat_metadata` `bot` `order`
|
|
130
|
+
`last_seen` `last_icm_timestamp` `last_interaction_props` `agent` `team`
|
|
131
|
+
`lead_source` `status` `privacy_status` `tags` `createdat` `updatedat`
|
|
132
|
+
- **Channel keys**, which `profile` is keyed by — `waba` `whatsapp` `facebook`
|
|
133
|
+
`instagram` `telegram` `email` `rcs` `webchat`
|
|
134
|
+
- **Built-in filter fields** — `referral_id` `system_last_seen`
|
|
135
|
+
`system_created_on` `system_identifier` `system_profile_name`
|
|
136
|
+
|
|
137
|
+
The check is case-insensitive. A slug already in use on the account is a
|
|
138
|
+
`409 conflict`.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const field = await crm.customFields.create({
|
|
142
|
+
slug: 'lead_score',
|
|
143
|
+
name: 'Lead score',
|
|
144
|
+
dataType: 'number',
|
|
145
|
+
required: true,
|
|
146
|
+
showInSidebar: true,
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## update
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
crm.customFields.update(ref: string, input: UpdateCustomFieldInput): Promise<CustomField>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`PATCH /v1/audience/custom-fields/:ref` · `custom_fields:write` · **200**
|
|
159
|
+
|
|
160
|
+
Every field from `create` except `slug`, all optional. The slug is not updatable:
|
|
161
|
+
values already stored under it would be orphaned.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## delete
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
crm.customFields.delete(ref: string): Promise<void>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`DELETE /v1/audience/custom-fields/:ref` · `custom_fields:write` · **204**, no body.
|
|
172
|
+
|
|
173
|
+
Deleting the definition does not rewrite the contacts that hold values under its
|
|
174
|
+
slug; those values stay in `custom_field_hash` and simply stop being described.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Export history
|
|
2
|
+
|
|
3
|
+
`crm.exports` — `/v1/audience/exports`
|
|
4
|
+
|
|
5
|
+
One row per bulk extract, moved by a worker through
|
|
6
|
+
`pending → processing → completed | failed`.
|
|
7
|
+
|
|
8
|
+
Creating a row **queues** the work. When the run completes, `file` holds the
|
|
9
|
+
storage key of the written file and `count` the number of rows in it; when it
|
|
10
|
+
fails, `error` says why.
|
|
11
|
+
|
|
12
|
+
The route is `/exports`, not `/contact-exports`: a row's `module` is `contact`
|
|
13
|
+
**or** `datastore`.
|
|
14
|
+
|
|
15
|
+
> **Worker status.** The row, its lifecycle and its counters are live. The
|
|
16
|
+
> worker that writes the file is still a stub — it moves a row to `processing`
|
|
17
|
+
> and then to `completed` without producing anything. No file is written yet.
|
|
18
|
+
|
|
19
|
+
| Action | Route | Scope |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| [`list`](#list) | `GET /v1/audience/exports` | `contacts:read` |
|
|
22
|
+
| [`get`](#get) | `GET /v1/audience/exports/:id` | `contacts:read` |
|
|
23
|
+
| [`create`](#create) | `POST /v1/audience/exports` | `contacts:read` |
|
|
24
|
+
| [`delete`](#delete) | `DELETE /v1/audience/exports/:id` | `contacts:write` |
|
|
25
|
+
|
|
26
|
+
There is no `update`: a row is moved by the worker, not by a caller.
|
|
27
|
+
|
|
28
|
+
Note the scope on `create` — reading contacts out is a **read**, so an export
|
|
29
|
+
can be started with read-only access.
|
|
30
|
+
|
|
31
|
+
## The export object
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
interface ExportHistory {
|
|
35
|
+
id: string;
|
|
36
|
+
member?: string; // the team member who started it
|
|
37
|
+
module: 'contact' | 'datastore';
|
|
38
|
+
file?: string; // storage key; absent until the worker writes one
|
|
39
|
+
title: string;
|
|
40
|
+
count: number; // rows written; 0 until it completes
|
|
41
|
+
status: 'pending' | 'processing' | 'completed' | 'failed';
|
|
42
|
+
error?: string; // why it failed, when it did
|
|
43
|
+
config: ExportConfig;
|
|
44
|
+
extra: Record<string, unknown>;
|
|
45
|
+
createdAt: string;
|
|
46
|
+
updatedAt: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ExportConfig {
|
|
50
|
+
segment?: string; // a saved segment's id or its name
|
|
51
|
+
filter?: ContactFilter; // ad-hoc conditions
|
|
52
|
+
fields?: string[]; // columns to write; [] means the worker's default set
|
|
53
|
+
[key: string]: unknown; // anything else is carried through
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`config` says **what to write out**, in the same vocabulary the contact list
|
|
58
|
+
speaks. Sending both a `segment` and a `filter` narrows the segment, exactly as
|
|
59
|
+
`crm.contacts.list` reads the pair — so an export means what the list it came
|
|
60
|
+
from means.
|
|
61
|
+
|
|
62
|
+
Unlike imports, the export schema has a real `error` column, so a failure goes
|
|
63
|
+
there rather than into `extra`.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## list
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
crm.exports.list(query?: Partial<ListExportHistoryQuery>): Promise<Paginated<ExportHistory>>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`GET /v1/audience/exports` · `contacts:read` · **200**
|
|
74
|
+
|
|
75
|
+
Newest first.
|
|
76
|
+
|
|
77
|
+
| Field | Type | Default | Notes |
|
|
78
|
+
| --- | --- | --- | --- |
|
|
79
|
+
| `page` | number | `1` | 1-based. |
|
|
80
|
+
| `rows` | number | `25` | 1–100. |
|
|
81
|
+
| `search` | string | — | 1–120 chars. Matches the **title**. |
|
|
82
|
+
| `module` | `'contact' \| 'datastore'` | — | Quick filter. |
|
|
83
|
+
| `status` | `HistoryStatus` | — | Quick filter. |
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## get
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
crm.exports.get(id: string): Promise<ExportHistory>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`GET /v1/audience/exports/:id` · `contacts:read` · **200**
|
|
94
|
+
|
|
95
|
+
The progress endpoint: the row with its current `status` and `count`, and — once
|
|
96
|
+
finished — `file`.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const run = await crm.exports.get(id);
|
|
100
|
+
if (run.status === 'completed') download(run.file!);
|
|
101
|
+
if (run.status === 'failed') console.error(run.error);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## create
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
crm.exports.create(input: CreateExportHistoryInput): Promise<ExportHistory>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`POST /v1/audience/exports` · `contacts:read` · **202**
|
|
113
|
+
|
|
114
|
+
**202, not 201**: the row exists, but no file has been written yet. The response
|
|
115
|
+
is the row at `status: 'pending'` with `count: 0` and no `file`.
|
|
116
|
+
|
|
117
|
+
### Body
|
|
118
|
+
|
|
119
|
+
| Field | Type | Required | Default | Notes |
|
|
120
|
+
| --- | --- | --- | --- | --- |
|
|
121
|
+
| `title` | string | yes | — | Trimmed, 1–200 chars. |
|
|
122
|
+
| `module` | `'contact' \| 'datastore'` | no | `'contact'` | |
|
|
123
|
+
| `config` | `ExportConfig` | no | `{}` | The default exports every contact with the worker's default columns. |
|
|
124
|
+
| `extra` | object | no | `{}` | Carried through untouched. |
|
|
125
|
+
|
|
126
|
+
Only a title is required:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
await crm.exports.create({ title: 'Everyone' });
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
A narrower one:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
await crm.exports.create({
|
|
136
|
+
title: 'High budget, Pune',
|
|
137
|
+
config: {
|
|
138
|
+
segment: 'High budget',
|
|
139
|
+
filter: {
|
|
140
|
+
conditions: [
|
|
141
|
+
{ operator: null, field_id: 'city', field_type: 'text', condition: 'equal to', value: 'Pune' },
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
fields: ['name.first_name', 'name.last_name', 'email', 'city'],
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`fields` names the columns to write; leave it out (or empty) for the worker's
|
|
150
|
+
default set.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## delete
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
crm.exports.delete(id: string): Promise<void>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`DELETE /v1/audience/exports/:id` · `contacts:write` · **204**, no body.
|
|
161
|
+
|
|
162
|
+
Removes the history row. **A file already written to storage is not deleted with
|
|
163
|
+
it** — take the `file` key first if you need to clean it up.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Import history
|
|
2
|
+
|
|
3
|
+
`crm.imports` — `/v1/audience/imports`
|
|
4
|
+
|
|
5
|
+
One row per bulk load, and the record a worker moves through
|
|
6
|
+
`pending → processing → completed | failed`.
|
|
7
|
+
|
|
8
|
+
Creating a row **queues** the work; it does not do it. The file itself is
|
|
9
|
+
uploaded out of band and named here by its storage key. `statistics` fill in as
|
|
10
|
+
the worker walks the file, so poll `get` to follow a run.
|
|
11
|
+
|
|
12
|
+
The route is `/imports`, not `/contact-imports`: a row's `module` is `contact`
|
|
13
|
+
**or** `datastore`, so these were never contact-only.
|
|
14
|
+
|
|
15
|
+
> **Worker status.** The row, its lifecycle and its counters are live. The
|
|
16
|
+
> worker that reads the file is still a stub — it moves a row to `processing`
|
|
17
|
+
> and then to `completed` without reading anything. Nothing parses a file yet.
|
|
18
|
+
|
|
19
|
+
| Action | Route | Scope |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| [`list`](#list) | `GET /v1/audience/imports` | `contacts:read` |
|
|
22
|
+
| [`get`](#get) | `GET /v1/audience/imports/:id` | `contacts:read` |
|
|
23
|
+
| [`create`](#create) | `POST /v1/audience/imports` | `contacts:write` |
|
|
24
|
+
| [`delete`](#delete) | `DELETE /v1/audience/imports/:id` | `contacts:write` |
|
|
25
|
+
|
|
26
|
+
There is no `update`: a row is moved by the worker, not by a caller.
|
|
27
|
+
|
|
28
|
+
## The import object
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
interface ImportHistory {
|
|
32
|
+
id: string;
|
|
33
|
+
member?: string; // the team member who started it
|
|
34
|
+
module: 'contact' | 'datastore';
|
|
35
|
+
file: string; // storage key of the uploaded file
|
|
36
|
+
title: string;
|
|
37
|
+
status: 'pending' | 'processing' | 'completed' | 'failed';
|
|
38
|
+
config: ImportConfig;
|
|
39
|
+
extra: Record<string, unknown>;
|
|
40
|
+
statistics: {
|
|
41
|
+
total: number;
|
|
42
|
+
success: number; // = new + duplicate
|
|
43
|
+
failed: number;
|
|
44
|
+
new: number; // rows that created a contact
|
|
45
|
+
duplicate: number; // rows that matched one that already existed
|
|
46
|
+
};
|
|
47
|
+
createdAt: string;
|
|
48
|
+
updatedAt: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ImportConfig {
|
|
52
|
+
mapping?: Record<string, string>; // column header -> contact field
|
|
53
|
+
duplicateStrategy?: 'skip' | 'update';
|
|
54
|
+
[key: string]: unknown; // anything else is carried through
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`config` and `extra` are untyped columns. The keys the platform acts on are
|
|
59
|
+
named above; anything else you put there is stored and handed back untouched.
|
|
60
|
+
A whole-run failure puts its reason in **`extra.error`** — the import schema has
|
|
61
|
+
no `error` column of its own.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## list
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
crm.imports.list(query?: Partial<ListImportHistoryQuery>): Promise<Paginated<ImportHistory>>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`GET /v1/audience/imports` · `contacts:read` · **200**
|
|
72
|
+
|
|
73
|
+
Newest first.
|
|
74
|
+
|
|
75
|
+
| Field | Type | Default | Notes |
|
|
76
|
+
| --- | --- | --- | --- |
|
|
77
|
+
| `page` | number | `1` | 1-based. |
|
|
78
|
+
| `rows` | number | `25` | 1–100. |
|
|
79
|
+
| `search` | string | — | 1–120 chars. Matches the **title**. |
|
|
80
|
+
| `module` | `'contact' \| 'datastore'` | — | Quick filter. |
|
|
81
|
+
| `status` | `HistoryStatus` | — | Quick filter. |
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const running = await crm.imports.list({ status: 'processing' });
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## get
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
crm.imports.get(id: string): Promise<ImportHistory>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`GET /v1/audience/imports/:id` · `contacts:read` · **200**
|
|
96
|
+
|
|
97
|
+
The progress endpoint: the row with its current `status` and `statistics`.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const poll = async (id: string) => {
|
|
101
|
+
for (;;) {
|
|
102
|
+
const run = await crm.imports.get(id);
|
|
103
|
+
if (run.status === 'completed' || run.status === 'failed') return run;
|
|
104
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## create
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
crm.imports.create(input: CreateImportHistoryInput): Promise<ImportHistory>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`POST /v1/audience/imports` · `contacts:write` · **202**
|
|
118
|
+
|
|
119
|
+
**202, not 201**: the row exists, but nothing has been read out of the file yet.
|
|
120
|
+
The response is the row at `status: 'pending'` with every counter at zero.
|
|
121
|
+
|
|
122
|
+
### Body
|
|
123
|
+
|
|
124
|
+
| Field | Type | Required | Default | Notes |
|
|
125
|
+
| --- | --- | --- | --- | --- |
|
|
126
|
+
| `file` | string | yes | — | 1–1024 chars. Storage key of the already-uploaded file; the worker resolves it. |
|
|
127
|
+
| `title` | string | yes | — | Trimmed, 1–200 chars. |
|
|
128
|
+
| `module` | `'contact' \| 'datastore'` | no | `'contact'` | |
|
|
129
|
+
| `config` | `ImportConfig` | no | `{}` | Defaults to an empty mapping and `duplicateStrategy: 'skip'`. |
|
|
130
|
+
| `extra` | object | no | `{}` | Carried through untouched. |
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const run = await crm.imports.create({
|
|
134
|
+
title: 'March contacts',
|
|
135
|
+
file: 'uploads/2026-03/contacts.csv',
|
|
136
|
+
config: {
|
|
137
|
+
mapping: { Email: 'email', 'First name': 'name.first_name', City: 'city' },
|
|
138
|
+
duplicateStrategy: 'update',
|
|
139
|
+
delimiter: ';', // unknown keys survive the round trip
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
run.status; // 'pending'
|
|
144
|
+
run.statistics; // { total: 0, success: 0, failed: 0, new: 0, duplicate: 0 }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`duplicateStrategy` decides what a row matching an existing contact does:
|
|
148
|
+
`'skip'` leaves the contact alone, `'update'` writes the row's values over it.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## delete
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
crm.imports.delete(id: string): Promise<void>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`DELETE /v1/audience/imports/:id` · `contacts:write` · **204**, no body.
|
|
159
|
+
|
|
160
|
+
Removes the history row. **Contacts the import already created are not touched** —
|
|
161
|
+
this deletes the receipt, not the work.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Segments
|
|
2
|
+
|
|
3
|
+
`crm.segments` — `/v1/audience/segments`
|
|
4
|
+
|
|
5
|
+
A saved [contact filter](../contact-filter.md) under a name.
|
|
6
|
+
|
|
7
|
+
Nothing is materialised: a segment holds no contact ids. It covers whatever its
|
|
8
|
+
conditions match at the moment it is next run — by
|
|
9
|
+
`crm.contacts.list({ segment })`, by an export's `config.segment`, or by a
|
|
10
|
+
campaign. Two runs a day apart can return different people, by design.
|
|
11
|
+
|
|
12
|
+
| Action | Route | Scope |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| [`list`](#list) | `GET /v1/audience/segments` | `segments:read` |
|
|
15
|
+
| [`get`](#get) | `GET /v1/audience/segments/:id` | `segments:read` |
|
|
16
|
+
| [`create`](#create) | `POST /v1/audience/segments` | `segments:write` |
|
|
17
|
+
| [`update`](#update) | `PATCH /v1/audience/segments/:id` | `segments:write` |
|
|
18
|
+
| [`delete`](#delete) | `DELETE /v1/audience/segments/:id` | `segments:write` |
|
|
19
|
+
|
|
20
|
+
## The segment object
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
interface Segment {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
filter: ContactFilter;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
updatedAt: string;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## list
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
crm.segments.list(query?: Partial<ListSegmentsQuery>): Promise<Paginated<Segment>>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`GET /v1/audience/segments` · `segments:read` · **200**
|
|
42
|
+
|
|
43
|
+
| Field | Type | Default | Notes |
|
|
44
|
+
| --- | --- | --- | --- |
|
|
45
|
+
| `page` | number | `1` | 1-based. |
|
|
46
|
+
| `rows` | number | `25` | 1–100. |
|
|
47
|
+
| `search` | string | — | 1–120 chars. Matches the **name and description**. |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## get
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
crm.segments.get(id: string): Promise<Segment>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`GET /v1/audience/segments/:id` · `segments:read` · **200**
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## create
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
crm.segments.create(input: CreateSegmentInput): Promise<Segment>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`POST /v1/audience/segments` · `segments:write` · **201**
|
|
68
|
+
|
|
69
|
+
| Field | Type | Required | Notes |
|
|
70
|
+
| --- | --- | --- | --- |
|
|
71
|
+
| `name` | string | yes | Trimmed, 1–120 chars. Addressable by name as well as by id wherever a segment is referenced. |
|
|
72
|
+
| `description` | string | no | Trimmed, up to 500 chars. |
|
|
73
|
+
| `filter` | `ContactFilter` | yes | Validated on write, not when first run — a segment that stores cannot fail later for a condition that never parsed. |
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const segment = await crm.segments.create({
|
|
77
|
+
name: 'High budget, Pune',
|
|
78
|
+
description: 'Lead score over 70 in Pune',
|
|
79
|
+
filter: {
|
|
80
|
+
conditions: [
|
|
81
|
+
{ operator: null, field_id: 'city', field_type: 'text', condition: 'equal to', value: 'Pune' },
|
|
82
|
+
{ operator: 'and', field_id: 'lead_score', field_type: 'number', condition: 'greater than', value: 70 },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Replay it
|
|
88
|
+
const contacts = await crm.contacts.list({ segment: segment.id });
|
|
89
|
+
// …or by name
|
|
90
|
+
const same = await crm.contacts.list({ segment: 'High budget, Pune' });
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## update
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
crm.segments.update(id: string, input: UpdateSegmentInput): Promise<Segment>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`PATCH /v1/audience/segments/:id` · `segments:write` · **200**
|
|
102
|
+
|
|
103
|
+
Same fields as `create`, all optional. Sending `filter` **replaces** the whole
|
|
104
|
+
condition list — there is no per-condition edit. Read the segment, change the
|
|
105
|
+
array, send it back.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## delete
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
crm.segments.delete(id: string): Promise<void>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`DELETE /v1/audience/segments/:id` · `segments:write` · **204**, no body.
|
|
116
|
+
|
|
117
|
+
Contacts are untouched: a segment never owned them. Anything still referencing
|
|
118
|
+
the segment by id or name — a scheduled export, a campaign — will start failing
|
|
119
|
+
with `404`, since listing everyone because the segment went missing would be the
|
|
120
|
+
wrong answer.
|