@curviate/sdk 0.43.0 → 0.45.0-next.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 +424 -1
- package/README.md +106 -28
- package/dist/index.d.ts +16279 -4766
- package/dist/index.js +3020 -709
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,430 @@ Versioning: semantic. Minor for additive changes, patch for bug fixes; no stabil
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Unreleased
|
|
11
|
+
|
|
12
|
+
### Save Connections as Contacts
|
|
13
|
+
|
|
14
|
+
- The account (`accounts.get`, `accounts.update`'s result) gains `save_connections_as_contacts` and `save_connections_paused_reason` (`"contact_limit_reached"` | `null`); both are absent on an account without Automate.
|
|
15
|
+
- `accounts.update` takes `save_connections_as_contacts`. Without Automate it throws `TIER_REQUIRED`.
|
|
16
|
+
- The generated types carry both fields (regenerated from the deployed document, staging 21b2531).
|
|
17
|
+
|
|
18
|
+
### Staging regen: Contact fields, roles, filters
|
|
19
|
+
|
|
20
|
+
- **Contact `company_linkedin_url`** (`string | null`, read-only): the current company's LinkedIn page, filled with `company` from a profile read. Served on every Contact read and export.
|
|
21
|
+
- **Contact `email`** reads as what you supplied, else the first email a LinkedIn profile read returned (yours is never overwritten). Description only; webhook payloads now deliver fields they stripped before (no schema change).
|
|
22
|
+
- **`contacts.list` / `contacts.export` `account_id`** takes up to 50 account ids (an array or a comma list; one id is unchanged). The bulk `filter` body takes a string or an array.
|
|
23
|
+
- **`person_name`** (`string | null`) on `analytics.events.list` items and the `accounts.overview` activity items: the person of a direct action, name only.
|
|
24
|
+
- **Same shape, new meaning:** `teams.listMembers` `name` falls back to the email (`null` only when neither is known). Managers now rename the team, set or remove its logo, manage Member invites, remove Members and change the Team safety policy (JSDoc and the `INSUFFICIENT_ROLE` text).
|
|
25
|
+
- **Same shape, new text:** `photo_url` is a stored copy refreshed daily, so it does not expire. The seeded active hours fall back to Monday to Friday 07:00 to 22:00.
|
|
26
|
+
|
|
27
|
+
### Served schema fixes (regenerated types)
|
|
28
|
+
|
|
29
|
+
- `GET /v1/billing/seats` now has a typed 200 response: each seat carries `lent_to` / `lent_from` (`{team_id, team_name}` or null).
|
|
30
|
+
- `Error.errors[]` items gain `line` and `col` on `WEBHOOK_TEMPLATE_INVALID`; their `severity` is `error` only on a refusal (warnings come back in `warnings[]` on success).
|
|
31
|
+
- The safety-policy write fields `timezone` and `activity_window_timezone` are typed `null`: any other value is a `400`.
|
|
32
|
+
- `DO_NOT_CONTACT` (409) and `SEAT_LENT` (409) are declared on the routes that answer them.
|
|
33
|
+
|
|
34
|
+
### Webhooks v2, incoming events, Enrollment values, the surface header
|
|
35
|
+
|
|
36
|
+
#### Changed (same shape, new meaning)
|
|
37
|
+
|
|
38
|
+
- **A delivery's `id` is the delivery id**, the same on every attempt and every redelivery (it changed per attempt before). It is now the key to de-duplicate retries on; the old `event` + `account_id` + `occurred_at` composite still works. `delivered_at` is the time the body was rendered, the same on every attempt (it was the attempt's time); the signature's `t=` stays the attempt's time.
|
|
39
|
+
- **`request_url` reads back masked** (`https://host/•••`): the path and query are never returned. Sending the masked value back on `webhooks.update` keeps the stored URL. New `request_url_fingerprint` changes when the URL does.
|
|
40
|
+
- **A `headers` entry with a key and no value keeps that header's stored value** on `webhooks.update`; a new key needs a value.
|
|
41
|
+
- **`emit` steps are first-class again** ("Send event"): `step.emit` is no longer described as legacy. `event` on a `do` or `tag` step stays.
|
|
42
|
+
- **`constructEvent` accepts any matching `v1`.** For 24 hours after a secret rotation `Curviate-Signature` carries two (`v1=<new>,v1=<old>`); either secret verifies, any other is still `invalid_signature`. Older SDKs check only the last `v1`: upgrade before you rotate.
|
|
43
|
+
- **Every request sends `Curviate-Client: sdk/<version>`**, once. Informational only (it shows which tools use a key). The new `client` option replaces the value, for a tool built on the SDK.
|
|
44
|
+
|
|
45
|
+
#### Added
|
|
46
|
+
|
|
47
|
+
- **Webhook deliveries:** `webhooks.deliveries.list(id, { failed, cursor, limit })` (last 30 days, newest first; `for await` pages) and `webhooks.deliveries.get(id, deliveryId)`, metadata only. `webhooks.redeliver(id, deliveryId)` sends the original bytes again, freshly signed, within `redeliverable_until` (receipt + 24 h).
|
|
48
|
+
- **`webhooks.rotateSecret(id)`:** a new secret, returned once, with `previous_valid_until`. Creator only.
|
|
49
|
+
- **`webhooks.test(id, { event })`:** a synthetic delivery for one subscribed event (default: the first), answering `delivery_id` and `test_id`.
|
|
50
|
+
- **Webhook fields:** `disabled_reason`, `failing_since`, `last_delivery_status_code`, `created_by`, `request_url_fingerprint`, `body_template`, `custom_event_names`, and `warnings` on an edit that sets a template.
|
|
51
|
+
- **`webhooks.variables({ source })`:** the variables a body template of that source may use; on `motion`, also the Enrollment values your Sequences save.
|
|
52
|
+
- **`validateWebhookTemplate(template, { source })` and `renderWebhookTemplate(template, context)`:** the API's own template lint and renderer, offline. `errors` equal what `WEBHOOK_TEMPLATE_INVALID` lists, each with `line` and `col`.
|
|
53
|
+
- **`incomingEvents`:** `create`, `list`, `get`, `update`, `delete`, `rotate`, `capture`, `arrivals`. A private URL your tools post JSON to, moving Enrollments waiting on `until event.<name>`.
|
|
54
|
+
- **`tenant.beta.get()` and `tenant.beta.set({ beta_enabled })`:** the Team's persisted beta consent (`set`: Owners only).
|
|
55
|
+
- **Sequences:** `save` (and `save_to_contact`) on `until event.<name>` waits, typed on `step.until` and the builder, number gates (`gte`, `lte`) on `enrollment.<name>` predicates, rules R25 to R28 and warnings W14 and W16 in `validateSequence`; every entry carries `severity`, and `candidates` for a did-you-mean. R29 is the API's only. The validator now runs the API's own rule code.
|
|
56
|
+
- **Enrollment `values`** on `enrollments.get`: what the Sequence's `save` maps took, empty once the Enrollment finishes.
|
|
57
|
+
- **Error codes:** `NOTHING_TO_REDELIVER` (409), `REDELIVERY_EXPIRED` (410), `WEBHOOK_DISABLED` (409), `DELIVERY_IN_FLIGHT` (409, retryable), `WEBHOOK_TEMPLATE_INVALID` (422), `INCOMING_URL_RETIRED` (410). Earlier SDKs decode them as `INTERNAL`. `errors[]` entries type `severity`, `candidates`, `line` and `col`.
|
|
58
|
+
|
|
59
|
+
### Unibox v2
|
|
60
|
+
|
|
61
|
+
- **Same shape, new behaviour, `listMessages`:** for an API key or MCP grant, `quoted` is omitted when the quoted message is from before a Motion chat's visible start.
|
|
62
|
+
|
|
63
|
+
#### Added
|
|
64
|
+
|
|
65
|
+
- **Message `origin` and `is_deleted`:** a message the connected account sent carries `origin` (`motion`, `unibox`, `api` or `linkedin`). It is omitted when unknown, absent on a received message and never on a chat's `last_message`. `is_deleted` is `true` for a deleted message.
|
|
66
|
+
- **`marked_unread`** on a Unibox chat: `{origin: "unibox" | "api" | "linkedin", at}` when the chat was made unread without a newer message from the other person, else `null`.
|
|
67
|
+
- **`unibox.list` options:** `person` (one Contact id or LinkedIn id), `motion_ids` also takes `"none"` (chats no Motion links), `include_total: true` adds `total` (the chats matching the filters, not the page).
|
|
68
|
+
- **Disconnected Accounts** appear in `unibox.accounts.list()` and `unibox.list()` (`account_status: "disconnected"`), read-only. Their messages come from the stored copy: `coverage.complete` and attachment `unavailable`. A write on one throws `REAUTH_REQUIRED` (409).
|
|
69
|
+
- **Webhook payload types:** `chat.updated` is `ChatUpdatedPayload` (always `chat_id`; `unread` and `origin` on a change of read state). `message.read`, `message.reaction`, `message.edited` and `message.deleted` are `OwnMessagePayload` (optional `origin`); `message.received` never carries `origin`. New exports `ChatUpdatedPayload`, `OwnMessagePayload`, `MessageOrigin`.
|
|
70
|
+
- **`analytics.events.list`:** `contact_id` keeps one person's events (`RESOURCE_NOT_FOUND` for an unknown, other-team or merged-away id); `origin` takes a comma list or an array.
|
|
71
|
+
- **Analytics `origin`** is `motion`, `unibox` or `api`; `direct` stays valid as an alias for `unibox` and `api`.
|
|
72
|
+
|
|
73
|
+
#### Changed
|
|
74
|
+
|
|
75
|
+
- **`unibox.refresh` cooldown is 2 minutes** (was 5). It also covers accounts with Motion chats or Motion outreach but no full-inbox sync (`not_synced` only for an account with none of those), and `sync.next_run_at` for a `complete` account is the next poll tick. Same shape, new meaning.
|
|
76
|
+
- **A send marks the chat read.** `startChat`, `sendMessage` and `sendInMail` through the API turn the chat's `unread` to `false` and clear `marked_unread`, and `chat.updated` reports it. A Motion send leaves the read state as it is.
|
|
77
|
+
- The `unibox.refresh` JSDoc no longer says "5 minutes".
|
|
78
|
+
|
|
79
|
+
### Daily counts and the raw reach
|
|
80
|
+
|
|
81
|
+
- **Analytics windows reach 3650 days** on `analytics.motions.list` / `get` and `accounts.overview` (a longer one is `400 INVALID_REQUEST` naming `from`; the old 365-day cap is gone). Beyond 12 months you get daily counts only. New `window.counts_only_through` and `window.paths_through` (`YYYY-MM-DD | null`): past `paths_through`, `paths` does not add up to the totals. `accounts.metrics` reads up to 3650 days.
|
|
82
|
+
- **`enrollments.get`:** new `history_truncated` (boolean). `history` reaches back 12 months, plus what `retry` reads.
|
|
83
|
+
- **Activity exports:** a window past the 12-month raw reach is `400 INVALID_REQUEST` naming `from`.
|
|
84
|
+
|
|
85
|
+
### Settings v2
|
|
86
|
+
|
|
87
|
+
#### Breaking
|
|
88
|
+
|
|
89
|
+
- **Limits are always enforced.** The warn posture is gone: `posture`,
|
|
90
|
+
`posture_source` and `tenant_default_posture` leave every read (the
|
|
91
|
+
safety policy and the `quotas[]` rows; on the policy write they remain
|
|
92
|
+
only as removed keys), and no success body carries a
|
|
93
|
+
`safety_warning` any more (`PostPublishedPayload.safety_warning` removed).
|
|
94
|
+
At a ceiling the action is refused with `BUDGET_EXHAUSTED`; to send more,
|
|
95
|
+
raise the limit. A policy write that still sends `posture` or
|
|
96
|
+
`tenant_default_posture` is accepted with a `POSTURE_REMOVED` notice until
|
|
97
|
+
2027-01-25 when it says `"enforce"` or `null`; `"warn"`, or any value after
|
|
98
|
+
that day, is `400 INVALID_REQUEST` naming the key in `details.removed_key`.
|
|
99
|
+
- **`hint.parameter` on an `activity_window` refusal** is now `active_hours`,
|
|
100
|
+
`rows[<row>].activity_window_start` / `_end`, or on the Team policy
|
|
101
|
+
`profiles[<profile>].rows[<row>].activity_window_start` / `_end`. It was
|
|
102
|
+
the bare `activity_window_start` / `activity_window_end`.
|
|
103
|
+
|
|
104
|
+
#### Added
|
|
105
|
+
|
|
106
|
+
- **`err.details`** on `CurviateError` (type `CurviateErrorDetails`): the
|
|
107
|
+
error body's per-code details, `removed_key` on the posture refusal among
|
|
108
|
+
them.
|
|
109
|
+
- **`active_hours`** at the top level of the account and Team safety policy:
|
|
110
|
+
`{days, start, end, source}` or `null`, where `source` (`account`, `team`
|
|
111
|
+
or `default`) is read-only. Write `{days, start, end}` or `null`.
|
|
112
|
+
- **`activity_window_source`** on every policy row, both scopes, read-only:
|
|
113
|
+
`account_row`, `account`, `team_row`, `team` or `default`.
|
|
114
|
+
|
|
115
|
+
#### Changed
|
|
116
|
+
|
|
117
|
+
- **`activity_window_start` and `activity_window_end`** on a policy row now
|
|
118
|
+
report the effective edges after the chain (row, account, Team, default).
|
|
119
|
+
Same shape, wider meaning: a row with no edges of its own reads the edges
|
|
120
|
+
it inherits.
|
|
121
|
+
- **`blocked`** on `CurviateError` is always `true` on a `BUDGET_EXHAUSTED`;
|
|
122
|
+
there is no `blocked: false` success payload.
|
|
123
|
+
|
|
124
|
+
### Account safety
|
|
125
|
+
|
|
126
|
+
#### Breaking
|
|
127
|
+
|
|
128
|
+
- **`activity_window`** on `accounts.get` (and the safety policy read)
|
|
129
|
+
loses `timezone` and `timezone_unset`. `effective_timezone` is
|
|
130
|
+
`string | null` (null while the zone is unknown); `source` is `country`,
|
|
131
|
+
`region`, `detected` or `none`; new `zone_state` (`known`, `pinned`,
|
|
132
|
+
`unknown`) and `fallback_timezones` (the candidate zones while unknown;
|
|
133
|
+
empty means the window is closed). `network.syncStatus()`'s `read_window`
|
|
134
|
+
carries the same four fields in place of `timezone` and `source`. The zone
|
|
135
|
+
follows where the account connects from: nobody sets it.
|
|
136
|
+
- **A time zone on the safety policy is refused.** The policy write's
|
|
137
|
+
`timezone` and a row's `activity_window_timezone` are `400 INVALID_REQUEST`
|
|
138
|
+
unless `null` (accepted and ignored, so an older document round-trips).
|
|
139
|
+
Change the account's `country` or own `proxy` instead.
|
|
140
|
+
- **`warm_up`**: `triggers` is now `connect`, `dormant` or
|
|
141
|
+
`post_restriction` (`account_age` and `connections` removed);
|
|
142
|
+
`unevaluated` removed. `state: "ramping"` keeps its shape and now follows
|
|
143
|
+
those triggers.
|
|
144
|
+
|
|
145
|
+
#### Added
|
|
146
|
+
|
|
147
|
+
- **`warm_up.enabled`** (default `true`, set with
|
|
148
|
+
`PATCH /v1/{account_id}/safety-policy` and `warm_up: { enabled }`; a
|
|
149
|
+
Team-scope `warm_up` is a 400) and **`warm_up.dormant`** on the account and
|
|
150
|
+
the policy read.
|
|
151
|
+
- **`accounts.getSafetyPolicy(accountId)` and
|
|
152
|
+
`accounts.updateSafetyPolicy(accountId, body)`**
|
|
153
|
+
(`GET` / `PATCH /v1/{account_id}/safety-policy`), typed from the generated
|
|
154
|
+
policy types. `updateSafetyPolicy(id, { warm_up: { enabled } })` is the
|
|
155
|
+
warm-up switch.
|
|
156
|
+
- **`warm_up: false`** on `auth.intent` and `connectRequests.create` connects
|
|
157
|
+
a new account with warm-up off. With `account_id` it is
|
|
158
|
+
`400 INVALID_REQUEST`.
|
|
159
|
+
- **`connection_location`** gains `checked_at`, `detected_country` and
|
|
160
|
+
`flags` (`country_mismatch`, `zone_moved`). No IP field anywhere.
|
|
161
|
+
|
|
162
|
+
#### Changed
|
|
163
|
+
|
|
164
|
+
- **`auth.intent` `timezone`** is `@deprecated` and ignored.
|
|
165
|
+
- **`err.resetAt`** is also `null` on an `activity_window` refusal while the
|
|
166
|
+
account's zone is unknown with no candidate zone (three null cases now).
|
|
167
|
+
`hint.parameter` never names `timezone` or `activity_window_timezone`.
|
|
168
|
+
- **Docs:** a live write outside the activity window is refused with
|
|
169
|
+
`BUDGET_EXHAUSTED` (`safetyReason: "activity_window"`); reads never are.
|
|
170
|
+
|
|
171
|
+
### Connect v2 (checkpoints, connect refusals)
|
|
172
|
+
|
|
173
|
+
#### Breaking
|
|
174
|
+
|
|
175
|
+
- **New `challenge_type` values** `checking` (LinkedIn is checking the
|
|
176
|
+
sign-in: poll it, nothing to answer) and `phone_register` (solve with
|
|
177
|
+
`(+CC)number`; it was `422 CHECKPOINT_UNSUPPORTED`). Breaking for callers
|
|
178
|
+
that branch on that 422 or switch exhaustively over `challenge_type`: add
|
|
179
|
+
both arms.
|
|
180
|
+
- **`auth.pollCheckpoint` never answers `status: "expired"`** and carries no
|
|
181
|
+
`recovery_hint`. Past `expires_at` (also after the expiry sweep, or when it
|
|
182
|
+
fires mid-poll) solve, poll, resend and request throw
|
|
183
|
+
`409 CHECKPOINT_EXPIRED`. Breaking for callers branching on either.
|
|
184
|
+
- **`CHECKPOINT_EXPIRED` is 409 everywhere** (it was 422 on some solve
|
|
185
|
+
paths), and a pending checkpoint that times out answers it too. Breaking
|
|
186
|
+
for callers branching on a 422 `CHECKPOINT_EXPIRED`.
|
|
187
|
+
- **Connect refusals:** a cookie connect rejected for any reason is
|
|
188
|
+
`401 LINKEDIN_COOKIE_INVALID`; the account cap is `503 PLATFORM_ERROR`
|
|
189
|
+
with `retryLikelyToSucceed: false` (`SUBSTRATE_CAP_REACHED` is no longer
|
|
190
|
+
sent; it stays in `ERROR_CODES` for older deployments); an intent that
|
|
191
|
+
times out before any checkpoint is `503 LINKEDIN_SERVICE_UNAVAILABLE`; a
|
|
192
|
+
poll's terminal refusal throws its own code (a transient error still
|
|
193
|
+
answers `pending`).
|
|
194
|
+
|
|
195
|
+
#### Added
|
|
196
|
+
|
|
197
|
+
- **`LINKEDIN_SIGN_IN_REQUIRED`** (403, user_fixable): LinkedIn wants the
|
|
198
|
+
person to sign in on linkedin.com first.
|
|
199
|
+
- **`channel`** (`email`, `sms`, `whatsapp` or `null`) on `otp` and
|
|
200
|
+
`otp_or_mobile_app_approval` checkpoints: where LinkedIn sent the code.
|
|
201
|
+
- **`user_languages`** on `auth.intent` (cookie connects only, at most 5
|
|
202
|
+
BCP 47 tags, each at most 35 characters).
|
|
203
|
+
- **`contract_selected`** on the connect 201, solve, poll `active` and the
|
|
204
|
+
reconnect 200 when `recruiter_contract_id` was sent.
|
|
205
|
+
|
|
206
|
+
#### Changed
|
|
207
|
+
|
|
208
|
+
- **`TRY_ANOTHER_WAY`** on `auth.solveCheckpoint`: same shape, new meaning.
|
|
209
|
+
It asks LinkedIn for another method and counts no attempt; it is
|
|
210
|
+
`400 INVALID_REQUEST` on `contract_selection`, `challenge_selection`,
|
|
211
|
+
`phone_register` and `checking`. Solve `code` accepts `( ) +`.
|
|
212
|
+
- **`expires_at`** is when Curviate stops accepting an answer; a chained
|
|
213
|
+
checkpoint keeps the sign-in's original `expires_at`.
|
|
214
|
+
- **`recruiter_contract_id`** is selected once the account is connected;
|
|
215
|
+
`contract_selected: false` means connected, not selected.
|
|
216
|
+
|
|
217
|
+
### Motions setup and engine v2
|
|
218
|
+
|
|
219
|
+
#### Breaking
|
|
220
|
+
|
|
221
|
+
- **`schedule.timezone` and `settings.stop_on_engagement` are retired.**
|
|
222
|
+
Days and hours are read in each sending account's own time zone. Both keys
|
|
223
|
+
are accepted with warning `W13` until 2027-01-25, never stored, and gone
|
|
224
|
+
from every Motion read; `stop_on_engagement: true` with an acting reply
|
|
225
|
+
route is a 400. `motions.start` no longer reports a missing time zone.
|
|
226
|
+
- **`settings.acceptance_floor` defaults to `null`** (off).
|
|
227
|
+
- **`finish_reason` gains `engaged_elsewhere`**: the Contact replied to, or
|
|
228
|
+
was messaged from, another account of the Motion. Add the arm to an
|
|
229
|
+
exhaustive switch.
|
|
230
|
+
|
|
231
|
+
#### Added
|
|
232
|
+
|
|
233
|
+
- **Several accounts per list read:** `account_id` on `motions.list`,
|
|
234
|
+
`enrollments.list` and the `enrollments.bulk` filter takes several
|
|
235
|
+
accounts (comma list, up to 50; an all-empty list is a 400).
|
|
236
|
+
- **`motions.list({ sort: "activity" })`** (latest step record first;
|
|
237
|
+
`updated` stays the default) and `last_activity_at` on every item. A
|
|
238
|
+
cursor belongs to its sort.
|
|
239
|
+
- **`enrollments.list({ step_id })`** takes up to 10 step ids (an array, or a
|
|
240
|
+
comma list); the bulk `filter.step_id` takes a string or an array.
|
|
241
|
+
- **Several mode** (`settings.accounts_per_contact` above 1): settings
|
|
242
|
+
`on_reply_elsewhere` (`stop` or `continue`), `withdraw_on_reply_elsewhere`
|
|
243
|
+
and `account_gap` (`1d` to `30d`), served only in that mode.
|
|
244
|
+
`accounts_per_contact` is locked once the Motion has started.
|
|
245
|
+
- **`motions.preview`**: `projection.audience` `{ total, skipped, to_enroll }`.
|
|
246
|
+
|
|
247
|
+
#### Changed
|
|
248
|
+
|
|
249
|
+
- **`motions.preview` `projection.contacts`** equals `audience.to_enroll`;
|
|
250
|
+
in Several mode `accounts[].contacts` counts Enrollments (same shape, new
|
|
251
|
+
meaning), and `projection.fits_before_end_at` is counted per account zone.
|
|
252
|
+
|
|
253
|
+
### Analytics v2, exports, Account fields
|
|
254
|
+
|
|
255
|
+
Types come from the served document (regenerated); the hand-written parts:
|
|
256
|
+
|
|
257
|
+
- **`analytics.export(params)`** streams the Analytics tables now
|
|
258
|
+
(`table`, `format`, the overview's filters) and resolves with the file's
|
|
259
|
+
bytes. **`exports.create` / `get` / `download`** run the same as a
|
|
260
|
+
background job: poll `status` to `ready`, then `download` within 24 hours;
|
|
261
|
+
`download` resolves with the file's exact bytes. One export per Team at a time. Types `Export`,
|
|
262
|
+
`ExportCreateBody`, `ExportGetResult`, `AnalyticsExportParams`.
|
|
263
|
+
- **`accounts.metrics(accountId, { from, to, timezone })`** (the Account's daily
|
|
264
|
+
snapshots, oldest first, at most 366 days; no LinkedIn call) and
|
|
265
|
+
**`accounts.overview(accountId, params)`** (one Account at a glance, the
|
|
266
|
+
overview grammar scoped to it).
|
|
267
|
+
- **Analytics options:** `account_id`, `motion_id`, `origin`, `include`,
|
|
268
|
+
`fields` and `sort` take an array as well as a comma list. Windows take
|
|
269
|
+
`range` (`7d`, `30d`, `90d`, `12m`, `all`) and `bucket`; `compare:
|
|
270
|
+
"previous_period"` adds `previous` and `delta` to every rate.
|
|
271
|
+
- **Same shape, new meaning, analytics:** every rate is the outcome SO FAR over
|
|
272
|
+
what was sent in the window, no longer over cohorts at least 7 days old; read
|
|
273
|
+
it with `maturing` and `low_sample`. Messages and InMails are counted apart
|
|
274
|
+
(`reply`, `inmail_reply`). Enrollment counts are by state right now and the
|
|
275
|
+
window does not change them; the state `pending` is now `queued` (list
|
|
276
|
+
filters accept `pending` until 2027-01-21).
|
|
277
|
+
- **Same shape, new meaning, `accounts.list`:** an item carries the profile,
|
|
278
|
+
`status_reason`, `warm_up`, `inmail_balance`, `motions_count`,
|
|
279
|
+
`motion_refs` and three `quotas` rows; reading the list never calls
|
|
280
|
+
LinkedIn.
|
|
281
|
+
- **`err.details` keys:** `account_id` on `TIER_REQUIRED` (one Account lacks
|
|
282
|
+
the feature while the Team has it; an Account on a seat lent from another
|
|
283
|
+
Team carries that seat's Tier: leave it out, or ask the Owner who lent the
|
|
284
|
+
seat) and `export_id` on `EXPORT_IN_PROGRESS`.
|
|
285
|
+
- **JSDoc:** `CHAT_NOT_VISIBLE` (409) on `messaging.startChat`,
|
|
286
|
+
`messaging.sendInMail`, `recruiter.startChat` and
|
|
287
|
+
`salesNavigator.startChat`: nothing is sent when the Account already has a
|
|
288
|
+
private conversation with the person that you may not read. The `EXPORT_*`
|
|
289
|
+
codes are documented on `exports` and in `ERROR_CODES`.
|
|
290
|
+
|
|
291
|
+
### Teams v2, seat lending
|
|
292
|
+
|
|
293
|
+
- **`teams.create({ name })`** returns the new Team and `next_step`: an
|
|
294
|
+
All-Teams key gets `{ team_header }` (the header line `X-Curviate-Team:
|
|
295
|
+
<team_id>`, usable at once; with this SDK pass the id to `withTeam`), any other
|
|
296
|
+
credential gets `{ command, url }`.
|
|
297
|
+
- **`teams.list({ include: "counts" })`** adds `tier`, `trial`, `accounts`,
|
|
298
|
+
`active_motions` and, on Owner rows, `seats` (`used`, `total`, `lent_out`,
|
|
299
|
+
`lent_in`). A dashboard session and an All-Teams key get counts on every
|
|
300
|
+
row; a Team key, or a token for one Team, only on its own row.
|
|
301
|
+
- **`teams.setLogo(png)`** (a PNG up to 1 MB; Owners only; `UNSUPPORTED_MEDIA_TYPE`
|
|
302
|
+
or `PAYLOAD_TOO_LARGE` otherwise), **`teams.removeLogo()`**, and `logo_url`
|
|
303
|
+
on every Team.
|
|
304
|
+
- **`billing.seats.lend({ team_id, seat_id? })`** and
|
|
305
|
+
**`billing.seats.return(seatId)`**. While a seat is lent, a connect naming
|
|
306
|
+
it or a cancel of it throws `SEAT_LENT`; a seat an Account still uses
|
|
307
|
+
throws `SEAT_NOT_EMPTY` on return.
|
|
308
|
+
|
|
309
|
+
### Do-not-contact list
|
|
310
|
+
|
|
311
|
+
- **`doNotContact.list` / `get` / `add` / `update` / `remove`** over the
|
|
312
|
+
Team-wide list. `add` takes `contact_id` or `linkedin_url` and an optional
|
|
313
|
+
`reason` (`asked_to_stop`, `existing_customer`, `competitor`, `partner`,
|
|
314
|
+
`employee`, `other`); a person already listed resolves with their entry.
|
|
315
|
+
`remove` is for Owners and Managers.
|
|
316
|
+
- **`DO_NOT_CONTACT` (409)** is documented on the writes it refuses:
|
|
317
|
+
`invites.send`, `messaging.startChat`, `sendMessage`, `sendInMail`,
|
|
318
|
+
`users.follow`, `endorseSkill`, `companies.followInvite`, `posts.react` and
|
|
319
|
+
the comment methods. `do_not_contact` joins the skip and finish reasons.
|
|
320
|
+
|
|
321
|
+
### Contacts and Network
|
|
322
|
+
|
|
323
|
+
- **`contacts.list` and `contacts.export`:** `list_id` takes up to 20 Contact
|
|
324
|
+
lists (an array or a comma list; any of them, or all with
|
|
325
|
+
`list_match: "all"`), plus `not_list_id` and `has_email`; `contacts.list`
|
|
326
|
+
adds opt-in `include_total` (`total` on the page).
|
|
327
|
+
- **`contacts.updateMany` and `contacts.deleteMany`** take exactly one of
|
|
328
|
+
`contact_ids` or `filter` (up to 10,000 matches; more throws
|
|
329
|
+
`INVALID_REQUEST` naming the count and changes nothing).
|
|
330
|
+
- **`contactLists.create({ name, from })`** starts a static list from a
|
|
331
|
+
filter.
|
|
332
|
+
- **`network.connections.list({ group: "person", include_total })`:** one item
|
|
333
|
+
per person (`object: "connection_person"`, with `accounts` and `contact`).
|
|
334
|
+
`Connection` is now a union of the two item shapes: branch on `object`.
|
|
335
|
+
- **`network.resync()`** needs no Account: with none it is Resync all and
|
|
336
|
+
resolves with `object: "network_resync_list"`. The result type is a union of
|
|
337
|
+
the two answers: branch on `object`.
|
|
338
|
+
- **Same shape, more often filled:** `location` on a Connection, and
|
|
339
|
+
`first_name` / `last_name` on `users.listRelations`, are non-null more often.
|
|
340
|
+
|
|
341
|
+
### Invoices (the billing part)
|
|
342
|
+
|
|
343
|
+
- **`billing.invoices.list(params)`** pages the Team's issued invoices (house
|
|
344
|
+
auto-pager over `items` / `cursor`; `await` for a page, `for await` for all;
|
|
345
|
+
Owners only) and **`billing.invoices.pdf(number)`** resolves with the PDF
|
|
346
|
+
bytes.
|
|
347
|
+
|
|
348
|
+
### All-Teams key
|
|
349
|
+
|
|
350
|
+
#### Added
|
|
351
|
+
|
|
352
|
+
- **`curviate.withTeam(team)`**: the same client acting in that Team. Every
|
|
353
|
+
call made through it sends `X-Curviate-Team: <team>`, overriding the
|
|
354
|
+
`team` client option, so one client can act per call in any of your
|
|
355
|
+
Teams. Works for every method, root and account-scoped.
|
|
356
|
+
- **`TEAM_REQUIRED`** (400): a key that works in all of your Teams called a
|
|
357
|
+
Team-level operation without naming the Team. Name one with `team` or
|
|
358
|
+
`withTeam`; a call on one of your accounts takes that account's Team.
|
|
359
|
+
|
|
360
|
+
#### Changed
|
|
361
|
+
|
|
362
|
+
- **`TEAM_ACCESS_DENIED`** and the `team` option copy: a key may work in one
|
|
363
|
+
Team or in all of a person's Teams. A Team key accepts only its own Team.
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## [0.44.0] - 2026-10-22
|
|
368
|
+
|
|
369
|
+
Dashboard v2 API. Fixture and types regenerated from deployed staging (server
|
|
370
|
+
`15b704443878c7b589e40c7955a202b5131a20f7`, 197 paths), and the Sequence
|
|
371
|
+
corpus re-vendored from the same server.
|
|
372
|
+
|
|
373
|
+
### Added
|
|
374
|
+
|
|
375
|
+
- **Contact `profile_picture_url`:** read-only, `string | null`. The person's
|
|
376
|
+
LinkedIn photo from profile data your team already holds; null when none is
|
|
377
|
+
held. The link is signed and expires.
|
|
378
|
+
- **Enrollments:** `enrollments.pause`, `enrollments.events` (send a custom
|
|
379
|
+
event to an `until event.<name>`), `enrollments.reassign` (move to another
|
|
380
|
+
sending account; `warnings` names Steps that will be skipped) and
|
|
381
|
+
`enrollments.bulk` (up to 1,000 by ids or filter, one result per
|
|
382
|
+
Enrollment, not atomic). `enrollments.resume` takes no body to lift your own
|
|
383
|
+
pause. `enrollments.retry(id)` retries a failed Step through the guarded
|
|
384
|
+
bulk path and throws `NOTHING_TO_RETRY` (or the Enrollment's other refusal)
|
|
385
|
+
as a `CurviateError`; `resume({ at })` is an explicit, unguarded jump to a
|
|
386
|
+
Step. New list filters:
|
|
387
|
+
`version`, `done_step` with `outcome`, `reason`, `variant_id`,
|
|
388
|
+
`relationship_at_enrollment`. Types `EnrollmentEventBody`,
|
|
389
|
+
`EnrollmentReassignBody`, `EnrollmentReassignResult`, `EnrollmentBulkBody`,
|
|
390
|
+
`EnrollmentBulkResult`, `EnrollmentBulkItem`, `EnrollmentReason` (the reasons catalogue).
|
|
391
|
+
- **Error codes** `SEQUENCE_CONFLICT` (with `err.current`: the stored
|
|
392
|
+
Sequence and its `sequence_revision`; send `if_sequence_revision` on
|
|
393
|
+
`motions.update`), `STEP_IN_FLIGHT`, `NOTHING_TO_RETRY`,
|
|
394
|
+
`ACCOUNT_ALREADY_ENGAGED`, `ALREADY_ENROLLED_ON_ACCOUNT`, `KNOWN_CHAT`,
|
|
395
|
+
`CHAT_NOT_ONE_TO_ONE`. Before, each decoded as `INTERNAL`.
|
|
396
|
+
- **Compiler-style Sequence errors:** every `validateSequence` entry, every
|
|
397
|
+
offline `SEQUENCE_INVALID` entry and every decoded wire `errors[]` entry of
|
|
398
|
+
a Sequence rule carries `code` (`R1` to `R24`, `W1` to `W13`), `message`,
|
|
399
|
+
`pointer` (a JSON Pointer into the document), `hint` and `docs_url`, beside
|
|
400
|
+
`path` and `rule`.
|
|
401
|
+
- **Sequence document:** short variables `{{first_name}}`,
|
|
402
|
+
`{{my_first_name}}` and `{{<key>}}` for a custom field (the long forms stay
|
|
403
|
+
valid); an optional `event` on `do` and `tag` steps; `until event.<name>`;
|
|
404
|
+
the `message.seen` predicate (`if` steps). Warnings `W11` (a short custom
|
|
405
|
+
field that looks like a typo of a standard field), `W12` (`message.seen`
|
|
406
|
+
before any message) and `W13` (a deprecated form).
|
|
407
|
+
- **`newStepId(document, kind, beforeId?)`:** the id the dashboard editor
|
|
408
|
+
gives a new step (`message`, `message_2`, `w_<step>`).
|
|
409
|
+
- **Webhooks:** `enrollment.accepted`, `enrollment.replied`, `enrollment.seen`,
|
|
410
|
+
`enrollment.reassigned` and `motion.custom_event` in `CurviateEvent`.
|
|
411
|
+
`account_ids` is optional (or `null`: every account you can operate) on
|
|
412
|
+
every source.
|
|
413
|
+
|
|
414
|
+
### Changed
|
|
415
|
+
|
|
416
|
+
- **A/B variants are exactly 2** (`MAX_VARIANTS`). 3 to 5 are accepted with
|
|
417
|
+
warning `W13` until 2027-01-19, then refused (`R23`); 1, or 6 and more,
|
|
418
|
+
are refused as before, and 6 and more no longer compile.
|
|
419
|
+
- **`{{motion.<key>}}`** gives `W13` (Motion variables were removed) beside
|
|
420
|
+
`W9`; still not an error offline until 2027-01-19.
|
|
421
|
+
|
|
422
|
+
### Deprecated
|
|
423
|
+
|
|
424
|
+
- `enrollments.signal` and `EnrollmentSignalBody`: use `enrollments.events`;
|
|
425
|
+
accepted until 2027-01-19.
|
|
426
|
+
- `until signal.<name>` (use `until event.<name>`) and the `motion.signal`
|
|
427
|
+
webhook event (renamed `motion.custom_event`), each accepted until
|
|
428
|
+
2027-01-19.
|
|
429
|
+
- `emit` steps: legacy; put `event` on the `do` or `tag` step that matters.
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## [0.43.0]
|
|
11
434
|
|
|
12
435
|
Fixture and types regenerated from deployed staging (server
|
|
13
436
|
`7359b1b1be0584f7f368f1367d39261f2de8f079`, 193 paths; earlier regens at
|