@curviate/sdk 0.42.0 → 0.44.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/CHANGELOG.md +286 -1
- package/README.md +201 -4
- package/dist/index.d.ts +32154 -16148
- package/dist/index.js +5157 -186
- package/package.json +1 -2
- package/src/generated/types.ts +0 -33261
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,292 @@ Versioning: semantic. Minor for additive changes, patch for bug fixes; no stabil
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
## [
|
|
10
|
+
## [0.44.0] - 2026-10-22
|
|
11
|
+
|
|
12
|
+
Dashboard v2 API. Fixture and types regenerated from deployed staging (server
|
|
13
|
+
`15b704443878c7b589e40c7955a202b5131a20f7`, 197 paths), and the Sequence
|
|
14
|
+
corpus re-vendored from the same server.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Contact `profile_picture_url`:** read-only, `string | null`. The person's
|
|
19
|
+
LinkedIn photo from profile data your team already holds; null when none is
|
|
20
|
+
held. The link is signed and expires.
|
|
21
|
+
- **Enrollments:** `enrollments.pause`, `enrollments.events` (send a custom
|
|
22
|
+
event to an `until event.<name>`), `enrollments.reassign` (move to another
|
|
23
|
+
sending account; `warnings` names Steps that will be skipped) and
|
|
24
|
+
`enrollments.bulk` (up to 1,000 by ids or filter, one result per
|
|
25
|
+
Enrollment, not atomic). `enrollments.resume` takes no body to lift your own
|
|
26
|
+
pause. `enrollments.retry(id)` retries a failed Step through the guarded
|
|
27
|
+
bulk path and throws `NOTHING_TO_RETRY` (or the Enrollment's other refusal)
|
|
28
|
+
as a `CurviateError`; `resume({ at })` is an explicit, unguarded jump to a
|
|
29
|
+
Step. New list filters:
|
|
30
|
+
`version`, `done_step` with `outcome`, `reason`, `variant_id`,
|
|
31
|
+
`relationship_at_enrollment`. Types `EnrollmentEventBody`,
|
|
32
|
+
`EnrollmentReassignBody`, `EnrollmentReassignResult`, `EnrollmentBulkBody`,
|
|
33
|
+
`EnrollmentBulkResult`, `EnrollmentBulkItem`, `EnrollmentReason` (the reasons catalogue).
|
|
34
|
+
- **Error codes** `SEQUENCE_CONFLICT` (with `err.current`: the stored
|
|
35
|
+
Sequence and its `sequence_revision`; send `if_sequence_revision` on
|
|
36
|
+
`motions.update`), `STEP_IN_FLIGHT`, `NOTHING_TO_RETRY`,
|
|
37
|
+
`ACCOUNT_ALREADY_ENGAGED`, `ALREADY_ENROLLED_ON_ACCOUNT`, `KNOWN_CHAT`,
|
|
38
|
+
`CHAT_NOT_ONE_TO_ONE`. Before, each decoded as `INTERNAL`.
|
|
39
|
+
- **Compiler-style Sequence errors:** every `validateSequence` entry, every
|
|
40
|
+
offline `SEQUENCE_INVALID` entry and every decoded wire `errors[]` entry of
|
|
41
|
+
a Sequence rule carries `code` (`R1` to `R24`, `W1` to `W13`), `message`,
|
|
42
|
+
`pointer` (a JSON Pointer into the document), `hint` and `docs_url`, beside
|
|
43
|
+
`path` and `rule`.
|
|
44
|
+
- **Sequence document:** short variables `{{first_name}}`,
|
|
45
|
+
`{{my_first_name}}` and `{{<key>}}` for a custom field (the long forms stay
|
|
46
|
+
valid); an optional `event` on `do` and `tag` steps; `until event.<name>`;
|
|
47
|
+
the `message.seen` predicate (`if` steps). Warnings `W11` (a short custom
|
|
48
|
+
field that looks like a typo of a standard field), `W12` (`message.seen`
|
|
49
|
+
before any message) and `W13` (a deprecated form).
|
|
50
|
+
- **`newStepId(document, kind, beforeId?)`:** the id the dashboard editor
|
|
51
|
+
gives a new step (`message`, `message_2`, `w_<step>`).
|
|
52
|
+
- **Webhooks:** `enrollment.accepted`, `enrollment.replied`, `enrollment.seen`,
|
|
53
|
+
`enrollment.reassigned` and `motion.custom_event` in `CurviateEvent`.
|
|
54
|
+
`account_ids` is optional (or `null`: every account you can operate) on
|
|
55
|
+
every source.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- **A/B variants are exactly 2** (`MAX_VARIANTS`). 3 to 5 are accepted with
|
|
60
|
+
warning `W13` until 2027-01-19, then refused (`R23`); 1, or 6 and more,
|
|
61
|
+
are refused as before, and 6 and more no longer compile.
|
|
62
|
+
- **`{{motion.<key>}}`** gives `W13` (Motion variables were removed) beside
|
|
63
|
+
`W9`; still not an error offline until 2027-01-19.
|
|
64
|
+
|
|
65
|
+
### Deprecated
|
|
66
|
+
|
|
67
|
+
- `enrollments.signal` and `EnrollmentSignalBody`: use `enrollments.events`;
|
|
68
|
+
accepted until 2027-01-19.
|
|
69
|
+
- `until signal.<name>` (use `until event.<name>`) and the `motion.signal`
|
|
70
|
+
webhook event (renamed `motion.custom_event`), each accepted until
|
|
71
|
+
2027-01-19.
|
|
72
|
+
- `emit` steps: legacy; put `event` on the `do` or `tag` step that matters.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## [0.43.0]
|
|
77
|
+
|
|
78
|
+
Fixture and types regenerated from deployed staging (server
|
|
79
|
+
`7359b1b1be0584f7f368f1367d39261f2de8f079`, 193 paths; earlier regens at
|
|
80
|
+
`16b2eaf1473e4fb2a7b7b4a0308b9e248b30a93e`, 193 paths,
|
|
81
|
+
`091f53a69decbc55c53336bbbbfa1dc5fd56e859`, 193 paths,
|
|
82
|
+
`13d5357a7f74772782872c791ec8a5835060e9fe`, 191 paths, and
|
|
83
|
+
`3b0b7503a516f6a1f39622bd40ec6fec9c217c38`, 176 paths). The previous fixture
|
|
84
|
+
was older, so this regen also carries earlier, unrelated server changes (see
|
|
85
|
+
Changed). Regenerate from production and confirm an empty diff before publishing.
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
|
|
89
|
+
- **`network`** (root, 2 methods plus **`network.connections`**, 1 method):
|
|
90
|
+
`connections.list`, `syncStatus`, `resync`. The stored LinkedIn Connections
|
|
91
|
+
of your accounts, read with no LinkedIn call and no limit used; `account_id`
|
|
92
|
+
takes several accounts. `resync` re-reads one account's Connections, once
|
|
93
|
+
per account per 24 hours.
|
|
94
|
+
- **`unibox`** (root, 6 methods plus **`unibox.accounts`**, 2 methods):
|
|
95
|
+
`list`, `get`, `unreadCount`, `refresh`, `tagContact`, `enroll`,
|
|
96
|
+
`accounts.list`, `accounts.update`. One inbox over every account you can
|
|
97
|
+
operate, from what Curviate stores; `accounts.update` turns full-inbox sync
|
|
98
|
+
on or off for an account. Messages and chat actions stay on `messaging`.
|
|
99
|
+
- **`analytics`** (root, 1 method plus **`analytics.motions`**, 2 methods,
|
|
100
|
+
and **`analytics.events`**, 1 method): `activity`, `motions.list`,
|
|
101
|
+
`motions.get`, `events.list`. Read-only, any Tier.
|
|
102
|
+
- **List iterators**: `network.connections.list`, `unibox.list` and
|
|
103
|
+
`analytics.events.list` resolve to their first page when awaited and yield
|
|
104
|
+
every item across pages under `for await` (type `Pageable`).
|
|
105
|
+
- **`BudgetRow`** type: the budget rows, `connections_sync` and `inbox_sync`
|
|
106
|
+
included.
|
|
107
|
+
- **Needs Automate** in the JSDoc of `network.resync`, `unibox.refresh`,
|
|
108
|
+
`unibox.tagContact`, `unibox.enroll` and `unibox.accounts.update` (turning
|
|
109
|
+
full-inbox sync on; off passes on any Tier).
|
|
110
|
+
- **Error code** `CLIENT_TIMEOUT`: no response within the client's `timeout`.
|
|
111
|
+
Before, it was `INTERNAL` ("Request timed out."), which read like a server
|
|
112
|
+
fault. A read is retried first; a write is not, may still have reached the
|
|
113
|
+
API, and carries `retryLikelyToSucceed: false` so a generic retry loop
|
|
114
|
+
does not resend it. A network failure with no response stays `INTERNAL`.
|
|
115
|
+
|
|
116
|
+
- **Offline Sequence module** (no network, key or file system; TypeScript 5.0
|
|
117
|
+
or later): `Sequence` (an immutable builder: `create`, `add`,
|
|
118
|
+
`insertBefore`, `replace`, `remove`, `onReply`, `validate`, `toDocument`,
|
|
119
|
+
`fromDocument`), `step` (one constructor per step kind: `do`, `wait`,
|
|
120
|
+
`until`, `if`, `emit`, `tag`), `validateSequence` and the published JSON
|
|
121
|
+
Schema `SEQUENCE_SCHEMA_V1` with `SEQUENCE_SCHEMA_URL`. The builder emits
|
|
122
|
+
exactly the Sequence document a Motion runs, keys in one canonical order, so
|
|
123
|
+
diffs show only real changes; `fromDocument` loads a fetched document and
|
|
124
|
+
re-emits it unchanged. With literal arguments a typo'd action, event,
|
|
125
|
+
reaction or predicate, a `{{...}}` that is not a v1 variable, a text with a
|
|
126
|
+
variable and no fallback, a malformed duration and a jump to a step you never
|
|
127
|
+
added fail to compile. `validateSequence` and `toDocument()` give the API's
|
|
128
|
+
own verdict offline: every error as `{path, rule}`, in step order, and the
|
|
129
|
+
warnings (`W1`, `W5` to `W9`). A document using an action or event added to
|
|
130
|
+
the API after this release is refused offline while the API accepts it:
|
|
131
|
+
upgrade the SDK. Not on the client: no request is ever sent. Like the API,
|
|
132
|
+
the first 100 errors are listed (the message counts all), an oversized
|
|
133
|
+
document gets the size error alone, and a NUL or lone surrogate in any
|
|
134
|
+
string is refused (`toDocument()` throws `INVALID_REQUEST`, as the API's 400).
|
|
135
|
+
- **`CurviateError.errors`**: every failure as `{path, rule}` on a refusal that
|
|
136
|
+
lists them (`SEQUENCE_INVALID`, `MOTION_NOT_STARTABLE`), decoded from the
|
|
137
|
+
response's `errors[]` and emitted by `toJSON()`. The offline `SEQUENCE_INVALID`
|
|
138
|
+
a builder throws carries the same list.
|
|
139
|
+
- **Error code** `SEQUENCE_INVALID` (422): the Sequence document breaks one or
|
|
140
|
+
more rules; nothing is saved.
|
|
141
|
+
|
|
142
|
+
- **`contacts`** (root, 9 methods): `list`, `get`, `upsert`, `update`,
|
|
143
|
+
`delete`, `deleteMany`, `updateMany`, `enrich`, `export`, plus
|
|
144
|
+
**`contacts.imports`** (4 methods): `create`, `get`, `list`, `rows`. Your
|
|
145
|
+
team's Contacts: a LinkedIn URL plus your own fields, Tags and list
|
|
146
|
+
memberships; any spelling of one person's URL resolves to one Contact, so
|
|
147
|
+
`upsert` creates or updates and reports `outcome`. `updateMany` adds or
|
|
148
|
+
removes Tags and list memberships on up to 1,000 Contacts. `enrich` fills
|
|
149
|
+
profile fields from what Curviate stores, else queues paced live profile
|
|
150
|
+
views (a real view the person may see). `export` returns the whole matching
|
|
151
|
+
set in one download: `format: "json"` resolves with the Contact array, `csv`
|
|
152
|
+
and `ndjson` (default) with the raw bytes. Imports take `rows` or a `csv`
|
|
153
|
+
string, up to 10,000 rows; up to 500 run inline, larger ones in the
|
|
154
|
+
background (poll `imports.get`, read the report with `imports.rows`).
|
|
155
|
+
`list` and `export` send an `enrollment_status` array as the one comma
|
|
156
|
+
list the API reads.
|
|
157
|
+
- **`contactLists`** (root, 5 methods): `create`, `list`, `get`, `update`,
|
|
158
|
+
`delete`. Static lists; membership changes go through
|
|
159
|
+
`contacts.updateMany` (`add_list_id`, `remove_list_id`).
|
|
160
|
+
- **`tags`** (root, 3 methods): `list`, `update` (rename), `delete`. No create
|
|
161
|
+
call: a write that names a new Tag creates it.
|
|
162
|
+
- **Error codes** for Contacts, lists and Tags: `NAME_TAKEN` (409),
|
|
163
|
+
`RESOURCE_IN_USE` (409, a live Motion or running import uses the list or
|
|
164
|
+
Tag), `CONTACT_LIMIT_REACHED` (422), `IMPORT_QUEUE_FULL` (429).
|
|
165
|
+
- **`motions`** (root, 13 methods): `create`, `list`, `get`, `update`,
|
|
166
|
+
`delete`, `start`, `pause`, `resume`, `archive`, `preview`,
|
|
167
|
+
`getSequenceVersion`, `addContacts`, `removeContacts`. `sequence` in
|
|
168
|
+
`create` and `update` takes a `Sequence` builder besides a document: the
|
|
169
|
+
builder is converted with `toDocument()` first, so an invalid one rejects
|
|
170
|
+
with `SEQUENCE_INVALID` and nothing is sent, and one with a jump to a step
|
|
171
|
+
never added does not compile. A plain object is sent as is (the API
|
|
172
|
+
validates it); `null` clears a draft's Sequence. `preview` is a read: it
|
|
173
|
+
never reaches LinkedIn.
|
|
174
|
+
- **`enrollments`** (root, 5 methods): `list`, `get` (with the Step
|
|
175
|
+
history), `stop`, `resume` (`at` a Step), `signal`.
|
|
176
|
+
- **Error codes** for Motions: `MOTION_NOT_STARTABLE` (422, every gap in
|
|
177
|
+
`errors`), `MOTION_STATUS_CONFLICT` (409), `ENROLLMENT_NOT_ACTIVE` (409),
|
|
178
|
+
`CONTACT_NOT_ENROLLABLE` (422).
|
|
179
|
+
|
|
180
|
+
- **`tier`** (root, 1 method): `get` reads `GET /v1/billing/tier`: the Tier the
|
|
181
|
+
team has now (`null` for none), the bought Tier, `period`, the free `trial`,
|
|
182
|
+
a granted `override`, a downgrade `pending` for the next renewal, the
|
|
183
|
+
`features` it includes and `early_access` per announced Tier. No prices.
|
|
184
|
+
- **`TIER_REQUIRED` details** on `CurviateError`: `feature`, `requiredTier`,
|
|
185
|
+
`currentTier` (both `null` when no Tier has the feature / the team has none)
|
|
186
|
+
and `upgradeUrl` (team Owners only), decoded from the 403 and emitted by
|
|
187
|
+
`toJSON()`. Writes in Drafts, Contacts, Motions (and later Network and
|
|
188
|
+
Unibox) on a team below Automate are refused with it; reads, exports,
|
|
189
|
+
deletes and stopping running work pass. The "Needs Automate" note is now in
|
|
190
|
+
the JSDoc of the 19 gated write methods: `drafts.create`, `update`,
|
|
191
|
+
`publish`, `uploadAttachment`; `contacts.upsert`, `update`, `updateMany`,
|
|
192
|
+
`enrich`, `imports.create`; `contactLists.create`, `update`; `tags.update`;
|
|
193
|
+
`motions.create`, `update`, `start`, `resume`, `addContacts`;
|
|
194
|
+
`enrollments.resume`, `signal`.
|
|
195
|
+
- **`tier_required`** is a member of the Motion `pause_reason`, the sync
|
|
196
|
+
`paused_reason` and the Contact enrichment `failure_code` types, and the
|
|
197
|
+
reason a `motion.status_changed` event carries when the team loses the Tier.
|
|
198
|
+
|
|
199
|
+
- **Unibox types** (no SDK method yet): `POST
|
|
200
|
+
/v1/unibox/chats/{account_id}/{chat_id}/contact` and `.../enrollments`, and
|
|
201
|
+
the typed `motion_context` on Unibox chats (was an empty object type).
|
|
202
|
+
|
|
203
|
+
- **`notifications` at the root** (4 methods): `list`, `unreadCount`,
|
|
204
|
+
`update`, `markAllRead`. Curviate's own Notifications for you in this team
|
|
205
|
+
(an account needs reconnecting, a Motion paused, an import finished, a
|
|
206
|
+
teammate joined or left), with per-person read and archived state. Check
|
|
207
|
+
`unreadCount()` at session start. `curviate.account(id).notifications` is
|
|
208
|
+
unchanged and still means the account's LinkedIn notifications: the same key
|
|
209
|
+
now carries a different class at each scope.
|
|
210
|
+
- **`connectRequests`** (root, 3 methods): `create`, `get`, `cancel`
|
|
211
|
+
(`/v1/auth/connect-requests`). The credential-free way for an agent to get an
|
|
212
|
+
account connected or reconnected: create one, relay its `url` to the person,
|
|
213
|
+
poll `get` until `status` leaves `pending`. Your code never handles a
|
|
214
|
+
LinkedIn password, cookie or code. `create` is not retry-safe: a retry makes
|
|
215
|
+
a second request.
|
|
216
|
+
- **`teams`** (root, 10 methods): `list`, `getCurrent`, `updateCurrent`,
|
|
217
|
+
`listMembers`, `updateMember`, `removeMember` (`"me"` leaves the team),
|
|
218
|
+
`createInvites`, `listInvites`, `resendInvite`, `revokeInvite`.
|
|
219
|
+
- **`accounts.setTeamAccess(accountId, teamAccess)`**
|
|
220
|
+
(`PUT /v1/accounts/{account_id}/team-access`): opt an account you connected
|
|
221
|
+
out of team use, or share it again.
|
|
222
|
+
- **`team` client option**, sent as `X-Curviate-Team`: the team a dashboard
|
|
223
|
+
session token acts in. An API key acts only in the team it was created in;
|
|
224
|
+
leave `team` unset with one. An empty `team` throws `INVALID_REQUEST` at
|
|
225
|
+
construction.
|
|
226
|
+
- **Error codes** that decoded to `INTERNAL` until now: `TEAM_ACCESS_DENIED`,
|
|
227
|
+
`ACCOUNT_NOT_SHARED`, `ACCOUNT_OWNER_ONLY`, `LAST_OWNER`,
|
|
228
|
+
`BILLING_OWNER_REQUIRED`, `DEFAULT_TEAM_PROTECTED`, `ALREADY_MEMBER`,
|
|
229
|
+
`INVITE_ALREADY_PENDING`, `INVITE_NOT_PENDING`, `INVITE_LIMIT_REACHED`,
|
|
230
|
+
`CONNECT_REQUEST_NOT_PENDING`, `CONNECT_REQUEST_IN_USE`,
|
|
231
|
+
`LINKEDIN_ACCOUNT_MISMATCH`.
|
|
232
|
+
|
|
233
|
+
- **`activity_window.effective_timezone` and `activity_window.source`** on the
|
|
234
|
+
account object (`accounts.get`, `accounts.update`) and on the per-account
|
|
235
|
+
safety policy (`GET`/`PATCH /v1/{account_id}/safety-policy`, types only: the
|
|
236
|
+
SDK has no safety-policy method). `effective_timezone` is the IANA zone the activity window is
|
|
237
|
+
read in; `source` is `account`, `browser`, `location` or `default`.
|
|
238
|
+
|
|
239
|
+
### Changed
|
|
240
|
+
|
|
241
|
+
- **Connect request types**: every field of the object `connectRequests`
|
|
242
|
+
returns is required (`id`, `url`, `status`, ...), so `req.id` needs no `!`.
|
|
243
|
+
- **Contacts export types**: the `csv` and `json` response media types are
|
|
244
|
+
typed beside `ndjson`.
|
|
245
|
+
|
|
246
|
+
- **`SequenceDocument` type**: `post`, `newer_than` and `reaction` in a
|
|
247
|
+
`post.react` or `post.comment` step's `with` are optional (the API applies
|
|
248
|
+
their defaults), and a `contact.extra.<key>` predicate compiles. Before, a
|
|
249
|
+
valid document omitting them, or using one, did not type-check.
|
|
250
|
+
|
|
251
|
+
- **The activity window applies to every account, with or without a
|
|
252
|
+
`timezone`.** It is no longer switched off when no zone is set: the zone is
|
|
253
|
+
the one you set, else the one the user's browser reported, else one inferred
|
|
254
|
+
from the LinkedIn profile location when it maps to one zone, else
|
|
255
|
+
`Europe/Berlin`. `timezone` and `timezone_unset` keep their meaning: what you
|
|
256
|
+
set. Behavior change on the server: under `enforce` an account with no
|
|
257
|
+
`timezone` is now refused outside 07:00 to 22:00 in its effective zone
|
|
258
|
+
(`BUDGET_EXHAUSTED`, `safetyReason: "activity_window"`), where before nothing
|
|
259
|
+
was refused. Description changes only on the generated types.
|
|
260
|
+
- Types regenerated from the staging document also include server changes that
|
|
261
|
+
were not in the previous fixture: the contacts, tags, contact lists, Motions,
|
|
262
|
+
enrollments, unibox, notifications, network sync and billing tier paths; new
|
|
263
|
+
`Error` fields and `reason` values; `owner_user_id` and `team_access` on the
|
|
264
|
+
account object. Types only, except notifications, teams and team access
|
|
265
|
+
(Added above).
|
|
266
|
+
- **An API key is one person's key in one team**, no longer one key per
|
|
267
|
+
workspace: it acts as the person who created it, with their role in that
|
|
268
|
+
team. `owner_user_id` on the account object is the person who connected it;
|
|
269
|
+
`team_access: false` means only they can use it.
|
|
270
|
+
- **`auth_method` on the account object** is the method of the last successful
|
|
271
|
+
connect or reconnect (`credentials`, `cookie` or `hosted`), not the method
|
|
272
|
+
of the first connect.
|
|
273
|
+
- **Two drift fixes the regen forced** (the SDK's own pins went red against the
|
|
274
|
+
new document): the seven `motion` webhook events (`motion.status_changed`,
|
|
275
|
+
`enrollment.started`, `enrollment.skipped`, `enrollment.finished`,
|
|
276
|
+
`step.executed`, `step.failed`, `motion.signal`) join `CurviateEvent` with a
|
|
277
|
+
loose `MotionPayload` (`motion_id`, `account_id` nullable), and the error
|
|
278
|
+
codes `INSUFFICIENT_ROLE` and `TIER_REQUIRED` join `ERROR_CODES` instead of
|
|
279
|
+
decoding to `INTERNAL`.
|
|
280
|
+
|
|
281
|
+
### Fixed
|
|
282
|
+
|
|
283
|
+
- **`jobs.get` `with_sections`** goes as one comma value. Several sections
|
|
284
|
+
were sent as a repeated key, of which the API keeps only the last.
|
|
285
|
+
- **Package size**: the tarball no longer ships `src/generated/types.ts`. The
|
|
286
|
+
same types are in `dist/index.d.ts`, and `paths`, `components` and
|
|
287
|
+
`operations` are exported from the package root; the file was not
|
|
288
|
+
importable through the package's `exports` anyway.
|
|
289
|
+
- **`motions.preview` `sample` and `motions.getSequenceVersion` `version`**
|
|
290
|
+
are typed as numbers, as the API reads them.
|
|
291
|
+
- **An empty array in a comma-list filter is no filter.** `status: []`
|
|
292
|
+
(`drafts.list`, `motions.list`, `enrollments.list`), `enrollment_status: []`
|
|
293
|
+
(`contacts.list`, `contacts.export`) and `contact_ids: []`
|
|
294
|
+
(`motions.preview`) leave the parameter out instead of sending it empty,
|
|
295
|
+
which the API refused.
|
|
11
296
|
|
|
12
297
|
## [0.42.0] - 2026-10-07
|
|
13
298
|
|
package/README.md
CHANGED
|
@@ -33,20 +33,40 @@ const curviate = new Curviate({
|
|
|
33
33
|
});
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
An API key belongs to one person in one team and acts with that person's role there. `curviate.teams.getCurrent()` says which team and role. A dashboard session token may act in any team you belong to: pick one with `team: "tnt_..."` (sent as `X-Curviate-Team`). Leave `team` unset with an API key; naming another team is refused with `TEAM_ACCESS_DENIED`.
|
|
37
|
+
|
|
36
38
|
---
|
|
37
39
|
|
|
38
|
-
##
|
|
40
|
+
## Connecting an account
|
|
41
|
+
|
|
42
|
+
Every LinkedIn operation (messages, member profiles, invites, posts) is tied to a **managed account**, a LinkedIn session connected to Curviate.
|
|
43
|
+
|
|
44
|
+
**Agents: use a connect request.** Your code never handles a LinkedIn password, cookie or verification code, so never ask for one. Create a request, give its `url` to the person, and they connect in their own Curviate dashboard:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const req = await curviate.connectRequests.create(); // new account
|
|
48
|
+
// const req = await curviate.connectRequests.create({ account_id: "acc_123" }); // reconnect
|
|
49
|
+
console.log(`Connect your LinkedIn account here: ${req.url}`);
|
|
50
|
+
|
|
51
|
+
// Poll until it leaves `pending`: completed, expired (15 minutes), failed or cancelled.
|
|
52
|
+
const done = await curviate.connectRequests.get(req.id);
|
|
53
|
+
if (done.status === "completed") console.log(done.account_id);
|
|
54
|
+
```
|
|
39
55
|
|
|
40
|
-
|
|
56
|
+
`create` is not retry-safe: a retry makes a second request. A pending request can be withdrawn with `connectRequests.cancel(id)`.
|
|
57
|
+
|
|
58
|
+
**Your own app, with the person's credentials in hand:** `curviate.auth.intent()`. A new connect names where LinkedIn sees the account connecting from, with exactly one of `country` (e.g. `"US"`), `ip` or your own `proxy`:
|
|
41
59
|
|
|
42
60
|
```ts
|
|
43
61
|
await curviate.auth.intent({ seat_id, auth_method: "credentials", credentials: { email, password }, country: "US" });
|
|
44
62
|
```
|
|
45
63
|
|
|
64
|
+
## Account-scoped accessor
|
|
65
|
+
|
|
46
66
|
The `curviate.account(id)` accessor fixes the `account_id` on every call so you do not have to thread it manually:
|
|
47
67
|
|
|
48
68
|
```ts
|
|
49
|
-
// Root-level:
|
|
69
|
+
// Root-level: team-wide operations (accounts, auth, connectRequests, webhooks, drafts, teams, notifications)
|
|
50
70
|
const { items: accounts } = await curviate.accounts.list();
|
|
51
71
|
|
|
52
72
|
// Account-scoped: all LinkedIn ops under a specific account
|
|
@@ -58,6 +78,176 @@ const me = await acc.users.get("me"); // your own profile
|
|
|
58
78
|
const profile = await acc.users.get("some-user-id"); // someone else's
|
|
59
79
|
```
|
|
60
80
|
|
|
81
|
+
## Notifications
|
|
82
|
+
|
|
83
|
+
`curviate.notifications` is Curviate telling you something needs attention: an account needs reconnecting, a Motion paused, an import finished, a teammate joined or left. Each person has their own list and read state. Check it at session start:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const { unread_count } = await curviate.notifications.unreadCount();
|
|
87
|
+
if (unread_count > 0) {
|
|
88
|
+
const { items } = await curviate.notifications.list({ filter: "unread" });
|
|
89
|
+
for (const n of items) console.log(n.type, n.title);
|
|
90
|
+
// Mark only what you saw: one that arrived since the list stays unread.
|
|
91
|
+
await curviate.notifications.markAllRead({ before: items[0]!.created_at });
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`type` is an open list: tolerate values you do not know. `curviate.account(id).notifications` is something else: that account's LinkedIn notifications.
|
|
96
|
+
|
|
97
|
+
## Teams
|
|
98
|
+
|
|
99
|
+
`curviate.teams` manages the team the credential acts in: `getCurrent`, `updateCurrent`, `listMembers`, `updateMember` (role), `removeMember` (`"me"` leaves), `createInvites`, `listInvites`, `resendInvite`, `revokeInvite`, plus `list` for every team you belong to. Member and invite writes are for Owners (`INSUFFICIENT_ROLE` otherwise). `curviate.accounts.setTeamAccess(accountId, false)` keeps an account you connected to yourself.
|
|
100
|
+
|
|
101
|
+
## Tiers
|
|
102
|
+
|
|
103
|
+
A Team's Tier decides which features it has. Drafts, Contacts, Motions (and Network and Unibox) need Automate: a write in one of those groups on a Team below it is refused with `TIER_REQUIRED`, before the request is even validated. Reads, exports, deletes and stopping running work (`motions.pause`, `motions.archive`, `enrollments.stop`) pass on any Tier. The refusal names what is missing:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
try {
|
|
107
|
+
await curviate.motions.start("mot_123");
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (isCurviateError(err) && err.code === "TIER_REQUIRED") {
|
|
110
|
+
console.warn(`${err.feature} needs ${err.requiredTier}; this team has ${err.currentTier ?? "no Tier"}.`);
|
|
111
|
+
// A team Owner also gets err.upgradeUrl; any other role has none and should ask an Owner.
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { tier, features } = await curviate.tier.get(); // check first: no write is sent
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`curviate.tier.get()` returns the Tier now (`null` for none), the bought Tier, the free trial or granted Tier that raises it, a downgrade due at the next renewal (`pending`), and the `features` it includes. It reports no prices. A Motion or a sync that stopped for the Tier reads `pause_reason` / `paused_reason: "tier_required"`.
|
|
119
|
+
|
|
120
|
+
## Contacts, lists and Tags
|
|
121
|
+
|
|
122
|
+
`curviate.contacts` keeps the people your team works with: a LinkedIn URL plus your own fields, Tags and Contact lists. Any spelling of one person's URL resolves to one Contact, so `upsert` is safe to repeat:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const { id, outcome } = await curviate.contacts.upsert({
|
|
126
|
+
linkedin_url: "https://www.linkedin.com/in/ana/",
|
|
127
|
+
job_title: "CTO",
|
|
128
|
+
tags: ["warm"],
|
|
129
|
+
list_name: "Q4 webinar", // created if new
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Bulk: rows or a CSV string, up to 10,000 rows. Over 500 runs in the background.
|
|
133
|
+
const csv = await readFile("leads.csv", "utf8"); // node:fs/promises
|
|
134
|
+
const imp = await curviate.contacts.imports.create({ csv, list_name: "Q4 webinar" });
|
|
135
|
+
// Once `status` is `completed` (poll `imports.get` for a background import):
|
|
136
|
+
const { items: skipped } = await curviate.contacts.imports.rows(imp.id, { outcome: "skipped" });
|
|
137
|
+
|
|
138
|
+
await curviate.contacts.updateMany({ contact_ids: [id], add_tags: ["vip"] });
|
|
139
|
+
const all = await curviate.contacts.export({ format: "json", tag: "vip" });
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`contacts.enrich` fills profile fields from what Curviate already stores, else queues paced live profile views from an account you name: a live view is a real profile view the person may see. `curviate.contactLists` and `curviate.tags` rename and delete; a Tag has no create call, the first write naming it creates it. Refusals: `NAME_TAKEN`, `RESOURCE_IN_USE` (a live Motion or a running import uses it), `CONTACT_LIMIT_REACHED`, `IMPORT_QUEUE_FULL`. Deletes are for Owners and Managers.
|
|
143
|
+
|
|
144
|
+
## Sequences
|
|
145
|
+
|
|
146
|
+
A Motion runs one Sequence: a JSON document of steps (`do`, `wait`, `until`, `if`, `tag`, and the legacy `emit`), run from the first step and ended explicitly with `"end"`. The SDK has an offline module for it: a typed builder that emits exactly that document, and `validateSequence`, which gives the verdict the API gives. Nothing in it touches the network, a key or the file system, so it runs in CI, browsers and edge functions. It needs TypeScript 5.0 or later.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { Sequence, step } from "@curviate/sdk";
|
|
150
|
+
|
|
151
|
+
const seq = Sequence.create({ on_reply: "end" }).add(
|
|
152
|
+
step.do("invite", "invite.send", { with: { note: null, withdraw_after: "21d" }, event: "invite_sent" }),
|
|
153
|
+
step.until("accepted", "invite.accepted", { timeout: "21d", then: "opener", else: "end" }),
|
|
154
|
+
step.do("opener", "message.send", {
|
|
155
|
+
variants: [
|
|
156
|
+
{ id: "named", text: "Hi {{first_name}}, {{opener}}" },
|
|
157
|
+
{ id: "plain", text: "{{opener}}" },
|
|
158
|
+
],
|
|
159
|
+
with: { fallback: "Hi, thanks for connecting." },
|
|
160
|
+
}),
|
|
161
|
+
step.until("r_opener", "message.replied", { timeout: "4d", then: "end" }),
|
|
162
|
+
step.if("seen", { "message.seen": true }, { then: "bump", else: "end" }),
|
|
163
|
+
step.do("bump", "message.send", { with: { text: "{{followup}}", fallback: "Quick follow-up in case this got buried." }, next: "end" }),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const document = seq.toDocument(); // the Sequence document, keys in canonical order
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Field names are the document's keys (`on_reply`, `note_fallback`, `withdraw_after`). Variables: `{{first_name}}` (any standard Contact field), `{{my_first_name}}`, `{{my_last_name}}`, `{{my_full_name}}` (the sending account), and any other `{{<key>}}` is the custom field `contact.extra.<key>`; the long forms `{{contact.first_name}}`, `{{contact.extra.opener}}` and `{{account.first_name}}` stay valid. A messaging step takes one text in `with` or exactly 2 `variants` (A/B). An optional `event` on a `do` or `tag` step sends a custom event each time the step runs; `until event.<name>` waits for one your agent sends (`enrollments.events`). With literal arguments, the type checker refuses what the API would: a typo'd action, event, reaction or predicate, a `{{...}}` that is not a variable (`{{firstName}}`, `{{my_title}}`, a `| default:` filter), a text with a variable and no fallback, one variant, a duration like `"1w"`, the reserved id `"end"`, and a jump to a step you never added (at `toDocument()`). Text the type checker cannot see (a `string` variable) is checked when you call `validate()` or `toDocument()`.
|
|
170
|
+
|
|
171
|
+
Every builder operation (`add`, `insertBefore`, `replace`, `remove`, `onReply`) returns a new builder. `toDocument()` throws `SEQUENCE_INVALID` with every error in `err.errors`, the same list the API returns: `path`, `rule`, and the compiler-style `code` (`R1` to `R24`), `pointer` (a JSON Pointer into the document), `hint` (the fix) and `docs_url`. Warnings (`W1` to `W13`) never block, read them with `validate()`. `JSON.stringify(seq)` equals `JSON.stringify(seq.toDocument())`. `newStepId(document, kind)` names a new step as the dashboard editor does (`message`, `message_2`, `w_<step>` for a wait before a step).
|
|
172
|
+
|
|
173
|
+
Raw JSON stays first class. Validate a document you wrote by hand, or load one (a Motion's `sequence`) to edit it:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { Sequence, step, validateSequence } from "@curviate/sdk";
|
|
177
|
+
|
|
178
|
+
const { errors, warnings } = validateSequence(JSON.parse(text));
|
|
179
|
+
for (const e of errors) console.error(`${e.path}: ${e.rule} (${e.code}: ${e.hint})`);
|
|
180
|
+
|
|
181
|
+
const edited = Sequence.fromDocument(motion.sequence) // throws SEQUENCE_INVALID when invalid
|
|
182
|
+
.insertBefore("opener", step.wait("settle", "1d"))
|
|
183
|
+
.toDocument();
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Deprecated forms are accepted with warning `W13` until 2027-01-19, then refused: 3 to 5 variants (use 2), `until signal.<name>` (use `until event.<name>`) and `{{motion.<key>}}` (write the text in place or use a custom field; offline it also gives `W9`). Warnings that need the Motion's sending accounts or settings come from the API only. `SEQUENCE_SCHEMA_V1` is the published JSON Schema (`SEQUENCE_SCHEMA_URL`) for editors and tooling. The validator knows the vocabulary of the schema it ships with: a document using an action or event added to the API later is refused offline while the API accepts it; upgrade the SDK.
|
|
187
|
+
|
|
188
|
+
## Motions and Enrollments
|
|
189
|
+
|
|
190
|
+
A Motion runs one Sequence for the Contacts of its Contact lists, on its sending accounts, inside its schedule. `sequence` in `motions.create` and `motions.update` takes a builder or a document:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { Sequence, step } from "@curviate/sdk";
|
|
194
|
+
|
|
195
|
+
const motion = await curviate.motions.create({
|
|
196
|
+
name: "Q4 webinar follow-up",
|
|
197
|
+
contact_list_ids: ["cl_123"],
|
|
198
|
+
account_ids: ["acc_123"],
|
|
199
|
+
schedule: { timezone: "Europe/Berlin" },
|
|
200
|
+
sequence: seq, // a builder: converted with toDocument(); an invalid one throws SEQUENCE_INVALID, nothing is sent
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const { paths, samples, projection } = await curviate.motions.preview(motion.id); // a read: nothing is sent to LinkedIn
|
|
204
|
+
await curviate.motions.start(motion.id);
|
|
205
|
+
|
|
206
|
+
// Edit later: load the stored document into a builder. if_sequence_revision refuses the save
|
|
207
|
+
// with SEQUENCE_CONFLICT (the stored document in err.current) when someone saved since.
|
|
208
|
+
const current = await curviate.motions.get(motion.id);
|
|
209
|
+
await curviate.motions.update(motion.id, {
|
|
210
|
+
sequence: Sequence.fromDocument(current.sequence).insertBefore("opener", step.wait("settle", "1d")),
|
|
211
|
+
if_sequence_revision: current.sequence_revision,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Enrollments: one Contact's run on one account.
|
|
215
|
+
const { items: failed } = await curviate.enrollments.list({ motion_id: motion.id, done_step: "opener", outcome: "failed" });
|
|
216
|
+
await curviate.enrollments.bulk({ action: "retry", ids: failed.map((e) => e.id) }); // one result per Enrollment
|
|
217
|
+
await curviate.enrollments.retry("enr_123"); // one: throws NOTHING_TO_RETRY unless its Step failed
|
|
218
|
+
await curviate.enrollments.events("enr_123", { name: "research_ready" }); // moves an `until event.research_ready`
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
A plain object goes as is and the API validates it; wrap hand-written JSON in `Sequence.fromDocument(json)` to have it checked offline first. `sequence: null` clears a draft's. Status operations: `start`, `pause`, `resume`, `archive` (irreversible), `delete` (drafts only); `addContacts` / `removeContacts` enroll or take out Contacts directly; `getSequenceVersion` reads a past version. `curviate.enrollments` lists one Contact's run per account (`list`, filtered by status, Step, Sequence version, or how a Step ended: `done_step` with `outcome`, `reason`, `variant_id`, `relationship_at_enrollment`; `get` with its Step history) and acts on it: `pause`, `resume()` (lift your pause), `retry(id)` (retry the failed Step; it acts only on a failure, else throws `NOTHING_TO_RETRY`, so a repeated call never sends a delivered message again), `resume({ at })` (an explicit jump to a Step, not guarded: it re-enters a Step that already sent; use `retry` for a failure), `events({ name })`, `reassign({ account_id })`, `stop`, and `bulk` for up to 1,000 at once by ids or filter. `signal` is the deprecated name of `events`, accepted until 2027-01-19. Refusals: `MOTION_NOT_STARTABLE` (every gap in `err.errors`), `MOTION_STATUS_CONFLICT`, `ENROLLMENT_NOT_ACTIVE`, `SEQUENCE_INVALID`, `SEQUENCE_CONFLICT`, `STEP_IN_FLIGHT` (retry in a minute), `NOTHING_TO_RETRY` (from `retry`, or per Enrollment in `bulk`), `ACCOUNT_ALREADY_ENGAGED`, `ALREADY_ENROLLED_ON_ACCOUNT`, `KNOWN_CHAT`.
|
|
222
|
+
|
|
223
|
+
## Network, Unibox and Analytics
|
|
224
|
+
|
|
225
|
+
Reads here come from what Curviate has stored: no LinkedIn call, no limit used. Lists with a cursor resolve to their first page when awaited and yield every item across pages under `for await`:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
// Stored Connections of several accounts; read coverage before trusting an empty page.
|
|
229
|
+
const { items, coverage } = await curviate.network.connections.list({ account_id: ["acc_1", "acc_2"] });
|
|
230
|
+
for await (const c of curviate.network.connections.list({ q: "founder" })) console.log(c.public_profile_url);
|
|
231
|
+
const { items: sync } = await curviate.network.syncStatus();
|
|
232
|
+
await curviate.network.resync({ account_id: "acc_1" }); // once per account per 24 h
|
|
233
|
+
|
|
234
|
+
// One inbox over every account you can operate.
|
|
235
|
+
for await (const chat of curviate.unibox.list({ unread: true })) console.log(chat.chat_id);
|
|
236
|
+
const { unread_chats } = await curviate.unibox.unreadCount();
|
|
237
|
+
await curviate.unibox.accounts.update("acc_1", { sync_full_inbox: true }); // by the person who connected it
|
|
238
|
+
await curviate.unibox.refresh();
|
|
239
|
+
await curviate.unibox.tagContact("acc_1", "chat_1", { tags: ["warm"] });
|
|
240
|
+
await curviate.unibox.enroll("acc_1", "chat_1", { motion_id: "mot_1" });
|
|
241
|
+
|
|
242
|
+
// Motion results, activity in budget units, the events feed.
|
|
243
|
+
const overview = await curviate.analytics.motions.list({ from: "2026-10-01", to: "2026-10-31" });
|
|
244
|
+
const detail = await curviate.analytics.motions.get("mot_1");
|
|
245
|
+
const { types } = await curviate.analytics.activity(); // per BudgetRow; never add rows together
|
|
246
|
+
for await (const event of curviate.analytics.events.list({ kind: ["message.replied"] })) console.log(event.motion_name);
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Needs Automate: `network.resync`, `unibox.refresh`, `unibox.tagContact`, `unibox.enroll` and turning full-inbox sync on (off passes on any Tier). Messages and actions on a chat stay on `curviate.account(id).messaging`.
|
|
250
|
+
|
|
61
251
|
## Drafts and scheduled posts
|
|
62
252
|
|
|
63
253
|
A Draft is a stored, editable, unpublished post. Give it an account and a `scheduled_at` and Curviate publishes it at that time. Drafts are tenant-wide, so `drafts` hangs off the root client (a Draft's `account_id` is a field, and optional). `acc.posts.create()` still publishes immediately and does not schedule.
|
|
@@ -150,6 +340,11 @@ try {
|
|
|
150
340
|
console.warn(`${err.budgetRow} is spent until ${err.resetAt ?? "no fixed time"}`);
|
|
151
341
|
console.warn(`change ${err.safetyHint?.parameter} to lift it`);
|
|
152
342
|
break;
|
|
343
|
+
case "TIER_REQUIRED":
|
|
344
|
+
// The team's Tier lacks err.feature; err.requiredTier has it. A team
|
|
345
|
+
// Owner also gets err.upgradeUrl. See "Tiers" above.
|
|
346
|
+
console.warn(`${err.feature} needs ${err.requiredTier}`);
|
|
347
|
+
break;
|
|
153
348
|
case "ACCOUNT_NOT_FOUND":
|
|
154
349
|
console.error("Account does not exist for this tenant.");
|
|
155
350
|
break;
|
|
@@ -169,6 +364,8 @@ try {
|
|
|
169
364
|
}
|
|
170
365
|
```
|
|
171
366
|
|
|
367
|
+
No response within the client's `timeout` throws `CLIENT_TIMEOUT` (not `INTERNAL`): a read is retried first and carries `retryLikelyToSucceed: true`; a write is not retried, carries `false`, and may still have reached the API, so read back before resending. A network failure with no response is `INTERNAL` with no `httpStatus`.
|
|
368
|
+
|
|
172
369
|
Every error code is documented in the [API reference](https://docs.curviate.com). The
|
|
173
370
|
exported `ErrorCode` type is the complete set, so `tsc` tells you when a `switch` over
|
|
174
371
|
`err.code` has missed one.
|
|
@@ -308,7 +505,7 @@ may fetch.
|
|
|
308
505
|
|
|
309
506
|
## Cursor pagination
|
|
310
507
|
|
|
311
|
-
|
|
508
|
+
`network.connections.list`, `unibox.list` and `analytics.events.list` iterate themselves (`for await (const x of curviate.unibox.list())`). For the other lists, `curviate.paginate()` is an async iterator that follows the `cursor` field automatically, so you pull items one at a time without managing cursors:
|
|
312
509
|
|
|
313
510
|
```ts
|
|
314
511
|
// Iterate over every chat across all pages
|