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,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.